31.10.2026 796 материалов

Режим энергосбережения отключил сервис доступности — когда приложение вправе включить его обратно?

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

Режим энергосбережения отключил сервис доступности — когда приложение вправе включить его обратно?

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

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

Разработчик QuickDial — небольшого приложения, которое добавляет кнопку прямого звонка в чатах мессенджера — разобрал, как в этой ситуации вести себя корректно. Разбор технически полезен независимо от самого приложения: он про то, как приложение может отличить вмешательство системы от решения пользователя и когда вообще имеет право что-то включать само.

Одна оговорка к источнику: публикация на dev.to выпущена ИИ-агентом от лица разработчика QuickDial. Основные технические утверждения проверяемы по API Android, но формулировки стоит читать как описание конкретной реализации, а не как обзор поведения всей платформы.

Почему сервис вообще выключается сам

Сервис доступности (Accessibility Service) — это системный механизм, изначально придуманный для вспомогательных технологий: скринридеров, экранных луп, альтернативного ввода. У него широкие права — он видит содержимое экрана, читает названия элементов, может имитировать действия. Поэтому приложения вроде QuickDial строят на нём функции, которым нужен доступ к интерфейсу стороннего приложения: сервис читает заголовок открытого чата, а приложение поверх него рисует кнопку вызова.

Проблема в том, что агрессивные оболочки производителей (MIUI, One UI, ColorOS и им подобные) при включении экономии батареи отключают сервисы доступности целиком — как раз потому, что они считаются тяжёлыми. И после выхода из режима они остаются выключенными. На «чистом» Android такого поведения нет: это вендорская доработка, и она не документирована ни в AOSP, ни в публичных гарантиях производителей. Отсюда главный архитектурный вывод: приложение не может рассчитывать на то, что его сервис будет работать постоянно, и обязано уметь обнаруживать его выключение.

Два сценария — и почему их нельзя путать

Сервис может быть выключен по двум совершенно разным причинам:

  1. Система выключила его сама — в составе режима энергосбережения, вместе со всеми остальными сервисами.

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

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

Может ли приложение включить сервис само

Обычно — нет. Список активных сервисов хранится в системной настройке ENABLED_ACCESSIBILITY_SERVICES, и запись в неё требует разрешения WRITE_SECURE_SETTINGS. Через диалог на экране телефона его не получить в принципе: выдаётся только с компьютера, через adb:

adb shell pm grant com.ashwar.quickdialbridge android.permission.WRITE_SECURE_SETTINGS

Практическое следствие жёсткое: автоматическое восстановление — опция для управляемых парков устройств (офисный парк, настроенный один раз по adb), а не для обычных установок из Google Play. Все остальные пользователи получают уведомление и кнопку перехода в настройки. Оба пути идут через одно и то же решение о том, можно ли включать.

Причину нужно фиксировать в момент события

Чтобы решить, что делать, нужно знать, почему сервис выключился. Узнать это можно ровно один раз — в момент отключения. Потом остаётся только гадать.

QuickDial следит за системной настройкой через ContentObserver и параллельно запоминает, когда последний раз менялось состояние энергосбережения. Когда сервис переходит из состояния «включён» в «выключен», причина классифицируется так:

enum class OffCause { USER, POWER_SAVE, UNOBSERVED }

const val POWER_SAVE_WINDOW_MS = 2 * 60 * 1000L

fun causeSeenTurningOff(powerSaveOn: Boolean, msSincePowerSaveChange: Long): OffCause =
    if (powerSaveOn || msSincePowerSaveChange in 0..POWER_SAVE_WINDOW_MS) {
        OffCause.POWER_SAVE
    } else {
        OffCause.USER
    }

Окно в две минуты нужно, чтобы покрыть проблему порядка событий: сервис может быть выключен чуть раньше или чуть позже, чем придёт broadcast о смене режима энергосбережения. Это эвристика, и о ней честно стоит сказать прямо: если пользователь вручную выключит сервис в течение этих двух минут после переключения режима, причина будет записана как POWER_SAVE. На обычных устройствах цена ошибки — лишнее уведомление. На устройствах с adb-разрешением — повторное включение сервиса против воли пользователя. Вероятность события низкая, но механизм неидеален.

Есть и третий случай. Экономия батареи часто убивает и само приложение, поэтому сервис может выключиться тогда, когда наблюдать за этим было некому. В следующий запуск (при загрузке, после обновления, при открытии приложения или при переподключении слушателя уведомлений) приложение видит выключенный сервис без записанной причины и сохраняет UNOBSERVED. Такой случай трактуется как отключение системой — логика в том, что пользователь, выключающий сервис в настройках при живом приложении, наблюдается, а ненаблюдаемый случай почти всегда системный.

Здесь тоже есть дыра: пользователь может выключить сервис вручную тогда, когда процесс приложения уже убит системой, и наблюдатель не работает. Тогда решение пользователя будет записано как UNOBSERVED и затем отменено автоматическим восстановлением. На практике сценарий маргинален (обычно приложение к моменту отключения уже что-то слушало), но как архитектурное ограничение его стоит держать в голове.

Всё правило — одна чистая функция

Само решение о восстановлении вынесено в чистую функцию без побочных эффектов:

fun shouldAutoRestore(
    canWriteSecureSettings: Boolean,
    wasEverEnabled: Boolean,
    cause: OffCause,
    powerSaveOnNow: Boolean,
): Boolean = canWriteSecureSettings && wasEverEnabled && cause != OffCause.USER && !powerSaveOnNow

Каждое условие закрывает конкретную дыру:

  • cause != USER — никогда не отменять выбор пользователя.
  • !powerSaveOnNow — восстановление внутри режима энергосбережения будет тут же откачено обратно, поэтому нужно дождаться его завершения. Broadcast о выключении режима — один из триггеров, так что проверка выполнится заново.
  • wasEverEnabled — если пользователь сервис вообще не включал, это проблема онбординга, а не восстановления.
  • canWriteSecureSettings — без adb-разрешения сценарий переходит в ветку «спросить пользователя».

Вынесение в чистую функцию — не дань красоте: все комбинации проверяются JVM-тестами без устройства, а на поведение, зависящее от таймингов OEM-оболочек, тесты всё равно не рассчитывают.

Дописывать, а не перезаписывать

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

fun withService(existing: String?, component: String): String {
    val parts = existing.orEmpty().split(':').map(String::trim).filter(String::isNotEmpty)
    if (parts.any { it.equals(component, ignoreCase = true) }) return parts.joinToString(":")
    return (parts + component).joinToString(":")
}

После записи выставляется ACCESSIBILITY_ENABLED = 1, состояние читается обратно, и только после подтверждения восстановление считается выполненным. Попытка восстановления дополнительно ограничена по частоте — не чаще раза в минуту, чтобы несколько триггеров, сработавших одновременно, не долбили системную настройку.

Если восстановить нельзя — сказать, что именно не работает

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

Никакого поллинга

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

Что стоит держать в голове

Весь сценарий существует только потому, что вендорская оболочка меняет поведение системного механизма без документированных гарантий. Ни то, что сервисы доступности выключаются при экономии энергии, ни то, что освобождение от оптимизации батареи защитит процесс от убийства, не гарантировано: на разных прошивках результат разный, и никакой тест это не покроет. Приложение, чьи ключевые функции живут в сервисе доступности, уязвимо по построению — и это же создаёт нагрузку на пользователей, которым приходится объяснять, почему оно «перестало работать».

Отдельно стоит заметить, что Google Play жёстко модерирует использование Accessibility API: приложение должно обосновать, почему разрешение ему нужно для основной функции. QuickDial в сторе опубликован, то есть обоснование прошло проверку, но это ограничение никуда не делось.

Практические выводы из разбора переносятся на любое приложение с сервисом доступности:

  • Сервис, выключенный пользователем, остаётся выключенным — вокруг этого строится вся логика восстановления.
  • Причину отключения фиксируйте в момент события, потом будет только гадание.
  • Отключение, пропущенное при мёртвом процессе, трактуйте как системное.
  • Не восстанавливайте сервис, пока режим энергосбережения активен, — дождитесь его окончания.
  • Настройка со списком сервисов общая: дописывайте свой компонент, не перезаписывайте чужие.
  • Без adb-разрешения — честно и один раз спросите пользователя и объясните, что именно осталось работать.

Автоматическое включение здесь — не «удобная фича», а привилегия для управляемых устройств. Для обычного пользователя корректный ответ приложения — не молча вернуть всё как было, а внятно сообщить, что изменилось и где это включить обратно.


✴ Meta* — входит в перечень общественных объединений и религиозных организаций, в отношении которых судом принято вступившее в законную силу решение о ликвидации или запрете деятельности по основаниям, предусмотренным Федеральным законом от 25.07.2002 № 114-ФЗ «О противодействии экстремистской деятельности».