長時間実行ワーカーをサポートする

WorkManager 2.3.0-alpha02 には、長時間実行ワーカーに関する最高クラスのサポートが追加されています。 これにより、このワーカーが実行されている間は可能な限りプロセスを維持するよう WorkManager から OS に通知することができます。これらのワーカーは、10 分以上実行できます。この新機能のユースケースには、一括アップロードまたはダウンロード(非チャンク型のみ)、ML モデルのローカル処理、アプリユーザーにとって重要なタスクなどが含まれます。

内部的には、WorkManager がフォアグラウンド サービスを管理、実行して WorkRequest を実行し、同時に設定可能な通知を表示します。

現在、ListenableWorkersetForegroundAsync() API をサポートしており、CoroutineWorker は、サスペンドする setForeground() API をサポートしています。デベロッパーはこれらの API を使用して、WorkRequest が重要なタスクか(ユーザーの観点から)、長時間実行ワーカーかを指定できます。

2.3.0-alpha03 以降の WorkManager では、PendingIntent の作成も可能になりました。createCancelPendingIntent() API を使用すれば、新しい Android コンポーネントを登録せずにワーカーをキャンセルできます。これを setForegroundAsync() または setForeground() API と組み合わせて、Worker をキャンセルする通知アクション追加するために使用すれば特に便利です。

長時間実行ワーカーの作成と管理

Kotlin あるいは Java のどちらでコーディングするかによって、アプローチが多少異なります。

Kotlin

Kotlin を使用している場合、CoroutineWorker を使用する必要があります。setForegroundAsync() を使用する代わりに、そのメソッドのサスペンド バージョン setForeground() を使用できます。

class DownloadWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    private val notificationManager =
        context.getSystemService(Context.NOTIFICATION_SERVICE) as
                NotificationManager

    override suspend fun doWork(): Result {
        val inputUrl = inputData.getString(KEY_INPUT_URL)
                       ?: return Result.failure()
        val outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME)
                       ?: return Result.failure()
        // Mark the Worker as important
        val progress = "Starting Download"
        setForeground(createForegroundInfo(progress))
        download(inputUrl, outputFile)
        return Result.success()
    }

    private fun download(inputUrl: String, outputFile: String) {
        // Downloads a file and updates bytes read
        // Calls setForegroundInfo() periodically when it needs to update
        // the ongoing Notification
    }
    // Creates an instance of ForegroundInfo which can be used to update the
    // ongoing notification.
    private fun createForegroundInfo(progress: String): ForegroundInfo {
        val id = applicationContext.getString(R.string.notification_channel_id)
        val title = applicationContext.getString(R.string.notification_title)
        val cancel = applicationContext.getString(R.string.cancel_download)
        // This PendingIntent can be used to cancel the worker
        val intent = WorkManager.getInstance(applicationContext)
                .createCancelPendingIntent(getId())

        // Create a Notification channel if necessary
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel()
        }

        val notification = NotificationCompat.Builder(applicationContext, id)
            .setContentTitle(title)
            .setTicker(title)
            .setContentText(progress)
            .setSmallIcon(R.drawable.ic_work_notification)
            .setOngoing(true)
            // Add the cancel action to the notification which can
            // be used to cancel the worker
            .addAction(android.R.drawable.ic_delete, cancel, intent)
            .build()

        return ForegroundInfo(notification)
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private fun createChannel() {
        // Create a Notification channel
    }

    companion object {
        const val KEY_INPUT_URL = "KEY_INPUT_URL"
        const val KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME"
    }
}

Java

ListenableWorker または Worker を使用している場合、ListenableFuture<Void> を返す setForegroundAsync() API を呼び出すことができます。setForegroundAsync() を呼び出すと、進行中の Notification も更新できます。

以下に、ファイルをダウンロードする長時間実行ワーカーの簡単な例を示します。このワーカーはダウンロードの進行状況を追跡し、その状況を示す Notification を継続的に更新します。

public class DownloadWorker extends Worker {
    private static final String KEY_INPUT_URL = "KEY_INPUT_URL";
    private static final String KEY_OUTPUT_FILE_NAME = "KEY_OUTPUT_FILE_NAME";

    private NotificationManager notificationManager;

    public DownloadWorker(
        @NonNull Context context,
        @NonNull WorkerParameters parameters) {
            super(context, parameters);
            notificationManager = (NotificationManager)
                context.getSystemService(NOTIFICATION_SERVICE);
    }

    @NonNull
    @Override
    public Result doWork() {
        Data inputData = getInputData();
        String inputUrl = inputData.getString(KEY_INPUT_URL);
        String outputFile = inputData.getString(KEY_OUTPUT_FILE_NAME);
        // Mark the Worker as important
        String progress = "Starting Download";
        setForegroundAsync(createForegroundInfo(progress));
        download(inputUrl, outputFile);
        return Result.success();
    }

    private void download(String inputUrl, String outputFile) {
        // Downloads a file and updates bytes read
        // Calls setForegroundAsync(createForegroundInfo(myProgress))
        // periodically when it needs to update the ongoing Notification.
    }

    @NonNull
    private ForegroundInfo createForegroundInfo(@NonNull String progress) {
        // Build a notification using bytesRead and contentLength

        Context context = getApplicationContext();
        String id = context.getString(R.string.notification_channel_id);
        String title = context.getString(R.string.notification_title);
        String cancel = context.getString(R.string.cancel_download);
        // This PendingIntent can be used to cancel the worker
        PendingIntent intent = WorkManager.getInstance(context)
                .createCancelPendingIntent(getId());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel();
        }

        Notification notification = new NotificationCompat.Builder(context, id)
                .setContentTitle(title)
                .setTicker(title)
                .setSmallIcon(R.drawable.ic_work_notification)
                .setOngoing(true)
                // Add the cancel action to the notification which can
                // be used to cancel the worker
                .addAction(android.R.drawable.ic_delete, cancel, intent)
                .build();

        return new ForegroundInfo(notification);
    }

    @RequiresApi(Build.VERSION_CODES.O)
    private void createChannel() {
        // Create a Notification channel
    }
}

長時間実行ワーカーにフォアグラウンド サービス タイプを追加する

Android 10(API レベル 29)以降をターゲットとし、位置情報へのアクセスを必要とする長時間実行ワーカーを含むアプリの場合は、そのワーカーが locationフォアグラウンド サービス タイプを使用していることを宣言します。また、Android 11(API レベル 30)以降をターゲットとし、カメラまたはマイクへのアクセスを必要とする長時間実行ワーカーを含むアプリの場合は、それぞれ camera または microphone のフォアグラウンド サービス タイプを宣言します。

これらのフォアグラウンド サービス タイプを追加するには、以下のセクションで説明する手順を実行します。

アプリ マニフェストでフォアグラウンド サービス タイプを宣言する

アプリのマニフェストで、ワーカーのフォアグラウンド サービス タイプを宣言します。次の例では、ワーカーは位置情報とマイクへのアクセスを必要とします。

AndroidManifest.xml

<service
    android:name="androidx.work.impl.foreground.SystemForegroundService"
    android:foregroundServiceType="location|microphone"
    tools:node="merge" />

フォアグラウンド サービス タイプをランタイムに指定する

次のコード スニペットで示すように、setForeground() または setForegroundAsync() を呼び出す際に、フォアグラウンド サービス タイプ(FOREGROUND_SERVICE_TYPE_LOCATIONFOREGROUND_SERVICE_TYPE_CAMERA、または FOREGROUND_SERVICE_TYPE_MICROPHONE)を指定します。

MyLocationAndMicrophoneWorker

Kotlin

private fun createForegroundInfo(progress: String): ForegroundInfo {
    // ...
    return ForegroundInfo(NOTIFICATION_ID, notification,
            FOREGROUND_SERVICE_TYPE_LOCATION or FOREGROUND_SERVICE_TYPE_MICROPHONE)
}

Java

@NonNull
private ForegroundInfo createForegroundInfo(@NonNull String progress) {
    // Build a notification...
    Notification notification = ...;
    return new ForegroundInfo(NOTIFICATION_ID, notification,
            FOREGROUND_SERVICE_TYPE_LOCATION | FOREGROUND_SERVICE_TYPE_MICROPHONE);
}