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.
排定使用者啟動的資料移轉作業
如要執行使用者啟動的資料移轉作業,請按照下列步驟操作:
請確認應用程式已在資訊清單中宣告
JobService
和相關聯的權限:<service android:name="com.example.app.CustomTransferService" android:permission="android.permission.BIND_JOB_SERVICE" android:exported="false"> ... </service>
此外,請為資料移轉定義
JobService
的具體子類別:Kotlin
class CustomTransferService : JobService() { ... }
Java
class CustomTransferService extends JobService() { .... }
在資訊清單中宣告
RUN_USER_INITIATED_JOBS
權限:<manifest ...> <uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS" /> <application ...> ... </application> </manifest>
建構
JobInfo
物件時,呼叫setUserInitiated()
方法。(這項方法適用於 Android 14 以上版本)。此外,建議您在建立工作時呼叫setEstimatedNetworkBytes()
,以提供酬載大小預估值。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) // ... .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) // ... .build();
執行工作時,請在
JobService
物件上呼叫setNotification()
。呼叫setNotification()
會告知使用者工作正在工作管理員和狀態列通知區域執行中。執行作業完成後,請呼叫
jobFinished()
以告知系統工作已經完成,或應重新為工作排程。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 } }
定期更新通知,讓使用者掌握工作狀態與進度。如果您無法在工作排程前確定轉移大小,或需要更新預估轉移大小,請使用新的 API
updateEstimatedNetworkBytes()
更新已知的轉移大小。
建議
如要有效執行 UIDT 作業,請採取下列做法:
明確定義網路限制和工作執行限制,指定工作執行時間。
在
onStartJob()
中以非同步方式執行工作;舉例來說,您可以使用協同程式執行這項操作。如果沒有非同步執行工作,工作會在主執行緒上執行,可能會封鎖主執行緒,進而導致 ANR。為避免工作執行時間過長,請在移轉完成時呼叫
jobFinished()
,無論移轉成功或失敗都一樣。這樣一來,工作就不會執行過久。如要瞭解工作停止的原因,請導入onStopJob()
回呼方法,並呼叫JobParameters.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.
由工作管理員的使用者執行
The user can stop a user-initiated data transfer job that appears in the Task Manager.
At the moment that the user presses Stop, the system does the following:
- Terminates your app's process immediately, including all other jobs or foreground services running.
- Doesn't call
onStopJob()
for any running jobs. - Prevents user-visible jobs from being rescheduled.
For these reasons, it's recommended to provide controls in the notification posted for the job to allow gracefully stopping and rescheduling the job.
Note that, under special circumstances, the Stop button doesn't appear next to the job in the Task Manager, or the job isn't shown in the Task Manager at all.
由系統執行
與一般工作不同的是,使用者啟動的資料移轉工作不會受到應用程式待命值區配額的影響。然而,若發生下列任一情況,系統也會停止工作:
- 某項限制不再符合開發人員定義的條件。
- 系統判定工作的執行時間比完成資料移轉工作所需的時間還要長。
- 系統會以系統健全狀態為優先考量,有鑑於溫度不斷升高,必須停止執行中的工作。
- 應用程式處理程序因裝置記憶體不足而終止。
如果系統因裝置記憶體不足以外的原因停止工作,系統會呼叫 onStopJob()
,並在系統認定的最佳時間點重新執行工作。請確認即使系統未呼叫 onStopJob()
,應用程式也可保持資料移轉狀態,並且確認再次呼叫 onStartJob()
後,應用程式可恢復此狀態。
可對使用者啟動的資料移轉作業進行排程的情況
應用程式必須在開放瀏覽權限的視窗中,或符合特定條件時,才能夠啟動使用者啟動的資料移轉作業:
- 如果應用程式可以從背景啟動活動,則也將可以從背景啟動使用者啟動的資料移轉作業。
- 如果應用程式只是在「近期活動」畫面的現有工作中,存在返回堆疊活動,則無法使系統允許執行使用者啟動的資料移轉作業。
如果排定在未滿足必要條件的時間執行作業,作業將會失敗,並傳回 RESULT_FAILURE
錯誤代碼。
使用者啟動的資料移轉作業許可的限制
To support jobs running at optimal points, Android offers the ability to assign constraints to each job type. These constraints are available as of Android 13.
Note: The following table only compares the constraints that vary between each job type. See JobScheduler developer page or work constraints for all constraints.
The following table shows the different job types that support a given job constraint, as well as the set of job constraints that WorkManager supports. Use the search bar before the table to filter the table by the name of a job constraint method.
These are the constraints allowed with user-initiated data transfer jobs:
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%