Глава 29. Фоновые службы будет завершен, AlarmManager будет выдавать интенты, снова и снова запуская PollService (Конечно, такое поведение в высшей степени возмутительно. Возможно, вам стоит удалить приложение, пока ситуация не будет исправлена.) PendingIntent Давайте поближе познакомимся с PendingIntent PendingIntent представляет собой объект-маркер. Когда вы получаете такой объект вызовом PendingIntent.getSer- vice(…) , вы тем самым говорите ОС: «Пожалуйста, запомни, что я хочу отправлять этот интент вызовом startService(Intent) ». Позднее вы можете вызвать send() для PendingIntent , и ОС отправит изначально упакованный интент — точно так, как вы приказали. А лучше всего здесь то, что если передать маркер PendingIntent другой стороне и эта сторона его использует, маркер будет отправлен от имени вашего приложе- ния . А поскольку объект PendingIntent существует в ОС, вы сохраняете полный контроль над ним. Например, вы можете (просто из вредности) предоставить ко- му-нибудь объект PendingIntent и немедленно отменить его, так что вызов send() ничего не сделает. Если вы запросите PendingIntent дважды с одним интентом, то получите тот же PendingIntent . Например, таким образом можно проверить существование PendingIntent или отменить ранее выданный объект PendingIntent Управление сигналами с использованием PendingIntent Для каждого объекта PendingIntent можно зарегистрировать только один сигнал. Именно так работает вызов setServiceAlarm(boolean) при ложном значении isOn : он вызывает AlarmManager.cancel(PendingIntent) для отмены сигнала, связанного с вашим объектом PendingIntent , а потом отменяет PendingIntent Так как PendingIntent также удаляется при отмене сигнала, вы можете проверить, существует ли PendingIntent , чтобы узнать, активен сигнал или нет. Эта операция выполняется передачей флага PendingIntent.FLAG_NO_CREATE вызову PendingIn- tent.getService(…) . Флаг говорит, что если объект PendingIntent не существует, то вместо его создания следует вернуть null Напишите новый метод isServiceAlarmOn(Context) , использующий флаг Pendin- gIntent.FLAG_NO_CREATE для проверки сигнала. Листинг 29.10. Добавление метода isServiceAlarmOn() (PollService.java) public static void setServiceAlarm(Context context, boolean isOn) { } public static boolean isServiceAlarmOn(Context context) { Intent i = new Intent(context, PollService.class); PendingIntent pi = PendingIntent.getService( Управление сигналом 483 context, 0, i, PendingIntent.FLAG_NO_CREATE); return pi != null; } Так как этот объект PendingIntent используется только для установки сигнала, null вместо PendingIntent означает, что сигнал не установлен. Управление сигналом Теперь, когда мы можем включать и отключать сигнал (а также определять, включен ли он), давайте добавим интерфейс для его включения и выключения. Добавьте в файл menu/fragment_photo_gallery.xml новую команду меню. Листинг 29.11. Переключение режима опроса (menu/fragment_photo_gallery.xml)
Затем необходимо добавить несколько новых строк — одну для начала опроса, одну для завершения опроса. (Позднее нам понадобится еще пара строк для оповещений на панели состояния; добавим их сейчас.) Листинг 29.12. Добавление строк для режима опроса (res/values/strings.xml)
Search Clear Search Poll for new pictures Stop polling New PhotoGallery Pictures You have new pictures in PhotoGallery. string>
Удалите старый отладочный код для запуска сигнала и добавьте реализацию коман- ды меню. 484Глава 29. Фоновые службы Листинг 29.13. Реализация команды переключения режима опроса (PhotoGalleryFragment.java) @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); setHasOptionsMenu(true); updateItems(); PollService.setServiceAlarm(getActivity(), true); mThumbnailThread = new ThumbnailDownloader (new Handler()); } @Override @TargetApi(11) public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_search: case R.id.menu_item_clear: updateItems(); return true; case R.id.menu_item_toggle_polling: boolean shouldStartAlarm = !PollService.isServiceAlarmOn(getActivity()); PollService.setServiceAlarm(getActivity(), shouldStartAlarm); return true; default: return super.onOptionsItemSelected(item); } } Теперь вы сможете включать и отключать сигнал. А как насчет команды меню? Обновление элемента командного меню Обычно командное меню достаточно просто заполнить. Однако в некоторых си- туациях требуется обновлять его элементы в соответствии с текущим состоянием приложения. Командные меню не заполняются при каждом использовании, даже если это команд- ные меню «старого стиля». Если вам потребуется обновить содержимое элемента командного меню, поместите соответствующий код в onPrepareOptionsMenu(Menu) Этот метод вызывается при каждом определении конфигурации меню, а не только при его изначальном создании.
Управление сигналом 485 Добавьте реализацию onPrepareOptionsMenu(Menu) , которая проверяет, установлен ли сигнал, а затем изменяет текст menu_item_toggle_polling , чтобы в меню ото- бражался соответствующий текст. Листинг 29.14. Добавление метода onPrepareOptionsMenu(Menu) (PhotoGalleryFragment.java) @Override public boolean onOptionsItemSelected(MenuItem item) { } @Override public void onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); MenuItem toggleItem = menu.findItem(R.id.menu_item_toggle_polling); if (PollService.isServiceAlarmOn(getActivity())) { toggleItem.setTitle(R.string.stop_polling); } else { toggleItem.setTitle(R.string.start_polling); } } На устройствах с версией до 3.0 этот метод вызывается при каждом отображении меню. Это гарантирует, что элементы меню всегда будут содержать правильный текст. Если хотите удостовериться в этом, запустите приложение PhotoGallery в эмуляторе с версией до 3.0. Однако для версий после 3.0 этого недостаточно. Панель действий не об- новляется автоматически. Вам придется специально приказать ей вызвать onPrepareOptionsMenu(Menu) и обновить элементы вызовом Activity.invalida- teOptionsMenu() Добавьте следующий код в onOptionsItemSelected(MenuItem) , чтобы приказать устройствам после 3.0 обновить свои панели действий. Листинг 29.15. Командное меню объявляется недействительным (PhotoGalleryFragment.java) @Override @TargetApi(11) public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_toggle_polling: boolean shouldStartAlarm = !PollService.isServiceAlarmOn(getActivity()); PollService.setServiceAlarm(getActivity(), shouldStartAlarm); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) getActivity().invalidateOptionsMenu(); return true; default: return super.onOptionsItemSelected(item); } }
486 Глава 29. Фоновые службы После этого код будет отлично работать в новом эмуляторе 4.2. И все же в приложении кое-чего не хватает. Оповещения Служба успешно работает в фоновом режиме. Однако пользователь об этом понятия не имеет, так что пользы от нее будет немного. Когда службе требуется передать какую-то информацию пользователю, для этого она почти всегда использует оповещения (notifications) — элементы, которые по- являются на выдвижной панели оповещений, которую пользователь вызывает, проводя пальцем вниз от верха экрана. Чтобы опубликовать оповещение, необходимо сначала создать объект Notification Объекты Notification создаются с использованием объектов-построителей — по- добно тому, как это делалось с AlertDialog из главы 12. Как минимум объект No- tification должен иметь: текст бегущей строки , отображаемый на панели состояния при первом по- явлении оповещения; значок , отображаемый на панели состояния после исчезновения бегущей строки; представление , отображаемое на выдвижной панели оповещений для вывода оповещения; объект PendingIntent , срабатывающий при нажатии на оповещении на выдвиж- ной панели. После того как объект Notification будет создан, его можно отправить вызовом метода notify(int, Notification) для системной службы NotificationManager Чтобы служба PollService оповещала пользователя о появлении нового результата, добавьте код из листинга 29.16. Этот код создает объект Notification и вызывает NotificationManager.notify(int, Notification) Листинг 29.16. Добавление оповещения (PollService.java) @Override public void onHandleIntent(Intent intent) { String resultId = items.get(0).getId(); if (!resultId.equals(lastResultId)) { Log.i(TAG, "Got a new result: " + resultId); Resources r = getResources(); PendingIntent pi = PendingIntent .getActivity(this, 0, new Intent(this, PhotoGalleryActivity.class), 0); Notification notification = new NotificationCompat.Builder(this) .setTicker(r.getString(R.string.new_pictures_title)) .setSmallIcon(android.R.drawable.ic_menu_report_image) .setContentTitle(r.getString(R.string.new_pictures_title))
Управление сигналом 487 .setContentText(r.getString(R.string.new_pictures_text)) .setContentIntent(pi) .setAutoCancel(true) .build(); NotificationManager notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); notificationManager.notify(0, notification); } prefs.edit() .putString(FlickrFetchr.PREF_LAST_RESULT_ID, resultId) .commit(); } Пройдемся по этому коду сверху вниз. Сначала мы задаем текст бегущей строки и значок вызовами setTicker(CharSequence) и setSmallIcon(int) После этого задается внешний вид оповещения на самой выдвижной панели. Можно полностью определить внешний вид оповещения, но проще использовать стандартное оформление со значком, заголовком и текстовой областью. Для значка будет использоваться значение из setSmallIcon(int) . Заголовок и текст задаются вызовами setContentTitle(CharSequence) и setContentText(CharSequence) соот- ветственно. Теперь необходимо указать, что происходит при нажатии на оповещении. Как и в случае с AlarmManager , для этого используется PendingIntent . Объект Pendin- gIntent , передаваемый setContentIntent(PendingIntent) , будет запускаться при нажатии пользователем на вашем оповещении на выдвижной панели. Вызов setAutoCancel(true) слегка изменяет это поведение: с этим вызовом оповещение при нажатии также будет удаляться с выдвижной панели оповещений. Код завершается вызовом NotificationManager.notify(…) . Передаваемый целочис- ленный параметр содержит идентификатор оповещения, уникальный в границах приложения. Если вы отправите второе оповещение с тем же идентификатором, оно заменит последнее оповещение, отправленное с этим идентификатором. Так реализуются индикаторы прогресса и другие динамические визуальные эффекты. Собственно, это все. Весь пользовательский интерфейс для фонового опроса! Ког- да вы убедитесь в том, что все работает правильно, замените константу интервала опроса более разумным значением. Листинг 29.17. Изменение периодичности опроса (PollService.java) public class PollService extends IntentService { private static final String TAG = "PollService"; public static final int POLL_INTERVAL = 1000 * 15; // 15 секунд public static final int POLL_INTERVAL = 1000 * 60 * 5; // 5 минут public PollService() { super(TAG); } 488Глава 29. Фоновые службы Для любознательных: подробнее о службахМы рекомендуем использовать IntentService для большинства реализаций служб. Если паттерн IntentService не подходит для вашей архитектуры, вам придется поближе познакомиться со службами для составления собственной реализации. Приготовьтесь, при изучении служб приходится учитывать множество подроб- ностей и нюансов. Что делают (и чего не делают) службыСлужба представляет собой компонент приложения, предоставляющий обратные вызовы жизненного цикла (в этом она похожа на активность). Эти обратные вы- зовы даже выполняются в главном потоке без каких-либо усилий с вашей стороны, как и в случае с активностью. В своем исходном состоянии служба не выполняет никакой код в фоновом потоке. Это главная причина, по которой мы рекомендуем IntentService . Для большинства нетривиальных служб потребуется тот или иной фоновый поток, а IntentService ав- томатически управляет шаблонным кодом, необходимым для достижения этой цели. Давайте посмотрим, какие обратные вызовы жизненного цикла предоставляет служба. Жизненный цикл службыЖизненный цикл службы, запущенной startService(Intent) , весьма прост. Ниже перечислены методы обратного вызова жизненного цикла. onCreate(…) — вызывается при создании службы. onStartCommand(Intent,int,int) — вызывается каждый раз, когда компонент запускает службу вызовом startService(Intent) . Два целочисленных параметра содержат набор флагов и идентификатор запуска. Флаги указывают, является ли интент повторной отправкой ранее доставленного интента, или повторной попыткой после неудачи при доставке. Идентификатор запуска отличается при разных вызовах onStartCommand(Intent,int,int) , поэтому по нему можно отличить эту команду от других. onDestroy() — вызывается, когда дальнейшее существование службы не требу- ется. Часто происходит после остановки службы. Остается один вопрос: как происходит остановка службы? Это можно сделать раз- ными способами в зависимости от типа службы. Тип службы определяется значе- нием, возвращаемым методом onStartCommand(…) ; возможные значения — Service. START_NOT_STICKY , START_REDELIVER_INTENT или START_STICKY Незакрепляемые службыIntentService является незакрепляемой (non-sticky) службой, поэтому начнем с нее. Незакрепляемая служба останавливается, когда сама служба сообщает Для любознательных: подробнее о службах 489о завершении своей работы. Чтобы сделать свою службу незакрепляемой, верните START_NOT_STICKY или START_REDELIVER_INTENT Чтобы сообщить Android о завершении работы службы, вызовите метод stopSelf() или stopSelf(int) . Первый метод, stopSelf() , является безусловным. Он всегда останавливает службу независим от того, сколько раз был вызван метод onStart- Command(…) IntentService использует вместо него stopSelf(int) . Этот метод получает иденти- фикатор запуска, полученный в onStartCommand(…) . Служба останавливается только в том случае, если этот идентификатор является самым последним из полученных идентификаторов запуска (так работает внутренняя реализация IntentService ). Чем же отличаются START_NOT_STICKY и START_REDELIVER_INTENT ? Поведени- ем службы, если системе потребуется завершить ее преждевременно. Служба START_NOT_STICKY просто прекращает существование и уходит в никуда. Служба START_REDELIVER_INTENT , напротив, попытается запуститься позднее, когда ресурсы не будут так ограничены. Выбор между START_NOT_STICKY и START_REDELIVER_INTENT определяется важностью этой операции для вашего приложения. Если служба не критична, выберите режим START_NOT_STICKY . В PhotoGallery служба запускается по сигналу. Пропажа одного вызова не критична, поэтому мы выбираем START_NOT_STICKY . Такое поведение используется по умолчанию для IntentService . Чтобы переключиться на режим START_REDELIVER_INTENT , вызовите IntentService.setIntentRedelivery(true) Закрепляемые службыЗакрепляемая (sticky) служба остается запущенной, пока кто-то находящийся вне службы не прикажет ей остановиться, вызвав метод Context.stopService(Intent) Чтобы сделать службу закрепляемой, верните значение START_STICKY После запуска закрепляемая служба остается «включенной», пока компонент не вызовет Context.stopService(Intent) . Если службу по какой-то причине потре- буется уничтожить, она будет снова перезапущена с передачей onStartCommand(…) null -интента. Закрепляемый режим хорошо подходит для долгоживущих служб (например, проигрывателя музыки), которые должны работать до тех пор, пока пользователь не прикажет им остановиться. Впрочем, даже в этом случае стоит рассмотреть альтернативную архитектуру с использованием незакрепляемых служб. Управлять закрепляемыми службами неудобно, потому что трудно определить, была ли уже запущена служба. Привязка к службамТакже существует возможность привязки (binding) к службе при помощи метода bi ndService(Intent,ServiceConnection,int) . Привязка к службе — механизм подклю- чения к службе и непосредственного вызова ее методов. Привязка осуществляется |