Содержание статьи
- Режимы энергосбережения Android
- Сценарий 1. Небольшая задержка в ответе некритична, переход в Doze некритичен
- Foreground service
- Права администратора
- Сценарий 2. Небольшая задержка в ответе некритична, в режиме Doze сервис должен работать
- Сценарий 3. Задержка в ответе недопустима, в режиме Doze сервис должен работать
- Вместо выводов
Давай представим, что у нас есть приложение, а у него, в свою очередь, есть служба (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 |
Сценарий 3. Задержка в ответе недопустима, в режиме Doze сервис должен работать
К сожалению, даже если ты добавишь приложение в исключения Doze, ты все равно не избежишь задержки в ответе. Одна минута может быть некритична для большинства приложений, но, если речь идет о чем-то вроде мессенджера, никакая задержка не допустима.
Чтобы избежать задержки, можно использовать трюк, примененный в мессенджере Signal. Работает он так: когда на смартфон отправляется сообщение, сервер отсылает на него же пустое push-уведомление, единственная задача которого — разбудить мессенджер и передать ему управление. После этого Signal благополучно получает сообщение с сервера и вновь засыпает.
Реализация push-уведомлений выходит далеко за рамки этой статьи, поэтому я не буду рассказывать о ней, но поделюсь некоторыми советами.
- Push-уведомления должны быть реализованы средствами Firebase Cloud Messaging (FCM, ранее Google Cloud Messaging) или с помощью библиотек и сервисов на основе этой технологии.
- Уведомления должны быть высокоприоритетными, иначе они не смогут вытащить твое приложение из Doze.
- Твое приложение должно находиться в списке исключений Doze, иначе, проснувшись после получения push, оно не сможет выйти в Сеть.
- Самый простой способ реализации push-уведомления — это сервис OneSignal. Он построен на базе FCM, очень легко интегрируется в приложение и предоставляет безлимитное количество пушей даже в бесплатной версии.
Вместо выводов
С каждой новой версией Android Google все больше ограничивает разработчиков приложений в возможностях. Однако обходные пути всегда можно отыскать. Да, порой эти пути больше напоминают извилистые лесные тропы, чем автомагистраль. Но они есть, и Google не закрывает все возможные пути реализации функциональности, намеренно оставляя лазейки.