Add `Configuration.workerCoroutineContext` to control CoroutineContext for CoroutineWorker
Relnote: "`Configiration.workerCoroutineContext` was added to for control of dispatcher
where CoroutineWorker is executed. It helps to completely avoid usage of
`Dispatchers.Default` in WorkManager."
bug: 245353737
Test: ConfigurationExecutorsTest
Change-Id: Icd1b7755fc90ea71ac2e8509c0f21d3e60ca8d53
diff --git a/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
index 3fdbecb..147d2a9 100644
--- a/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
+++ b/work/work-multiprocess/src/androidTest/java/androidx/work/multiprocess/RemoteListenableWorkerTest.kt
@@ -239,6 +239,7 @@
0,
0,
mConfiguration.executor,
+ mConfiguration.workerCoroutineContext,
mTaskExecutor,
mConfiguration.workerFactory,
progressUpdater,
diff --git a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
index cdfda32f..5aa72cc 100644
--- a/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
+++ b/work/work-multiprocess/src/main/java/androidx/work/multiprocess/parcelable/ParcelableWorkerParameters.java
@@ -185,6 +185,7 @@
mRunAttemptCount,
mGeneration,
configuration.getExecutor(),
+ configuration.getWorkerCoroutineContext(),
taskExecutor,
configuration.getWorkerFactory(),
progressUpdater,
diff --git a/work/work-runtime/api/current.txt b/work/work-runtime/api/current.txt
index 6d1f114..452cc68 100644
--- a/work/work-runtime/api/current.txt
+++ b/work/work-runtime/api/current.txt
@@ -29,6 +29,7 @@
method public androidx.work.RunnableScheduler getRunnableScheduler();
method public androidx.core.util.Consumer<java.lang.Throwable>? getSchedulingExceptionHandler();
method public java.util.concurrent.Executor getTaskExecutor();
+ method public kotlin.coroutines.CoroutineContext getWorkerCoroutineContext();
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerExecutionExceptionHandler();
method public androidx.work.WorkerFactory getWorkerFactory();
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerInitializationExceptionHandler();
@@ -43,6 +44,7 @@
property public final androidx.work.RunnableScheduler runnableScheduler;
property public final androidx.core.util.Consumer<java.lang.Throwable>? schedulingExceptionHandler;
property public final java.util.concurrent.Executor taskExecutor;
+ property public final kotlin.coroutines.CoroutineContext workerCoroutineContext;
property public final androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? workerExecutionExceptionHandler;
property public final androidx.work.WorkerFactory workerFactory;
property public final androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? workerInitializationExceptionHandler;
@@ -65,6 +67,7 @@
method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler);
method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> schedulingExceptionHandler);
method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor taskExecutor);
+ method public androidx.work.Configuration.Builder setWorkerCoroutineContext(kotlinx.coroutines.CoroutineDispatcher dispatcher);
method public androidx.work.Configuration.Builder setWorkerExecutionExceptionHandler(androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo> workerExceptionHandler);
method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory workerFactory);
method public androidx.work.Configuration.Builder setWorkerInitializationExceptionHandler(androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo> workerExceptionHandler);
diff --git a/work/work-runtime/api/restricted_current.txt b/work/work-runtime/api/restricted_current.txt
index 6d1f114..452cc68 100644
--- a/work/work-runtime/api/restricted_current.txt
+++ b/work/work-runtime/api/restricted_current.txt
@@ -29,6 +29,7 @@
method public androidx.work.RunnableScheduler getRunnableScheduler();
method public androidx.core.util.Consumer<java.lang.Throwable>? getSchedulingExceptionHandler();
method public java.util.concurrent.Executor getTaskExecutor();
+ method public kotlin.coroutines.CoroutineContext getWorkerCoroutineContext();
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerExecutionExceptionHandler();
method public androidx.work.WorkerFactory getWorkerFactory();
method public androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? getWorkerInitializationExceptionHandler();
@@ -43,6 +44,7 @@
property public final androidx.work.RunnableScheduler runnableScheduler;
property public final androidx.core.util.Consumer<java.lang.Throwable>? schedulingExceptionHandler;
property public final java.util.concurrent.Executor taskExecutor;
+ property public final kotlin.coroutines.CoroutineContext workerCoroutineContext;
property public final androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? workerExecutionExceptionHandler;
property public final androidx.work.WorkerFactory workerFactory;
property public final androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo>? workerInitializationExceptionHandler;
@@ -65,6 +67,7 @@
method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler runnableScheduler);
method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable> schedulingExceptionHandler);
method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor taskExecutor);
+ method public androidx.work.Configuration.Builder setWorkerCoroutineContext(kotlinx.coroutines.CoroutineDispatcher dispatcher);
method public androidx.work.Configuration.Builder setWorkerExecutionExceptionHandler(androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo> workerExceptionHandler);
method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory workerFactory);
method public androidx.work.Configuration.Builder setWorkerInitializationExceptionHandler(androidx.core.util.Consumer<androidx.work.WorkerExceptionInfo> workerExceptionHandler);
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/ConfigurationExecutorsTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/ConfigurationExecutorsTest.kt
new file mode 100644
index 0000000..40e6d54
--- /dev/null
+++ b/work/work-runtime/src/androidTest/java/androidx/work/ConfigurationExecutorsTest.kt
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2024 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
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.work.impl.WorkManagerImpl
+import androidx.work.impl.constraints.trackers.Trackers
+import androidx.work.impl.testutils.TrackingWorkerFactory
+import androidx.work.testutils.GreedyScheduler
+import androidx.work.testutils.TestEnv
+import androidx.work.testutils.WorkManager
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executors
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.currentCoroutineContext
+import kotlinx.coroutines.runBlocking
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class ConfigurationExecutorsTest {
+ val workerFactory = TrackingWorkerFactory()
+ val executor = Executors.newSingleThreadExecutor {
+ Thread(it).also { thread -> thread.name = threadTestName }
+ }
+
+ @Test
+ fun testSetExecutor() = runBlocking {
+ val configuration = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .setExecutor(executor)
+ .build()
+ val env = TestEnv(configuration)
+ val trackers = Trackers(context = env.context, taskExecutor = env.taskExecutor)
+ val workManager = WorkManager(env, listOf(GreedyScheduler(env, trackers)), trackers)
+ WorkManagerImpl.setDelegate(workManager)
+
+ val blockingRequest = OneTimeWorkRequest.from(ThreadNameWorker::class.java)
+ workManager.enqueue(blockingRequest)
+ val blockingWorker = workerFactory.await(blockingRequest.id) as ThreadNameWorker
+ assertThat(blockingWorker.threadNameDeferred.await()).isEqualTo(threadTestName)
+
+ val coroutineRequest = OneTimeWorkRequest.from(CoroutineDispatcherWorker::class.java)
+ workManager.enqueue(coroutineRequest)
+ val coroutineWorker = workerFactory.await(coroutineRequest.id) as CoroutineDispatcherWorker
+
+ val coroutineDispatcher = coroutineWorker.coroutineDispatcherDeferred.await()
+ assertThat(coroutineDispatcher?.asExecutor()).isEqualTo(executor)
+ }
+
+ @Test
+ fun testSetWorkerCoroutineDispatcher() = runBlocking {
+ val dispatcher = executor.asCoroutineDispatcher()
+ val configuration = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .setWorkerCoroutineContext(dispatcher)
+ .build()
+ val env = TestEnv(configuration)
+ val trackers = Trackers(context = env.context, taskExecutor = env.taskExecutor)
+ val workManager = WorkManager(env, listOf(GreedyScheduler(env, trackers)), trackers)
+ WorkManagerImpl.setDelegate(workManager)
+
+ val blockingRequest = OneTimeWorkRequest.from(ThreadNameWorker::class.java)
+ workManager.enqueue(blockingRequest)
+ val blockingWorker = workerFactory.await(blockingRequest.id) as ThreadNameWorker
+ assertThat(blockingWorker.threadNameDeferred.await()).isEqualTo(threadTestName)
+
+ val coroutineRequest = OneTimeWorkRequest.from(CoroutineDispatcherWorker::class.java)
+ workManager.enqueue(coroutineRequest)
+ val coroutineWorker = workerFactory.await(coroutineRequest.id) as CoroutineDispatcherWorker
+
+ val coroutineDispatcher = coroutineWorker.coroutineDispatcherDeferred.await()
+ assertThat(coroutineDispatcher).isEqualTo(dispatcher)
+ }
+
+ @Test
+ fun testSetBoth() = runBlocking {
+ val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+ val configuration = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .setExecutor(executor)
+ .setWorkerCoroutineContext(dispatcher)
+ .build()
+ val env = TestEnv(configuration)
+ val trackers = Trackers(context = env.context, taskExecutor = env.taskExecutor)
+ val workManager = WorkManager(env, listOf(GreedyScheduler(env, trackers)), trackers)
+ WorkManagerImpl.setDelegate(workManager)
+
+ val blockingRequest = OneTimeWorkRequest.from(ThreadNameWorker::class.java)
+ workManager.enqueue(blockingRequest)
+ val blockingWorker = workerFactory.await(blockingRequest.id) as ThreadNameWorker
+ assertThat(blockingWorker.threadNameDeferred.await()).isEqualTo(threadTestName)
+
+ val coroutineRequest = OneTimeWorkRequest.from(CoroutineDispatcherWorker::class.java)
+ workManager.enqueue(coroutineRequest)
+ val coroutineWorker = workerFactory.await(coroutineRequest.id) as CoroutineDispatcherWorker
+
+ val coroutineDispatcher = coroutineWorker.coroutineDispatcherDeferred.await()
+ assertThat(coroutineDispatcher).isEqualTo(dispatcher)
+ }
+
+ @Test
+ fun testSetNeither() = runBlocking {
+ val configuration = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .build()
+ val env = TestEnv(configuration)
+ val trackers = Trackers(context = env.context, taskExecutor = env.taskExecutor)
+ val workManager = WorkManager(env, listOf(GreedyScheduler(env, trackers)), trackers)
+ WorkManagerImpl.setDelegate(workManager)
+
+ val coroutineRequest = OneTimeWorkRequest.from(CoroutineDispatcherWorker::class.java)
+ workManager.enqueue(coroutineRequest)
+ val coroutineWorker = workerFactory.await(coroutineRequest.id) as CoroutineDispatcherWorker
+
+ val coroutineDispatcher = coroutineWorker.coroutineDispatcherDeferred.await()
+ assertThat(coroutineDispatcher).isEqualTo(Dispatchers.Default)
+ }
+
+ @Test
+ fun testSetCoroutineContextOverride() = runBlocking {
+ val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+ val configuration = Configuration.Builder()
+ .setWorkerFactory(workerFactory)
+ .setExecutor(executor)
+ .setWorkerCoroutineContext(dispatcher)
+ .build()
+ val env = TestEnv(configuration)
+ val trackers = Trackers(context = env.context, taskExecutor = env.taskExecutor)
+ val workManager = WorkManager(env, listOf(GreedyScheduler(env, trackers)), trackers)
+ WorkManagerImpl.setDelegate(workManager)
+
+ val coroutineRequest = OneTimeWorkRequest.from(CoroutineContextOverridingWorker::class.java)
+ workManager.enqueue(coroutineRequest)
+ val coroutineWorker = workerFactory.await(coroutineRequest.id)
+ as CoroutineContextOverridingWorker
+
+ val coroutineDispatcher = coroutineWorker.coroutineDispatcherDeferred.await()
+ @Suppress("DEPRECATION")
+ assertThat(coroutineDispatcher).isEqualTo(coroutineWorker.coroutineContext)
+ }
+}
+
+private const val threadTestName = "configuration_test"
+
+class ThreadNameWorker(
+ context: Context,
+ workerParams: WorkerParameters
+) : Worker(context, workerParams) {
+ val threadNameDeferred = CompletableDeferred<String>()
+
+ override fun doWork(): Result {
+ threadNameDeferred.complete(Thread.currentThread().name)
+ return Result.success()
+ }
+}
+
+class CoroutineDispatcherWorker(
+ appContext: Context,
+ params: WorkerParameters
+) : CoroutineWorker(appContext, params) {
+ val coroutineDispatcherDeferred = CompletableDeferred<CoroutineDispatcher?>()
+
+ @OptIn(ExperimentalStdlibApi::class)
+ override suspend fun doWork(): Result {
+ coroutineDispatcherDeferred.complete(currentCoroutineContext()[CoroutineDispatcher])
+ return Result.success()
+ }
+}
+
+class CoroutineContextOverridingWorker(
+ appContext: Context,
+ params: WorkerParameters,
+) : CoroutineWorker(appContext, params) {
+ private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+ @Suppress("OVERRIDE_DEPRECATION")
+ override val coroutineContext: CoroutineDispatcher
+ get() = dispatcher
+
+ val coroutineDispatcherDeferred = CompletableDeferred<CoroutineDispatcher?>()
+
+ @OptIn(ExperimentalStdlibApi::class)
+ override suspend fun doWork(): Result {
+ coroutineDispatcherDeferred.complete(currentCoroutineContext()[CoroutineDispatcher])
+ return Result.success()
+ }
+}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java b/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
index 20c26d7..cd18321 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/DefaultWorkerFactoryTest.java
@@ -39,6 +39,8 @@
import java.util.concurrent.Executor;
+import kotlinx.coroutines.Dispatchers;
+
@RunWith(AndroidJUnit4.class)
public class DefaultWorkerFactoryTest extends DatabaseTest {
@@ -73,6 +75,7 @@
1,
0,
executor,
+ Dispatchers.getDefault(),
new WorkManagerTaskExecutor(executor),
mDefaultWorkerFactory,
mProgressUpdater,
@@ -101,6 +104,7 @@
1,
0,
executor,
+ Dispatchers.getDefault(),
new WorkManagerTaskExecutor(executor),
mDefaultWorkerFactory,
mProgressUpdater,
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt
index 29ce8c5..bc03195 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/DelegatingWorkerFactoryTest.kt
@@ -25,6 +25,7 @@
import androidx.work.worker.FailureWorker
import androidx.work.worker.TestWorker
import java.util.UUID
+import kotlinx.coroutines.Dispatchers
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.notNullValue
import org.hamcrest.MatcherAssert.assertThat
@@ -98,6 +99,7 @@
1,
0,
SynchronousExecutor(),
+ Dispatchers.Default,
WorkManagerTaskExecutor(SynchronousExecutor()),
factory,
progressUpdater,
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
index 3794ddb..9a680b7 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkForegroundRunnableTest.kt
@@ -34,6 +34,7 @@
import com.google.common.util.concurrent.ListenableFuture
import java.util.UUID
import java.util.concurrent.Executor
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.junit.Assert.fail
import org.junit.Before
@@ -168,6 +169,7 @@
1,
0,
executor,
+ Dispatchers.Default,
taskExecutor,
configuration.workerFactory,
progressUpdater,
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
index ddb9e31..8510b51 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkerWrapperTest.java
@@ -114,6 +114,8 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
+import kotlinx.coroutines.Dispatchers;
+
@RunWith(AndroidJUnit4.class)
public class WorkerWrapperTest extends DatabaseTest {
@@ -230,6 +232,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1007,6 +1010,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1036,6 +1040,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1056,6 +1061,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1085,6 +1091,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1115,6 +1122,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1149,6 +1157,7 @@
1,
0,
mSynchronousExecutor,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
@@ -1397,6 +1406,7 @@
1,
0,
executorService,
+ Dispatchers.getDefault(),
mWorkTaskExecutor,
mConfiguration.getWorkerFactory(),
mMockProgressUpdater,
diff --git a/work/work-runtime/src/main/java/androidx/work/Configuration.kt b/work/work-runtime/src/main/java/androidx/work/Configuration.kt
index ddcbc4d..8f33b1d 100644
--- a/work/work-runtime/src/main/java/androidx/work/Configuration.kt
+++ b/work/work-runtime/src/main/java/androidx/work/Configuration.kt
@@ -27,8 +27,14 @@
import java.util.concurrent.Executors
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.ContinuationInterceptor
+import kotlin.coroutines.CoroutineContext
import kotlin.math.max
import kotlin.math.min
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
/**
* The Configuration object used to customize [WorkManager] upon initialization.
@@ -44,6 +50,11 @@
val executor: Executor
/**
+ * The [CoroutineContext] used by [WorkManager] to execute [CoroutineWorker]s.
+ */
+ val workerCoroutineContext: CoroutineContext
+
+ /**
* The [Executor] used by [WorkManager] for all its internal business logic
*/
val taskExecutor: Executor
@@ -150,7 +161,19 @@
val isUsingDefaultTaskExecutor: Boolean
init {
- executor = builder.executor ?: createDefaultExecutor(isTaskExecutor = false)
+ val builderWorkerDispatcher = builder.workerContext
+
+ executor = builder.executor ?: builderWorkerDispatcher?.asExecutor()
+ ?: createDefaultExecutor(isTaskExecutor = false)
+
+ workerCoroutineContext = when {
+ builderWorkerDispatcher != null -> builderWorkerDispatcher
+ // we don't want simply always use executor.asCoroutineDispatcher()
+ // as compatibility measure
+ builder.executor != null -> executor.asCoroutineDispatcher()
+ else -> Dispatchers.Default
+ }
+
isUsingDefaultTaskExecutor = builder.taskExecutor == null
// This executor is used for *both* WorkManager's tasks and Room's query executor.
// So this should not be a single threaded executor. Writes will still be serialized
@@ -182,6 +205,7 @@
*/
class Builder {
internal var executor: Executor? = null
+ internal var workerContext: CoroutineContext? = null
internal var workerFactory: WorkerFactory? = null
internal var inputMergerFactory: InputMergerFactory? = null
internal var taskExecutor: Executor? = null
@@ -226,7 +250,7 @@
initializationExceptionHandler = configuration.initializationExceptionHandler
schedulingExceptionHandler = configuration.schedulingExceptionHandler
workerInitializationExceptionHandler =
- configuration.workerInitializationExceptionHandler
+ configuration.workerInitializationExceptionHandler
workerExecutionExceptionHandler = configuration.workerExecutionExceptionHandler
defaultProcessName = configuration.defaultProcessName
}
@@ -254,7 +278,10 @@
}
/**
- * Specifies a custom [Executor] for WorkManager.
+ * Specifies a custom [Executor] to run [Worker.doWork].
+ *
+ * If [setWorkerCoroutineContext] wasn't called then the [executor] will be used as
+ * [CoroutineDispatcher] to run [CoroutineWorker] as well.
*
* @param executor An [Executor] for running [Worker]s
* @return This [Builder] instance
@@ -265,6 +292,20 @@
}
/**
+ * Specifies a custom [CoroutineDispatcher] to run [CoroutineWorker.doWork].
+ *
+ * If [setExecutor] wasn't called then [dispatcher] will be used as [Executor]
+ * to run [Worker] as well.
+ *
+ * @param dispatcher A [CoroutineDispatcher] for running [CoroutineWorker]s
+ * @return This [Builder] instance
+ */
+ fun setWorkerCoroutineContext(dispatcher: CoroutineDispatcher): Builder {
+ this.workerContext = dispatcher
+ return this
+ }
+
+ /**
* Specifies a [Executor] which will be used by WorkManager for all its
* internal book-keeping.
*
@@ -512,7 +553,7 @@
}
}
-internal val DEFAULT_CONTENT_URI_TRIGGERS_WORKERS_LIMIT = 8
+internal const val DEFAULT_CONTENT_URI_TRIGGERS_WORKERS_LIMIT = 8
private fun createDefaultExecutor(isTaskExecutor: Boolean): Executor {
val factory = object : ThreadFactory {
@@ -529,3 +570,6 @@
factory
)
}
+
+private fun CoroutineContext?.asExecutor(): Executor? =
+ (this?.get(ContinuationInterceptor) as? CoroutineDispatcher)?.asExecutor()
diff --git a/work/work-runtime/src/main/java/androidx/work/CoroutineWorker.kt b/work/work-runtime/src/main/java/androidx/work/CoroutineWorker.kt
index d56aec9..b546f1c 100644
--- a/work/work-runtime/src/main/java/androidx/work/CoroutineWorker.kt
+++ b/work/work-runtime/src/main/java/androidx/work/CoroutineWorker.kt
@@ -18,33 +18,52 @@
import android.content.Context
import com.google.common.util.concurrent.ListenableFuture
+import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
+import kotlinx.coroutines.Runnable
/**
- * A [ListenableWorker] implementation that provides interop with Kotlin Coroutines. Override
+ * A [ListenableWorker] implementation that provides interop with Kotlin Coroutines. Override
* the [doWork] function to do your suspending work.
- * <p>
- * By default, CoroutineWorker runs on [Dispatchers.Default]; this can be modified by
- * overriding [coroutineContext].
+ *
+ * By default, CoroutineWorker runs on [Dispatchers.Default] if neither
+ * [Configuration.Builder.setExecutor] or [Configuration.Builder.setWorkerCoroutineContext]
+ * were set.
+ *
* <p>
* A CoroutineWorker is given a maximum of ten minutes to finish its execution and return a
- * [ListenableWorker.Result]. After this time has expired, the worker will be signalled to stop.
+ * [ListenableWorker.Result]. After this time has expired, the worker will be signalled to stop.
*/
public abstract class CoroutineWorker(
appContext: Context,
- params: WorkerParameters
+ private val params: WorkerParameters
) : ListenableWorker(appContext, params) {
/**
- * The coroutine context on which [doWork] will run. By default, this is [Dispatchers.Default].
+ * The coroutine context on which [doWork] will run.
+ *
+ * If this property is overridden then it takes precedent over [Configuration.executor] or
+ * [Configuration.workerCoroutineContext].
+ *
+ * By default, this is a dispatcher delegating to [Dispatchers.Default]
*/
@Deprecated(message = "use withContext(...) inside doWork() instead.")
- public open val coroutineContext: CoroutineDispatcher = Dispatchers.Default
+ public open val coroutineContext: CoroutineDispatcher = DeprecatedDispatcher
@Suppress("DEPRECATION")
public final override fun startWork(): ListenableFuture<Result> {
+ // if a developer didn't override coroutineContext property, then
+ // we use Dispatchers.Default directly.
+ // We can't fully implement delegating CoroutineDispatcher, because CoroutineDispatcher
+ // has experimental and internal apis.
+ val coroutineContext = if (coroutineContext != DeprecatedDispatcher) {
+ coroutineContext
+ } else {
+ params.workerContext
+ }
+
return launchFuture(coroutineContext + Job()) { doWork() }
}
@@ -107,4 +126,15 @@
public final override fun onStopped() {
super.onStopped()
}
+
+ private object DeprecatedDispatcher : CoroutineDispatcher() {
+ val dispatcher = Dispatchers.Default
+ override fun dispatch(context: CoroutineContext, block: Runnable) {
+ dispatcher.dispatch(context, block)
+ }
+
+ override fun isDispatchNeeded(context: CoroutineContext): Boolean {
+ return dispatcher.isDispatchNeeded(context)
+ }
+ }
}
diff --git a/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java b/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
index fa1d5dd..fa8f5a2 100644
--- a/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
+++ b/work/work-runtime/src/main/java/androidx/work/WorkerParameters.java
@@ -26,6 +26,8 @@
import androidx.annotation.RestrictTo;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
+import kotlin.coroutines.CoroutineContext;
+
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@@ -46,6 +48,7 @@
private @NonNull RuntimeExtras mRuntimeExtras;
private int mRunAttemptCount;
private @NonNull Executor mBackgroundExecutor;
+ private @NonNull CoroutineContext mWorkerContext;
private @NonNull TaskExecutor mWorkTaskExecutor;
private @NonNull WorkerFactory mWorkerFactory;
private @NonNull ProgressUpdater mProgressUpdater;
@@ -63,6 +66,7 @@
@IntRange(from = 0) int runAttemptCount,
@IntRange(from = 0) int generation,
@NonNull Executor backgroundExecutor,
+ @NonNull CoroutineContext workerContext,
@NonNull TaskExecutor workTaskExecutor,
@NonNull WorkerFactory workerFactory,
@NonNull ProgressUpdater progressUpdater,
@@ -74,6 +78,7 @@
mRunAttemptCount = runAttemptCount;
mGeneration = generation;
mBackgroundExecutor = backgroundExecutor;
+ mWorkerContext = workerContext;
mWorkTaskExecutor = workTaskExecutor;
mWorkerFactory = workerFactory;
mProgressUpdater = progressUpdater;
@@ -184,6 +189,13 @@
/**
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public @NonNull CoroutineContext getWorkerContext() {
+ return mWorkerContext;
+ }
+
+ /**
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public @NonNull TaskExecutor getTaskExecutor() {
return mWorkTaskExecutor;
}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
index fd7b5f6..9e1e1b47 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkerWrapper.kt
@@ -173,6 +173,7 @@
workSpec.runAttemptCount,
workSpec.generation,
configuration.executor,
+ configuration.workerCoroutineContext,
workTaskExecutor,
configuration.workerFactory,
WorkProgressUpdater(workDatabase, workTaskExecutor),
diff --git a/work/work-rxjava2/src/test/java/androidx/work/RxForegroundInfoTest.kt b/work/work-rxjava2/src/test/java/androidx/work/RxForegroundInfoTest.kt
index 99ae5f7..cd6c543 100644
--- a/work/work-rxjava2/src/test/java/androidx/work/RxForegroundInfoTest.kt
+++ b/work/work-rxjava2/src/test/java/androidx/work/RxForegroundInfoTest.kt
@@ -25,6 +25,7 @@
import io.reactivex.Single
import java.util.UUID
import java.util.concurrent.Executor
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -94,6 +95,7 @@
1,
0,
executor,
+ EmptyCoroutineContext,
RxWorkerTest.InstantWorkTaskExecutor(),
DefaultWorkerFactory,
progressUpdater,
diff --git a/work/work-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt b/work/work-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt
index 25e93b7..ada365e 100644
--- a/work/work-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt
+++ b/work/work-rxjava2/src/test/java/androidx/work/RxWorkerTest.kt
@@ -26,6 +26,7 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import kotlin.coroutines.EmptyCoroutineContext
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert
@@ -127,6 +128,7 @@
1,
0,
executor,
+ EmptyCoroutineContext,
InstantWorkTaskExecutor(),
DefaultWorkerFactory,
progressUpdater,
diff --git a/work/work-rxjava2/src/test/java/androidx/work/SetCompletableProgressTest.kt b/work/work-rxjava2/src/test/java/androidx/work/SetCompletableProgressTest.kt
index fec7eec..e6a60c4 100644
--- a/work/work-rxjava2/src/test/java/androidx/work/SetCompletableProgressTest.kt
+++ b/work/work-rxjava2/src/test/java/androidx/work/SetCompletableProgressTest.kt
@@ -22,6 +22,7 @@
import androidx.work.impl.utils.futures.SettableFuture
import java.util.UUID
import java.util.concurrent.Executor
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -64,6 +65,7 @@
1,
0,
executor,
+ EmptyCoroutineContext,
RxWorkerTest.InstantWorkTaskExecutor(),
DefaultWorkerFactory,
progressUpdater,
diff --git a/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxForegroundInfoTest.kt b/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxForegroundInfoTest.kt
index 51c70f6..35a6b31 100644
--- a/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxForegroundInfoTest.kt
+++ b/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxForegroundInfoTest.kt
@@ -31,6 +31,7 @@
import io.reactivex.rxjava3.core.Single
import java.util.UUID
import java.util.concurrent.Executor
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -100,6 +101,7 @@
1,
0,
executor,
+ EmptyCoroutineContext,
RxWorkerTest.InstantWorkTaskExecutor(),
DefaultWorkerFactory,
progressUpdater,
diff --git a/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxWorkerTest.kt b/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxWorkerTest.kt
index 9edd2de..4d497f3 100644
--- a/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxWorkerTest.kt
+++ b/work/work-rxjava3/src/test/java/androidx/work/rxjava3/RxWorkerTest.kt
@@ -32,6 +32,7 @@
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executor
import java.util.concurrent.TimeUnit
+import kotlin.coroutines.EmptyCoroutineContext
import org.hamcrest.CoreMatchers.`is`
import org.hamcrest.MatcherAssert.assertThat
import org.junit.Assert
@@ -133,6 +134,7 @@
1,
0,
executor,
+ EmptyCoroutineContext,
InstantWorkTaskExecutor(),
DefaultWorkerFactory,
progressUpdater,
diff --git a/work/work-rxjava3/src/test/java/androidx/work/rxjava3/SetCompletableProgressTest.kt b/work/work-rxjava3/src/test/java/androidx/work/rxjava3/SetCompletableProgressTest.kt
index 5bc2ab1..b528b562 100644
--- a/work/work-rxjava3/src/test/java/androidx/work/rxjava3/SetCompletableProgressTest.kt
+++ b/work/work-rxjava3/src/test/java/androidx/work/rxjava3/SetCompletableProgressTest.kt
@@ -27,6 +27,7 @@
import androidx.work.impl.utils.futures.SettableFuture
import java.util.UUID
import java.util.concurrent.Executor
+import kotlin.coroutines.EmptyCoroutineContext
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -69,6 +70,7 @@
1,
0,
executor,
+ EmptyCoroutineContext,
RxWorkerTest.InstantWorkTaskExecutor(),
DefaultWorkerFactory,
progressUpdater,
diff --git a/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java b/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java
index 8a65620..c06abc9 100644
--- a/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java
+++ b/work/work-testing/src/main/java/androidx/work/testing/TestListenableWorkerBuilder.java
@@ -42,6 +42,8 @@
import java.util.UUID;
import java.util.concurrent.Executor;
+import kotlinx.coroutines.Dispatchers;
+
/**
* Builds instances of {@link androidx.work.ListenableWorker} which can be used for testing.
*
@@ -321,6 +323,7 @@
mGeneration,
// This is unused for ListenableWorker
getExecutor(),
+ Dispatchers.getDefault(),
getTaskExecutor(),
mWorkerFactory,
getProgressUpdater(),