Skip to content
Snippets Groups Projects
Unverified Commit 93e0e12c authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

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.
parent be4c773d
No related branches found
No related tags found
No related merge requests found
Showing
with 296 additions and 37 deletions
......@@ -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'
......
......@@ -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()
......
......@@ -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>
}
......@@ -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> {
......
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)
}
}
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
}
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.")
}
}
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>
}
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>)
......@@ -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>
}
......@@ -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>
}
......@@ -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>
}
......@@ -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>
}
......@@ -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>
}
......@@ -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> {
......
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()
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment