С релизом Android 12 приложения, где новая версия операционки будет указана в targetSdkVersion, получат запрет на запуск foreground-сервисов в бэкграунде. В качестве альтернативы Google предлагает WorkManager, который с появлением expedited jobs станет предпочтительным вариантом для запуска высокоприоритетных фоновых задач.
О нём и пойдёт речь в статье — под катом обсудим новые возможности инструмента, подключим его к приложению и реализуем миграцию с foreground-сервиса.
WorkManager и foreground service
Для справки:
Foreground service — это какой-либо сервис, о котором знает пользователь через нотификацию в статус-баре. Например, воспроизведение музыки или работа GPS в картах.
WorkManager — это API для планирования задач, которые будут выполняться, даже если выйти из приложения или перезагрузить устройство.
WorkManager уже давно является приоритетным способом выполнения длительных фоновых задач. К таким относятся синхронизация данных с бэкендом, отправка аналитики, периодическая проверка свободного места в системе с помощью PeriodicWork и так далее.
Но в WorkManager присутствовал и недостаток — не было никаких гарантий, что джоба начнётся незамедлительно после создания. В версии 2.3.0 разработчики добавили для воркеров метод setForegroundAsync(), который, по сути, превращал фоновую задачу в foreground-сервис и позволял немедленно приступить к её выполнению.
Такой подход ничем особо не отличался от разработки foreground-сервиса вручную, когда необходимо создавать объекты Notification и NotificationChannel при таргете выше, чем на Android Nougat.
private fun createInfo(): ForegroundInfo {
return ForegroundInfo(getNotificationId(), createNotification())
}
Сейчас setForegroundAsync() объявлен устаревшим, а при попытке запустить сервис из бэкграунда на выходе будет ForegroundServiceStartNotAllowedException.
И тут на сцену выходят expedited jobs.
Expedited jobs
Этот тип джобов позволяет приложениям выполнять короткие и важные задачи, давая системе больше контроля над доступом к ресурсам. Он находится где-то между foreground-сервисами и привычными джобами WorkManager. От последних их отличает:
минимально отложенное время запуска;
обход ограничений Doze-mode на использование сети;
меньшая вероятность быть «убитыми» системой.
А ещё в них не поддерживаются ограничения по заряду батареи и режиму работы девайса:
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.NOT_ROAMING)
.setRequiresStorageNotLow(true)
/*
Неподдерживаемые ограничения
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setRequiresBatteryNotLow(false)
*/
.build()
У expedited job больший приоритет на ускоренный запуск, поэтому операционная система строже регулирует их количество. Например, если попытаться запланировать джобу при исчерпаном лимите, то сразу вернётся JobScheduler#RESULT_FAILURE.
Если же ограничения по квоте, сети и памяти устройства выполнены, то у джобы будет около минуты на выполнение своих функций. Иногда больше, но это сильно зависит от лимита и общей загруженности системы.
Миграция foreground service на expedited job
Стандартный сервис для выполнения фоновых задач обычно выглядит примерно так:
class ExampleService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification: Notification = createNotification()
startForeground(UPLOAD_ID, notification)
runHeavyJob()
return START_NOT_STICKY
}
private fun createNotification(): Notification {
val pendingIntent: PendingIntent =
Intent(this, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
}
val nb = NotificationCompat.Builder(this, createNotificationChannel())
return buildNotification(nb)
}
private fun runHeavyJob() {
//some great stuff
}
}
А запускается так:
private fun startHeavyTask() {
Intent(this, ExampleService::class.java).also { intent ->
startService(intent)
}
}
Поговорим о том, как перевести этот сервис на expedited job. Происходит это буквально в три простых шага.
1. Подключаем WorkManager к проекту:
implementation 'androidx.work:work-runtime:2.7.0-alpha04'
2. Создаём класс, наследующийся от Worker (он будет выполнять задачу, которую раньше делал сервис):
class ExampleWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
runHeavyTask()
return Result.success()
}
@SuppressLint("CheckResult")
private fun runHeavyTask() {
//some great stuff
}
}
3. Создаём WorkRequest и передаём его для запуска в WorkManager:
fun runHeavyWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresStorageNotLow(true)
.build()
val heavyWorkRequest: WorkRequest =
OneTimeWorkRequest.Builder(ExampleWorker::class.java)
.setConstraints(constraints) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager
.getInstance(context)
.enqueue(heavyWorkRequest)
}
Здесь есть важный параметр OutOfQuotaPolicy, который отвечает за поведение при невозможности запустить джобу немедленно. Он существует в двух вариантах:
RUN_AS_NON_EXPEDITED_WORK_REQUEST — при заполненной квоте запустится обычная джоба, не expedited.
DROP_WORK_REQUEST — при заполненной квоте запрос на выполнение сразу зафейлится.
На этом, собственно, базовая миграция заканчивается.
Вместо заключения
Переехать на expedited job довольно легко, особенно, если в проекте уже подключен WorkManager.
Сейчас пропала необходимость держать нотификацию в статус-баре, а в условиях выполнения задачи появилась дополнительная гибкость благодаря возможностям WorkManager. Например, теперь можно пережить «смерть» процесса, тонко настраивать ретраи, периодичность выполнения задач и многое другое.
Комментарии (19)
kunix
26.07.2021 22:33+1И чем были плохи foreground services?
vics001
26.07.2021 22:56Подозреваю, сервис - это то, что запущено продолжительное время и зависит от user input (запись файла на диктофон), чтобы закончиться, а job - это то, что уже пользователь запустил, но оно завершится само (загрузка файла типичный пример).
linreal Автор
27.07.2021 13:17+2В общем случае это всё часть политики Google по заботе о батарейке устройства. Ну и уведомление в статус-баре действительно многих раздражало.
VDG
27.07.2021 07:26у джобы будет около минуты на выполнение своих функций
У меня в foreground services создаётся окно (по типу плавающей кнопки), которое висит «вечно», пока сам не остановлю сервис. Теперь всем таким приложениям хана?MaxDgd
27.07.2021 09:45+1Сам ForegroundService никто не запрещает, пока что. Запрещают запускать его из бекграунда
deinlandel
27.07.2021 10:58+1Да, поэтому хана всем приложениям, которым надо запускаться надолго по расписанию, например. Типа фитнес-трекеров.
asso
28.07.2021 08:56+2Вот почему мне, разработчику фитнес-трекера, после каждого обновления Андроида очень хочется нецензурно ругаться.
LynXzp
29.07.2021 13:45+1Мне как пользователю, все нравится так же: одни приложения нельзя заставить надежно работать в фоне, а другие нельзя заставить надежно не работать в фоне пока я их не запущу. Лучей щастья гуглу. Причем никто и не думает решать это альтернативной прошивкой, они в большинстве случаев только UI меняют и список неудаляемых программ.
EviGL
29.07.2021 22:17Хм, много раз слышал, что в сторонних прошивках регулируется автозапуск приложения, даже видел это в оболочках мелких производителей. Если запретить автозапуск и "смахнуть" приложение, оно не будет работать в фоне.
То, которое надо чтобы работало, чаще всего достаточно добавить в исключения энергосбережения. Если приложение вообще умеет работать в фоне и рассчитано на это. Ну и с Android 12 не знаю что случится, не пробовал.
LynXzp
29.07.2021 22:25Возможно. Все не пробовал. Спасибо, посмотрю что доступно.
Добавить в исключения энергосбережения недостаточно. Даже взял телефон специально с 6Gb оперативки. Как не посмотрю свободно больше половины, но приложения все равно иногда выгружаются. Раз в неделю где-то, но это будильник. LineageOS. Есть отдельный телефон которым не пользуюсь Unihertz Pro, можно было бы туда поставить, но нет, он выгружает просто все и всегда, правда понятно почему так сделали для этого телефона. Но зачем тогда эта настройка исключений — не понятно.
OlegReksha
10.08.2021 15:21Так вроде же в доке написано что можно:
https://developer.android.com/about/versions/12/foreground-services#cases-fgs-background-starts-allowed
Your app uses the Companion Device Manager and declares theREQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND
permission or theREQUEST_COMPANION_RUN_IN_BACKGROUND
permission. Whenever possible, useREQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND
.
mmmisha
02.08.2021 20:29Для приложения, которое пишет трек передвижения юзера в фоне, WorkManager не подходит, тут нужен именно сервис, в котором разворачивается LocationManager и обрабатываются обновления локации. И вот у меня есть кейс, когда нужно запустить на смартфоне запись трека командой с приложения-компаньена с Wear OS девайса. Получается теперь Foreground Service не получится запустить, если приложение на смартфоне в бекграунде?
KivApple
Мне очень интересно, возможно ли сейчас для Android написать надёжный, но при этом не-системный будильник?
Как я смотрю, все текущие API планирования не гарантируют запуск задачи, а только дают некоторую вероятность, что она будет запущена.
Нет ли какого-нибудь API будильников? Могли бы уж сделать, пусть даже с дополнительным запросом прав или обязательством показать какую-то Activity как результат работы задачи.
IL_Agent
https://developer.android.com/reference/android/app/AlarmManager#setExactAndAllowWhileIdle(int,%20long,%20android.app.PendingIntent)
linreal Автор
Да, реально точный и надёжный будильник в Android сделать практически нельзя. К сожалению, даже setExactAndAllowWhileIdle() не даёт 100% гарантии — его нельзя запускать часто. А начиная с Android 12, он еще и дополнительный пермишн будет требовать.