Современные Android-смартфоны уже давно стали мощными устройствами, способными выполнять множество операций. Вся наша коммуникация стала асинхронной, мы передаем и получаем множество данных. Для этого всего важно гарантировать выполнение работы в фоне, т. е. когда приложение свёрнуто.

В этой статье я разберу актуальные API для выполнения различного рода задач в фоне. Тут очень важно слово “актуальные”. На момент выхода этой статьи уже вышел Android 14, поэтому список API и подходы будут актуальны. Да-да, как вы могли узнать из моей статьи про “Ограничения фоновой работы”, уже на протяжении многих лет мы получаем новые ограничения и через пару лет обязательно появятся еще новее, как и новые API. Рекомендую прочитать статью, указанную выше, чтобы лучше понять API, про которые я расскажу в этой статье.

Все, о чем я буду рассказывать в этой статье, касается современного Android 14 и нескольких последних версий. На старых версиях Android API, о которых я расскажу, этого может не быть, либо оно будет работать иначе. Чем старше версия Android, тем и ограничений на запуск работы в фоне тоже будет меньше, поэтому вы сможете использовать альтернативные решения и подходы. Я за следование правилам из новых Android на всех поддерживаемых вашим приложением версиях Android, чтобы добиваться унификации поведения. Но это только мой взгляд, и вы вольны делать то, что вам хочется.

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

Если вам интересно следить за самыми последними новостями Android разработки и получать подборку интересных статей по этой тематике, тогда вам стоит подписаться на Телеграм-канал Android Broadcast и мой YouTube канал "Android Broadcast".

Классификация фоновой работы

Чтобы правильно выбрать API для выполнения работы в фоне, важно классифицировать вашу задачу. Я вывел несколько критериев, на основе которых делаю выбор:

  • длительность выполнения (короткая или долгая);

  • нужно ли пользователю знать о задаче или управлять ее выполнением;

  • важность выполнения задачи как можно быстрее.

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

Я собрал все вопросы, на основе которых я делаю выбор, и свел их в диаграмму принятия решения, которую вы можете увидеть внизу. Далее я сделаю краткий обзор каждого из API и когда его стоит делать.

 Алгоритм выбора API для запуска задачи в фоне
Алгоритм выбора API для запуска задачи в фоне

Специальные API. Download Manager

Для начала быстро пробежимся по специальным API и начнем с самого простого и, как мне кажется, непопулярного – DownloadManager. Это системный сервис, который обрабатывает загрузку файлов с публичных HTTP/HTTPS адресов. Сервис возьмет на себя всё взаимодействие по HTTP и повтор загрузки после ошибки (или когда изменилось соединение). Также во время загрузки можно показать уведомление с прогрессом, чтобы пользователь видел прогресс загрузки и мог им управлять. А можно сделать загрузку без этого, но я вам так не рекомендую делать. Пусть всё будет прозрачно и понятно!

При создании запроса на загрузку вы можете добавить заголовки к HTTP запросу, указать условия, при которых должна происходить загрузка, а также место на внешнем хранилище, где сохранить файл.

val downloadManager: DownloadManager = context.getSystemService()
    
val request = DownloadManager.Request(uri)
    // Указываем условия для запуска
    .setAllowedOverMetered(true)
    .setAllowedOverRoaming(false)
    .setRequiresCharging(false)
    // Указываем, куда сохранить файл
    .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, "file_download")
    // Настраиваем уведомление
    .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
    .setTitle("Важная загрузка")
    .setDescription("Загрузка файлов для работы приложения")

val downloadId = downloadManager.enqueue(request)
val downloadManager: DownloadManager = checkNotNull(context.getSystemService<DownloadManager>())

val downloadQuery = DownloadManager.Query()
    .setFilterById(downloadId)

// Получаем ответ в виде Cursor
val download: Cursor = downloadManager.query(downloadQuery)

if (download.moveToFirst()) {
    val columnIndex = download.getColumnIndex(DownloadManager.COLUMN_STATUS)
    if (columnIndex >= 0) {
        val status = download.getInt(columnIndex)
    }
}

Download Manager не подойдет для сложных случаев, когда вам надо делать настройку HTTP клиента для подключения к защищенному серверу или сохранить файл вне допустимых папок. Введение Scoped Storage в Android 11 снизило риски похищения файлов и лучше защищает директорию вашего приложения на внешнем хранилище.

Также не забывайте, что всё скачанное из интернета перед использованием нужно проверить на предмет подмены файла. Например, можете зашифровать данные или проверять контрольную сумму скачанного файла.

Я рекомендую вам использовать Download Manager для простой загрузки файлов, чтобы она надежно работала в фоне, независимо от версии Android.

Sync Adapter

Видели в системных настройках Android возможность создавать и управлять аккаунтами приложения? Помимо этого, там также можно выбрать различные данные для синхронизации. Под всем этим скрывается Sync Adapter API. Он позволяет синхронизировать данные между удаленным сервером и вашим устройством, чтобы везде всё было актуально. Например, контакты можно редактировать на телефоне, и они обновятся на сервере, а также на всех устройствах, связанным с аккаунтом, при условии, что включена синхронизация.

Sync Adapter используется крайне редко и в приложениях, которые используют общий аккаунт на несколько сервисов, таких как Google, Яндекс и др. Часть приложений заводят аккаунт только для интеграции с системой. Реализация синхронизации через Sync Adapter является нетривиальной задачей и требует несколько этапов создания аккаунта приложения через системные возможности, а не внутри приложения. Описывать весь процесс я не буду, но он включает в себя работу с AccountManager, Bound Service, ContentProvider, специальными XML по конфигурации, а также реализацию SyncAdapter.

 Пример реализации Sync Adapter и взаимодействия с частями системы
Пример реализации Sync Adapter и взаимодействия с частями системы

Это API может вам пригодиться, если на каждом привязанному к аккаунту устройстве надо хранить актуальные данные и при их изменении отправлять их на сервер. Можно также настроить периодическую синхронизацию, но минимальная её частота – 1 час, плюс возможны смещения из-за экономии энергии и других режимов оптимизации расхода батареи. Я думал, что Sync Adapter используется редко, но вот один из подписчиков рассказал мне, что использует его для синхронизации файлов с сервером в фоне. Возможно, это одно из самых надежных решений для синхронизации в фоне, так как я не нашел никаких упоминаний о развитии API или появлении серьезных изменений в его поведении. Либо их не озвучивают, либо, думаю, что API мало востребовано сторонними приложениями.

Service

Давайте двигаться к теме наиболее распространенных API, о которых вы уже слышали, а скорее всего, уже использовали. Обычный вопрос на собеседованиях – стандартные компоненты Android, и я надеюсь, что вы их знаете. Service – это компонент для работы приложения без графического интерфейса. Например, если вам надо выполнить долгую работу в фоне и не отвлекать пользователя, а дать ему посмотреть видос или посидеть в соцсетях. К сожалению, на момент выхода этой статьи использовать обычный Service, он же Background, практически невозможно, т. к. скрытая от пользователя работа приложений не приветствуется системой и убивается через пару секунд после ухода приложения из состояния Foreground. Background Service может полноценно работать только в случае, когда приложение видно пользователю. Чтобы Service остался работать после ухода приложения с экрана, надо успеть трансформировать Background Service в Foreground, на что как раз и дается время после ухода приложения в фон. Стандартных callback-ов для этого нет, так что определения ухода в фон придется писать самостоятельно. На практике мало кто заморачивается такими сложностями с запуском Background Service и отслеживанием состояния приложения, поэтому сразу используют Foreground Service, о котором мы и поговорим дальше, но прежде вспомним про еще один тип Service – Bound.

Bound Service

Bound Service – это взаимодействие по принципу “клиент-сервер”, которое позволяет компоненту приложения, например Activity, подключиться к Service и отправлять запросы через вызовы методов, а не коммуникацией с помощью Intent. Основное предназначение этого формата Service – обеспечение взаимодействия между приложениями, которые работают в разных процессах, но это необязательно. Фактически Inter Process Communication или сокращенно IPC – основная цель, с которой создавали Bound Service.

class LocalService : Service() {

    /**
     * Класс для коммуникации клиентов с Service
     */
    class LocalBinder internal constructor(private val service: LocalService) : Binder() {

        val randomNumber: Int
            get() = service.randomNumber
    }

    private val binder by lazy { LocalBinder(this) }
    private val generator by lazy { Random(System.currentTimeMillis()) }

    override fun onBind(intent: Intent?) = binder

    private val randomNumber: Int
        get() = generator.nextInt(100)
}
// В роли держателя соединения с Service может выступить Activity
class ServiceConnectionHolder(private val context: Context) {

    private var serviceBinder: LocalBinder? = null

    // Слушатель установки и разрыва соединения с Service
    private val connection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(
            className: ComponentName,
            service: IBinder
        ) {
            val binder = service as LocalBinder
            serviceBinder = binder
        }

        override fun onServiceDisconnected(name: ComponentName) {
            serviceBinder = null
        }
    }

    fun bindService() {
        val intent = Intent(context, LocalService::class.java)
        context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
        // Ожидаем binder
        val binder = serviceBinder ?: return
        val randomNumber = binder.randomNumber
    }
}

Разъяснение того, как организовать межпроцессную коммуникацию (Interprocess Communication) через Bound Service, выходит за рамки этой статьи, но обычно такой метод используют разработчики библиотек. Например, Google Play Billing.

В современных версиях Android жизнь Bound Service ограничена как у Background Service, но Bound Service привязан к жизни компонента, который выполнил binding этого Service. Bound Service используется для передачи данных между приложениями, для выполнения долгой работы он вам не подойдет, а вот Foreground Service уже может намного больше!

Foreground Service

Foreground Service отличается от обычного Service тем, что он виден пользователю через показ уведомления в системной панели и его привязке к Service. Также Foreground Service повышает гарантии, что приложение не будет убито, по сравнению с Background Service.

Foreground Service стоит использовать для долгих, либо высокоприоритетных задач, которые не могут быть отложены, а пользователю надо показать информацию о выполнении, например, прогресс, а также дать возможность управлять работой через кнопки в уведомлениях. Под такие типы задач попадает проигрывание медиа, запись разговора, процессинг данных в фоне, загрузка данных с или на сервер и др.

На момент выхода этой статьи Foreground Service имеют типы операций, для которых они могут запускаться. А в Android 14 указание типа стало обязательным, и Google Play будет контролировать действительную необходимость использовать Foreground Service для задач вашего приложения. Довольно подробно я рассказал об этом в статье про изменения в Android 14.

Для запуска Foreground Service надо использовать метод Context.startForegroundService(), который запускает Background Service, но ждет, что в течение 5 секунд вы сделаете его Foreground вызовом Service.startForeground(), указав уведомление, которое будет показываться в панели уведомлений.

// Запускаем Service и говорим системе, что он будет Foreground
context.startForegroundService(Intent(context, MediaService::class))

class MediaService : Service() {

    override fun onCreate() {
        super.onCreate()
        // Между вызовом С Context.startForegroundService() и startForeground()
        // должно быть не более 5 секунд

        // Делаем сервис Foreground
        startForeground(NOTIFICATION_ID, newOngoingNotification())
    }

    private fun newOngoingNotification() : Notification
}

Если это не сделать, будет крэш приложения. Начиная с Android 12, запуск Foreground Service из фона невозможен, за исключением специальных ролей приложения или когда это будет вызвано действием пользователя. Фактически их запуск теперь должен происходить, когда приложение видно пользователю.

Если ваша задача не должна запуститься прямо сейчас, а пользователю не нужно ей управлять, то тут мы переходим к основному API для работы в фоне – WorkManager.

WorkManager/JobScheduler

Каждый Service приложения запускается без общего понимания нагрузки на систему, учитывая только объем свободной оперативной памяти. Чтобы централизовать запуск любой фоновой работы, в Android 5 появился новый системный сервис JobScheduler, который запускает задачи на основе нагрузки на систему. Для приложения это осталось запуском специального Service в момент, когда решит система.

class DeepJobService : JobService() { // JobService подкласс Service

		// Вызывается при выполнении условий запуска и наличии квоты на запуск
    override fun onStartJob(params: JobParameters?): Boolean {
        // Вызывается при запуске работы
        return false // Будет ли продолжена работа в фоне
    }

    override fun onStopJob(params: JobParameters?): Boolean {
        return true // Возвращаем true если надо чтобы работа была запущена снова позже
    }
}

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

Единственным исключением для использования JobScheduler я вижу только те случаи, когда там есть возможности, которые еще не портировали в WorkManager, например, User Initiated Data Transfer Job из Android 14, но на замену можно взять Foreground режим выполнения в WorkManager. Для подробной информации про User Initiated Data Transfer Job вам стоит прочитать статью с разбором нововведений Android 14.

Я же сосредоточусь на рассказе про WorkManager. Небольшое интро для тех, кто не в курсе. WorkManager – это Jetpack библиотека, которая позволяет выполнять задачи в фоне, запускать их отложено, либо при выполнении требований к состоянию устройства. Также можно управлять очередью из задач и перезапускать их, даже после перезагрузки устройства. API работает на основе Alarm Manager и Service на Android 4.0 и выше. Начиная с Android 6.0, API перешел на реализацию на основе JobScheduler из Android SDK. WorkManager является аналогом JobScheduler, копируя его в возможностях и расширяя их.

Базовой функцией WorkManager является запустить работу, которую вам надо выполнить, задав необходимые условия для старта. По окончании работы вам надо будет вернуть результат об успешном или неуспешном выполнении. Помимо информации для разработчика, это позволит также запустить задачу повторно позже, что задается политикой при создании работы.

// Реализуем Worker, выполняющий задачу
class UploadWorker(
	context: Context,
	params: WorkerParameters
) : CoroutineWorker(context, params) {

		override suspend fun doWork() : ListenableWorker.Result {
        // Загружаем файл на сервер
        return ListenableWorker.Result.success()
		}
}
val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.CONNECTED)
   .setRequiresBatteryNotLow(true)
   .build()

// Создаем запрос на выполнение задачи
val uploadWorkRequest: WorkRequest =
   OneTimeWorkRequest.Builder(UploadWorker::class.java)
			 .setConstraints(constraints)
       // Задаем, когда перезапускать работу (ограничения от 10 сек до 5 часов)
       .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
       // Настраиваем задачу
       .build()


// Добавляем запрос в очередь
WorkManager.getInstance(context).enqueue(uploadWorkRequest)

Важно помнить, что WorkManager имеет как свои ограничения, так и ограничения API, на основе которых работает выполнение задач, а именно:

  • Система сама решает, когда запустить задачу, как много их может одновременно выполняться и сколько давать времени на выполнение в зависимости от версии Android, а также настроек вендора.

  • Запускать задачу повторно нельзя чаще, чем раз в 15 минут.

  • Задача не может выполняться долго, максимум 10 минут на выполнение.

Из-за этих ограничений появились специальные возможности по выполнению работы через WorkManager. Для запуска долгих задач есть возможность работы в Foreground Service или на основе Expedited JobScheduler, начиная с Android 12. При запуске работы вы должны вызвать метод setForeground() и передать туда объекты ForegroundInfo, которые содержит необходимую информацию для запуска Foreground Service().

// Реализуем Worker, выполняющий задачу
class UploadWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        setForeground(newForegroundInfo())
        // загружаем файл на сервер
    }

    private fun newForegroundInfo(): ForegroundInfo {
        return ForegroundInfo(
            UPLOAD_NOTIFICATION_ID,
            newUploadOngoingNotification(),
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
        )
    }

    private fun newUploadOngoingNotification(): Notification
}

Помимо длительного выполнения, есть возможность запуска работы с повышенной гарантией запуска. Система не дает абсолютных гарантий для запуска Expedited Job и времени выполнения, но они выше, чем у обычной job. Важно, что таким образом должны помечаться задачи, очень важные для пользователя, иначе это негативно скажется на опыте.

OneTimeWorkRequest.Builder(UploadWorker::class.java)
     .setConstraints(constraints)
     // Помечаем задачу как высоко приоритетную с указанием политики
     // Как запускать job, если нет квоты для expedited
     .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
     // Настраиваем задачу
     .build()

WorkManager удобен для выполнения задач, объединяя в себе возможности для запуска работы через JobScheduler, Foreground Services и учитывая ограничения и фичи на разных версия ОС. Это универсальное API, подходящее для выполнения большинства задач, которые имеют четкий результат. Очень мало случаев, когда через него вы не сможете выполнить задачу в фоне. Важно помнить, что WorkManager не выполняет работу бесконечно. На момент выхода статьи максимальное время для работы - 10 минут, но оно может разниться в различных версиях Android.

Service, WorkManager и другие API, о которых я рассказал, не могут одного - запуститься в точное время, а порой это критично. Для этого у нас есть AlarmManager.

Выполнение задачи в точное время

AlarmManager – это системный сервис, позволяющий установить напоминание для мгновенного запуска в будущем. Начиная с Android 4.4, AlarmManager не гарантирует срабатывания в заданное время. API у AlarmManager запутанно, есть метод set() для установки простого будильника, который система может отложить до более удачного времени. Помимо этого, есть метод setAndAllowWhileIdle(), который разрешит сработать будильнику в Doze Mode, т. е. когда устройство переходит в спящий режим и экономит расход заряда батареи.

// Самый простой будильник без точного срабатывания
alarmManager.set(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

// Аналог set(), но может сработать в Doze Mode
alarmManager.setAndAllowWhileIdle(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

Есть методы setExact(), которые сообщают системе, что переносить будильник не стоит, так как это важно. Однако он также может сработать позже.

// Аналог set(), но уже говорит системе, что срабатывать позже не стоит
alarmManager.setExact(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

// Аналог setExact(), но может сработать в Doze Mode
alarmManager.setExactAndAllowWhileIdle(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    operation = alarmPendingIntent
)

Также есть метод setWindow(), который аналогичен методу set(), но дает указать длину интервала, в рамках которого должен сработать будильник, но опять же, он может сработать и за его пределами, если у системы не будет возможности вызвать его в заданном окне.

// Установка будильника в заданном временном окне
alarmManager.setWindow(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    windowStart = SystemClock.elapsedRealtime() + 1.minutes().toMillis()
    windowLength = 5.minutes().toMillis(),
    action = alarmPendingIntent
)

Не менее интересная ситуация с повторяющимися будильниками. Есть метод setRepeating() и setInexactRepeating() – оба без гарантий точного срабатывания. Они могут легко отложить срабатывание будильника за минуту до следующего, хотя желаемый интервал был один час.

// Обычный повторяющийся будильник
alarmManager.setRepeating(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    interval = 15.minutes().toMillis()
    operation = alarmPendingIntent
)

// Тоже повторяющийся будильник без точного срабатывания,
// Эффективнее по расходу батареи, чем setRepeating()
// РЕКОМЕНДУЕТСЯ вместо setRepeating
alarmManager.setInexactRepeating(
    type = AlarmManager.ELAPSED_REALTIME_WAKEUP,
    triggerAt = SystemClock.elapsedRealtime() + 1.minutes().toMillis(),
    interval = 15.minutes().toMillis()
    operation = alarmPendingIntent
)

Для установки важных будильников для пользовательского опыта нужно использовать метод setAlarmClock(), предназначенный для будильников, календарей и специальных уведомлений, которые должны включить экран, проиграть звук, вибрировать или другим образом привлечь внимание к устройству.

alarmManager.setAlarmClock(
    info = AlarmClockInfo(
        triggerTime = System.currentTimeMillis() + 1.minutes().toMillis(),
        showIntent = alarmIntent
    ),
    operation = alarmIntent
)

Почему для API, которое должно быть точным, сделали такие ограничения в работе? Все это для того, чтобы разработчики не имели возможности будить устройство сколько им угодно, тем самым расходуя батарею, а то и вовсе делая что-то плохое в фоне. Для запуска будильников очень не хватает API, где пользователь сможет дать приложению гарантии для срабатывания будильников. Это появилось в Android 12. Для использования методов setExact() и setAlarmClock() надо получать разрешение пользователя SCHEDULE_EXACT_ALARM, а позже появилось еще одно разрешение USE_EXACT_ALARM.

При загрузке приложений в Google Play вам надо будет доказать модерации магазина, что вашему приложению нужно разрешение USE_EXACT_ALARM, так как оно по умолчанию выдается при установке приложения, а вот SCHEDULE_EXACT_ALARM может получить любое приложение, но вам надо будет убедиться, что пользователь выдал это разрешение. Он может сделать это через системные настройки, поэтому перед открытием системных настроек рекомендуется показать пользователю пояснение, зачем нужно это разрешение.

val requestScheduleExactAlarm = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult -> 
    when (result.resultCode) {
        Activity.RESULT_OK -> // разрешение получено
        Activity.RESULT_CANCELED -> // разрешение НЕ получено
    }
}


// Проверяем, что разрешение не выдано
if (!alarmManager.canScheduleExactAlarms()) {
    // Показываем пояснение, зачем нужно дать разрешение
    // Затем отправляем пользователя в системные настройки
    requestScheduleExactAlarm.launch(
        Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
    )
}

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

Отключение оптимизаций в фоне

Почему так много способов запуска и настроек времени выполнения задач? Связано это с тем, что бездумный запуск разработчиками работы в фоне приводит к чрезмерному расходу батареи, зачастую полностью игнорируя потребности пользователя. Начиная с Android 6.0, началась серия изменений в работе Android и возможностей разработчиков, которые были призваны к стандартизации подходов, и запуск фоновой работы по оптимальным сценариям, по мнению системы. Например, в Android 12 нельзя запустить Foreground Service из фона, за исключением отдельных случаев, в которые попадают важные для пользователя сценарии, приложения с особыми ролями, а также точные будильники. Кстати, об истории ограничений есть статья.

Скажу сразу, что панику нагонять не стоит, так как все важные сценарии для пользователя продолжают работать и зачастую вы не увидите проблем. Самое важное, что вы должны знать - используйте API, предназначенные для вашей задачи. Стандартная рекомендация - WorkManager, который позволит запустить работу в фоне на пользовательской версии Android.

Не нужно думать, что Google изменил правила безвозвратно. Разработчикам оставили возможность отключить все оптимизации системы для приложения, которые связаны с оптимизацией расхода батареи и ограничением работы в фоне. Но не все так просто…

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

Единственный вариант попросить пользователя отключить все оптимизации для приложения — запустить стандартный Intent, который появился в Android 6.0, и надеяться, чтобы пользователь это сделал. Тогда приложение может начать лучше работать в фоне, но результат никто не гарантирует.

val powerManager: PowerManager = getSystemService()
if (!powerManager.isIgnoringBatteryOptimizations(context.packageName)) {
		try {
        context.startActivity(
						Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
				)
    } catch (e: ActivityNotFoundException) {
        // Обрабатываем, если экрана нет
    }
}

Помимо этого, в Android 13 появилась настройка расхода заряда батареи для каждого приложения. По умолчанию происходит оптимизация для всех сторонних приложений, а пользователь может выбрать настройку “Ограничено” или “Без ограничений”, которые, соответственно, практически запретят работать в фоне или дадут очень много дополнительных возможностей приложени.. Я не нашел способа запустить этот экран напрямую, кроме как отправить пользователя в настройки приложения и сказать, какой параметр ему надо открыть.

Вроде бы все просто и понятно, но когда дело доходит до устройств сторонних производителей, начинается зоопарк решений. Каждый производитель пытается выделиться среди других Android-устройств и придумывает свои собственные улучшения и оптимизации расхода заряда батареи, ограничивая приложения. С разработчиками популярных приложений есть договоренности и на уровне прошивки устройства для такого софта не будет агрессивной оптимизации. Если вы не крупный производитель или у вас нет связей с производителями, готовьтесь выживать, так как ради того, чтобы продать устройство, производители готовы убивать все процессы, что не попадают в список исключений.

Если вы хотите узнать подробнее про особенности различных вендоров и какие оптимизации применяются, то посетите сайт Don’t kill my app. На сайте вы узнаете, где найти стандартные настройки, связанные с оптимизацией расхода батареи, и те, что придумал вендор сам. Все представлено в виде скриншотов для разных версий оболочки. Это будет полезно для разработчиков, тестировщиков и поддержки.

Заключение

Самый главный вопрос, который остался нераскрытым - как выбрать API для запуска моей задачи в фоне? Я постарался собрать все известные API на момент, когда последней версией ОС является Android 14, и пришел к схеме, которую вы можете видите ниже. В итоге я выбрал одно – Foreground Service – в отдельных случаях и WorkManager – для всех других. Нужно больше – объясните пользователю "зачем" и просите отключить оптимизации системы для вашего приложения.

 Алгоритм выбора API для запуска задачи в фоне
Алгоритм выбора API для запуска задачи в фоне

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

На мой взгляд, ограничение выполнения задач в фоне для приложений и оптимизация работы в фоне - правильный шаг. Особенно учитывая то, что разработчикам предоставляют много API для работы в фоне, а в крайнем случае пользователь может дать приложению полную свободу. Тенденция использования Foreground Service мне нравится тем, что я как пользователь знаю, что приложение что-то делает в фоне, а на любые операции за рамками этого я должен выдать разрешения. Конечно, эти правила не полностью касаются системных приложений, но тут остается только принимать условия.

Все ограничения на запуск фоновой работы, которые мы имеем сейчас в Android, появились из-за нас. Мы слишком сильно злоупотребляли возможностями, и пришлось вводить регулирование на уровне системы и магазина Google Play, чтобы устройство пользователя и другие приложения на нем могли нормально работать.

Это все, что я хотел вам рассказать про современную фоновую работу. Возможно, что вы читаете эту статью, когда уже вышел Android 15 или новее, тогда не все части будут актуальны, и стоит ждать новых правил.

Делитесь своим мнением касательно API для фоновой работы, сложностями, с которыми столкнулись на устройствах, и других особенностях, которые я не рассказал в статье.

Комментарии (11)


  1. orekh
    28.06.2024 13:41

    Когда-то смартфоны привнесли многозадачность, открыв возможности полноценных ОС звонилкам, а сейчас отбирают её в погоне за временем автономной работы.


    1. kirich1409 Автор
      28.06.2024 13:41

      Когда это привнесли то на смартфоне не было столько приложений и возможностей.

      Если хотите чтобы было как раньше - держите телефон на Power Bank. Тогда ограничения по питанию не будут такими строгими. Также вариант отключить все оптимизации расхода батарейки.


      1. orekh
        28.06.2024 13:41
        +1

        Хе, раньше хотя бы музыкальный плеер не умирал в фоне. А настройки нисколько не помогают в трех последних моих смартфонах, двух сяоми и последнем ноунейм китайфоне. Подключение к зарядке тоже. Времена, когда я мог днями копаться на форумах чтобы разблокировать загрузчик и подобрать не самую глючную прошивку уже прошли. Да, тут определённо есть белые списки для некоторых приложений, и что иронично Don'tKillMyApp туда входит, а приложения для подкастов - нет, ха.


        1. saege5b
          28.06.2024 13:41

          Тогда всего-то была аппаратная кнопка резета :) и она была самой нажимаемой.


        1. ganzmavag
          28.06.2024 13:41
          +1

          Может дело в самом приложении? Это же про какое-то конкретное речь? Потому что не наблюдал такого ни на одном смартфоне


        1. kirich1409 Автор
          28.06.2024 13:41

          Слушаю подкасты и музыку постоянно - никаких проблем. Для медиа есть специальные исключения в системе, которые разработчики используют. Попробуйте другое приложение для я подкастов, чтобы схожу говорить что Android/телефон плохой


        1. VADemon
          28.06.2024 13:41
          +1

          емнип именно Xiaomi и схожие китайцы в угоду автономности на установки приложений и настройки почихивают. Тем более обидно, когда священные коровы типа Whatsapp в белом списке.

          Уж не помню у какого-то проекта на Github.io была страница к каждому почти бренду. У одного так и было написано, что телефон так или иначе приложение прибьет.

          В Android 12 появился ещё один стоковый механизм, зависящий от кол-ва процессов. Чуть было не стал приговором для Termux, но договорились. Google добавили что-то в настройки. Поэтому тут все хороши


  1. mxkmn
    28.06.2024 13:41

    Спасибо за статью, прочитать иногда удобне, чем смотреть)

    Помимо этого, в Android 13 появилась настройка расхода заряда батареи для каждого приложения.

    Она есть и в 12ом. Состояние "без ограничений" автоматически появляется при выдаче прав через пункт в настройках "Экономия заряда батареи", о котором в статье выше. В этом можно убедиться лично просто подёргав переключалки. Поэтому именно переход в настройки приложения не требуется - можно воспользоваться Intent'ом.

    Выдача этих прав через intent наиболее удобным способом требует своеобразный (разрешённый лишь в некоторых случаях) пермишен ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, после его получения достаточно вывести запрос на получение разрешения и нажать на одну кнопку:

    startActivity(
     Intent(
      Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
      Uri.parse("package:$packageName"),
     )
    )

    Кроме того, в статье не описана проблема App Hibernation - эта фича может полностью заморозить приложение, если им не пользуются, соответственно работа сломается. Способ его отключения и множество других подробностей работы с WorkManager PeriodicRequest я описывал в статье на StackOverflow.


    1. kirich1409 Автор
      28.06.2024 13:41

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


      1. mxkmn
        28.06.2024 13:41
        +1

        Для большинства приложений это действительно верное решение, но не для каждого. Например, моё приложение добавляет события в календарь каждые N часов (периодическая работа WorkManager) и после настройки пользователю нет необходимости входить в приложение - это просто не нужно, всё без этого работает. Требуется предотвратить заморозку, чтобы оно не сломалось со временем.

        Посчитал важным это указать: итоговый эффект после заморозки будет губительнее Restricted Bucket'а, отключение которого не относится напрямую к статье, но всё же было описано (то самое состояние "без ограничений" в настройках батареи и запрещает выдачу Restricted для приложения). А ещё у Restricted Bucket'а и App Hibernation, кажется, общие корни - появились примерно в одно время и имели схожий по длительности срок до наложения ограничений.


        1. kirich1409 Автор
          28.06.2024 13:41

          Может вам попробовать Sync Adapter API чтобы данные синхронизировать. Не знаю зачем ещё из фона резко появляются события в календаре кроме как синхронизации с сервером