Статьи

Выполнение сетевых обновлений

В этом заключительном выпуске этой серии статей о Android Widgets мы будем опираться на предыдущие статьи, взяв наш периодически обновляемый виджет и добавляя фоновый сервис для извлечения данных из сети. Чтобы упростить для себя задачу, мы покажем последний источник твитов из общедоступной хроники из Твиттера в качестве нашего источника данных, но его легко можно будет адаптировать к другим источникам. Мы также добавим действие по настройке, предлагая пользователю виджета ввести пользователя Twitter, для которого он хочет отображать твиты, до отображения виджета.

Если вам нужно наверстать упущенное, предыдущие статьи были:

Готовим Демо

В нашем предыдущем примере с использованием AlarmManager у нас был рабочий виджет с часами, который обновлялся каждую секунду благодаря использованию AlarmManager. Это идеальная отправная точка для нас, так как мы хотим периодически обновлять данные. Нам нужно будет немного набрать частоту, хотя бы раз в 5 минут или около того.

Если у вас еще нет исходного кода, скачайте его из моего репозитория GitHub для Android-примеров с помощью frequentUpdates обновления. Это можно сделать, выполнив следующие команды. Мы предполагаем, что у вас установлена ​​командная строка Git, но, конечно, у вас есть.

 git clone https://github.com/brucejcooper/Android-Examples.git cd Android-Examples/WidgetExample git tag frequentUpdates 

Теперь мы готовы добавить наши сетевые изменения

Фоновые задачи — Услуги

Выполнение вызова сетевой службы, особенно такой удобной для пользователя, как Twitter, является относительно простым делом на Android. Однако мы не можем просто выполнить вызов в нашем провайдере виджетов, поскольку сетевой вызов займет относительно много времени, и нам не разрешено блокировать основной поток приложения. Это должно быть запущено в отдельном потоке. Опять же, это не важно, но есть еще одно осложнение. Процессы Android имеют определенный жизненный цикл и в любой момент могут быть уничтожены операционной системой для восстановления памяти. Нам нужен способ указать операционной системе, что мы делаем что-то в фоновом режиме и не должны быть уничтожены, даже если мы не показываем что-то на экране напрямую. Это делается через класс Service .

Служба похожа на действие, которое мы обсуждали в разделе « Действия, задачи и задачи» , за исключением того, что оно предназначено для выполнения кода в фоновом режиме. Он имеет жизненный цикл, аналогичный жизненному циклу, и может быть либо явно запущен, либо запущен в ответ на намерение. Служба может быть частной по отношению к приложению, в котором она живет, или предоставлять услуги всей системе посредством межпроцессного взаимодействия (IPC). В этом случае мы собираемся создать очень простой Сервис, который явно запускается, останавливается и не имеет никаких привязок.

Как и со всеми компонентами, доступными для ОС, ваша служба должна быть объявлена ​​в файле ApplicationManifest.xml . Поскольку наш Сервис очень прост, мы просто указываем имя класса, в котором размещается Сервис.

 ... <application android:icon="@drawable/icon" android:label="@string/app_name"> <service android:name=".TwitterFetcherService"/> ... 

onCreate() Сервис должен реализовывать методы жизненного цикла onCreate() , onStartCommand() , onDestroy() и onBind() . Мы не используем связывание, поэтому onBind() может просто вернуть null. Вместо этого мы используем onStartCommand() для порождения потока, который фактически будет выполнять выборку по сети.

 @Override public void onCreate() { mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); // Display a notification about us starting. We put an icon in the status bar. showNotification(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(LOG_TAG, "Received start id " + startId + ": " + intent); SharedPreferences settings = getSharedPreferences(PREFS_FILE, MODE_PRIVATE); String username = settings.getString("twitterUser", null); if (fetcherThread != null &amp;&amp; fetcherThread.isAlive()) { Log.d(LOG_TAG, "Killing existing fetch thread, so we can start a new one"); fetcherThread.interrupt(); } fetcherThread = new Fetcher(username); fetcherThread.start(); // We want this service to continue running until it is explicitly // stopped, so return sticky. // WE have no reason to exist! return START_STICKY; } @Override public void onDestroy() { // Cancel the persistent notification. mNM.cancel(NOTIFICATION); if (fetcherThread.isAlive()) { fetcherThread.interrupt(); } Log.i(LOG_TAG, "Stopped Service"); } 

Код также создает уведомление, которое будет отображаться в панели уведомлений вашего телефона. Я сделал это так, чтобы нам было очевидно, как мы узнаем, что сервис работает. Я бы не стал делать это в реальном виджете, так как это отвлекает пользователя. Им все равно, когда делается звонок, только то, что виджет делает свою работу.

Звонить

Android имеет встроенный Apache HttpClient , что делает вызов в сеть очень простым. Чтобы упростить эту статью, я выбрал относительно простой Twitter API пользовательской шкалы времени для извлечения временной шкалы пользователя с помощью простой операции GET . URL, который мы получаем:

 https://api.twitter.com/1/statuses/user_timeline.json?include_entities=false&amp;include_rts=true&amp;screen_name=username&amp;count=1 

Это вернет последний отправленный пользователем твит в виде JSON вместе с кучей дополнительной информации, которая нас не волнует. Полный код для потока, который выполняет выборку, показан ниже:

 public class Fetcher extends Thread { private String username; public Fetcher(String username) { this.username= username; } public void run() { // Set a timeout on connections HttpParams httpParams = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(httpParams, 10 * 1000); HttpConnectionParams.setSoTimeout(httpParams, 10 * 1000); HttpClient client = new DefaultHttpClient(httpParams); try { if (username == null) { Log.d(LOG_TAG, "Stopping before I've started. How sad"); } else { Log.d(LOG_TAG, "Fetching tweet"); HttpGet fetch = new HttpGet("https://api.twitter.com/1/statuses/user_timeline.json?include_entities=false&amp;include_rts=true&amp;screen_name="+username+"&amp;count=1"); if (!isInterrupted()) { HttpResponse response = client.execute(fetch); if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { JSONArray tweets = new JSONArray(EntityUtils.toString(response.getEntity())); String tweetTxt = Html.fromHtml(tweets.getJSONObject(0).getString("text")).toString(); updateWidgets(tweetTxt); } else { showError(null); // An Error happened. Deal with it somehow } } } } catch (InterruptedIOException e) { Log.d(LOG_TAG, "I was interrupted!"); } catch (Exception e) { showError(e); } finally { Log.d(LOG_TAG, "Shutting myself down"); // We're done. Shut ourself down. stopSelf(); } } private void showError(Exception ex) { Log.e(LOG_TAG, "Error fetching tweets", ex); updateWidgets( "Fetching Tweets failed"); } } 

Обратите особое внимание на вызов stopSelf() в блоке finally. Как только мы закончим обработку, Сервис не будет продолжать работать. Мы используем Сервис в пожаре и забываем. В результате, Служба несет ответственность за то, чтобы отключить себя, когда это будет сделано. Вот что делает этот звонок.

Наконец, нам нужен метод для обновления самого виджета, как только мы получим данные. Это точно такой же код, который у нас был раньше, за исключением того, что мы переместили его из WidgetProvider в Сервис.

 public void updateWidgets(String tweetTxt) { // Get the widget manager and ids for this widget provider, then call the shared // clock update method. ComponentName thisAppWidget = new ComponentName(getPackageName(), ExampleAppWidgetProvider.class.getName()); AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this); int ids[] = appWidgetManager.getAppWidgetIds(thisAppWidget); for (int appWidgetID: ids) { if (pendingIntent == null) { // Create an Intent to launch ExampleActivity Intent intent = new Intent(this, WidgetExampleActivity.class); pendingIntent = PendingIntent.getActivity(this, 0, intent, 0); } RemoteViews updateViews = new RemoteViews(getPackageName(), R.layout.widget1); // Set the Button Action updateViews.setOnClickPendingIntent(R.id.button, pendingIntent); // Update the text. updateViews.setTextViewText(R.id.widget1label, tweetTxt); appWidgetManager.updateAppWidget(appWidgetID, updateViews); } } 

Теперь, когда мы настроили наш код службы, просто ExampleAppWidgetProvider класс провайдера приложений ExampleAppWidgetProvider для вызова нашей службы вместо того, чтобы обновлять текст виджета, чтобы он соответствовал ExampleAppWidgetProvider времени. Чтобы запустить Сервис напрямую, вызывается startService() . Откройте ExampleAppWidgetProvider, удалите код, который в данный момент обновляет текст, и замените его вызовом этого метода:

 public static void triggerUpdate(Context context) { // Start the service which will load the twitter data from the server. context.startService(new Intent(context, TwitterFetcherService.class)); } 

Настройка виджета

Теперь у нас есть виджет, загружающий данные из Интернета, но как мы узнаем, для какого пользователя мы хотим получить данные? Многие виджеты требуют настройки, и Android предоставляет простой способ добавления действия активности, которое отображается перед созданием виджета. Активность конфигурации указывается в файле метаданных виджета. В этом случае файл метаданных находится в файле res/xml/widget1_info.xml . Измените его, чтобы включить атрибут android:configure . После того, как мы внесем изменения, файл должен выглядеть следующим образом:

 <?xml version="1.0" encoding="utf-8"?> <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="294dp" android:minHeight="72dp" android:updatePeriodMillis="1800000" android:configure="com.eightbitcloud.example.widget.ConfigureActivity" android:initialLayout="@layout/widget1" > </appwidget-provider> 

Задание является стандартным заданием и может быть написано как любое другое, с двумя исключениями. Во-первых, спецификация Activity в файле AndroidManifest.xml должна содержать фильтр для действия android.appwidget.action.APPWIDGET_CONFIGURE .

 <activity android:name="com.eightbitcloud.example.widget.ConfigureActivity" android:label="@string/configure_title"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" /> </intent-filter> </activity> 

Убедитесь, что имя класса здесь полностью определено, так как класс будет найден контейнером Widget (домашнее приложение). Во-вторых, Activity должна возвращать результат после завершения настройки, чтобы контейнер Widget знал, что настройка Widget завершена.

 public class ConfigureActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.configure); } public void createClicked(View view) { String username = ((TextView)findViewById(R.id.configure_username)).getText().toString().trim(); if (username == null || username.length() == 0) { Toast.makeText(this, "Username is required", Toast.LENGTH_SHORT); return; } Intent intent = getIntent(); Bundle extras = intent.getExtras(); if (extras != null) { int mAppWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); Log.d("ConfigureActivity", "App Widget ID is " + mAppWidgetId); // Save the username to application preferences Editor settings = getSharedPreferences(TwitterFetcherService.PREFS_FILE, MODE_PRIVATE).edit(); settings.putString("twitterUser", username); settings.apply(); // Kick off a refresh, as when you have a configure activity android does not do one automatically the first time. startService(new Intent(this, TwitterFetcherService.class)); // Tell android that we're done Intent resultValue = new Intent(); resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId); setResult(RESULT_OK, resultValue); finish(); } } } 

Заворачивать

И там у нас это есть. После трех статей мы имеем полностью настраиваемый, часто обновляемый Android Widget с поддержкой сети. Проекты, созданные таким образом, станут основой практически для каждого виджета. Я надеюсь, что вы получили что-то из этого.

В этой статье было много чего рассказать, так что я не показал абсолютно все изменения, которые нужно внести, чтобы это заработало. Взгляните на тег networkFetch моего репозитория GitHub для Android для получения полного исходного кода.