С релизом 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)


  1. KivApple
    26.07.2021 22:06
    +4

    Мне очень интересно, возможно ли сейчас для Android написать надёжный, но при этом не-системный будильник?

    Как я смотрю, все текущие API планирования не гарантируют запуск задачи, а только дают некоторую вероятность, что она будет запущена.

    Нет ли какого-нибудь API будильников? Могли бы уж сделать, пусть даже с дополнительным запросом прав или обязательством показать какую-то Activity как результат работы задачи.



    1. linreal Автор
      27.07.2021 13:15
      +2

      Да, реально точный и надёжный будильник в Android сделать практически нельзя. К сожалению, даже setExactAndAllowWhileIdle() не даёт 100% гарантии — его нельзя запускать часто. А начиная с Android 12, он еще и дополнительный пермишн будет требовать.


  1. kunix
    26.07.2021 22:33
    +1

    И чем были плохи foreground services?


    1. vics001
      26.07.2021 22:56

      Подозреваю, сервис - это то, что запущено продолжительное время и зависит от user input (запись файла на диктофон), чтобы закончиться, а job - это то, что уже пользователь запустил, но оно завершится само (загрузка файла типичный пример).


    1. fleissig
      27.07.2021 09:45

      Показывает уведомление на несколько секунд, что отвлекает


    1. linreal Автор
      27.07.2021 13:17
      +2

      В общем случае это всё часть политики Google по заботе о батарейке устройства. Ну и уведомление в статус-баре действительно многих раздражало.


  1. VDG
    27.07.2021 07:26

    у джобы будет около минуты на выполнение своих функций
    У меня в foreground services создаётся окно (по типу плавающей кнопки), которое висит «вечно», пока сам не остановлю сервис. Теперь всем таким приложениям хана?


    1. MaxDgd
      27.07.2021 09:45
      +1

      Сам ForegroundService никто не запрещает, пока что. Запрещают запускать его из бекграунда


      1. deinlandel
        27.07.2021 10:58
        +1

        Да, поэтому хана всем приложениям, которым надо запускаться надолго по расписанию, например. Типа фитнес-трекеров.


        1. asso
          28.07.2021 08:56
          +2

          Вот почему мне, разработчику фитнес-трекера, после каждого обновления Андроида очень хочется нецензурно ругаться.


          1. LynXzp
            29.07.2021 13:45
            +1

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


            1. EviGL
              29.07.2021 22:17

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

              То, которое надо чтобы работало, чаще всего достаточно добавить в исключения энергосбережения. Если приложение вообще умеет работать в фоне и рассчитано на это. Ну и с Android 12 не знаю что случится, не пробовал.


              1. LynXzp
                29.07.2021 22:25

                Возможно. Все не пробовал. Спасибо, посмотрю что доступно.

                Добавить в исключения энергосбережения недостаточно. Даже взял телефон специально с 6Gb оперативки. Как не посмотрю свободно больше половины, но приложения все равно иногда выгружаются. Раз в неделю где-то, но это будильник. LineageOS. Есть отдельный телефон которым не пользуюсь Unihertz Pro, можно было бы туда поставить, но нет, он выгружает просто все и всегда, правда понятно почему так сделали для этого телефона. Но зачем тогда эта настройка исключений — не понятно.


          1. 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 the REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND permission or the REQUEST_COMPANION_RUN_IN_BACKGROUND permission. Whenever possible, use REQUEST_COMPANION_START_FOREGROUND_SERVICES_FROM_BACKGROUND.


  1. mmmisha
    02.08.2021 20:29

    Для приложения, которое пишет трек передвижения юзера в фоне, WorkManager не подходит, тут нужен именно сервис, в котором разворачивается LocationManager и обрабатываются обновления локации. И вот у меня есть кейс, когда нужно запустить на смартфоне запись трека командой с приложения-компаньена с Wear OS девайса. Получается теперь Foreground Service не получится запустить, если приложение на смартфоне в бекграунде?


    1. asso
      03.08.2021 14:14

      Точно такая же ситуация. Пожалуйста, дайте знать если найдете ответ.


      1. mmmisha
        03.09.2021 18:52

        Хорошо. И вы тоже дайте знать, если первым найдете).


  1. XO490
    08.08.2021 13:25
    +1

    Увлекательно. Благодарю.