Глава 28. Поиск
Рис. 28.5. Интеграция поиска в активность приложения
После правильной настройки
SearchView ведет себя точно так же, как наши пре- дыдущие реализации поиска.
Кроме одной маленькой подробности: если вы попытаетесь использовать аппарат- ную клавиатуру в эмуляторе, поиск выполняется два раза, один за другим.
Оказывается, в
SearchView имеется ошибка. Нажатие клавиши ввода на аппаратной клавиатуре приводит к двукратному инициированию интента поиска. В активно- стях с режимом запуска по умолчанию это приводит к запуску двух одинаковых активностей для одного поиска.
Мы уже подготовились к этой ошибке ранее, когда выбрали режим запуска sin- gleTop
. Это гарантирует, что интенты будут сначала отправляться существующей активности, поэтому при отправке дубликата поискового интента новая активность запускаться не будет. К сожалению, из-за дубликата поиск все равно будет выпол- няться два раза подряд, но это гораздо лучше запуска двух одинаковых активностей для одного поиска.
Упражнения
Упражнения этой главы не слишком сложны. Первое заключается в использовании метода
Activity.startSearch(…)
Во внутренней реализации onSearchRequested()
вызывает
Activity.startS- earch(…)
— более детализированный способ запуска диалогового окна поиска.
Упражнения
473
Метод startSearch(…)
позволяет задать исходный запрос, отображаемый в поле
EditText
, добавить объект
Bundle с данными, отправляемыми поисковой актив- ности-получателю, в дополнениях интентов, или запросить глобальное диалоговое окно веб-поиска (аналогичное тому, которое вы увидите при нажатии кнопки по- иска на домашнем экране).
В первом небольшом упражнении используйте метод
Activity.startSearch(…)
для заполнения диалогового окна поиска текущим запросом и его выделения.
Второе упражнение требует, чтобы при запуске нового поиска выводилось общее количество доступных результатов поиска в оповещении
Toast
. Для этого вам придется обратиться к разметке XML, полученной от Flickr. В ней присутствует атрибут верхнего уровня с количеством возвращенных результатов.
Фоновые службыВесь код, написанный нами до настоящего момента, был
связан с активностью; это подразумевало, что он связывается с некой информацией на экране, видимой пользователю.
А если приложение не использует экран? Что, если выполняемые им операции не требуют визуального представления — как, скажем, воспроизведение музыки или проверка новых сообщений в блогах из поставки RSS? Для таких целей следует создать
службу (service).
В этой главе мы добавим в PhotoGallery новую функцию фонового оповещения о появлении новых результатов поиска. Каждый раз, когда становится доступным новый результат, пользователь получает уведомление на панели состояния.
Создание IntentServiceНачнем с создания службы. В этой главе мы будем использовать класс
IntentSer- vice
. Это не единственная разновидность служб, но, пожалуй, самая распростра- ненная. Создайте субкласс
IntentService с именем
PollService
. Эта служба будет использоваться нами для опроса результатов поиска.
Заглушка метода onHandleIntent(Intent)
класса
PollService будет сгенерирована автоматически. Включите в onHandleIntent(Intent)
команду регистрации в журнале, добавьте тег для журнала и определите конструктор по умолчанию.
Листинг 29.1. Создание класса PollService (PollService.java)
public class PollService extends IntentService {
private static final String TAG = "PollService"; public PollService() { super(TAG); }29
Создание IntentService
475
@Override protected void onHandleIntent(Intent intent) {
Log.i(TAG, "Received an intent: " + intent);
}
}
Это очень простая реализация
IntentService
. Что она делает? В общем-то она от- части похожа на активность. Она является контекстом (
Service
— субкласс
Context
) и реагирует на интенты (как видно из onHandleIntent(Intent)
).
Интенты службы называются командами (commands). Каждая команда представ- ляет собой инструкцию по выполнению некоторой операции для службы. Способ обработки команды зависит от вида службы.
1. Получен командный интент 1.
Служба создана.
2. Получен командный интент 2.
3. Получен командный интент 3.
4. Получен командный интент 1.
5. Командный интент 2 завершен.
6. Командный интент 3 завершен.
Служба уничтожена.
Очередь команд
Очередь команд
Очередь команд
Очередь команд
Очередь команд
Рис. 29.1. Как IntentService обслуживает команды
IntentService обрабатывает команды, организованные в очередь. При получении первой команды
IntentService инициализируется, запускает фоновый поток и по- мещает команду в очередь.
Затем
IntentService переходит к последовательной обработке команд, с вызовом метода onHandleIntent(Intent)
своего фонового потока для каждой команды. Новые
476Глава 29. Фоновые службы поступающие команды ставятся в очередь. Когда в очереди не остается ни одной команды, служба останавливается и уничтожается.
Приведенное описание относится только к
IntentService
. Позднее в этой главе служ- бы и принципы обработки команд будут рассмотрены в более широкой перспективе.
Из того, что вы только что узнали о работе
IntentService
,
возникает предположение, что службы реагируют на интенты. И это действительно так! А поскольку службы, как и активности, реагируют на интенты, они также должны объявляться в файле
AndroidManifest.xml
. Добавьте в манифест элемент для службы
PollService
Листинг 29.2. Добавление службы в манифест (AndroidManifest.xml)
... >
... >
... >
Добавьте код запуска службы в
PhotoGalleryFragment
Листинг 29.3. Добавление кода запуска службы (PhotoGalleryFragment.java)
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
updateItems();
Intent i = new Intent(getActivity(), PollService.class); getActivity().startService(i); mThumbnailThread = new ThumbnailDownloader
(new Handler());
}
Запустите приложение и посмотрите, что получилось (рис. 29.2).
Рис. 29.2. Первые шаги нашего приложения
Зачем нужны службы
477Зачем нужны службыЛадно, признаем: все эти записи LogCat выглядят скучно. Но сам-то код очень интересный! Почему? Что с ним можно сделать?
Пора вернуться в вымышленный мир, где мы уже не программисты, а хозяева об- увного магазина с продавцами-супергероями.
Продавцы могут работать в двух местах: в торговом зале, где они общаются с по- купателями, и на складе, куда покупатели не заходят. Склад может быть большим или маленьким в зависимости от магазина.
До настоящего момента весь наш код выполнялся в активностях. Активности —
«прилавок» приложений Android. Весь этот код направлен на то, чтобы обеспечить приятные визуальные впечатления у пользователя.
Службы — своего рода «склад» приложений Android. Здесь происходит то, о чем пользователю знать не обязательно. Работа здесь может продолжаться после того, как торговый зал будет закрыт, когда активности давно перестали существовать.
Впрочем, довольно о магазинах. Что такого можно сделать со службой, чего нельзя сделать с активностью? Например, службу можно запустить, пока пользователь занимается другими делами.
Безопасные сетевые операции в фоновом режимеНаша служба будет опрашивать Flickr в фоновом режиме. Чтобы выполнение сетевых
операций в фоновом режиме проходило безопасно, потребуется дополнительный код.
Android дает пользователю возможность отключить сетевые операции для фоновых приложений. Если вы используете множество приложений, интенсивно использую- щих вычислительные ресурсы, это может существенно повысить производительность.
Однако это означает, что при выполнении операций в фоновом режиме необходимо при помощи объекта
ConnectivityManager убедиться в том, что сеть доступна. Поскольку исторически API изменялся, для этого потребуются две проверки, а не одна. Первая проверяет истинность
ConnectivityManager.getBackgroundDataSetting()
, а вторая — что результат
ConnectivityManager.getActiveNetworkInfo()
отличен от null
Добавьте код из листинга 29.4 для выполнения этих проверок, а потом займемся техническими подробностями.
Листинг 29.4. Проверка доступности сети для фоновых операций (PollService.java)
@Override public void onHandleIntent(Intent intent) {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); @SuppressWarnings("deprecation") boolean isNetworkAvailable = cm.getBackgroundDataSetting() && cm.getActiveNetworkInfo() != null; if (!isNetworkAvailable) return; Log.i(TAG, "Received an intent: " + intent);
}
478Глава 29. Фоновые службы
Почему нужны две проверки? В старых версиях Android полагалось проверять getBackgroundDataSetting()
и прерывать операцию, если вызов возвращал false
Если проверка не выполнялась, приложение преспокойно использовало сетевые данные. Такая схема была слишком несовершенной: программист мог случайно забыть о проверке, а то и проигнорировать ее.
В Android 4.0, Ice Cream Sandwich, схема была изменена: при запрете фоновых операций сеть просто блокировалась. Вот почему мы проверяем, не возвращает ли getActiveNetworkInfo()
значение null
. Это правильно с точки зрения пользователя, потому что смысл режима фоновых операций всегда соответствует его ожиданиям.
Конечно, это немного усложняет жизнь разработчика.
Чтобы использовать метод getActiveNetworkInfo()
, также необходимо получить разрешение
ACCESS_NETWORK_STATE
Листинг 29.5. Получение разрешения для проверки состояния сети (AndroidManifest.xml)
package="com.bignerdranch.android.photogallery"
android:versionCode="1"
android:versionName="1.0" >
android:targetSdkVersion="17" />
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
Поиск новых результатовНаша служба будет опрашивать Flickr на
появление новых результатов, поэтому ей нужно знать результат последней выборки. Для этой работы идеально подой- дет механизм
SharedPreferences
. Добавьте в
FlickrFetchr еще одну константу для хранения идентификатора последней загруженной фотографии.
Листинг 29.6. Добавление константы для хранения идентификатора (FlickrFetchr.java)
public class FlickrFetchr {
public static final String TAG = "PhotoFetcher";
public static final String PREF_SEARCH_QUERY = "searchQuery";
public static final String PREF_LAST_RESULT_ID = "lastResultId";
Поиск новых результатов
479
private static final String ENDPOINT = "https://api.flickr.com/services/rest/";
private static final String API_KEY = "xxx";
Следующим шагом станет заполнение кода службы. Необходимо сделать следующее:
1. Прочитать текущий запрос и идентификатор последнего результата из Shared-
Preferences.
2. Загрузить последний набор результатов с использованием
FlickrFetchr
3. Если набор не пуст, получить первый результат.
4. Проверить, отличается ли его идентификатор от идентификатора последнего результата.
5. Сохранить первый результат в
SharedPreferences
Давайте вернемся к файлу
PollService.java и претворим этот план в жизнь. В листин- ге 29.7 содержится довольно длинный блок кода, но в нем нет ничего, что бы вы не видели ранее.
Листинг 29.7. Проверка новых результатов (PollService.java)
@Override protected void onHandleIntent(Intent intent) {
if (!isNetworkAvailable) return;
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
String query = prefs.getString(FlickrFetchr.PREF_SEARCH_QUERY, null);
String lastResultId = prefs.getString(FlickrFetchr.PREF_LAST_RESULT_ID, null);
ArrayList items;
if (query != null) {
items = new FlickrFetchr().search(query);
} else {
items = new FlickrFetchr().fetchItems();
}
if (items.size() == 0)
return;
String resultId = items.get(0).getId();
if (!resultId.equals(lastResultId)) {
Log.i(TAG, "Got a new result: " + resultId);
} else {
Log.i(TAG, "Got an old result: " + resultId);
}
prefs.edit()
.putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId)
.commit();
}
480
Глава 29. Фоновые службы
Видите каждый шаг из приведенного списка? Хорошо. Запустите приложение
PhotoGallery и вы увидите, как приложение получает исходные результаты. Если поисковый запрос уже выбран, вероятно, при последующих запусках будут ото- бражаться устаревшие результаты.
Отложенное выполнение и AlarmManager
Чтобы служба реально использовалась в фоновом режиме, нам понадобится какой-то механизм организации операций при отсутствии работающих активностей — напри- мер, таймер, который срабатывает каждые пять минут, или что-нибудь в этом роде.
Это можно сделать при помощи
Handler
, вызовами методов
Handler.sendMes- sageDelayed(…)
или
Handler.postDelayed(…)
. Впрочем, такое решение с большой вероятностью перестанет работать, если пользователь уйдет со всех активностей.
Процесс закроется, и вместе с ним прекратят существование сообщения
Handler
По этой причине вместо
Handler мы будем использовать
AlarmManager
— системную службу, которая может отправлять интенты за вас.
Как сообщить
AlarmManager
, какие интенты нужно отправить? При помощи объекта
PendingIntent
. По сути, в объекте
PendingIntent упаковывается пожелание: «Я хочу запустить
PollService
». Затем это пожелание отправляется другим компонентам системы, таким как
AlarmManager
Включите в
PollService новый метод с именем setServiceAlarm(Context,boole an)
, который включает и отключает сигнал за вас. Метод будет объявлен статиче- ским; это делается для того, чтобы код сигнала размещался рядом с другим кодом
PollService
, с которым он связан, но мог вызываться другими компонентами.
Обычно включение и отключение должно осуществляться из интерфейсного кода фрагмента или другого контроллера.
Листинг 29.8. Добавление сигнального метода (PollService.java)
public class PollService extends IntentService {
private static final String TAG = "PollService";
private static final int POLL_INTERVAL = 1000 * 15; // 15 секунд
public PollService() {
super(TAG);
}
@Override public void onHandleIntent(Intent intent) {
}
public static void setServiceAlarm(Context context, boolean isOn) {
Intent i = new Intent(context, PollService.class);
PendingIntent pi = PendingIntent.getService(
context, 0, i, 0);
AlarmManager alarmManager = (AlarmManager)
Отложенное выполнение и AlarmManager
481
context.getSystemService(Context.ALARM_SERVICE);
if (isOn) {
alarmManager.setRepeating(AlarmManager.RTC,
System.currentTimeMillis(), POLL_INTERVAL, pi);
} else {
alarmManager.cancel(pi);
pi.cancel();
}
}
}
Метод начинается с создания объекта
PendingIntent
, который запускает
PollService
Задача решается вызовом метода
PendingIntent.getService(…)
, в котором упако- вывается вызов
Context.startService(Intent)
. Метод получает четыре параметра:
Context для отправки интента; код запроса, по которому этот объект
PendingIntent отличается от других; отправляемый объект
Intent
; и наконец, набор флагов, управ- ляющий процессом создания
PendingIntent
(вскоре мы используем один из них).
После этого сигнал либо устанавливается, либо отменяется. Чтобы установить сигнал, следует вызвать
AlarmManager.setRepeating(…)
. Этот метод тоже получает четыре параметра: константу для описания временной базы сигнала (об этом чуть позже), время запуска сигнала, временной интервал повторения сигнала, и наконец, объект
PendingIntent
, запускаемый при срабатывании сигнала.
Отмена сигнала осуществляется вызовом
AlarmManager.cancel(PendingIntent)
Обычно при этом также следует отменить и
PendingIntent
. Вскоре вы увидите, как отмена
PendingIntent помогает в отслеживании статуса сигнала.
Добавьте простой тестовый код для запуска сигнала из
PhotoGalleryFragment
Листинг 29.9. Добавление кода запуска сигнала (PhotoGalleryFragment.java)
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
updateItems();
Intent i = new Intent(getActivity(), PollService.class);
getActivity().startService(i);
PollService.setServiceAlarm(getActivity(), true);
mThumbnailThread = new ThumbnailDownloader(new Handler());
}
Введите этот код и запустите PhotoGallery. Немедленно нажмите кнопку
Back и вый дите из приложения.
Замечаете что-нибудь в LogCat?
PollService честно продолжает работать, запуска- ясь каждые 15 секунд. Для этого и нужен класс
AlarmManager
. Даже если процесс