RemoteCoroutineWorker.kt

/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.work.multiprocess

import android.content.Context
import androidx.work.Data
import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.impl.utils.futures.SettableFuture
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch

/**
 * An implementation of [RemoteListenableWorker] that can bind to a remote process.
 *
 * To be able to bind to a remote process, A [RemoteCoroutineWorker] needs additional
 * arguments as part of its input [androidx.work.Data].
 *
 * The arguments [RemoteListenableWorker.ARGUMENT_PACKAGE_NAME],
 * [RemoteListenableWorker.ARGUMENT_CLASS_NAME] are used to determine the [android.app.Service]
 * that the [RemoteCoroutineWorker] can bind to.
 *
 * [doRemoteWork] is then subsequently called in the process that the [android.app.Service] is
 * running in.
 */
public abstract class RemoteCoroutineWorker(context: Context, parameters: WorkerParameters) :
    RemoteListenableWorker(context, parameters) {

    private val job = Job()
    private val future: SettableFuture<Result> = SettableFuture.create()

    init {
        future.addListener(
            Runnable {
                if (future.isCancelled) {
                    job.cancel()
                }
            },
            taskExecutor.backgroundExecutor
        )
    }

    /**
     * Override this method to define the work that needs to run in the remote process.
     * [Dispatchers.Default] is the coroutine dispatcher being used when this method is called.
     *
     * A [RemoteCoroutineWorker] has a well defined
     * [execution window](https://d.android.com/reference/android/app/job/JobScheduler) to finish
     * its execution and return a [androidx.work.ListenableWorker.Result]. Note that the
     * execution window also includes the cost of binding to the remote process.
     */
    public abstract suspend fun doRemoteWork(): Result

    override fun startRemoteWork(): ListenableFuture<Result> {
        val scope = CoroutineScope(Dispatchers.Default + job)
        scope.launch {
            try {
                val result = doRemoteWork()
                future.set(result)
            } catch (exception: Throwable) {
                future.setException(exception)
            }
        }
        return future
    }

    /**
     * Updates the progress for the [RemoteCoroutineWorker]. This is a suspending function unlike
     * [setProgressAsync] API which returns a [ListenableFuture].
     *
     * @param data The progress [Data]
     */
    public suspend fun setProgress(data: Data) {
        setProgressAsync(data).await()
    }

    public final override fun onStopped() {
        super.onStopped()
        future.cancel(true)
    }
}