If you need to perform a data transfer that may take a long time, you can create a JobScheduler job and identify it as a user-initiated data transfer (UIDT) job. UIDT jobs are intended for longer-duration data transfers that are initiated by the device user, such as downloading a file from a remote server. UIDT jobs were introduced with Android 14 (API level 34).
User-initiated data transfer jobs are started by the user. These jobs require a notification, start immediately, and may be able to run for an extended period of time as system conditions allow. You can run several user-initiated data transfer jobs concurrently.
User initiated jobs must be scheduled while the application is visible to the user (or in one of the allowed conditions). After all constraints are met, user initiated jobs can be executed by the OS, subject to system health restrictions. The system may also use the provided estimated payload size to determine how long the job executes.
排定使用者啟動的資料移轉作業
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().
回溯相容性
目前沒有支援 UIDT 作業的 Jetpack 程式庫。因此,建議您使用程式碼控管變更,確認您是在 Android 14 以上版本上執行作業。在較舊的 Android 版本中,您可以使用 WorkManager 的前景服務實作做為備用方法。
以下是檢查適當系統版本的程式碼範例:
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 工作
使用者和系統均可停止由使用者啟動的移轉作業。
由工作管理員的使用者執行
使用者可以停止在工作管理員中顯示的使用者啟動資料移轉作業。
當使用者按下「停止」時,系統會執行以下操作:
- 立即終止應用程式的處理程序,包括所有其他執行中的工作或前景服務。
- 不會針對任何執行中的工作呼叫
onStopJob()。 - 防止使用者開放瀏覽權限的工作重新排程。
基於上述理由,建議您針對工作發布的通知中提供控制項,以便有條理地停止工作並重新排程。
請注意,在特殊情況下,工作管理員中所列的工作旁不會顯示「停止」按鈕,抑或工作完全沒有顯示在工作管理員中。
由系統執行
與一般工作不同的是,使用者啟動的資料移轉工作不會受到應用程式待命值區配額的影響。然而,若發生下列任一情況,系統也會停止工作:
- 某項限制不再符合開發人員定義的條件。
- 系統判定工作的執行時間比完成資料移轉工作所需的時間還要長。
- 系統會以系統健全狀態為優先考量,有鑑於溫度不斷升高,必須停止執行中的工作。
- 應用程式處理程序因裝置記憶體不足而終止。
如果系統因裝置記憶體不足以外的原因停止工作,系統會呼叫 onStopJob(),並在系統認定的最佳時間點重新執行工作。請確認即使系統未呼叫 onStopJob(),應用程式也可保持資料移轉狀態,並且確認再次呼叫 onStartJob() 後,應用程式可恢復此狀態。
可對使用者啟動的資料移轉作業進行排程的情況
Apps can only start a user-initiated data transfer job if the app is in the visible window, or if certain conditions are met:
- If an app can launch activities from the background, it can also launch user-initiated data transfer jobs from the background.
- If an app has an activity in the back stack of an existing task on the Recents screen, that alone doesn't allow a user-initiated data transfer job to run.
If the job is scheduled to run at a time when the necessary conditions are not
met, the job fails and returns a RESULT_FAILURE error code.
使用者啟動的資料移轉作業許可的限制
為支援在最佳時間點執行的工作,Android 為各個工作類型提供指派限制的功能。這些限制自 Android 13 起已可供使用。
附註:下表僅就各種工作類型之間的限制差異提出比較。如要瞭解所有限制,請參閱 JobScheduler 開發人員頁面或工作限制條件。
下表說明支援特定工作限制的各種工作類型,並列出 WorkManager 支援的工作限制組合。使用表格前的搜尋列,即可按照工作限制方法的名稱篩選表格。
以下限制適用於使用者啟動的資料移轉作業:
setBackoffCriteria(JobInfo.BACKOFF_POLICY_EXPONENTIAL)setClipData()setEstimatedNetworkBytes()setMinimumNetworkChunkBytes()setPersisted()setNamespace()setRequiredNetwork()setRequiredNetworkType()setRequiresBatteryNotLow()setRequiresCharging()setRequiresStorageNotLow()
測試
The following list shows some steps on how to test your app's jobs manually:
- To get the job ID, get the value that is defined upon the job being built.
To run a job immediately, or to retry a stopped job, run the following command in a terminal window:
adb shell cmd jobscheduler run -f APP_PACKAGE_NAME JOB_ID
To simulate the system force-stopping a job (due to system health or out-of-quota conditions), run the following command in a terminal window:
adb shell cmd jobscheduler timeout TEST_APP_PACKAGE TEST_JOB_ID
另請參閱
其他資源
如要進一步瞭解使用者發起的資料轉移作業,請參閱下列其他資源:
- 使用者啟動的資料轉移整合案例研究:Google 地圖使用使用者啟動的資料轉移 API,將下載可靠性提升 10%