From 93e0e12c79c588917b7b406823682587028d41cc Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Fri, 30 Oct 2020 14:01:56 +0100
Subject: [PATCH] Dependency injection for workers (DEV) (#1503)

* Allow WorkerManager's workers to be injected.

* Remove static access for Playbook

* Add mock for playbook so dagger can create the worker factory map.
---
 Corona-Warn-App/build.gradle                  |  3 +
 .../coronawarnapp/CoronaWarnApplication.kt    |  8 +-
 .../nearby/ExposureStateUpdateWorker.kt       | 12 ++-
 .../util/di/ApplicationComponent.kt           |  5 +-
 .../util/worker/CWAWorkerFactory.kt           | 37 +++++++++
 .../util/worker/InjectedWorkerFactory.kt      |  9 +++
 .../util/worker/WorkManagerSetup.kt           | 28 +++++++
 .../coronawarnapp/util/worker/WorkerBinder.kt | 58 ++++++++++++++
 .../coronawarnapp/util/worker/WorkerKey.kt    | 10 +++
 .../worker/BackgroundNoiseOneTimeWorker.kt    | 18 +++--
 .../worker/BackgroundNoisePeriodicWorker.kt   | 15 ++--
 .../DiagnosisKeyRetrievalOneTimeWorker.kt     | 12 ++-
 .../DiagnosisKeyRetrievalPeriodicWorker.kt    | 12 ++-
 ...gnosisTestResultRetrievalPeriodicWorker.kt | 27 ++++---
 .../task/testtasks/timeout/BaseTimeoutTask.kt |  3 +-
 .../util/worker/WorkerBinderTest.kt           | 76 +++++++++++++++++++
 16 files changed, 296 insertions(+), 37 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/InjectedWorkerFactory.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerSetup.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerKey.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt

diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 0848d3372..5ed563edf 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -329,6 +329,7 @@ dependencies {
     implementation 'com.google.dagger:dagger-android-support:2.28.1'
     kapt 'com.google.dagger:dagger-compiler:2.28.1'
     kapt 'com.google.dagger:dagger-android-processor:2.28.1'
+    kaptTest 'com.google.dagger:dagger-compiler:2.28.1'
     kaptAndroidTest 'com.google.dagger:dagger-compiler:2.28.1'
 
     compileOnly 'com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2'
@@ -366,6 +367,8 @@ dependencies {
     androidTestImplementation "io.kotest:kotest-assertions-core-jvm:4.3.0"
     androidTestImplementation "io.kotest:kotest-property-jvm:4.3.0"
 
+    testImplementation "io.github.classgraph:classgraph:4.8.90"
+
     // Testing - Instrumentation
     androidTestImplementation 'junit:junit:4.13.1'
     androidTestImplementation 'androidx.test:runner:1.3.0'
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
index fa995ee84..473252103 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
@@ -7,8 +7,6 @@ import android.content.IntentFilter
 import android.os.Bundle
 import android.view.WindowManager
 import androidx.localbroadcastmanager.content.LocalBroadcastManager
-import androidx.work.Configuration
-import androidx.work.WorkManager
 import dagger.android.AndroidInjector
 import dagger.android.DispatchingAndroidInjector
 import dagger.android.HasAndroidInjector
@@ -22,6 +20,7 @@ import de.rki.coronawarnapp.util.ForegroundState
 import de.rki.coronawarnapp.util.WatchdogService
 import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.di.ApplicationComponent
+import de.rki.coronawarnapp.util.worker.WorkManagerSetup
 import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.flow.launchIn
@@ -42,6 +41,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
     @Inject lateinit var watchdogService: WatchdogService
     @Inject lateinit var taskController: TaskController
     @Inject lateinit var foregroundState: ForegroundState
+    @Inject lateinit var workManagerSetup: WorkManagerSetup
     @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree
 
     override fun onCreate() {
@@ -55,9 +55,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
         Timber.plant(rollingLogHistory)
 
         Timber.v("onCreate(): Initializing WorkManager")
-        Configuration.Builder()
-            .apply { setMinimumLoggingLevel(android.util.Log.DEBUG) }.build()
-            .let { WorkManager.initialize(this, it) }
+        workManagerSetup.setup()
 
         NotificationHelper.createNotificationChannel()
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt
index 8af906afe..9fb027c19 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt
@@ -5,16 +5,21 @@ import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
 import com.google.android.gms.common.api.ApiException
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.NoTokenException
 import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.storage.ExposureSummaryRepository
 import de.rki.coronawarnapp.transaction.RiskLevelTransaction
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import timber.log.Timber
 
-class ExposureStateUpdateWorker(val context: Context, workerParams: WorkerParameters) :
-    CoroutineWorker(context, workerParams) {
+class ExposureStateUpdateWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters
+) : CoroutineWorker(context, workerParams) {
     companion object {
         private val TAG = ExposureStateUpdateWorker::class.simpleName
     }
@@ -44,4 +49,7 @@ class ExposureStateUpdateWorker(val context: Context, workerParams: WorkerParame
 
         return Result.success()
     }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<ExposureStateUpdateWorker>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
index 1e6ce322a..ea9200f77 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
@@ -39,6 +39,7 @@ import de.rki.coronawarnapp.util.device.DeviceModule
 import de.rki.coronawarnapp.util.security.EncryptedPreferencesFactory
 import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool
 import de.rki.coronawarnapp.util.serialization.SerializationModule
+import de.rki.coronawarnapp.util.worker.WorkerBinder
 import de.rki.coronawarnapp.verification.VerificationModule
 import javax.inject.Singleton
 
@@ -66,8 +67,8 @@ import javax.inject.Singleton
         TaskModule::class,
         DeviceForTestersModule::class,
         BugReportingModule::class,
-        SerializationModule::class
-
+        SerializationModule::class,
+        WorkerBinder::class
     ]
 )
 interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt
new file mode 100644
index 000000000..9b491808f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt
@@ -0,0 +1,37 @@
+package de.rki.coronawarnapp.util.worker
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import dagger.Reusable
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Provider
+
+@Reusable
+class CWAWorkerFactory @Inject constructor(
+    private val factories: @JvmSuppressWildcards Map<
+        Class<out ListenableWorker>, Provider<InjectedWorkerFactory<out ListenableWorker>>
+        >
+) : WorkerFactory() {
+
+    init {
+        Timber.v("CWAWorkerFactory ready. Known factories: %s", factories)
+    }
+
+    override fun createWorker(
+        appContext: Context,
+        workerClassName: String,
+        workerParameters: WorkerParameters
+    ): ListenableWorker? {
+        Timber.v("Looking up worker for %s", workerClassName)
+        val factory = factories.entries.find {
+            Class.forName(workerClassName).isAssignableFrom(it.key)
+        }?.value
+
+        requireNotNull(factory) { "Unknown worker: $workerClassName" }
+        Timber.v("Creating worker for %s with %s", workerClassName, workerParameters)
+        return factory.get().create(appContext, workerParameters)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/InjectedWorkerFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/InjectedWorkerFactory.kt
new file mode 100644
index 000000000..1986ef02b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/InjectedWorkerFactory.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.util.worker
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerParameters
+
+interface InjectedWorkerFactory<T : ListenableWorker> {
+    fun create(context: Context, workerParams: WorkerParameters): T
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerSetup.kt
new file mode 100644
index 000000000..3e7c0ee22
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerSetup.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.util.worker
+
+import android.content.Context
+import androidx.work.Configuration
+import androidx.work.WorkManager
+import de.rki.coronawarnapp.util.di.AppContext
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class WorkManagerSetup @Inject constructor(
+    @AppContext private val context: Context,
+    private val cwaWorkerFactory: CWAWorkerFactory
+) {
+
+    fun setup() {
+        Timber.v("Setting up WorkManager.")
+        val configuration = Configuration.Builder().apply {
+            setMinimumLoggingLevel(android.util.Log.DEBUG)
+            setWorkerFactory(cwaWorkerFactory)
+        }.build()
+
+        WorkManager.initialize(context, configuration)
+
+        Timber.v("WorkManager setup done.")
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
new file mode 100644
index 000000000..7bae929f2
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
@@ -0,0 +1,58 @@
+package de.rki.coronawarnapp.util.worker
+
+import androidx.work.ListenableWorker
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker
+import de.rki.coronawarnapp.worker.BackgroundNoiseOneTimeWorker
+import de.rki.coronawarnapp.worker.BackgroundNoisePeriodicWorker
+import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker
+import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalPeriodicWorker
+import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker
+
+@Module
+abstract class WorkerBinder {
+
+    @Binds
+    @IntoMap
+    @WorkerKey(ExposureStateUpdateWorker::class)
+    abstract fun bindExposureStateUpdate(
+        factory: ExposureStateUpdateWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(BackgroundNoiseOneTimeWorker::class)
+    abstract fun backgroundNoiseOneTime(
+        factory: BackgroundNoiseOneTimeWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(BackgroundNoisePeriodicWorker::class)
+    abstract fun backgroundNoisePeriodic(
+        factory: BackgroundNoisePeriodicWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(DiagnosisKeyRetrievalOneTimeWorker::class)
+    abstract fun diagnosisKeyRetrievalOneTime(
+        factory: DiagnosisKeyRetrievalOneTimeWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(DiagnosisKeyRetrievalPeriodicWorker::class)
+    abstract fun diagnosisKeyRetrievalPeriodic(
+        factory: DiagnosisKeyRetrievalPeriodicWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(DiagnosisTestResultRetrievalPeriodicWorker::class)
+    abstract fun testResultRetrievalPeriodic(
+        factory: DiagnosisTestResultRetrievalPeriodicWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerKey.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerKey.kt
new file mode 100644
index 000000000..27d263540
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerKey.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.util.worker
+
+import androidx.work.ListenableWorker
+import dagger.MapKey
+import kotlin.reflect.KClass
+
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+@MapKey
+internal annotation class WorkerKey(val value: KClass<out ListenableWorker>)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt
index 724ac760c..4dfc187b4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt
@@ -3,22 +3,21 @@ package de.rki.coronawarnapp.worker
 import android.content.Context
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.playbook.Playbook
-import de.rki.coronawarnapp.util.di.AppInjector
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 
 /**
  * One time background noise worker
  *
  * @see BackgroundWorkScheduler
  */
-class BackgroundNoiseOneTimeWorker(
-    val context: Context,
-    workerParams: WorkerParameters
-) :
-    CoroutineWorker(context, workerParams) {
-
+class BackgroundNoiseOneTimeWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters,
     private val playbook: Playbook
-        get() = AppInjector.component.playbook
+) : CoroutineWorker(context, workerParams) {
 
     /**
      * Work execution
@@ -41,4 +40,7 @@ class BackgroundNoiseOneTimeWorker(
 
         return result
     }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<BackgroundNoiseOneTimeWorker>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt
index 3b9a93eb9..3869efb0a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt
@@ -3,7 +3,10 @@ package de.rki.coronawarnapp.worker
 import android.content.Context
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.storage.LocalData
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop
 import org.joda.time.DateTime
 import org.joda.time.DateTimeZone
@@ -14,11 +17,10 @@ import timber.log.Timber
  *
  * @see BackgroundWorkScheduler
  */
-class BackgroundNoisePeriodicWorker(
-    val context: Context,
-    workerParams: WorkerParameters
-) :
-    CoroutineWorker(context, workerParams) {
+class BackgroundNoisePeriodicWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters
+) : CoroutineWorker(context, workerParams) {
 
     companion object {
         private val TAG: String? = BackgroundNoisePeriodicWorker::class.simpleName
@@ -61,4 +63,7 @@ class BackgroundNoisePeriodicWorker(
     private fun stopWorker() {
         BackgroundWorkScheduler.WorkType.BACKGROUND_NOISE_PERIODIC_WORK.stop()
     }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<BackgroundNoisePeriodicWorker>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
index 95cf66a25..275708013 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
@@ -3,7 +3,10 @@ package de.rki.coronawarnapp.worker
 import android.content.Context
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import timber.log.Timber
 
 /**
@@ -12,8 +15,10 @@ import timber.log.Timber
  *
  * @see BackgroundWorkScheduler
  */
-class DiagnosisKeyRetrievalOneTimeWorker(val context: Context, workerParams: WorkerParameters) :
-    CoroutineWorker(context, workerParams) {
+class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters
+) : CoroutineWorker(context, workerParams) {
 
     /**
      * Work execution
@@ -59,4 +64,7 @@ class DiagnosisKeyRetrievalOneTimeWorker(val context: Context, workerParams: Wor
         Timber.d("$id: doWork() finished with %s", result)
         return result
     }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalOneTimeWorker>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
index f7baa0f08..f0dc4742c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
@@ -3,6 +3,9 @@ package de.rki.coronawarnapp.worker
 import android.content.Context
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import timber.log.Timber
 
 /**
@@ -12,8 +15,10 @@ import timber.log.Timber
  * @see BackgroundWorkScheduler
  * @see DiagnosisKeyRetrievalOneTimeWorker
  */
-class DiagnosisKeyRetrievalPeriodicWorker(val context: Context, workerParams: WorkerParameters) :
-    CoroutineWorker(context, workerParams) {
+class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters
+) : CoroutineWorker(context, workerParams) {
 
     /**
      * Work execution
@@ -60,4 +65,7 @@ class DiagnosisKeyRetrievalPeriodicWorker(val context: Context, workerParams: Wo
         Timber.d("$id: doWork() finished with %s", result)
         return result
     }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalPeriodicWorker>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
index 03543b63c..e89997277 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
@@ -4,6 +4,8 @@ import android.content.Context
 import androidx.core.app.NotificationCompat
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.notification.NotificationHelper
@@ -11,6 +13,7 @@ import de.rki.coronawarnapp.service.submission.SubmissionService
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.util.TimeAndDateExtensions
 import de.rki.coronawarnapp.util.formatter.TestResult
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop
 import timber.log.Timber
 
@@ -19,11 +22,10 @@ import timber.log.Timber
  *
  * @see BackgroundWorkScheduler
  */
-class DiagnosisTestResultRetrievalPeriodicWorker(
-    val context: Context,
-    workerParams: WorkerParameters
-) :
-    CoroutineWorker(context, workerParams) {
+class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters
+) : CoroutineWorker(context, workerParams) {
 
     companion object {
         private val TAG: String? = DiagnosisTestResultRetrievalPeriodicWorker::class.simpleName
@@ -44,13 +46,15 @@ class DiagnosisTestResultRetrievalPeriodicWorker(
 
         Timber.d("Background job started. Run attempt: $runAttemptCount")
         BackgroundWorkHelper.sendDebugNotification(
-            "TestResult Executing: Start", "TestResult started. Run attempt: $runAttemptCount ")
+            "TestResult Executing: Start", "TestResult started. Run attempt: $runAttemptCount "
+        )
 
         if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
             Timber.d("Background job failed after $runAttemptCount attempts. Rescheduling")
 
             BackgroundWorkHelper.sendDebugNotification(
-                "TestResult Executing: Failure", "TestResult failed with $runAttemptCount attempts")
+                "TestResult Executing: Failure", "TestResult failed with $runAttemptCount attempts"
+            )
 
             BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
             return Result.failure()
@@ -72,7 +76,8 @@ class DiagnosisTestResultRetrievalPeriodicWorker(
         }
 
         BackgroundWorkHelper.sendDebugNotification(
-            "TestResult Executing: End", "TestResult result: $result ")
+            "TestResult Executing: End", "TestResult result: $result "
+        )
 
         return result
     }
@@ -121,6 +126,10 @@ class DiagnosisTestResultRetrievalPeriodicWorker(
         BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop()
 
         BackgroundWorkHelper.sendDebugNotification(
-            "TestResult Stopped", "TestResult Stopped")
+            "TestResult Stopped", "TestResult Stopped"
+        )
     }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<DiagnosisTestResultRetrievalPeriodicWorker>
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/timeout/BaseTimeoutTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/timeout/BaseTimeoutTask.kt
index 7879b015a..d5e513abb 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/timeout/BaseTimeoutTask.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/timeout/BaseTimeoutTask.kt
@@ -9,7 +9,6 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.asFlow
 import timber.log.Timber
-import javax.inject.Inject
 import javax.inject.Provider
 
 abstract class BaseTimeoutTask : Task<DefaultProgress, TimeoutTaskResult> {
@@ -39,7 +38,7 @@ abstract class BaseTimeoutTask : Task<DefaultProgress, TimeoutTaskResult> {
         isCanceled = true
     }
 
-    abstract class Factory @Inject constructor(
+    abstract class Factory constructor(
         private val taskByDagger: Provider<BaseTimeoutTask>
     ) : TaskFactory<DefaultProgress, TimeoutTaskResult> {
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
new file mode 100644
index 000000000..1f41df012
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
@@ -0,0 +1,76 @@
+package de.rki.coronawarnapp.util.worker
+
+import androidx.work.ListenableWorker
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import de.rki.coronawarnapp.playbook.Playbook
+import de.rki.coronawarnapp.util.di.AssistedInjectModule
+import io.github.classgraph.ClassGraph
+import io.kotest.matchers.collections.shouldContainAll
+import io.mockk.mockk
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import timber.log.Timber
+import javax.inject.Provider
+import javax.inject.Singleton
+
+class WorkerBinderTest : BaseTest() {
+
+    /**
+     * If one of our factories is not part of the factory map provided to **[CWAWorkerFactory]**,
+     * then the lookup will fail and an exception thrown.
+     * This can't be checked at compile-time and may create subtle errors that will not immediately be caught.
+     *
+     * This test uses the ClassGraph library to scan our package, find all worker classes,
+     * and makes sure that they are all bound into our factory map.
+     * Creating a new factory that is not registered or removing one from **[WorkerBinder]**
+     * will cause this test to fail.
+     */
+    @Test
+    fun `all worker factory are bound into the factory map`() {
+        val component = DaggerWorkerTestComponent.factory().create()
+        val factories = component.factories
+
+        Timber.v("We know %d worker factories.", factories.size)
+        factories.keys.forEach {
+            Timber.v("Registered: ${it.name}")
+        }
+        require(component.factories.isNotEmpty())
+
+        val scanResult = ClassGraph()
+            .acceptPackages("de.rki.coronawarnapp")
+            .enableClassInfo()
+            .scan()
+
+        val ourWorkerClasses = scanResult
+            .getSubclasses("androidx.work.ListenableWorker")
+            .filterNot { it.name.startsWith("androidx.work") }
+
+        Timber.v("Our project contains %d worker classes.", ourWorkerClasses.size)
+        ourWorkerClasses.forEach { Timber.v("Existing: ${it.name}") }
+
+        val boundFactories = factories.keys.map { it.name }
+        val existingFactories = ourWorkerClasses.map { it.name }
+        boundFactories shouldContainAll existingFactories
+    }
+}
+
+@Singleton
+@Component(modules = [AssistedInjectModule::class, WorkerBinder::class, MockProvider::class])
+interface WorkerTestComponent {
+
+    val factories: @JvmSuppressWildcards Map<Class<out ListenableWorker>, Provider<InjectedWorkerFactory<out ListenableWorker>>>
+
+    @Component.Factory
+    interface Factory {
+        fun create(): WorkerTestComponent
+    }
+}
+
+@Module
+class MockProvider {
+    // For BackgroundNoiseOneTimeWorker
+    @Provides
+    fun playbook(): Playbook = mockk()
+}
-- 
GitLab