[go: nahoru, domu]

Add a RemoteCoroutineWorker.

* Like `RemoteListenableWorker` but suspending.

Test: Added unit tests.
Relnote: Add a `RemoteCoroutineWorker` which is an implementation of [RemoteListenableWorker] that can bind to a remote process.
Change-Id: I30578ea87b8bbff82f8d5b70c6cf97a105b387f9
diff --git a/work/workmanager-multiprocess/api/current.txt b/work/workmanager-multiprocess/api/current.txt
index bcdaaae..e033a49 100644
--- a/work/workmanager-multiprocess/api/current.txt
+++ b/work/workmanager-multiprocess/api/current.txt
@@ -1,6 +1,14 @@
 // Signature format: 4.0
 package androidx.work.multiprocess {
 
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
   public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
     ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
diff --git a/work/workmanager-multiprocess/api/public_plus_experimental_current.txt b/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
index bcdaaae..e033a49 100644
--- a/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
+++ b/work/workmanager-multiprocess/api/public_plus_experimental_current.txt
@@ -1,6 +1,14 @@
 // Signature format: 4.0
 package androidx.work.multiprocess {
 
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
   public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
     ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
diff --git a/work/workmanager-multiprocess/api/restricted_current.txt b/work/workmanager-multiprocess/api/restricted_current.txt
index bcdaaae..e033a49 100644
--- a/work/workmanager-multiprocess/api/restricted_current.txt
+++ b/work/workmanager-multiprocess/api/restricted_current.txt
@@ -1,6 +1,14 @@
 // Signature format: 4.0
 package androidx.work.multiprocess {
 
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result> p);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
   public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
     ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
     method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
diff --git a/work/workmanager-multiprocess/build.gradle b/work/workmanager-multiprocess/build.gradle
index a81a41f..76bcadb 100644
--- a/work/workmanager-multiprocess/build.gradle
+++ b/work/workmanager-multiprocess/build.gradle
@@ -40,9 +40,11 @@
 }
 
 dependencies {
-    api project(":work:work-runtime")
-    implementation("androidx.room:room-runtime:2.2.5")
+    api project(":work:work-runtime-ktx")
+    api(KOTLIN_STDLIB)
+    api(KOTLIN_COROUTINES_ANDROID)
     api(GUAVA_LISTENABLE_FUTURE)
+    implementation("androidx.room:room-runtime:2.2.5")
     androidTestImplementation(KOTLIN_STDLIB)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_CORE)
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteCoroutineWorkerTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteCoroutineWorkerTest.kt
new file mode 100644
index 0000000..b9de545
--- /dev/null
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteCoroutineWorkerTest.kt
@@ -0,0 +1,168 @@
+/*
+ * 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 android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.work.Configuration
+import androidx.work.Data
+import androidx.work.OneTimeWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkRequest
+import androidx.work.impl.Processor
+import androidx.work.impl.Scheduler
+import androidx.work.impl.WorkDatabase
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.WorkerWrapper
+import androidx.work.impl.foreground.ForegroundProcessor
+import androidx.work.impl.utils.SerialExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.`when`
+import org.mockito.Mockito.mock
+import java.util.concurrent.Executor
+
+@RunWith(AndroidJUnit4::class)
+public class RemoteCoroutineWorkerTest {
+    private lateinit var mConfiguration: Configuration
+    private lateinit var mTaskExecutor: TaskExecutor
+    private lateinit var mScheduler: Scheduler
+    private lateinit var mProcessor: Processor
+    private lateinit var mForegroundProcessor: ForegroundProcessor
+    private lateinit var mWorkManager: WorkManagerImpl
+    private lateinit var mExecutor: Executor
+
+    // Necessary for the reified function
+    public lateinit var mContext: Context
+    public lateinit var mDatabase: WorkDatabase
+
+    @Before
+    public fun setUp() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        mContext = InstrumentationRegistry.getInstrumentation().context
+        mExecutor = Executor {
+            it.run()
+        }
+        mConfiguration = Configuration.Builder()
+            .setExecutor(mExecutor)
+            .setTaskExecutor(mExecutor)
+            .build()
+        mTaskExecutor = mock(TaskExecutor::class.java)
+        `when`(mTaskExecutor.backgroundExecutor).thenReturn(SerialExecutor(mExecutor))
+        `when`(mTaskExecutor.mainThreadExecutor).thenReturn(mExecutor)
+        mScheduler = mock(Scheduler::class.java)
+        mForegroundProcessor = mock(ForegroundProcessor::class.java)
+        mWorkManager = mock(WorkManagerImpl::class.java)
+        mDatabase = WorkDatabase.create(mContext, mExecutor, true)
+        val schedulers = listOf(mScheduler)
+        // Processor
+        mProcessor = Processor(mContext, mConfiguration, mTaskExecutor, mDatabase, schedulers)
+        // WorkManagerImpl
+        `when`(mWorkManager.configuration).thenReturn(mConfiguration)
+        `when`(mWorkManager.workTaskExecutor).thenReturn(mTaskExecutor)
+        `when`(mWorkManager.workDatabase).thenReturn(mDatabase)
+        `when`(mWorkManager.schedulers).thenReturn(schedulers)
+        `when`(mWorkManager.processor).thenReturn(mProcessor)
+        WorkManagerImpl.setDelegate(mWorkManager)
+    }
+
+    @Test
+    @MediumTest
+    public fun testRemoteSuccessWorker() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val request = buildRequest<RemoteSuccessWorker>()
+        val wrapper = buildWrapper(request)
+        wrapper.run()
+        wrapper.future.get()
+        val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+        assertEquals(workSpec.state, WorkInfo.State.SUCCEEDED)
+    }
+
+    @Test
+    @MediumTest
+    public fun testRemoteFailureWorker() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val request = buildRequest<RemoteFailureWorker>()
+        val wrapper = buildWrapper(request)
+        wrapper.run()
+        wrapper.future.get()
+        val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+        assertEquals(workSpec.state, WorkInfo.State.FAILED)
+    }
+
+    @Test
+    @MediumTest
+    public fun testRemoteRetryWorker() {
+        if (Build.VERSION.SDK_INT <= 27) {
+            // Exclude <= API 27, from tests because it causes a SIGSEGV.
+            return
+        }
+
+        val request = buildRequest<RemoteRetryWorker>()
+        val wrapper = buildWrapper(request)
+        wrapper.run()
+        wrapper.future.get()
+        val workSpec = mDatabase.workSpecDao().getWorkSpec(request.stringId)
+        assertEquals(workSpec.state, WorkInfo.State.ENQUEUED)
+    }
+
+    private inline fun <reified T : RemoteCoroutineWorker> buildRequest(): OneTimeWorkRequest {
+        val inputData = Data.Builder()
+            .putString(RemoteListenableWorker.ARGUMENT_PACKAGE_NAME, mContext.packageName)
+            .putString(
+                RemoteListenableWorker.ARGUMENT_CLASS_NAME,
+                RemoteWorkerService::class.java.name
+            )
+            .build()
+
+        val request = OneTimeWorkRequest.Builder(T::class.java)
+            .setInputData(inputData)
+            .build()
+
+        mDatabase.workSpecDao().insertWorkSpec(request.workSpec)
+        return request
+    }
+
+    private fun buildWrapper(request: WorkRequest): WorkerWrapper {
+        return WorkerWrapper.Builder(
+            mContext,
+            mConfiguration,
+            mTaskExecutor,
+            mForegroundProcessor,
+            mDatabase,
+            request.stringId
+        ).build()
+    }
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
index fcc9575..4571665 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteFailureWorker.kt
@@ -19,27 +19,21 @@
 import android.content.Context
 import androidx.work.Data
 import androidx.work.WorkerParameters
-import androidx.work.impl.utils.futures.SettableFuture
-import com.google.common.util.concurrent.ListenableFuture
+import androidx.work.workDataOf
+import kotlinx.coroutines.delay
 
-/**
- * A Remote Listenable Worker which always fails.
- */
-public class RemoteFailureWorker(context: Context, workerParameters: WorkerParameters) :
-    RemoteListenableWorker(context, workerParameters) {
-    override fun startRemoteWork(): ListenableFuture<Result> {
-        val future = SettableFuture.create<Result>()
-        val result = Result.failure(outputData())
-        future.set(result)
-        return future
+public class RemoteFailureWorker(
+    context: Context,
+    parameters: WorkerParameters
+) : RemoteCoroutineWorker(context, parameters) {
+    override suspend fun doRemoteWork(): Result {
+        delay(100)
+        return Result.failure(outputData())
     }
 
     public companion object {
         public fun outputData(): Data {
-            return Data.Builder()
-                .put("output_1", 1)
-                .put("output_2,", "test")
-                .build()
+            return workDataOf("failure" to true)
         }
     }
 }
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
index d490a33..283b0d7 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
@@ -166,4 +166,4 @@
             request.stringId
         ).build()
     }
-}
\ No newline at end of file
+}
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
index 68afb87b..d8ca8ab 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteRetryWorker.kt
@@ -18,18 +18,14 @@
 
 import android.content.Context
 import androidx.work.WorkerParameters
-import androidx.work.impl.utils.futures.SettableFuture
-import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.delay
 
-/**
- * A Remote Listenable Worker which always retries.
- */
-public class RemoteRetryWorker(context: Context, workerParameters: WorkerParameters) :
-    RemoteListenableWorker(context, workerParameters) {
-    override fun startRemoteWork(): ListenableFuture<Result> {
-        val future = SettableFuture.create<Result>()
-        val result = Result.retry()
-        future.set(result)
-        return future
+public class RemoteRetryWorker(
+    context: Context,
+    parameters: WorkerParameters
+) : RemoteCoroutineWorker(context, parameters) {
+    override suspend fun doRemoteWork(): Result {
+        delay(100)
+        return Result.retry()
     }
 }
diff --git a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
index 810f3ef..ee73724 100644
--- a/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
+++ b/work/workmanager-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteSuccessWorker.kt
@@ -19,27 +19,21 @@
 import android.content.Context
 import androidx.work.Data
 import androidx.work.WorkerParameters
-import androidx.work.impl.utils.futures.SettableFuture
-import com.google.common.util.concurrent.ListenableFuture
+import androidx.work.workDataOf
+import kotlinx.coroutines.delay
 
-/**
- * A Remote Listenable Worker which always succeeds.
- */
-public class RemoteSuccessWorker(context: Context, workerParameters: WorkerParameters) :
-    RemoteListenableWorker(context, workerParameters) {
-    override fun startRemoteWork(): ListenableFuture<Result> {
-        val future = SettableFuture.create<Result>()
-        val result = Result.success(outputData())
-        future.set(result)
-        return future
+public class RemoteSuccessWorker(
+    context: Context,
+    parameters: WorkerParameters
+) : RemoteCoroutineWorker(context, parameters) {
+    override suspend fun doRemoteWork(): Result {
+        delay(100)
+        return Result.success(outputData())
     }
 
     public companion object {
         public fun outputData(): Data {
-            return Data.Builder()
-                .put("output_1", 1)
-                .put("output_2,", "test")
-                .build()
+            return workDataOf("success" to true)
         }
     }
 }
diff --git a/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteCoroutineWorker.kt b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteCoroutineWorker.kt
new file mode 100644
index 0000000..538d062
--- /dev/null
+++ b/work/workmanager-multiprocess/src/main/java/androidx/work/multiprocess/RemoteCoroutineWorker.kt
@@ -0,0 +1,97 @@
+/*
+ * 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] is given a maximum of ten minutes to finish its execution and
+     * return a [androidx.work.ListenableWorker.Result]. Note that the 10 minute 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)
+    }
+}