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)
+ }
+}