Служба, которая не спит. 3 способа обойти режимы энергосбережения Android в своей программе

Давным-давно, когда Android еще не был мейнстримом, любой разработчик мог написать приложение, которое сможет спокойно висеть в фоне и в режиме реального времени обмениваться данными с сервером. Но чем дальше, тем более жесткие техники сохранения энергии применяет Google, и сегодня так просто сетевое реалтаймовое приложение уже не реализуешь. Однако есть несколько трюков, которые позволяют это сделать.

Давай представим, что у нас есть приложение, а у него, в свою очередь, есть служба (service), которая должна постоянно висеть в фоне, обрабатывать команды, полученные от сетевого сервера, и отправлять ответы. Связь с сервером, как это и положено мобильным устройствам, поддерживается с помощью long poll запросов, то есть приложение подключается к удаленному серверу и ждет, пока тот отправит что-либо в ответ, а потом переподключается и ждет снова. Это эффективный и очень экономный в плане заряда батареи способ, который в том числе используется в механизме push-уведомлений самого Android.

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

Режимы энергосбережения Android

В Android 4.4–5.1 (версии ниже мы рассматривать не будем — они стремительно устаревают) служба будет работать и моментально откликаться на запросы сервера, но только до тех пор, пока экран включен. Через несколько секунд после отключения экрана смартфон перейдет в режим сна (suspend), и промежуток между отправкой запроса и ответом нашего приложения будет составлять примерно минуту. Это срок между maintenance-пробуждениями устройства, и повлиять на него мы не можем.

В Android 6.0–7.1 ситуация будет примерно такой же, однако спустя примерно час смартфон перейдет в так называемый режим Doze. После этого ответ от приложения можно либо не получить вовсе, либо получить спустя час или два. А все потому, что в режиме Doze смартфон фактически не дает работать сторонним приложениям и их службам и полностью отрезает им доступ в Сеть. Управление они могут получить только на короткий промежуток времени спустя час после перехода в режим Doze, затем два часа, четыре часа, со все большим увеличением промежутков между пробуждениями.

Хорошие новости в том, что Doze работает общесистемно и включается спустя час после отключения экрана и только если не трогать смартфон (в 7.0–7.1 можно и трогать), а отключается сразу после разблокировки смартфона, подсоединения к заряднику или движения смартфона (опять же не в 7.0–7.1). То есть можно надеяться на то, что хотя бы днем наш сервис будет работать нормально.

Плохие же новости в том, что, помимо Doze, в Android 6.0–7.1 есть и другой механизм энергосбережения под названием App Standby. Работает он примерно так: система следит за тем, какие приложения использует юзер, и применяет к редко используемым приложениям те же ограничения, что и в случае с режимом Doze. При подключении к заряднику все переведенные в режим Standby приложения получают амнистию. К приложениям, имеющим уведомление или права администратора (не root), режим Standby не применяется.

Итого, в Android есть сразу три механизма, с которыми придется бороться:

  • Suspend — обычный режим энергосбережения, может замедлить получение ответа от устройства примерно на одну минуту;
  • App Standby — агрессивный режим энергосбережения, способный замедлить получение ответа на сутки;
  • Doze — агрессивный общесистемный режим энергосбережения, который применяется ко всем приложениям.

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

Сценарий 1. Небольшая задержка в ответе некритична, переход в Doze некритичен

В этом сценарии у тебя есть приложение, для которого задержка в ответе до одной минуты некритична, а переход смартфона в режим агрессивного энергосбережения совсем не страшен. Все, что тебе нужно, — это чтобы система не отправляла приложение в состояние Standby.

Два самых простых способа добиться этого — либо вывести службу на передний план (foreground service), либо дать приложению права администратора устройства. Начнем с первого варианта.

Foreground service

Foreground service в терминологии Android — это служба, которая имеет уведомление в шторке. Система относится к таким службам гораздо бережнее. Например, при нехватке памяти она будет убита в последнюю очередь, она не будет убита при смахивании приложения в меню управления запущенными приложениями, и да, к ней не будет применен режим Standby.

Создать foreground service очень просто. Достаточно вставить в код службы примерно такие строки:

Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

Notification notification = new Notification.Builder(this)
    .setContentTitle(getText(R.string.notification_title))
    .setContentText(getText(R.string.notification_message))
    .setSmallIcon(R.drawable.icon)
    .setContentIntent(pendingIntent)
    .setTicker(getText(R.string.ticker_text))
    .build();

startForeground(0, notification);

Этот пример создает уведомление, при тапе на которое будет запущена ExampleActivity, в конце с помощью startForeground() служба переводится в статус foreground.

Права администратора

Другой вариант — это дать приложению права администратора. Такие права обеспечивают возможность управлять политикой формирования паролей экрана блокировки, делать удаленную блокировку и вайп устройства.

В свое время Google ввела понятие «администратор устройства» для компаний, которые хотели бы управлять смартфонами своих сотрудников. То есть компания создает приложение, которое получает права администратора и может заблокировать или сбросить телефон после команды от сервера. Именно поэтому приложение с правами администратора не переходит в режим Standby, ведь команда на блокировку может прийти в любой момент.

Получить, а точнее запросить права администратора опять же просто. Для начала нам понадобится пара колбэков, которые будут вызваны после того, как права получены или отозваны:

public static class myDeviceAdminReceiver extends DeviceAdminReceiver {
    @Override
    public void onEnabled(Context context, Intent intent) {
        Toast.makeText(context, "Admin rights granted", Toast.LENGTH_SHORT).show;
        Settings.put(C.S_USE_ADMIN, "true");
    }

    @Override
    public void onDisabled(Context context, Intent intent) {
        Toast.makeText(context, "Admin rights disbaled", Toast.LENGTH_SHORT).show;
        Settings.put(C.S_USE_ADMIN, "false");
    }
}

А затем достаточно вызвать интент DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN, который приведет к появлению на экране окна для активации прав администратора:

mDeviceAdmin = new ComponentName(activity, myDeviceAdminReceiver.class);

Intent intent = new Intent(DevicePolicyManager.ACTION_ADD_DEVICE_ADMIN);
intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, mDeviceAdmin);

startActivityForResult(intent, 1);
Запрос на активацию прав администратора и окно управления администраторами
Запрос на активацию прав администратора и окно управления администраторами
Запрос на активацию прав администратора и окно управления администраторами

Сценарий 2. Небольшая задержка в ответе некритична, в режиме Doze сервис должен работать

Проблема с предыдущим сценарием в том, что, хотя приложение и не будет переходить в состояние Standby, режим Doze продолжит на него действовать. К счастью, Android позволяет частично отключить Doze для выбранных приложений и даже предоставляет средства для вывода диалога с запросом на добавление приложения в список исключений. Все, что нужно сделать, — это вызвать следующий интент:

Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS

Пример:

@TargetApi(23)
public static void requestIgnoreBatteryOptimisation(Context context) {
    Intent intent = new Intent();
    String packageName = context.getPackageName();
    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

    if (!pm.isIgnoringBatteryOptimizations(packageName)) {
        intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
        intent.setData(Uri.parse("package:" + packageName));
        context.startActivity(intent);
    }
}

Плюс добавить такую строку в Manifest.xml:

<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

Если юзер согласится добавить приложение в исключение, нажав «Да», наша служба сможет работать с Сетью и устанавливать вейклоки (partial wakelock), даже когда смартфон находится в режиме Doze.

Проблема этого подхода только в том, что Google не пропустит такое приложение в Play Store. Точнее, она должна его пропустить, если подобная функциональность действительно необходима приложению (об этом ясно сказано в документации). Но по факту робот Гугла сразу отшибает любые приложения с пермишеном REQUEST_IGNORE_BATTERY_OPTIMIZATIONS.

Обойти эту проблему можно, если вместо того, чтобы напрямую просить юзера добавить приложение в список исключений, просто кинуть его на экран управления исключениями Doze (Настройка → Батарея → Меню → Экономия заряда батареи), предварительно предупредив, что юзер должен сам найти приложение в списке и выключить для него режим энергосбережения. Сделать это можно с помощью такого метода:

@TargetApi(23)
public static void openBatteryOptimizationSettings(Context context) {
    Intent intent = new Intent();
    String packageName = context.getPackageName();
    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

    if (!pm.isIgnoringBatteryOptimizations(packageName)) {
        intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
        context.startActivity(intent);
    }
}
Окно исключений режима Doze
Окно исключений режима Doze
Окно исключений режима Doze

Сценарий 3. Задержка в ответе недопустима, в режиме Doze сервис должен работать

К сожалению, даже если ты добавишь приложение в исключения Doze, ты все равно не избежишь задержки в ответе. Одна минута может быть некритична для большинства приложений, но, если речь идет о чем-то вроде мессенджера, никакая задержка не допустима.

Чтобы избежать задержки, можно использовать трюк, примененный в мессенджере Signal. Работает он так: когда на смартфон отправляется сообщение, сервер отсылает на него же пустое push-уведомление, единственная задача которого — разбудить мессенджер и передать ему управление. После этого Signal благополучно получает сообщение с сервера и вновь засыпает.

Реализация push-уведомлений выходит далеко за рамки этой статьи, поэтому я не буду рассказывать о ней, но поделюсь некоторыми советами.

  1. Push-уведомления должны быть реализованы средствами Firebase Cloud Messaging (FCM, ранее Google Cloud Messaging) или с помощью библиотек и сервисов на основе этой технологии.
  2. Уведомления должны быть высокоприоритетными, иначе они не смогут вытащить твое приложение из Doze.
  3. Твое приложение должно находиться в списке исключений Doze, иначе, проснувшись после получения push, оно не сможет выйти в Сеть.
  4. Самый простой способ реализации push-уведомления — это сервис OneSignal. Он построен на базе FCM, очень легко интегрируется в приложение и предоставляет безлимитное количество пушей даже в бесплатной версии.
Панель управления OneSignal

Вместо выводов

С каждой новой версией Android Google все больше ограничивает разработчиков приложений в возможностях. Однако обходные пути всегда можно отыскать. Да, порой эти пути больше напоминают извилистые лесные тропы, чем автомагистраль. Но они есть, и Google не закрывает все возможные пути реализации функциональности, намеренно оставляя лазейки.

Евгений Зобнин: Редактор рубрики X-Mobile. По совместительству сисадмин. Большой фанат Linux, Plan 9, гаджетов и древних видеоигр.

Комментарии (3)

  • Очередная мега крутая статья для подписчиков из 4pda, я смотрю вы особо даже не паритесь, где же эксклюзив, что-то эдакое?!

  • Почему же ни слова не сказано о вещах вроде setAlarmClock или setAndAllowWhileIdle? На постоянно живущую службу нужно больше ресурсов, чем на редкие будильники.

    • Ключевое слово "редкие будильники". Смысл статьи как раз в том, чтобы создать службу, которая будет реагировать на сообщения от сервера максимально быстро, а не просыпаться время от времени и проверять. Лучший способ сделать это - longpoll запросы к серверу, когда служба устанавливает соединение и не закрывает его пока не появятся данные. Но проблема в том, что Android может службу усыпить, отрезать ей инет и тд. Статья как раз и описывает методы борьбы с таким поведением Android.

Похожие материалы