Современные Android-смартфоны уже давно стали мощными устройствами, способными выполнять множество операций. Вся наша коммуникация стала асинхронной, мы передаем и получаем множество данных. Для этого всего важно гарантировать выполнение работы в фоне, т. е. когда приложение свёрнуто.
В этой статье я разберу актуальные API для выполнения различного рода задач в фоне. Тут очень важно слово “актуальные”. На момент выхода этой статьи уже вышел Android 14, поэтому список API и подходы будут актуальны. Да-да, как вы могли узнать из моей статьи про “Ограничения фоновой работы”, уже на протяжении многих лет мы получаем новые ограничения и через пару лет обязательно появятся еще новее, как и новые API. Рекомендую прочитать статью, указанную выше, чтобы лучше понять API, про которые я расскажу в этой статье.
Все, о чем я буду рассказывать в этой статье, касается современного Android 14 и нескольких последних версий. На старых версиях Android API, о которых я расскажу, этого может не быть, либо оно будет работать иначе. Чем старше версия Android, тем и ограничений на запуск работы в фоне тоже будет меньше, поэтому вы сможете использовать альтернативные решения и подходы. Я за следование правилам из новых Android на всех поддерживаемых вашим приложением версиях Android, чтобы добиваться унификации поведения. Но это только мой взгляд, и вы вольны делать то, что вам хочется.
Прежде чем начнете читать этот разбор, я настоятельно рекомендую прочитать статью про историю ограничений выполнения работы в фоне.
Если вам интересно следить за самыми последними новостями Android разработки и получать подборку интересных статей по этой тематике, тогда вам стоит подписаться на Телеграм-канал Android Broadcast и мой YouTube канал "Android Broadcast".
Классификация фоновой работы
Чтобы правильно выбрать 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.
Это 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 – для всех других. Нужно больше – объясните пользователю "зачем" и просите отключить оптимизации системы для вашего приложения.
Вы можете сказать, что система слишком жестко "закручивает болты" и не дает жизни разработчикам. Возможно, мы так говорим, потому что с первых версий Android разработчикам давали делать все, что угодно и без ограничений, а когда начинают что-то забирать, то сразу появляется недовольство. Например, iOS вообще имеет минимум возможностей по работе приложений в фоне, но продажи iPhone и популярность iOS растут, что говорит само за себя.
На мой взгляд, ограничение выполнения задач в фоне для приложений и оптимизация работы в фоне - правильный шаг. Особенно учитывая то, что разработчикам предоставляют много API для работы в фоне, а в крайнем случае пользователь может дать приложению полную свободу. Тенденция использования Foreground Service мне нравится тем, что я как пользователь знаю, что приложение что-то делает в фоне, а на любые операции за рамками этого я должен выдать разрешения. Конечно, эти правила не полностью касаются системных приложений, но тут остается только принимать условия.
Все ограничения на запуск фоновой работы, которые мы имеем сейчас в Android, появились из-за нас. Мы слишком сильно злоупотребляли возможностями, и пришлось вводить регулирование на уровне системы и магазина Google Play, чтобы устройство пользователя и другие приложения на нем могли нормально работать.
Это все, что я хотел вам рассказать про современную фоновую работу. Возможно, что вы читаете эту статью, когда уже вышел Android 15 или новее, тогда не все части будут актуальны, и стоит ждать новых правил.
Делитесь своим мнением касательно API для фоновой работы, сложностями, с которыми столкнулись на устройствах, и других особенностях, которые я не рассказал в статье.