Если вам необходимо выполнить передачу данных, которая может занять много времени, вы можете создать задание JobScheduler и обозначить его как задание передачи данных, инициируемое пользователем (UIDT) . Задания UIDT предназначены для длительной передачи данных, инициируемой пользователем устройства, например, для загрузки файла с удалённого сервера. Задания UIDT появились в Android 14 (API уровня 34).
Задания по передаче данных, инициированные пользователем, запускаются пользователем. Эти задания требуют уведомления, запускаются немедленно и могут выполняться в течение длительного времени, если позволяют системные условия. Вы можете запускать несколько заданий по передаче данных, инициированных пользователем, одновременно.
Задания, инициированные пользователем, должны планироваться, пока приложение видимо пользователю (или при соблюдении одного из допустимых условий ). После выполнения всех ограничений, инициированные пользователем задания могут быть выполнены ОС с учётом ограничений, связанных с состоянием системы. Система также может использовать предоставленный предполагаемый размер полезной нагрузки для определения длительности выполнения задания.
Планирование заданий по передаче данных, инициируемых пользователем
To run a user initiated data-transfer job, do the following:
Make sure your app has declared the
JobServiceand associated permissions in its manifest:<service android:name="com.example.app.CustomTransferService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"> ... </service>Also, define a concrete subclass of
JobServicefor your data transfer:Kotlin
class CustomTransferService : JobService() { ... }
Java
class CustomTransferService extends JobService() { .... }
Declare the
RUN_USER_INITIATED_JOBSpermission in the manifest:<manifest ...> <uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" /> <application ...> ... </application> </manifest>Call the
setUserInitiated()method when building aJobInfoobject. (This method is available beginning with Android 14.) We also recommend that you offer a payload size estimate by callingsetEstimatedNetworkBytes()while creating your job.Kotlin
val networkRequestBuilder = NetworkRequest.Builder() // Add or remove capabilities based on your requirements. // For example, this code specifies that the job won't run // unless there's a connection to the internet (not just a local // network), and the connection doesn't charge per-byte. .addCapability(NET_CAPABILITY_INTERNET) .addCapability(NET_CAPABILITY_NOT_METERED) .build() val jobInfo = JobInfo.Builder(jobId, ComponentName(mContext, CustomTransferService::class.java)) // ... .setUserInitiated(true) .setRequiredNetwork(networkRequestBuilder) // Provide your estimate of the network traffic here .setEstimatedNetworkBytes(1024 * 1024 * 1024, 1024 * 1024 * 1024) // ... .build()
Java
NetworkRequest networkRequest = new NetworkRequest.Builder() // Add or remove capabilities based on your requirements. // For example, this code specifies that the job won't run // unless there's a connection to the internet (not just a local // network), and the connection doesn't charge per-byte. .addCapability(NET_CAPABILITY_INTERNET) .addCapability(NET_CAPABILITY_NOT_METERED) .build(); JobInfo jobInfo = JobInfo.Builder(jobId, new ComponentName(mContext, CustomTransferService.class)) // ... .setUserInitiated(true) .setRequiredNetwork(networkRequest) // Provide your estimate of the network traffic here .setEstimatedNetworkBytes(1024 * 1024 * 1024, 1024 * 1024 * 1024) // ... .build();
While the job is being executed, call
setNotification()on theJobServiceobject. CallingsetNotification()makes the user aware that the job is running, both in the Task Manager and in the status bar notification area.When execution is complete, call
jobFinished()to signal to the system that the job is complete, or that the job should be rescheduled.Kotlin
class CustomTransferService: JobService() { private val scope = CoroutineScope(Dispatchers.IO) @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) override fun onStartJob(params: JobParameters): Boolean { val notification = Notification.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) .setContentTitle("My user-initiated data transfer job") .setSmallIcon(android.R.mipmap.myicon) .setContentText("Job is running") .build() setNotification(params, notification.id, notification, JobService.JOB_END_NOTIFICATION_POLICY_DETACH) // Execute the work associated with this job asynchronously. scope.launch { doDownload(params) } return true } private suspend fun doDownload(params: JobParameters) { // Run the relevant async download task, then call // jobFinished once the task is completed. jobFinished(params, false) } // Called when the system stops the job. override fun onStopJob(params: JobParameters?): Boolean { // Asynchronously record job-related data, such as the // stop reason. return true // or return false if job should end entirely } }
Java
class CustomTransferService extends JobService{ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) @Override public boolean onStartJob(JobParameters params) { Notification notification = Notification.Builder(getBaseContext(), NOTIFICATION_CHANNEL_ID) .setContentTitle("My user-initiated data transfer job") .setSmallIcon(android.R.mipmap.myicon) .setContentText("Job is running") .build(); setNotification(params, notification.id, notification, JobService.JOB_END_NOTIFICATION_POLICY_DETACH) // Execute the work associated with this job asynchronously. new Thread(() -> doDownload(params)).start(); return true; } private void doDownload(JobParameters params) { // Run the relevant async download task, then call // jobFinished once the task is completed. jobFinished(params, false); } // Called when the system stops the job. @Override public boolean onStopJob(JobParameters params) { // Asynchronously record job-related data, such as the // stop reason. return true; // or return false if job should end entirely } }
Periodically update the notification to keep the user informed of the job's status and progress. If you cannot determine the transfer size ahead of scheduling the job, or need to update the estimated transfer size, use the new API,
updateEstimatedNetworkBytes()to update the transfer size after it becomes known.
Recommendations
To run UIDT jobs effectively, do the following:
Clearly define network constraints and job execution constraints to specify when the job should be executed.
Execute the task asynchronously in
onStartJob(); for example, you can do this by using a coroutine. If you don't run the task asynchronously, the work runs on the main thread and might block it, which can cause an ANR.To avoid running the job longer than necessary, call
jobFinished()when the transfer finishes, whether it succeeds or fails. That way, the job doesn't run longer than necessary. To discover why a job was stopped, implement theonStopJob()callback method and callJobParameters.getStopReason().
Обратная совместимость
There is currently no Jetpack library that supports UIDT jobs. For this reason, we recommend that you gate your change with code that verifies that you're running on Android 14 or higher. On lower Android versions, you can use WorkManager's foreground service implementation as a fallback approach.
Here's an example of code that checks for the appropriate system version:
Kotlin
fun beginTask() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { scheduleDownloadFGSWorker(context) } else { scheduleDownloadUIDTJob(context) } } private fun scheduleDownloadUIDTJob(context: Context) { // build jobInfo val jobScheduler: JobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler jobScheduler.schedule(jobInfo) } private fun scheduleDownloadFGSWorker(context: Context) { val myWorkRequest = OneTimeWorkRequest.from(DownloadWorker::class.java) WorkManager.getInstance(context).enqueue(myWorkRequest) }
Java
public void beginTask() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { scheduleDownloadFGSWorker(context); } else { scheduleDownloadUIDTJob(context); } } private void scheduleDownloadUIDTJob(Context context) { // build jobInfo JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.schedule(jobInfo); } private void scheduleDownloadFGSWorker(Context context) { OneTimeWorkRequest myWorkRequest = OneTimeWorkRequest.from(DownloadWorker.class); WorkManager.getInstance(context).enqueue(myWorkRequest) }
Остановить задания UIDT
Both the user and the system can stop user-initiated transfer jobs.
Пользователь, из диспетчера задач
Пользователь может остановить инициированное пользователем задание по передаче данных, которое отображается в диспетчере задач .
В тот момент, когда пользователь нажимает «Стоп» , система делает следующее:
- Немедленно завершает процесс вашего приложения, включая все другие задания или работающие службы приоритета.
- Не вызывает
onStopJob()для любых запущенных заданий. - Предотвращает перепланирование видимых пользователю заданий.
По этим причинам рекомендуется предоставить элементы управления в уведомлении, публикуемом для задания, чтобы можно было корректно останавливать и перепланировать задание.
Обратите внимание, что в особых случаях кнопка «Стоп» не отображается рядом с заданием в диспетчере задач или задание вообще не отображается в диспетчере задач.
По системе
Unlike regular jobs, user-initiated data transfer jobs are unaffected by App Standby Buckets quotas. However, the system still stops the job if any of the following conditions occur:
- A developer-defined constraint is no longer met.
- The system determines that the job has run for longer than necessary to complete the data transfer task.
- The system needs to prioritize system health and stop jobs due to increased thermal state.
- The app process is killed due to low device memory.
When the job is stopped by the system for reasons other than low device
memory, the system calls onStopJob(), and the system retries the job at a time
that the system deems to be optimal. Make sure that your app can persist the
data transfer state even if onStopJob() isn't called, and that your app can
restore this state when onStartJob() is called again.
Условия, разрешенные для планирования заданий по передаче данных, инициируемых пользователем
Приложения могут запускать инициированное пользователем задание по передаче данных только в том случае, если приложение находится в видимом окне или выполняются определенные условия:
- Если приложение может запускать действия в фоновом режиме , оно также может запускать в фоновом режиме инициируемые пользователем задания по передаче данных.
- Если в приложении есть действие в стеке существующей задачи на экране «Последние» , это само по себе не позволяет выполнить задание по передаче данных, инициированное пользователем.
Если выполнение задания запланировано на момент, когда необходимые условия не выполняются, задание завершается с ошибкой и возвращается код ошибки RESULT_FAILURE .
Ограничения, разрешенные для заданий передачи данных, инициируемых пользователем
Для поддержки оптимального выполнения заданий Android предлагает возможность назначать ограничения для каждого типа задания. Эти ограничения доступны начиная с Android 13.
Примечание : В следующей таблице сравниваются только ограничения, различающиеся для каждого типа задания. Подробнее об ограничениях см. на странице разработчика JobScheduler или в разделе «Ограничения работы» .
В следующей таблице показаны различные типы заданий, поддерживающие заданное ограничение, а также набор ограничений, поддерживаемых WorkManager. Используйте строку поиска перед таблицей, чтобы отфильтровать таблицу по названию метода ограничения задания.
Ниже приведены ограничения, допустимые для заданий по передаче данных, инициированных пользователем:
-
setBackoffCriteria(JobInfo.BACKOFF_POLICY_EXPONENTIAL) -
setClipData() -
setEstimatedNetworkBytes() -
setMinimumNetworkChunkBytes() -
setPersisted() -
setNamespace() -
setRequiredNetwork() -
setRequiredNetworkType() -
setRequiresBatteryNotLow() -
setRequiresCharging() -
setRequiresStorageNotLow()
Тестирование
В следующем списке показаны некоторые шаги по тестированию заданий вашего приложения вручную:
- Чтобы получить идентификатор задания, получите значение, определенное при создании задания.
Чтобы немедленно запустить задание или повторить остановленное задание, выполните следующую команду в окне терминала:
adb shell cmd jobscheduler run -f APP_PACKAGE_NAME JOB_ID
Чтобы смоделировать принудительную остановку задания системой (из-за состояния системы или превышения квоты), выполните следующую команду в окне терминала:
adb shell cmd jobscheduler timeout TEST_APP_PACKAGE TEST_JOB_ID
См. также
Дополнительные ресурсы
Дополнительные сведения о передаче данных, инициируемой пользователем, см. в следующих дополнительных ресурсах:
- Практический пример интеграции UIDT: Карты Google повысили надежность загрузки на 10 % благодаря API передачи данных, инициируемому пользователем.