長時間かかる可能性のあるデータ転送を実行する必要がある場合は、JobScheduler ジョブを作成し、ユーザーが開始したデータ転送(UIDT)ジョブとして識別できます。UIDT ジョブは、デバイスユーザーが開始するデータ転送(リモート サーバーからのファイルのダウンロードなど)で、転送に時間がかかることが予想される場合を想定しています。UIDT ジョブは Android 14(API レベル 34)で導入されました。
ユーザーが開始するデータ転送ジョブは、ユーザーによって開始されます。これらのジョブは通知を必要とし、直ちに開始されるとともに、システム条件が許せば実行時間が延長される可能性があります。ユーザーが開始するデータ転送ジョブは、同時に複数実行できます。
ユーザーが開始するジョブのスケジュールは、アプリがユーザーに表示されている状態(または許可された条件のいずれかで)設定される必要があります。システムの健全性に関わる制約にもよりますが、すべての制約が満たされていれば、ユーザーが開始するジョブは OS で実行できます。また、システムは提供された推定ペイロード サイズを使用して、ジョブの実行にかかる時間を推定することもできます。
ユーザーが開始するデータ転送ジョブのスケジュールを設定する
ユーザーが開始するデータ転送ジョブを実行するには、次の操作を行います。
アプリのマニフェストで
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, 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();
ジョブの実行中に、
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.
システムによる停止
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()
テスト
アプリのジョブを手動でテストする方法のリストを次に示します。
- ジョブ ID を取得するには、ビルドされるジョブで定義されている値を取得します。
ジョブをすぐに実行する場合、または停止したジョブを再試行する場合は、ターミナル ウィンドウで次のコマンドを実行します。
adb shell cmd jobscheduler run -f APP_PACKAGE_NAME JOB_ID
システムの健全性や割り当て不足の状態により、ジョブが強制的に停止される場合をシミュレートするには、ターミナル ウィンドウで次のコマンドを実行します。
adb shell cmd jobscheduler timeout TEST_APP_PACKAGE TEST_JOB_ID
関連ドキュメント
参考情報
ユーザーが開始したデータ転送の詳細については、次のリソースをご覧ください。
- UIDT 統合に関する事例紹介: Google マップはユーザーが開始したデータ転送 API を使用してダウンロードの信頼性を 10% 向上させました