Иногда при разработке под OS Android возникает необходимость выполнять ресурсоемкие операции периодически, регулярно или по запросу, и для этих операций важно, например, наличие интернета или чтобы устройство "не спало". Чаще всего при решении подобных задач применяются AlarmManager, WakefulBroadcastReceiver, либо же вообще WakeLock контролируется вручную. Все это не рекомендуется в документации для разработчиков под Android, а WakefulBroadcastReceiver уже отмечен как deprecated с API level 26.0.0.  Что же мы можем сделать, чтобы следовать рекомендациям Google и создавать приложения с более гибким поведением на версиях Android 5.0+, в которых энергосбережению уделяется все больше внимания? Если вы готовы выставить минимальный API level 21.0.0 для своего приложения, предлагаю под катом пример использования JobScheduler в связке с IntentService для последовательного выполнения трудоемких задач. Достаточно открыть документацию по классу WakefulBroadcastReceiver, чтобы увидеть интересную ситуацию: класс добавлен в 22.0.0 версии API, а отмечен как deprecated в 26.0.0 версии. Можно предположить, что сначала разработчики Android решили добавить удобный класс для выполнения задач с удержанием WakeLock, но потом оказалось, что никто не гарантирует, что приложение будет работать на переднем плане, да и вообще когда каждое "умное" приложение пытается удержать WakeLock, когда же устройству экономить энергию? Да и принцип работы WakefulBroadcastReceiver стал идти в разрез со стремлением продлить время жизни заряда батареи, ведь кроме всего прочего незакрытый правильным образом ресивер мог привести к утечкам WakeLock'ов. С другой стороны у рядовых разработчиков может возникнуть вопрос, как при всех новых ограничениях безопасно выполнять периодические задачи, когда, например, активен Doze mode? Чтобы уравновесить баланс ограничений и возможностей был создан JobScheduler, который берет на себя решение вопросов о том, когда задаче можно выполниться, каким приемлемым образом предоставить ей возможность выполнения, если это действительно очень нужно, и как при этом не нарушить политику энергосбережения и не потерять где-нибудь не отпущенный WakeLock. Пока готовилась данная статья, на Хабре появилась статья другого автора, в которой раскрыто чуть больше теории и уделено чуть меньше внимания практике. Она будет полезной для быстрого старта и более глубоко понимания существующих альтернатив JobScheduler'у. Для простоты примера представим, что у нас есть задача по записи слова "Exercise" в файл, при этом нам нужен интернет, причем желательно не мобильный, потому что наша задача не стоит траты мобильного трафика пользователя. При этом мы хотим, чтобы каждая новая задача по записи слова выполнялась друг за другом. Так как нам нельзя блокировать главный поток (тут и работа с файлами и, возможно, какие-то сетевые запросы), и у нас задачи могут выстраиваться в очередь, на помощь нам приходит IntentService.  Для полного воспроизведения шагов, описанных в данной статье, необходимо создать новый проект без активити, с минимальной версией SDK 21. Также все действия можно производить в уже существующем проекте с минимальной версией SDK не ниже 21.  Добавляем в проект новый IntentService, называем его ExerciseIntentService, используем стандартный подход, основанный на автоматически сгенерированных методах, почистив лишнее и переименовав методы и константы под наши условия.  В результате несложных манипуляций получаем следующее тело для ExerciseIntentService: И так, сервис, который будет непосредственно выполнять наши однотипные условно-трудоемкие задачи по порядку, готов. Для того, чтобы создавать и планировать задачи, необходима точка входа, в качестве которой можно использовать BroadcastReceiver. Добавим в проект стандартными средствами BroadcastReciever и назовем его ExerciseRequestsReceiver. В дальнейшем мы сможем откуда угодно отправлять бродкасты нашему приложению, чтобы оно планировало выполнение задач (например, это можно сделать с помощью инструментальных тестов, что будет показано ближе к концу статьи). Минимально необходимый код ExerciseRequestsReceiver выглядит так: String ACTION_PERFORM_EXERCISE — действие для идентификации необходимости запуска процесса планирования задачи.
int sJobId — переменная для идентификатора задачи, который будет использован при планировании задач.
scheduleJob(context) — вызов метода, который будет содержать всю необходимую логику для планирования задачи. Теперь при получении бродкаста мы могли бы отправить интент нашему ExerciseIntentService, и все бы было хорошо, только в нашем случае необходим WakeLock, а так как мы договорились больше не использовать WakefulBroadcastReceiver, то необходимо создать и запланировать новую задачу для JobScheduler, а дальше он все сделает за нас (почти).  Кстати, при использовании JobScheduler не нужно разрешение на WakeLock, в отличие от других способов решения подобной задачи. JobScheduler требует отдельный сервис, унаследованный от JobService. Назовем его ExerciseJobService и добавим как обычный сервис, заменив родительский класс и добавив разрешение в манифест модуля: Разрешение android.permission.BIND_JOB_SERVICE необходимо, чтобы данный сервис смог взаимодействовать с JobScheduler. Кроме этого обязательными для реализации являются два метода onStartJob() и onStopJob().  Для правильной обработки ситуации с onStopJob() можно реализовать статические флаги, какие-либо дополнительные бродкасты, а также можно использовать другие средства взаимодействия сервисов. В текущей статье это будет опущено и введено допущение, что если наш IntentService не смог выполнить задачу, то ничего страшного для логики приложения и целостности данных не произойдет.  onStopJob также имеет возвращаемое значение, если это true — то JobScheduler поставит прерванную задачу в очередь выполнения снова, false — задача будет считаться выполненной и будет удалена из очереди, если она не была периодической.  Так как обработка внештатных ситуаций опускается, вернем из этого метода true, чтобы задача перезапланировалась, заодно это нам позволит рассмотреть использование критерия повтора. Таким образом, установив возвращаемые значения из двух основных методов и добавив запуск нашего ExerciseIntentService в onStartJob(), получаем следующий достаточно емкий сервис: На данный момент уже подготовлен минимальный набор классов для реализации выполнения задачи через JobScheduler в IntentService, а именно: ExerciseIntentService — выполняет непосредственно необходимые для задачи операции в отдельных потоках, ExerciseJobService — ловит события от JobScheduler'a и запускает ExerciseIntentService, а ExerciseRequestsReceiver — входная точка для работы нашего комплекса, где мы ловим бродкасты извне и должны инициализировать задачу для JobScheduler, чем далее и займемся. Для создания задачи для JobScheduler понадобится JobInfo.Builder. Его конструктор принимает два параметра: идентификатор задачи и ComponentName нашего ExerciseJobService.  С идентификатором все просто (но не без нюансов) — любое целочисленное значение:  Если вдруг ваше приложение системное, либо же у вас есть несколько приложений с одним sharedUserId, то нужно учитывать дополнительное условие: id не должен пересекаться среди всех приложений с одним uid. Таким образом, если приложение использует android.uid.system, то нужно учитывать, что некоторые системные задачи также используют JobScheduler, и уникальность id нужно поддерживать самостоятельно. Кстати, при использовании таких методов у JobScheduler как removeAll() мы можем удалить и чужие задачи с тем же uid. Статья на английском о том, как можно контролировать подобную ситуацию. В рассматриваемом примере не нужно забоится об UID и в качестве идентификатора используется инкрементируемое значение sJobId. sJobId определен следующим образом: С ComponentName все гораздо проще, это объект в конструктор которого передается ExerciseJobService.class. Ниже рассмотрим основной набор методов JobInfo.Builder. минимальное время, которое пройдет прежде чем задача будет выполнена, другими словами время отложенного старта. максимальное время, в течение которого задача может находиться в очереди / запланированном состоянии. Если по истечению 5 секунд (в нашем случае) благоприятные условия так и не наступили, задача начнет выполняться ни смотря ни на что (если это не противоречит другим политикам JobScheduler). Если не использовать, тогда задача будет выполнена только при наступлении необходимых условий. задается тип подключения, например, нам нужен интернет, но чтобы это был свободный WIFi (не hotspot) или Ethernet, тогда мы выбираем NETWORK_TYPE_UNMETERED. Определяет состояние, когда пользователь не взаимодействует с устройством, в нашем случае это не важно. В нашем примере предположим, что нам без разницы, заряжается устройство или нет. Можно установить для задачи критерий повтора (подробнее об этом чуть ниже): Кроме этого есть возможность сделать задачу периодической: Когда мы устанавливаем задаче периодичность, мы сталкиваемся с логичными ограничениями: Это основные отличия периодической задачи от обычной. В текущем примере мы не будем делать задачу периодической. setBackoffCriteria() позволяет задать правило, по которому будет произведена повторная попытка выполнения задачи в случае необходимости (например в onStopJob() мы вернули true).  JobScheduler предлагает нам две политики: линейная и экспоненциальная.  Формула линейной политики следующая: т.е. от текущего момента времени следующая попытка будет предпринята через заданное количество времени умноженное на количество неудач. Формула экспоненциальной политики: Здесь же время следующей попытки растет гораздо большими шагами. Все достаточно просто и прозрачно, но что будет, если наша задача не может выполниться успешно множество раз, 10, 20...? При заданном начальном времени повтора в 1 минуту к 10 попытке пройдет практически час. Отлавливать такие ситуации достаточно не просто, потому что мы не можем предсказать что будет через час. JobScheduler ограничивает такие повтороения пятью часами. Таким образом с параметром setBackoffCriteria() нужно обращаться очень осторожно, тщательно продумывать начальное время и тип политики в соотвествии с поставленными задачами. Возможно также придется осуществлять дополнительную обработку, например, количества повторов и удалять задачу из JobScheduler. Таким образом у нас готов JobBuilder со всеми необходимыми нам параметрами. Для добавления задачи в очередь выполнения необходимо получить у системы инстанс JobScheduler: И вызвать метод для добавления JobInfo из билдера: Для надежности можно проверить возвращаемое значение последнего метода, который нам скажет об успешном или не успешном добавлении задачи в очередь. Далее когда JobScheduler находит, что условия для выполнения задачи оптимальные, вызывается метод onStartJob() в ExerciseJobService, который уже разобран выше. Если подытожить, то в результате у нас получилось тестовое приложение, которое позволяет планировать одноразовые задачи, которые выполняют условно-тяжелую операцию в IntentService.  Для проверки работоспособности предлагаю добавить небольшой инструментальный тест, который выглядит примерно так: Также, для проверки работы примера в исходный код были добавлены некоторые логи, которые не указаны выше в выдержках. Если мы запустим тест, то мы увидим, что наша задача с "id: 1" стартует и завершается, стартует и завершается… Точнее её принудительно завершает JobScheduler.  В данном примере из метода onStartJob() мы вернули true, а это значит, что мы сообщили JobScheduler'у что выполнение задачи продолжается где-то в побочном потоке. Так как мы не уведомляем о завершении задачи, JobScheduler завершает её принудительно, а так как из onStopJob() мы тоже возвращаем true срабатывает политика повтора, и задача перепланируется и запускается заново. Чтобы такого не происходило, нужно вызывать метод jobFinished() в классе сервиса ExerciseJobService, о его использовании и различных вариантах передачи информации о завершении задачи из IntentService я постараюсь рассказать в следующих статьях. На этом создание тестового примера завершается, он готов к использованию и применению в рабочих проектах для планирования задач. Для выполнения задач в фоне здесь был использован IntentService, но допустимы и другие способы, например, использование ThreadPoolExecutor или HandlerThread. А в случае разработки исключительно под Android O и выше, рекомендую также обратить внимание на JobIntentService. Полный код рассматриваемого примера приведен на GitHub. Также можно ознакомиться с официальным пример реализации JobScheduler с Activity на developer.android.com. Иллюстрация: Anni ART (копирование и воспроизведение только с согласия автора). Android developer