Android SDK: укрощение диспетчера батареи
Android печально известен агрессивным уничтожением фоновых служб. Китайские OEM-производители (такие как Xiaomi, Huawei) даже имеют специальные менеджеры питания, которые полностью нарушают стандартные ограничения фонового выполнения Android.
На этой странице объясняется, почему Android убивает ваше приложение и как конкретные конфигурации Android Tracelet решают эту проблему в реальном мире.
Дополнительная зависимость: высокоточное определение местоположения GMS.
Android SDK Tracelet может использовать Местоположение сервисов Google Play — FusedLocationProviderClient
(лучшая точность и расход заряда батареи), аппаратное распознавание активности (ходьба/вождение/неподвижность),
и аппаратное обеспечение геофенсинга. Чтобы сделать SDK легким и удобным для использования на устройствах без Google, это
зависимость не входит в комплект. Вы добавляете его только в том случае, если вам нужен путь высокой точности:
// android/app/build.gradle.kts
dependencies {
implementation("com.google.android.gms:play-services-location:21.3.0")
}Это необязательно, и SDK постепенно ухудшается. Без play-services-location Tracelet
возвращается к стандарту AOSP LocationManager (обычный GPS). Отслеживание по-прежнему работает — вам просто
потеряете преимущества точности/аккумулятора, распознавание активности оборудования и аппаратные преимущества плавленого поставщика.
геозоны. Рекомендуется добавить его, если вы специально не ориентируетесь на устройства без Google или AOSP.
(например, Huawei без GMS или ПЗУ без Google).
Минимальная версия: 21.2.0. Код Tracelet для Android вызывает
API FusedLocationProviderClient и ActivityRecognitionClient, которые только стали
интерфейсы в play-services-location 21.2.0. Более старые версии (например, 19.0.0) поставляются с ними.
как конкретные классы, поэтому вызов их выбрасывает
java.lang.IncompatibleClassChangeError во время выполнения. Tracelet публикует Gradle
зависимость ограничение, которое автоматически переводит play-services-location в 21.2.0+, если
другая зависимость использует более старую версию, но если вы закрепите версию самостоятельно, сохраните ее по адресу
21.2.0 или новее (мы рекомендуем 21.3.0).
С play-services-location | Без него (резервный вариант AOSP) |
|---|---|
| Расположение предохранителя (лучшая точность + батарея) | Необработанный GPS/сеть через LocationManager |
Распознавание активности оборудования (onActivityChange) | Обнаружение движения только с помощью акселерометра |
| Аппаратное геозонирование | Программное обеспечение (в SDK) для оценки геозон |
Необязательная зависимость: Play Integrity (аттестация устройства).
Если вы используете функцию аттестации устройства Tracelet (AttestationConfig), Android
сторона генерирует свой криптографический токен через Целостность Google Play. Точно так же, как
play-services-location, эта зависимость не входит в комплект — вы добавляете ее только тогда, когда
включить аттестацию:
// android/app/build.gradle.kts
dependencies {
implementation("com.google.android.play:integrity:1.6.0")
}Это необязательно, и аттестация постепенно ухудшается. Без целостности игры.
зависимости, все остальное в Tracelet работает нормально — затрагивается только аттестация:
AttestationConfig(enabled: true) регистрирует предупреждение, а Tracelet.getAttestationToken()
вместо токена возвращает null вместо сбоя. Добавляйте зависимость, только если вы
включите аттестацию и вам нужен вердикт Play Integrity на Android. См.
Руководство по аттестации устройств
для полного рабочего процесса и проверки на стороне сервера.
Это должно быть implementation, а не compileOnly. Честность игры внутри compileOnly.
Tracelet SDK (поэтому он никогда не поставляется и не применяется в приложениях, не использующих аттестацию). Если
вы включаете аттестацию без добавления зависимости в модуль app, как
implementation, классы отсутствуют во время выполнения, и аттестация автоматически возвращает null.
Разрешения и настройка манифеста
Tracelet автоматически вводит необходимые разрешения в ваш AndroidManifest.xml при компиляции приложения. По умолчанию он запрашивает:
ACCESS_COARSE_LOCATIONиACCESS_FINE_LOCATION(необходимы для отслеживания)ACCESS_BACKGROUND_LOCATION(требуется для отслеживания закрытия приложения)FOREGROUND_SERVICEиFOREGROUND_SERVICE_LOCATION(требуется для непрерывного фонового выполнения)ACTIVITY_RECOGNITION(требуется для интеллектуального механизма обнаружения движения)POST_NOTIFICATIONS(требуется для пользовательского интерфейса службы переднего плана Android 13+)SCHEDULE_EXACT_ALARM(требуется для точного отслеживания в периодическом режиме)
Удаление неиспользуемых разрешений (слияние манифеста)
Если вашему приложению не нужна определенная функция (например, вашему клиенту не требуется обнаружение движения или вы не используете периодический режим), вы можете принудительно удалить разрешение с помощью директивы tools:node="remove" слияния манифеста Android в вашем уровне приложения AndroidManifest.xml (android/app/src/main/AndroidManifest.xml).
Собственный код Tracelet полностью защищен checkSelfPermission(). Отсутствие разрешений приведет к плавному откату, а не к сбою.
| Разрешение | Эффект при удалении (tools:node="remove") |
|---|---|
| НОТРАНС0ЛАТЕ | Безопасно. Возврат к базовому обнаружению движения только с помощью акселерометра. Поток onActivityChange не сработает (нет классификации ходьбы/бега/вождения). |
| НОТРАНС0ЛАТЕ | Безопасно. Android 10+ ограничивает отслеживание, когда приложение полностью переведено в фоновый режим или удалено. |
| НОТРАНС0ЛАТЕ | Безопасно. В периодическом режиме используются экономичные, но неточные таймеры WorkManager. |
| НОТРАНС0ЛАТЕ | Безопасно. Постоянное уведомление службы переднего плана скрыто на Android 13+. Служба все еще работает. |
| НОТРАНС0ЛАТЕ | Безопасно. Вы не сможете запросить освобождение от оптимизации батареи из ОС. |
FOREGROUND_SERVICE / FOREGROUND_SERVICE_LOCATION | Безопасно для приложений, использующих только геозоны или только периодические действия. Их удаление отключает непрерывное отслеживание переднего плана (start()), но стандартное геозонирование и периодический режим продолжают работать. Требуется для непрерывного фонового отслеживания. |
ACCESS_FINE_LOCATION / COARSE | НЕБЕЗОПАСНО. Без разрешения на определение местоположения Tracelet не сможет получить координаты. |
Правила Google Play в отношении приоритетных служб (вступают в силу 28 октября 2026 г.).
больше не является разрешенным вариантом использования для приоритетного местоположения службы. Если ваше приложение использует
службу переднего плана исключительно для геозон, необходимо удалить
FOREGROUND_SERVICE_LOCATION (и FOREGROUND_SERVICE) из объединенных
манифест. Стандартный режим геозоны Tracelet использует собственный API геозон и не
не запускать службу переднего плана, поэтому приложения, работающие только с геозонами, соответствуют требованиям — просто
удалите разрешения ниже и сохраните foregroundService.enabled = false. См.
Руководство по геозонированию
для получения подробной информации.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Geofence-only / periodic-only app: no continuous foreground tracking -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" tools:node="remove" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" tools:node="remove" />
</manifest>Пример: удаление разрешений на движение и фон
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Client doesn't want Activity/Motion tracking -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" tools:node="remove" />
<!-- Client only wants foreground tracking -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" tools:node="remove" />
<!-- Remove Exact Alarms -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" tools:node="remove" />
</manifest>Автоматические правила ProGuard/R8
Tracelet активно использует фоновые сервисы Kotlin, привязки JNA для ядра Rust и отражающие каналы Pigeon. Если они запутаны или неправильно сжаты, фоновое отслеживание автоматически завершится неудачей, когда ваше приложение будет скомпилировано для выпуска.
Вам не нужно вручную настраивать ProGuard для Tracelet.
Плагин tracelet_android автоматически поставляется со встроенным consumer-rules.pro. Когда вы создаете приложение Flutter в режиме выпуска, устройство сжатия Android R8 автоматически извлекает эти правила и гарантирует, что все необходимые классы (такие как привязки HeadlessTaskService, BootReceiver и Rust uniffi) выживут при сжатии.
Сценарий 1: Драйвер доставки (непрерывное отслеживание)
Изученные понятия: Режим ожидания, службы переднего плана, Wakelocks.
Проблема
Ваш пользователь — водитель по доставке пиццы. Они положили телефон в карман, выключив экран. Через 15 минут Android перейдет в Режим сна. Он отключает доступ к сети, откладывает фоновые задания и строго ограничивает пробуждение процессора для экономии заряда батареи. Если ваше приложение использует простой таймер для получения GPS, Android просто откажется запускать ваш таймер.
Как Tracelet решает эту проблему: служба переднего плана
Служба переднего плана сообщает Android: * «Эй, я сейчас делаю что-то невероятно важное, и пользователь полностью об этом знает. Не убивайте меня». *
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
enabled: true,
channelName: 'Delivery Tracking',
notificationText: 'Tracking your route to the customer',
notificationOngoing: true, // User cannot swipe it away
showNotificationOnPauseOnly: false, // Always visible — see the section below
actions: ['Pause', 'Complete'], // Adds interactive buttons to the notification
),
)Отображая постоянное уведомление в строке состояния, Tracelet повышает приоритет вашего приложения почти до уровня пользовательского интерфейса переднего плана. Android позволит Tracelet работать бесконечно, минуя ограничения режима ожидания.
Настройка внешнего вида уведомлений
Чтобы постоянное уведомление соответствовало фирменному оформлению вашего приложения, вы можете настроить значки и цвета:
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
enabled: true,
notificationTitle: 'Delivery Mode',
notificationText: 'Tracking your route...',
notificationColor: '#0F9D58', // Your brand hex color
notificationSmallIcon: 'ic_tracelet_icon', // The icon name
notificationLargeIcon: 'ic_large_logo',
),
)Важные правила использования значков:
- Расположение файла: Файл изображения должен быть помещен в доступную для рисования папку вашего проекта Android (
android/app/src/main/res/drawable/ic_tracelet_icon.png). - Ограничения на мелкие значки. Android требует, чтобы
notificationSmallIconбыл абсолютно плоским, прозрачным и только белым. Если вы используете цветной логотип, Android просто отобразит его в виде сплошного серого или белого квадрата. - Свойство Color: Свойство
notificationColorбудет тонировать фон вашего маленького значка в соответствии с вашим брендом.
Android 13+ Примечание. Перед запуском службы необходимо запросить разрешение POST_NOTIFICATIONS, в противном случае уведомление будет автоматически подавляться операционной системой. См. страницу Flutter SDK: Разрешения, чтобы узнать, как именно запросить это из вашего кода Dart.
Обновление уведомления во время отслеживания (updateNotification())
Необходимо изменить уведомление после начала отслеживания — например. обновить заголовок с «В пути» на «Прибытие», поменять текст местами или изменить кнопки действий в середине поездки? Вызов setConfig() с новым ForegroundServiceConfig сохраняет значения, но изменение только для уведомлений не повторно публикует уведомление, которое Android уже показывает — новый контент появится только после несвязанного перезапуска службы или перехода на передний план.
Начиная с v3.6.8, Tracelet.updateNotification() обновляет живое уведомление на месте без перезапуска конвейера отслеживания:
// 1. Apply the new notification content.
await Tracelet.setConfig(
const tl.Config(
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
notificationTitle: 'Arriving',
notificationText: 'Your driver is 2 minutes away',
),
),
),
);
// 2. Repost the active notification with the new content.
await Tracelet.updateNotification();Безопасно по задумке. updateNotification() никогда не перезапускает отслеживание и не выполняет никаких операций, если служба переднего плана в данный момент не запущена (нечего обновлять). В iOS вместо этого обновляется Live Activity (если вы выбрали его через liveActivityConfig); в Интернете это неактивно, поэтому один и тот же вызов безопасен на любой платформе.
Тихое обновление уведомления (notificationOnlyAlertOnce)
В приведенном выше шаблоне есть подвох. Android рассматривает каждый репост как новое оповещение, если вы не укажете иное, поэтому уведомление, которое вы обновляете раз в минуту, воспроизводит звук уведомления раз в минуту, пока работает отслеживание.
Канал Tracelet создан с отключенной вибрацией и подсветкой, так что это звук, а не жужжание — но на notificationPriority по умолчанию он слышен, и пользователи это быстро замечают.
Установите notificationOnlyAlertOnce, чтобы только первые оповещения о публикации и каждое последующее обновление выполнялись автоматически:
await Tracelet.setConfig(
const tl.Config(
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
notificationOnlyAlertOnce: true,
),
),
),
);Это стоит настроить для любого уведомления, содержимое которого меняется во время отслеживания — количество оставшихся остановок, ожидающие загрузки, текущая геозона, точность в реальном времени — а не только для таймера ниже.
Почему бы просто не понизить приоритет? Изменение значения notificationPriority на low или min также приводит к отключению канала, но при этом отключается и первое сообщение, а Android фиксирует важность канала при его создании. Приложение, уже установленное с каналом важности по умолчанию, не может понизить его позже, не изменив при этом channelId, что приведет к потере старого канала в системных настройках. notificationOnlyAlertOnce не имеет никаких проблем и работает с любым приоритетом.
По умолчанию используется false, что сохраняет поведение каждого существующего приложения. Ничего не изменится, пока вы не согласитесь.
Скрытие уведомления при открытом приложении (showNotificationOnPauseOnly)
showNotificationOnPauseOnly: true удаляет уведомление об отслеживании, пока пользователь просматривает ваше приложение, и возвращает его, когда он уходит. Это действительно более приятный опыт — панель уведомлений остается чистой, пока приложение отображается на экране, — но у него есть одно жесткое ограничение, и это ограничение неочевидно.
Скрытие уведомления означает понижение статуса службы. В Android не существует понятия службы переднего плана без уведомления, поэтому единственный способ скрыть ее — stopForeground(). Отслеживание продолжается, и служба продолжает работать, но пока уведомление скрыто, ваш процесс не содержит служб переднего плана.
Это именно то состояние, которое Android уничтожает, когда пользователь удаляет ваше приложение из списка последних.
showNotificationOnPauseOnly игнорируется, пока stopOnTerminate: false (#378 ). Две настройки запрашивают несовместимые вещи, и обещание выживания побеждает: уведомление остается видимым.
Почему его нельзя заставить работать
Когда задача удаляется, ActivityManager решает, какие процессы уничтожить из proc.foregroundServices — и решает до того, как onTaskRemoved будет отправлен в основной поток вашего приложения. Таким образом, SDK не может спасти ситуацию, разместив уведомление во время удаления; к тому времени вердикт будет вынесен. У Tracelet было именно такое смягчение, и оно не помогло.
Результатом стала гонка, которую никто не мог увидеть. Смахните приложение в промежутке между его уходом с экрана и возвращением уведомления, и процесс остановился: ни одной задачи без заголовка, ни событий, ни журналов, ничего из того, что может гарантировать stopOnTerminate: false. Проведите через секунду, и все заработало идеально. Задержка составила 285 мс на Pixel Fold и 700–1500 мс на устройстве Android 15.
Что вместо этого делает Tracelet
| Ваша конфигурация | Поведение |
|---|---|
stopOnTerminate: false + showNotificationOnPauseOnly: true | Флаг игнорируется. Уведомление остается видимым, служба никогда не понижается в должности, а пролистывание из раздела «Недавние» никогда не может остановить процесс. |
stopOnTerminate: true + showNotificationOnPauseOnly: true | Работает точно так, как описано — скрыто, пока приложение отображается на экране. Здесь ничего не обещано, кроме смахивания, поэтому защищать нечего. |
| НОТРАНС0ЛАТЕ | Уведомление всегда видно. Это значение по умолчанию. |
Переопределение не бесшумное — это половина того, что сделало первоначальную ошибку дорогой. Одна строка записывается в постоянно включенный канал журнала жизненного цикла для каждого start() и доступна для чтения с помощью Tracelet.getLogs() на любом уровне журнала, в том числе при запуске в состоянии уничтожения:
notification: showNotificationOnPauseOnly ignored because stopOnTerminate=false — hiding the
notification demotes the foreground service, and a swipe from recents in that window kills the
process (#378). Set stopOnTerminate=true to hide it while the app is open, or
showNotificationOnPauseOnly=false to stop asking.getForegroundServiceHealth() сообщает то же самое с другой стороны. Когда уведомление законно скрыто (stopOnTerminate: true), serviceForeground — это false, а lastForegroundPromotionResult — это suppressed — понижение в должности, о котором вы просили, что отличается от повышения, в котором ОС отказалась.
Выбор
Решите, какой из них вам действительно нужен:
- Отслеживание должно продолжаться после того, как пользователь смахнет приложение → оставьте
stopOnTerminate: falseи позвольте уведомлению отображаться. Это правильный вариант по умолчанию для приложений доставки, автопарка, рабочей силы и безопасности. - Чистая панель уведомлений важнее, чем выживание при пролистывании → установите
stopOnTerminate: true. Отслеживание заканчивается, когда приложение удаляется из списка недавних, что для многих потребительских приложений в любом случае является именно тем, чего ожидает пользователь.
Обратите внимание, что то, что вы получаете во втором случае, ограничено: на Android 14+ служба приоритетного плана определения местоположения должна показывать свое уведомление, когда ваше приложение находится за пределами экрана, независимо от того, поэтому скрытие применяется только тогда, когда пользователь находится в вашем приложении.
Это только для Android. В iOS нет службы переднего плана, нет уведомлений, которые нужно скрыть, и нет такого рода уничтожения задач — showNotificationOnPauseOnly живет на AndroidConfig, и iOS SDK не читает его.
Отображение истекшего таймера в уведомлении
Для приложений, созданных на основе ограниченного сеанса — доставки, смены, задания, поездки — в уведомлении могут отображаться текущие часы, отсчитывающие от выбранного вами момента:
final shiftStartedAt = DateTime.now();
await Tracelet.setConfig(
tl.Config(
android: tl.AndroidConfig(
foregroundService: tl.ForegroundServiceConfig(
notificationTitle: 'Shift in progress',
notificationStartedAt: shiftStartedAt.millisecondsSinceEpoch,
notificationShowTimer: true,
notificationOnlyAlertOnce: true,
),
),
),
);
await Tracelet.updateNotification();Android отображает часы самостоятельно. Вы вызываете это один раз; ОС выбирает отображение со вторым разрешением на протяжении всего срока действия уведомления. Ваше приложение никогда не просыпается, чтобы перерисовать его, а повторная публикация уведомления по несвязанной причине (новый текст, изменение конфигурации) не перезапускает счетчик.
Вы предоставляете notificationStartedAt, а не Tracelet, берущий его из start(), поскольку период, о котором заботится пользователь, часто начинается до того, как началось отслеживание, или сохраняется после перезапуска отслеживания. Чтобы отобразить «с момента начала отслеживания», пропустите момент вызова start().
Семантика, которую стоит знать.
notificationShowTimer: false(по умолчанию) отображает именно сегодняшнее уведомление.notificationShowTimer: trueбезnotificationStartedAtпоказывает отсутствие таймера и регистрирует причину. Замена «сейчас» приведет к перезапуску часов при каждом репосте и искажению данных о сеансе.notificationStartedAtв будущем привязывается к текущему времени для каждого сообщения, оставляя сохраненное значение нетронутым, поэтому коррекция часов устройства восстанавливается при следующем репосте.- Таймер только считает. Чтобы прекратить его отображение, установите
notificationShowTimer: false— момент начала сохраняется, но неактивен.
Вы можете визуализировать прошедшее время самостоятельно, переписав notificationText для таймера, а для статического подсчета это подходящий инструмент. Причина, по которой я предпочитаю это, — это стоимость и разрешение: поминутная перезапись — это полный переход к нативному коду плюс репост — примерно 480 из них за восьмичасовую смену — и показывать тикающие секунды таким образом совершенно непрактично. Хронометр ОС – один звонок.
Аналогом iOS является таймер Live Activity, который принимает те же два поля, что и liveActivityConfig.
Сценарий 2. Приложение «Погода» (периодическое отслеживание)
Изученные концепции: WorkManager и Exact Alarms
Проблема
Ваш пользователь загрузил местное приложение о погоде. Вы хотите просыпаться в фоновом режиме каждые 4 часа, узнавать их местоположение и загружать местный прогноз.
Вам не нужно постоянное уведомление службы переднего плана. Пользователю не хотелось бы видеть постоянное уведомление «Отслеживание погоды» в своем ящике.
Как Tracelet решает эту проблему: периодический режим
Вместо непрерывного обслуживания Tracelet может использовать планировщики заданий Android, чтобы ненадолго проснуться, получить исправление и снова заснуть.
android: tl.AndroidConfig(
periodicUseForegroundService: false,
periodicUseExactAlarms: true, // Uses AlarmManager instead of WorkManager
)Два планировщика
-
WorkManager (
periodicUseExactAlarms: false) Значение по умолчанию. Это очень экономно расходует аккумулятор. Однако Android решает когда его запустить. Если вы попросите его запускаться каждые 15 минут, Android может подождать 45 минут и запустить его, когда пользователь разблокирует свой телефон для проверки Instagram (так называемая «пакетная обработка»). Это весьма неточно. -
Менеджер Алармов (
periodicUseExactAlarms: true) Если вам абсолютно НЕОБХОДИМО разбудить устройство точно в определенное время, используйте это. Для этого вам необходимо объявить разрешениеSCHEDULE_EXACT_ALARMв вашемandroid/app/src/main/AndroidManifest.xml:<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />Примечание. Google Play строго проверяет это разрешение. Используйте его только в том случае, если ваше приложение представляет собой будильник, календарь или абсолютно точно требует точного времени.
Сценарий 3: Агрессия OEM
Изученные концепции: API-интерфейс работоспособности настроек.
Проблема
Вы все сделали правильно. У вас есть служба переднего плана. Но у пользователя есть телефон Xiaomi. В MIUI есть фирменная функция «Экономия заряда», которая принудительно отключает даже службы переднего плана после 5 минут отключения экрана.
Как Tracelet решает эту проблему: автоматическое смягчение последствий и подсказки
Tracelet автоматически применяет внутренние средства защиты, где это возможно, но иногда пользователю физически приходится внести ваше приложение в белый список в собственном меню настроек OEM.
final health = await tl.Tracelet.getSettingsHealth();
if (health['isAggressiveOem'] == true) {
// Automatically opens the manufacturer-specific settings screen
// (e.g. Xiaomi Autostart, Huawei App Launch, Samsung Sleeping Apps)
await tl.Tracelet.showPowerManager();
}Подробный пример того, как реализовать это в пользовательском интерфейсе Flutter, см. на странице Инструменты диагностики и управление питанием.