diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt
index 76a5bb2f119b9ec2556b0d9e9c6d52291b407cb8..6771827fa9cab7f2cc67ea55cdb6a0c537a6f3ec 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt
@@ -2,13 +2,15 @@ package de.rki.coronawarnapp.test
 
 import android.content.Context
 import android.text.format.Formatter
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.TransactionException
-import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionFinished
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionStarted
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadFinished
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadStarted
 import de.rki.coronawarnapp.risk.RiskLevelTask
 import de.rki.coronawarnapp.task.Task
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
+import de.rki.coronawarnapp.task.submitAndListen
 import de.rki.coronawarnapp.util.di.AppInjector
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.map
@@ -39,7 +41,7 @@ class RiskLevelAndKeyRetrievalBenchmark(
         var resultInfo = StringBuilder()
             .append(
                 "MEASUREMENT Running for Countries:\n " +
-                        "${countries.joinToString(", ")}\n\n"
+                    "${countries.joinToString(", ")}\n\n"
             )
             .append("Result: \n\n")
             .append("#\t Combined \t Download \t Sub \t Risk \t File # \t  F. size\n")
@@ -56,20 +58,16 @@ class RiskLevelAndKeyRetrievalBenchmark(
             var keyFilesSize: Long = -1
             var apiSubmissionDuration: Long = -1
 
-            try {
-                measureDiagnosticKeyRetrieval(
-                    label = "#$index",
-                    countries = countries,
-                    downloadFinished = { duration, keyCount, totalFileSize ->
-                        keyFileCount = keyCount
-                        keyFileDownloadDuration = duration
-                        keyFilesSize = totalFileSize
-                    }, apiSubmissionFinished = { duration ->
-                        apiSubmissionDuration = duration
-                    })
-            } catch (e: TransactionException) {
-                keyRetrievalError = e.message.toString()
-            }
+            measureDiagnosticKeyRetrieval(
+                label = "#$index",
+                countries = countries,
+                downloadFinished = { duration, keyCount, totalFileSize ->
+                    keyFileCount = keyCount
+                    keyFileDownloadDuration = duration
+                    keyFilesSize = totalFileSize
+                }, apiSubmissionFinished = { duration ->
+                    apiSubmissionDuration = duration
+                })
 
             var calculationDuration: Long = -1
             var calculationError = ""
@@ -137,32 +135,27 @@ class RiskLevelAndKeyRetrievalBenchmark(
         var keyFileDownloadStart: Long = -1
         var apiSubmissionStarted: Long = -1
 
-        try {
-            RetrieveDiagnosisKeysTransaction.onKeyFilesDownloadStarted = {
-                Timber.v("MEASURE [Diagnostic Key Files] $label started")
-                keyFileDownloadStart = System.currentTimeMillis()
-            }
-
-            RetrieveDiagnosisKeysTransaction.onKeyFilesDownloadFinished = { count, size ->
-                Timber.v("MEASURE [Diagnostic Key Files] $label finished")
-                val duration = System.currentTimeMillis() - keyFileDownloadStart
-                downloadFinished(duration, count, size)
-            }
-
-            RetrieveDiagnosisKeysTransaction.onApiSubmissionStarted = {
-                apiSubmissionStarted = System.currentTimeMillis()
-            }
-
-            RetrieveDiagnosisKeysTransaction.onApiSubmissionFinished = {
-                val duration = System.currentTimeMillis() - apiSubmissionStarted
-                apiSubmissionFinished(duration)
+        AppInjector.component.taskController.submitAndListen(
+            DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(countries))
+        ).collect { progress: Task.Progress ->
+            when (progress) {
+                is KeyFilesDownloadStarted -> {
+                    Timber.v("MEASURE [Diagnostic Key Files] $label started")
+                    keyFileDownloadStart = System.currentTimeMillis()
+                }
+                is KeyFilesDownloadFinished -> {
+                    Timber.v("MEASURE [Diagnostic Key Files] $label finished")
+                    val duration = System.currentTimeMillis() - keyFileDownloadStart
+                    downloadFinished(duration, progress.keyCount, progress.fileSize)
+                }
+                is ApiSubmissionStarted -> {
+                    apiSubmissionStarted = System.currentTimeMillis()
+                }
+                is ApiSubmissionFinished -> {
+                    val duration = System.currentTimeMillis() - apiSubmissionStarted
+                    apiSubmissionFinished(duration)
+                }
             }
-
-            // start diagnostic key transaction
-            RetrieveDiagnosisKeysTransaction.start(countries)
-        } catch (e: TransactionException) {
-            e.report(ExceptionCategory.INTERNAL)
-            throw e
         }
     }
 }
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
index add8047c5e6ceb4138286da92f5ebf02ec1bf4d0..d0a85e7f0f7f54678a5258bb921b8bca8595726b 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt
@@ -9,6 +9,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureInformation
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
@@ -24,7 +25,7 @@ import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.RiskLevelRepository
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
+import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider
 import de.rki.coronawarnapp.util.KeyFileHelper
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
@@ -76,13 +77,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
     }
 
     fun retrieveDiagnosisKeys() {
-        viewModelScope.launch {
-            try {
-                RetrieveDiagnosisKeysTransaction.start()
-                calculateRiskLevel()
-            } catch (e: Exception) {
-                e.report(ExceptionCategory.INTERNAL)
-            }
+        launch {
+            taskController.submitBlocking(
+                DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments())
+            )
+            calculateRiskLevel()
         }
     }
 
@@ -174,9 +173,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
                         "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" +
                         "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" +
                         "Attenuation Durations: [${
-                        exposureSummary.attenuationDurationsInMinutes?.get(
-                            0
-                        )
+                            exposureSummary.attenuationDurationsInMinutes?.get(
+                                0
+                            )
                         }," +
                         "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," +
                         "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" +
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DownloadDiagnosisKeysTaskModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DownloadDiagnosisKeysTaskModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..626496d130889ccf34e081534774ccd3d5f421c6
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DownloadDiagnosisKeysTaskModule.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.diagnosiskeys
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.TaskTypeKey
+
+@Module
+abstract class DownloadDiagnosisKeysTaskModule {
+
+    @Binds
+    @IntoMap
+    @TaskTypeKey(DownloadDiagnosisKeysTask::class)
+    abstract fun downloadDiagnosisKeysTaskFactory(
+        factory: DownloadDiagnosisKeysTask.Factory
+    ): TaskFactory<out Task.Progress, out Task.Result>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..552dc370b440abca3609f95419dc10db966cac08
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
@@ -0,0 +1,232 @@
+package de.rki.coronawarnapp.diagnosiskeys.download
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.environment.EnvironmentSetup
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
+import de.rki.coronawarnapp.risk.RollbackItem
+import de.rki.coronawarnapp.storage.LocalData
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskCancellationException
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.ui.toLazyString
+import de.rki.coronawarnapp.worker.BackgroundWorkHelper
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import org.joda.time.DateTime
+import org.joda.time.DateTimeZone
+import org.joda.time.Duration
+import timber.log.Timber
+import java.io.File
+import java.util.Date
+import java.util.UUID
+import javax.inject.Inject
+import javax.inject.Provider
+
+class DownloadDiagnosisKeysTask @Inject constructor(
+    private val enfClient: ENFClient,
+    private val environmentSetup: EnvironmentSetup,
+    private val appConfigProvider: AppConfigProvider,
+    private val keyFileDownloader: KeyFileDownloader,
+    private val timeStamper: TimeStamper
+) : Task<DownloadDiagnosisKeysTask.Progress, Task.Result> {
+
+    private val internalProgress = ConflatedBroadcastChannel<Progress>()
+    override val progress: Flow<Progress> = internalProgress.asFlow()
+
+    private var isCanceled = false
+
+    override suspend fun run(arguments: Task.Arguments): Task.Result {
+        val rollbackItems = mutableListOf<RollbackItem>()
+        try {
+            Timber.d("Running with arguments=%s", arguments)
+            arguments as Arguments
+
+            if (arguments.withConstraints) {
+                if (!noKeysFetchedToday()) return object : Task.Result {}
+            }
+
+            /**
+             * Handles the case when the ENClient got disabled but the Task is still scheduled
+             * in a background job. Also it acts as a failure catch in case the orchestration code did
+             * not check in before.
+             */
+            if (!InternalExposureNotificationClient.asyncIsEnabled()) {
+                Timber.tag(TAG).w("EN is not enabled, skipping RetrieveDiagnosisKeys")
+                return object : Task.Result {}
+            }
+
+            checkCancel()
+            val currentDate = Date(timeStamper.nowUTC.millis)
+            Timber.tag(TAG).d("Using $currentDate as current date in task.")
+
+            /****************************************************
+             * RETRIEVE TOKEN
+             ****************************************************/
+            val token = retrieveToken(rollbackItems)
+            checkCancel()
+
+            // RETRIEVE RISK SCORE PARAMETERS
+            val exposureConfiguration = appConfigProvider.getAppConfig().exposureDetectionConfiguration
+
+            internalProgress.send(Progress.ApiSubmissionStarted)
+            internalProgress.send(Progress.KeyFilesDownloadStarted)
+
+            val requestedCountries = arguments.requestedCountries
+            val availableKeyFiles = getAvailableKeyFiles(requestedCountries)
+            checkCancel()
+
+            val totalFileSize = availableKeyFiles.fold(0L, { acc, file ->
+                file.length() + acc
+            })
+
+            internalProgress.send(
+                Progress.KeyFilesDownloadFinished(
+                    availableKeyFiles.size,
+                    totalFileSize
+                )
+            )
+
+            Timber.tag(TAG).d("Attempting submission to ENF")
+            val isSubmissionSuccessful = enfClient.provideDiagnosisKeys(
+                keyFiles = availableKeyFiles,
+                configuration = exposureConfiguration,
+                token = token
+            )
+            Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", isSubmissionSuccessful, token)
+
+            internalProgress.send(Progress.ApiSubmissionFinished)
+            checkCancel()
+
+            if (isSubmissionSuccessful) {
+                saveTimestamp(currentDate, rollbackItems)
+            }
+
+            return object : Task.Result {}
+        } catch (error: Exception) {
+            Timber.tag(TAG).e(error)
+
+            rollback(rollbackItems)
+
+            throw error
+        } finally {
+            Timber.i("Finished (isCanceled=$isCanceled).")
+            internalProgress.close()
+        }
+    }
+
+    private fun saveTimestamp(
+        currentDate: Date,
+        rollbackItems: MutableList<RollbackItem>
+    ) {
+        val lastFetchDateForRollback = LocalData.lastTimeDiagnosisKeysFromServerFetch()
+        rollbackItems.add {
+            LocalData.lastTimeDiagnosisKeysFromServerFetch(lastFetchDateForRollback)
+        }
+        Timber.tag(TAG).d("dateUpdate(currentDate=%s)", currentDate)
+        LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate)
+    }
+
+    private fun retrieveToken(rollbackItems: MutableList<RollbackItem>): String {
+        val googleAPITokenForRollback = LocalData.googleApiToken()
+        rollbackItems.add {
+            LocalData.googleApiToken(googleAPITokenForRollback)
+        }
+        return UUID.randomUUID().toString().also {
+            LocalData.googleApiToken(it)
+        }
+    }
+
+    private fun noKeysFetchedToday(): Boolean {
+        val currentDate = DateTime(timeStamper.nowUTC, DateTimeZone.UTC)
+        val lastFetch = DateTime(
+            LocalData.lastTimeDiagnosisKeysFromServerFetch(),
+            DateTimeZone.UTC
+        )
+        return (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
+            currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()).also {
+            if (it) {
+                Timber.tag(TAG)
+                    .d("No keys fetched today yet (last=%s, now=%s)", lastFetch, currentDate)
+                BackgroundWorkHelper.sendDebugNotification(
+                    "Start Task",
+                    "No keys fetched today yet \n${DateTime.now()}\nUTC: $currentDate"
+                )
+            }
+        }
+    }
+
+    private fun rollback(rollbackItems: MutableList<RollbackItem>) {
+        try {
+            Timber.tag(TAG).d("Initiate Rollback")
+            for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke()
+        } catch (rollbackException: Exception) {
+            Timber.tag(TAG).e(rollbackException, "Rollback failed.")
+        }
+    }
+
+    private suspend fun getAvailableKeyFiles(requestedCountries: List<String>?): List<File> {
+        val availableKeyFiles =
+            keyFileDownloader.asyncFetchKeyFiles(if (environmentSetup.useEuropeKeyPackageFiles) {
+                listOf("EUR")
+            } else {
+                requestedCountries
+                    ?: appConfigProvider.getAppConfig().supportedCountries
+            }.map { LocationCode(it) })
+
+        if (availableKeyFiles.isEmpty()) {
+            Timber.tag(TAG).w("No keyfiles were available!")
+        }
+        return availableKeyFiles
+    }
+
+    private fun checkCancel() {
+        if (isCanceled) throw TaskCancellationException()
+    }
+
+    override suspend fun cancel() {
+        Timber.w("cancel() called.")
+        isCanceled = true
+    }
+
+    sealed class Progress : Task.Progress {
+        object ApiSubmissionStarted : Progress()
+        object ApiSubmissionFinished : Progress()
+
+        object KeyFilesDownloadStarted : Progress()
+        data class KeyFilesDownloadFinished(val keyCount: Int, val fileSize: Long) : Progress()
+
+        override val primaryMessage = this::class.java.simpleName.toLazyString()
+    }
+
+    class Arguments(
+        val requestedCountries: List<String>? = null,
+        val withConstraints: Boolean = false
+    ) : Task.Arguments
+
+    data class Config(
+        @Suppress("MagicNumber")
+        override val executionTimeout: Duration = Duration.standardMinutes(8), // TODO unit-test that not > 9 min
+
+        override val collisionBehavior: TaskFactory.Config.CollisionBehavior =
+            TaskFactory.Config.CollisionBehavior.ENQUEUE
+
+    ) : TaskFactory.Config
+
+    class Factory @Inject constructor(
+        private val taskByDagger: Provider<DownloadDiagnosisKeysTask>
+    ) : TaskFactory<Progress, Task.Result> {
+
+        override val config: TaskFactory.Config = Config()
+        override val taskProvider: () -> Task<Progress, Task.Result> = {
+            taskByDagger.get()
+        }
+    }
+
+    companion object {
+        private val TAG: String? = DownloadDiagnosisKeysTask::class.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt
index cfd3f3f408fbc76bc5138c4498e8245be8cebfc4..0961774da36df652d96f19c151f3332eb883f233 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt
@@ -1,11 +1,11 @@
 package de.rki.coronawarnapp.service.submission
 
-import de.rki.coronawarnapp.exception.NoGUIDOrTANSetException
 import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException
 import de.rki.coronawarnapp.playbook.BackgroundNoise
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.SubmissionRepository
+import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.formatter.TestResult
 import de.rki.coronawarnapp.verification.server.VerificationKeyType
@@ -16,41 +16,34 @@ object SubmissionService {
     private val playbook: Playbook
         get() = AppInjector.component.playbook
 
-    suspend fun asyncRegisterDevice() {
-        val testGUID = LocalData.testGUID()
-        val testTAN = LocalData.teletan()
+    private val timeStamper: TimeStamper
+        get() = TimeStamper()
 
-        when {
-            testGUID != null -> asyncRegisterDeviceViaGUID(testGUID)
-            testTAN != null -> asyncRegisterDeviceViaTAN(testTAN)
-            else -> throw NoGUIDOrTANSetException()
-        }
-        LocalData.devicePairingSuccessfulTimestamp(System.currentTimeMillis())
-        BackgroundNoise.getInstance().scheduleDummyPattern()
-    }
-
-    private suspend fun asyncRegisterDeviceViaGUID(guid: String) {
+    suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult {
         val (registrationToken, testResult) =
             playbook.initialRegistration(
                 guid,
                 VerificationKeyType.GUID
             )
-
         LocalData.registrationToken(registrationToken)
         deleteTestGUID()
         SubmissionRepository.updateTestResult(testResult)
+        LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis)
+        BackgroundNoise.getInstance().scheduleDummyPattern()
+        return testResult
     }
 
-    private suspend fun asyncRegisterDeviceViaTAN(tan: String) {
+    suspend fun asyncRegisterDeviceViaTAN(tan: String) {
         val (registrationToken, testResult) =
             playbook.initialRegistration(
                 tan,
                 VerificationKeyType.TELETAN
             )
-
         LocalData.registrationToken(registrationToken)
         deleteTeleTAN()
         SubmissionRepository.updateTestResult(testResult)
+        LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis)
+        BackgroundNoise.getInstance().scheduleDummyPattern()
     }
 
     suspend fun asyncRequestTestResult(): TestResult {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt
index b52252c0819b0bdd2bf6ce1355cd8785825a2fa5..86baa1239d015ffc5bddc26f18034b7cd98e9523 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt
@@ -1,8 +1,8 @@
 package de.rki.coronawarnapp.storage
 
-import de.rki.coronawarnapp.CoronaWarnApplication
+import android.content.Context
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
 import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
@@ -11,11 +11,12 @@ import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPe
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.TaskInfo
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.timer.TimerHelper
 import de.rki.coronawarnapp.tracing.TracingProgress
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
 import de.rki.coronawarnapp.util.ConnectivityHelper
 import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.util.di.AppContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -35,11 +36,11 @@ import javax.inject.Singleton
  *
  * @see LocalData
  * @see InternalExposureNotificationClient
- * @see RetrieveDiagnosisKeysTransaction
  * @see RiskLevelRepository
  */
 @Singleton
 class TracingRepository @Inject constructor(
+    @AppContext private val context: Context,
     @AppScope private val scope: CoroutineScope,
     private val taskController: TaskController,
     enfClient: ENFClient
@@ -88,18 +89,18 @@ class TracingRepository @Inject constructor(
      * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a
      * successful go through from the RetrievelDiagnosisKeysTransaction.
      *
-     * @see RetrieveDiagnosisKeysTransaction
      * @see RiskLevelRepository
      */
     fun refreshDiagnosisKeys() {
         scope.launch {
             retrievingDiagnosisKeys.value = true
-            try {
-                RetrieveDiagnosisKeysTransaction.start()
-                taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
-            } catch (e: Exception) {
-                e.report(ExceptionCategory.EXPOSURENOTIFICATION)
-            }
+            taskController.submitBlocking(
+                DefaultTaskRequest(
+                    DownloadDiagnosisKeysTask::class,
+                    DownloadDiagnosisKeysTask.Arguments()
+                )
+            )
+            taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
             refreshLastTimeDiagnosisKeysFetchedDate()
             retrievingDiagnosisKeys.value = false
             TimerHelper.startManualKeyRetrievalTimer()
@@ -121,59 +122,53 @@ class TracingRepository @Inject constructor(
     /**
      * Launches the RetrieveDiagnosisKeysTransaction and RiskLevelTransaction in the viewModel scope
      *
-     * @see RiskLevelTransaction
      * @see RiskLevelRepository
      */
     // TODO temp place, this needs to go somewhere better
     fun refreshRiskLevel() {
-        scope.launch {
-            try {
 
-                // get the current date and the date the diagnosis keys were fetched the last time
-                val currentDate = DateTime(Instant.now(), DateTimeZone.UTC)
-                val lastFetch = DateTime(
-                    LocalData.lastTimeDiagnosisKeysFromServerFetch(),
-                    DateTimeZone.UTC
-                )
+        // get the current date and the date the diagnosis keys were fetched the last time
+        val currentDate = DateTime(Instant.now(), DateTimeZone.UTC)
+        val lastFetch = DateTime(
+            LocalData.lastTimeDiagnosisKeysFromServerFetch(),
+            DateTimeZone.UTC
+        )
 
-                // check if the keys were not already retrieved today
-                val keysWereNotRetrievedToday =
-                    LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
-                        currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()
-
-                // check if the network is enabled to make the server fetch
-                val isNetworkEnabled =
-                    ConnectivityHelper.isNetworkEnabled(CoronaWarnApplication.getAppContext())
-
-                // only fetch the diagnosis keys if background jobs are enabled, so that in manual
-                // model the keys are only fetched on button press of the user
-                val isBackgroundJobEnabled =
-                    ConnectivityHelper.autoModeEnabled(CoronaWarnApplication.getAppContext())
-
-                Timber.tag(TAG)
-                    .v("Keys were not retrieved today $keysWereNotRetrievedToday")
-                Timber.tag(TAG).v("Network is enabled $isNetworkEnabled")
-                Timber.tag(TAG)
-                    .v("Background jobs are enabled $isBackgroundJobEnabled")
-
-                if (keysWereNotRetrievedToday && isNetworkEnabled && isBackgroundJobEnabled) {
-                    // TODO shouldn't access this directly
-                    retrievingDiagnosisKeys.value = true
-
-                    // start the fetching and submitting of the diagnosis keys
-                    RetrieveDiagnosisKeysTransaction.start()
-                    refreshLastTimeDiagnosisKeysFetchedDate()
-                    TimerHelper.checkManualKeyRetrievalTimer()
-                }
-            } catch (e: TransactionException) {
-                e.cause?.report(ExceptionCategory.INTERNAL)
-            } catch (e: Exception) {
-                e.report(ExceptionCategory.INTERNAL)
-            }
+        // check if the keys were not already retrieved today
+        val keysWereNotRetrievedToday =
+            LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
+                currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()
 
-            taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
+        // check if the network is enabled to make the server fetch
+        val isNetworkEnabled = ConnectivityHelper.isNetworkEnabled(context)
+
+        // only fetch the diagnosis keys if background jobs are enabled, so that in manual
+        // model the keys are only fetched on button press of the user
+        val isBackgroundJobEnabled = ConnectivityHelper.autoModeEnabled(context)
+
+        Timber.tag(TAG).v("Keys were not retrieved today $keysWereNotRetrievedToday")
+        Timber.tag(TAG).v("Network is enabled $isNetworkEnabled")
+        Timber.tag(TAG).v("Background jobs are enabled $isBackgroundJobEnabled")
+
+        if (keysWereNotRetrievedToday && isNetworkEnabled && isBackgroundJobEnabled) {
             // TODO shouldn't access this directly
-            retrievingDiagnosisKeys.value = false
+            retrievingDiagnosisKeys.value = true
+
+            // start the fetching and submitting of the diagnosis keys
+            scope.launch {
+                taskController.submitBlocking(
+                    DefaultTaskRequest(
+                        DownloadDiagnosisKeysTask::class,
+                        DownloadDiagnosisKeysTask.Arguments()
+                    )
+                )
+                refreshLastTimeDiagnosisKeysFetchedDate()
+                TimerHelper.checkManualKeyRetrievalTimer()
+
+                taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
+                // TODO shouldn't access this directly
+                retrievingDiagnosisKeys.value = false
+            }
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskControllerExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskControllerExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2166ace118dc16417307a1f2ec6775de7656e69e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskControllerExtensions.kt
@@ -0,0 +1,30 @@
+package de.rki.coronawarnapp.task
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flatMapMerge
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import timber.log.Timber
+
+suspend fun TaskController.submitBlocking(ourRequest: TaskRequest) =
+    tasks.flatMapMerge { it.asFlow() }.map { it.taskState }.onStart {
+        submit(ourRequest)
+        Timber.v("submitBlocking(request=%s) waiting for result...", ourRequest)
+    }.first {
+        it.request.id == ourRequest.id && it.isFinished
+    }.also {
+        Timber.v("submitBlocking(request=%s) continuing with result %s", ourRequest, it)
+    }
+
+suspend fun TaskController.submitAndListen(ourRequest: TaskRequest): Flow<Task.Progress> {
+    submit(ourRequest)
+    Timber.v("submitAndListen(request=%s) waiting for progress flow...", ourRequest)
+
+    return tasks.flatMapMerge { it.asFlow() }.first {
+        it.taskState.request.id == ourRequest.id
+    }.progress.also {
+        Timber.v("submitAndListen(request=%s) continuing with flow %s", ourRequest, it)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt
deleted file mode 100644
index 5d118d7e7e252f580cd9d87ebd0dfb642f9364e7..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.rki.coronawarnapp.transaction
-
-import de.rki.coronawarnapp.environment.EnvironmentSetup
-import de.rki.coronawarnapp.nearby.ENFClient
-import de.rki.coronawarnapp.util.GoogleAPIVersion
-import javax.inject.Inject
-import javax.inject.Singleton
-
-// TODO Remove once we have refactored the transaction and it's no longer a singleton
-@Singleton
-data class RetrieveDiagnosisInjectionHelper @Inject constructor(
-    val transactionScope: TransactionCoroutineScope,
-    val googleAPIVersion: GoogleAPIVersion,
-    val cwaEnfClient: ENFClient,
-    val environmentSetup: EnvironmentSetup
-)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
deleted file mode 100644
index 493bb0c9e41d74b398d64b5c83012a72da296d5f..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt
+++ /dev/null
@@ -1,342 +0,0 @@
-/******************************************************************************
- * Corona-Warn-App                                                            *
- *                                                                            *
- * SAP SE and all other contributors /                                        *
- * copyright owners license this file to you 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 de.rki.coronawarnapp.transaction
-
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
-import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader
-import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
-import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
-import de.rki.coronawarnapp.environment.EnvironmentSetup
-import de.rki.coronawarnapp.nearby.ENFClient
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
-import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.API_SUBMISSION
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.CLOSE
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.FETCH_DATE_UPDATE
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.FILES_FROM_WEB_REQUESTS
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.RETRIEVE_RISK_SCORE_PARAMS
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.SETUP
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.TOKEN
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.rollback
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.start
-import de.rki.coronawarnapp.util.CWADebug
-import de.rki.coronawarnapp.util.di.AppInjector
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
-import org.joda.time.Instant
-import timber.log.Timber
-import java.io.File
-import java.util.Date
-import java.util.UUID
-import java.util.concurrent.atomic.AtomicReference
-
-/**
- * The RetrieveDiagnosisKeysTransaction is used to define an atomic Transaction for Key Retrieval. Its states allow an
- * isolated work area that can recover from failures and keep a consistent key state even through an
- * unclear, potentially dangerous state within the transaction itself. It is guaranteed that the keys used in the
- * transaction will be successfully retrieved from the Google API and accepted by the server and that the transaction
- * has completed its work after completing the [start] coroutine.
- *
- * It has to be noted that this is not a real, but a transient transaction that does not have an explicit commit stage.
- * As such we do not define an execution plan, but rather commit in each transaction state and do a [rollback] based
- * on given state. (given that we do not operate on a database layer but on a business logic requiring states to be run
- * asynchronously and in a distributed context)
- *
- * There is currently a simple rollback behavior defined for this transaction. This means that persisted files up until
- * that point are deleted. Also, The last fetch date will be rolled back on best effort base and the Google API token
- * reset if necessary.
- *
- * The Transaction undergoes multiple States:
- * 1. [SETUP]
- * 2. [TOKEN]
- * 3. [RETRIEVE_RISK_SCORE_PARAMS]
- * 4. [FILES_FROM_WEB_REQUESTS]
- * 5. [API_SUBMISSION]
- * 6. [FETCH_DATE_UPDATE]
- * 7. [CLOSE]
- *
- * This transaction is special in terms of concurrent entry-calls (e.g. calling the transaction again before it closes and
- * releases its internal mutex. The transaction will not queue up like a normal mutex, but instead completely omit the last
- * execution. Execution Privilege is First In.
- *
- * @see Transaction
- *
- * @throws de.rki.coronawarnapp.exception.TransactionException An Exception thrown when an error occurs during Transaction Execution
- * @throws de.rki.coronawarnapp.exception.RollbackException An Exception thrown when an error occurs during Rollback of the Transaction
- */
-object RetrieveDiagnosisKeysTransaction : Transaction() {
-
-    override val TAG: String? = RetrieveDiagnosisKeysTransaction::class.simpleName
-
-    /** possible transaction states */
-    private enum class RetrieveDiagnosisKeysTransactionState :
-        TransactionState {
-        /** Initial Setup of the Transaction and Transaction ID Generation and Date Lock */
-        SETUP,
-
-        /** Initialisation of the identifying token used during the entire transaction */
-        TOKEN,
-
-        /** Retrieval of Risk Score Parameters used for the Key Submission to the Google API */
-        RETRIEVE_RISK_SCORE_PARAMS,
-
-        /** Retrieval of actual Key Files based on the URLs */
-        FILES_FROM_WEB_REQUESTS,
-
-        /** Submission of parsed KeyFiles into the Google API */
-        API_SUBMISSION,
-
-        /** Update of the Fetch Date to reflect a complete Transaction State */
-        FETCH_DATE_UPDATE,
-
-        /** Transaction Closure */
-        CLOSE
-    }
-
-    /** atomic reference for the rollback value for the last fetch date */
-    private val lastFetchDateForRollback = AtomicReference<Date>()
-
-    /** atomic reference for the rollback value for the google api */
-    private val googleAPITokenForRollback = AtomicReference<String>()
-
-    /** atomic reference for the rollback value for created files during the transaction */
-    private val exportFilesForRollback = AtomicReference<List<File>>()
-
-    private val transactionScope: TransactionCoroutineScope by lazy {
-        AppInjector.component.transRetrieveKeysInjection.transactionScope
-    }
-    private val keyCacheRepository: KeyCacheRepository by lazy {
-        AppInjector.component.keyCacheRepository
-    }
-    private val keyFileDownloader: KeyFileDownloader by lazy {
-        AppInjector.component.keyFileDownloader
-    }
-
-    var onApiSubmissionStarted: (() -> Unit)? = null
-    var onApiSubmissionFinished: (() -> Unit)? = null
-
-    var onKeyFilesDownloadStarted: (() -> Unit)? = null
-    var onKeyFilesDownloadFinished: ((keyCount: Int, fileSize: Long) -> Unit)? = null
-
-    private val enfClient: ENFClient
-        get() = AppInjector.component.transRetrieveKeysInjection.cwaEnfClient
-
-    private val environmentSetup: EnvironmentSetup
-        get() = AppInjector.component.transRetrieveKeysInjection.environmentSetup
-
-    suspend fun startWithConstraints() {
-        val currentDate = DateTime(Instant.now(), DateTimeZone.UTC)
-        val lastFetch = DateTime(
-            LocalData.lastTimeDiagnosisKeysFromServerFetch(),
-            DateTimeZone.UTC
-        )
-        if (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
-            currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()
-        ) {
-            Timber.tag(TAG).d("No keys fetched today yet (last=%s, now=%s)", lastFetch, currentDate)
-            BackgroundWorkHelper.sendDebugNotification(
-                "Start RetrieveDiagnosisKeysTransaction",
-                "No keys fetched today yet \n${DateTime.now()}\nUTC: $currentDate"
-            )
-            start()
-        }
-    }
-
-    /** initiates the transaction. This suspend function guarantees a successful transaction once completed.
-     * @param requestedCountries defines which countries (country codes) should be used. If not filled the
-     * country codes will be loaded from the ApplicationConfigurationService
-     */
-    suspend fun start(
-        requestedCountries: List<String>? = null
-    ) = lockAndExecute(unique = true, scope = transactionScope) {
-
-        /**
-         * Handles the case when the ENClient got disabled but the Transaction is still scheduled
-         * in a background job. Also it acts as a failure catch in case the orchestration code did
-         * not check in before.
-         */
-        if (!InternalExposureNotificationClient.asyncIsEnabled()) {
-            Timber.tag(TAG).w("EN is not enabled, skipping RetrieveDiagnosisKeys")
-            executeClose()
-            return@lockAndExecute
-        }
-        /****************************************************
-         * INIT TRANSACTION
-         ****************************************************/
-        val currentDate = executeSetup()
-
-        /****************************************************
-         * RETRIEVE TOKEN
-         ****************************************************/
-        val token = executeToken()
-
-        // RETRIEVE RISK SCORE PARAMETERS
-        val exposureConfiguration = executeRetrieveRiskScoreParams()
-
-        val countries = if (environmentSetup.useEuropeKeyPackageFiles) {
-            listOf("EUR")
-        } else {
-            requestedCountries
-                ?: AppInjector.component.appConfigProvider.getAppConfig().supportedCountries
-        }
-        invokeSubmissionStartedInDebugOrBuildMode()
-
-        val availableKeyFiles = executeFetchKeyFilesFromServer(countries)
-
-        if (availableKeyFiles.isEmpty()) {
-            Timber.tag(TAG).w("No keyfiles were available!")
-        }
-
-        if (CWADebug.isDebugBuildOrMode) {
-            val totalFileSize = availableKeyFiles.fold(0L, { acc, file ->
-                file.length() + acc
-            })
-
-            onKeyFilesDownloadFinished?.invoke(availableKeyFiles.size, totalFileSize)
-            onKeyFilesDownloadFinished = null
-            invokeSubmissionStartedInDebugOrBuildMode()
-        }
-
-        val isSubmissionSuccessful = executeAPISubmission(
-            exportFiles = availableKeyFiles,
-            exposureConfiguration = exposureConfiguration,
-            token = token
-        )
-
-        invokeSubmissionFinishedInDebugOrBuildMode()
-
-        if (isSubmissionSuccessful) executeFetchDateUpdate(currentDate)
-
-        executeClose()
-    }
-
-    private fun invokeSubmissionStartedInDebugOrBuildMode() {
-        if (CWADebug.isDebugBuildOrMode) {
-            onApiSubmissionStarted?.invoke()
-            onApiSubmissionStarted = null
-        }
-    }
-
-    private fun invokeSubmissionFinishedInDebugOrBuildMode() {
-        if (CWADebug.isDebugBuildOrMode) {
-            onApiSubmissionFinished?.invoke()
-            onApiSubmissionFinished = null
-        }
-    }
-
-    override suspend fun rollback() {
-        super.rollback()
-        try {
-            if (SETUP.isInStateStack()) {
-                rollbackSetup()
-            }
-            if (TOKEN.isInStateStack()) {
-                rollbackToken()
-            }
-        } catch (e: Exception) {
-            // We handle every exception through a RollbackException to make sure that a single EntryPoint
-            // is available for the caller.
-            handleRollbackError(e)
-        }
-    }
-
-    private fun rollbackSetup() {
-        Timber.tag(TAG).v("rollback $SETUP")
-        LocalData.lastTimeDiagnosisKeysFromServerFetch(lastFetchDateForRollback.get())
-    }
-
-    private fun rollbackToken() {
-        Timber.tag(TAG).v("rollback $TOKEN")
-        LocalData.googleApiToken(googleAPITokenForRollback.get())
-    }
-
-    /**
-     * Executes the INIT Transaction State
-     */
-    private suspend fun executeSetup() = executeState(SETUP) {
-        lastFetchDateForRollback.set(LocalData.lastTimeDiagnosisKeysFromServerFetch())
-        val currentDate = Date(System.currentTimeMillis())
-        Timber.tag(TAG).d("using $currentDate as current date in Transaction.")
-        currentDate
-    }
-
-    /**
-     * Executes the TOKEN Transaction State
-     */
-    private suspend fun executeToken() = executeState(TOKEN) {
-        googleAPITokenForRollback.set(LocalData.googleApiToken())
-        val tempToken = UUID.randomUUID().toString()
-        LocalData.googleApiToken(tempToken)
-        return@executeState tempToken
-    }
-
-    /**
-     * Executes the RETRIEVE_RISK_CORE_PARAMS Transaction State
-     */
-    private suspend fun executeRetrieveRiskScoreParams() =
-        executeState(RETRIEVE_RISK_SCORE_PARAMS) {
-            AppInjector.component.appConfigProvider.getAppConfig().exposureDetectionConfiguration
-        }
-
-    /**
-     * Executes the WEB_REQUESTS Transaction State
-     */
-    private suspend fun executeFetchKeyFilesFromServer(
-        countries: List<String>
-    ) = executeState(FILES_FROM_WEB_REQUESTS) {
-        val locationCodes = countries.map { LocationCode(it) }
-        keyFileDownloader.asyncFetchKeyFiles(locationCodes)
-    }
-
-    private suspend fun executeAPISubmission(
-        token: String,
-        exportFiles: Collection<File>,
-        exposureConfiguration: ExposureConfiguration?
-    ): Boolean = executeState(API_SUBMISSION) {
-        Timber.tag(TAG).d("Attempting submission to ENF")
-        val success = enfClient.provideDiagnosisKeys(
-            keyFiles = exportFiles,
-            configuration = exposureConfiguration,
-            token = token
-        )
-        Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", success, token)
-        return@executeState success
-    }
-
-    /**
-     * Executes the FETCH_DATE_UPDATE Transaction State
-     */
-    private suspend fun executeFetchDateUpdate(
-        currentDate: Date
-    ) = executeState(FETCH_DATE_UPDATE) {
-        Timber.tag(TAG).d("executeFetchDateUpdate(currentDate=%s)", currentDate)
-        LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate)
-    }
-
-    /**
-     * Executes the CLOSE Transaction State
-     */
-    private suspend fun executeClose() = executeState(CLOSE) {
-        exportFilesForRollback.set(null)
-        lastFetchDateForRollback.set(null)
-        googleAPITokenForRollback.set(null)
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt
deleted file mode 100644
index e206e1a2a8b7148c6ae6306fb392e9556571a456..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt
+++ /dev/null
@@ -1,289 +0,0 @@
-/******************************************************************************
- * Corona-Warn-App                                                            *
- *                                                                            *
- * SAP SE and all other contributors /                                        *
- * copyright owners license this file to you 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 de.rki.coronawarnapp.transaction
-
-import de.rki.coronawarnapp.BuildConfig
-import de.rki.coronawarnapp.exception.RollbackException
-import de.rki.coronawarnapp.exception.TransactionException
-import de.rki.coronawarnapp.risk.TimeVariables
-import de.rki.coronawarnapp.transaction.Transaction.InternalTransactionStates.INIT
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.withTimeout
-import timber.log.Timber
-import java.util.UUID
-import java.util.concurrent.atomic.AtomicReference
-import kotlin.coroutines.CoroutineContext
-import kotlin.system.measureTimeMillis
-
-/**
- * The Transaction is used to define an internal process that can go through various states.
- * It contains a mutex that is used to reference the current coroutine context and also a thread-safe
- * Transaction ID that can be used to identify a transaction instance in the entire system.
- *
- * The Transaction uses an internal State Handling that is defined so that error cases can be caught by state
- *
- * @throws TransactionException An Exception thrown when an error occurs during Transaction Execution
- * @throws RollbackException An Exception might get thrown if rollback behavior is implemented
- */
-abstract class Transaction {
-
-    @Suppress("VariableNaming", "PropertyName") // Done as the Convention is TAG for every class
-    abstract val TAG: String?
-
-    /**
-     * This is the State Stack that is used inside the Transaction. It is an atomic reference held only by the
-     * internal transaction and cannot be interacted with directly to ensure atomic operation.
-     *
-     * It is modified by executing a state. It will contain the latest state after execution of a state.
-     *
-     * @see executeState
-     * @see resetExecutedStateStack
-     * @see getExecutedStates
-     *
-     * @see finalizeState
-     * @see setState
-     * @see currentTransactionState
-     * @see isInStateStack
-     */
-    private val executedStatesStack: AtomicReference<MutableList<TransactionState>> =
-        AtomicReference(ArrayList())
-
-    /**
-     * Finalizes a state by adding the state to the executedStatesStack
-     */
-    private fun finalizeState() = executedStatesStack.get().add(currentTransactionState.get())
-
-    /**
-     * Sets the transaction state and logs the state change.
-     *
-     * @param state the new transaction state
-     */
-    private fun setState(state: TransactionState) =
-        currentTransactionState.set(state)
-            .also {
-                Timber.tag(TAG).d("$transactionId - STATE CHANGE: ${currentTransactionState.get()}")
-            }
-
-    /**
-     * The atomic Transaction ID that should be set during Transaction Start. Used to identify execution context and errors.
-     */
-    protected val transactionId = AtomicReference<UUID>()
-
-    /**
-     * The mutual exclusion lock used to handle the lock during the execution across contexts.
-     */
-    private val internalMutualExclusionLock = Mutex()
-
-    /**
-     * The atomic Transaction State that should be set during Transaction steps.
-     * It should be updated by the implementing Transaction.
-     */
-    private val currentTransactionState = AtomicReference<TransactionState>()
-
-    /**
-     * Checks if a given Transaction State is in the current Stack Trace.
-     *
-     * @see executedStatesStack
-     */
-    protected fun TransactionState.isInStateStack() = executedStatesStack.get().contains(this)
-
-    /**
-     * Checks for all already executed states from the state stack.
-     *
-     * @return list of all executed states
-     * @see executedStatesStack
-     */
-    private fun getExecutedStates() = executedStatesStack.get().toList()
-
-    /**
-     * Resets the state stack. this method needs to be called after successful transaction execution in order to
-     * not contain any states from a previous transaction
-     *
-     * @see executedStatesStack
-     */
-    private fun resetExecutedStateStack() = executedStatesStack.get().clear()
-
-    /**
-     * Executes a given state and sets it as the active State, then executing the coroutine that should
-     * be called for this state, and then finalizing the state by adding the state to the executedStatesStack.
-     *
-     * Should an error occur during the state execution, an exception can take a look at the currently executed state
-     * as well as the transaction ID to refer to the concrete Error case.
-     *
-     * @param T The generic Return Type used for typing the state return value.
-     * @param context The context used to spawn the coroutine in
-     * @param state The state that should be executed and added to the state stack.
-     * @param block Any function containing the actual Execution Code for that state
-     * @return The return value of the state, useful for piping to a wrapper or a lock without a message bus or actor
-     */
-    private suspend fun <T> executeState(
-        context: CoroutineContext,
-        state: TransactionState,
-        block: suspend CoroutineScope.() -> T
-    ): T = withContext(context) {
-        setState(state)
-        val result = block.invoke(this)
-        finalizeState()
-        return@withContext result
-    }
-
-    /**
-     * Convenience method to call for a state execution with the Default Dispatcher. For more details, refer to
-     * the more detailed executeState that this call wraps around.
-     *
-     * @see executeState
-     * @param T The generic Return Type used for typing the state return value.
-     * @param state The state that should be executed and added to the state stack.
-     * @param block Any function containing the actual Execution Code for that state
-     * @return The return value of the state, useful for piping to a wrapper or a lock without a message bus or actor
-     */
-    protected suspend fun <T> executeState(
-        state: TransactionState,
-        block: suspend CoroutineScope.() -> T
-    ): T =
-        executeState(Dispatchers.Default, state, block)
-
-    /**
-     * Attempts to go into the internal lock context (mutual exclusion coroutine) and executes the given suspending
-     * function. Standard Logging is executed to inform about the transaction status.
-     * The Lock will run under the Timeout defined under TRANSACTION_TIMEOUT_MS. If the coroutine executed during this
-     * transaction does not returned within the specified timeout, an error will be thrown.
-     *
-     * After invoking the suspending function, the internal state stack will be reset for the next execution.
-     *
-     * Inside the given function one should execute executeState() as this will set the Transaction State accordingly
-     * and allow for atomic rollbacks.
-     *
-     * In an error scenario, during the handling of the transaction error, a rollback will be executed on best-effort basis.
-     *
-     * @param unique Executes the transaction as Unique. This results in the next execution being omitted in case of a race towards the lock.
-     * @param block the suspending function that should be used to execute the transaction.
-     * @param timeout the timeout for the transcation (in milliseconds)
-     * @throws TransactionException the exception that wraps around any error that occurs inside the lock.
-     *
-     * @see executeState
-     * @see executedStatesStack
-     */
-    suspend fun lockAndExecute(
-        unique: Boolean = false,
-        scope: CoroutineScope,
-        timeout: Long = TimeVariables.getTransactionTimeout(),
-        block: suspend CoroutineScope.() -> Unit
-    ) {
-
-        if (unique && internalMutualExclusionLock.isLocked) {
-            Timber.tag(TAG).w(
-                "TRANSACTION WITH ID %s ALREADY RUNNING (%s) AS UNIQUE, SKIPPING EXECUTION.",
-                transactionId, currentTransactionState
-            )
-            return
-        }
-
-        val deferred = scope.async {
-            internalMutualExclusionLock.withLock {
-                executeState(INIT) { transactionId.set(UUID.randomUUID()) }
-
-                val duration = measureTimeMillis {
-                    withTimeout(timeout) {
-                        block.invoke(this)
-                    }
-                }
-
-                Timber.tag(TAG).i(
-                    "TRANSACTION %s COMPLETED (%d) in %d ms, STATES EXECUTED: %s",
-                    transactionId, System.currentTimeMillis(), duration, getExecutedStates()
-                )
-
-                resetExecutedStateStack()
-            }
-        }
-
-        withContext(scope.coroutineContext) {
-            try {
-                deferred.await()
-            } catch (e: Exception) {
-                handleTransactionError(e)
-            }
-        }
-    }
-
-    private enum class InternalTransactionStates : TransactionState {
-        INIT
-    }
-
-    /**
-     * Handles the Transaction Error by performing a rollback, resetting the state stack for consistency and then
-     * throwing a Transaction Exception with the given error as cause
-     *
-     * @throws TransactionException an error containing the cause of the transaction failure as cause, if provided
-     *
-     * @param error the error that lead to an error case in the transaction that cannot be handled inside the
-     * transaction but has to be caught from the exception caller
-     */
-    protected open suspend fun handleTransactionError(error: Throwable): Nothing {
-        val wrap = TransactionException(
-            transactionId.get(),
-            currentTransactionState.toString(),
-            error
-        )
-        Timber.tag(TAG).e(wrap)
-
-        rollback()
-        resetExecutedStateStack()
-
-        throw wrap
-    }
-
-    /**
-     * Initiates rollback based on the atomic rollback value references inside the transaction and the current stack.
-     * It is called during the Handling of Transaction Errors and should call handleRollbackError on an error case.
-     *
-     * Atomic references towards potential rollback targets need to be kept in the concrete Transaction Implementation,
-     * as this can differ on a case by case basis. Nevertheless, rollback will always be executed, no matter if an
-     * override is provided or not
-     *
-     * @throws RollbackException throws a rollback exception when handleRollbackError() is called
-     */
-    protected open suspend fun rollback() {
-        if (BuildConfig.DEBUG) Timber.tag(TAG).d("Initiate Rollback")
-    }
-
-    /**
-     * Handles the Rollback Error by throwing a RollbackException with the given error as cause
-     *
-     * @throws TransactionException an error containing the cause of the rollback failure as cause, if provided
-     *
-     * @param error the error that lead to an error case in the rollback
-     */
-    protected open fun handleRollbackError(error: Throwable?): Nothing {
-        val wrap = RollbackException(
-            transactionId.get(),
-            currentTransactionState.toString(),
-            error
-        )
-        Timber.tag(TAG).e(wrap)
-        throw wrap
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionState.kt
deleted file mode 100644
index da1f2c072d18f5b9270bda3ba33f6a914c4b81bb..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionState.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package de.rki.coronawarnapp.transaction
-
-/**
- * An Interface used by Transactions to define different states during execution.
- *
- * @see Transaction
- */
-interface TransactionState
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt
index 1ecb64ba822eb0f531f8cf37d4fc769e464f1925..b9b618c3088a2d80f62d9ac752b85bb911b9a17e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt
@@ -21,7 +21,6 @@ import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.CameraPermissionHelper
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
-import de.rki.coronawarnapp.util.observeEvent
 import de.rki.coronawarnapp.util.ui.doNavigate
 import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
@@ -32,7 +31,8 @@ import javax.inject.Inject
 /**
  * A simple [Fragment] subclass.
  */
-class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_code_scan), AutoInject {
+class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_code_scan),
+    AutoInject {
 
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
     private val viewModel: SubmissionQRCodeScanViewModel by cwaViewModels { viewModelFactory }
@@ -58,22 +58,29 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co
 
         binding.submissionQrCodeScanViewfinderView.setCameraPreview(binding.submissionQrCodeScanPreview)
 
-        viewModel.scanStatus.observeEvent(viewLifecycleOwner) {
-            if (ScanStatus.SUCCESS == it) {
-                viewModel.doDeviceRegistration()
-            }
-
+        viewModel.scanStatusValue.observe2(this) {
             if (ScanStatus.INVALID == it) {
                 showInvalidScanDialog()
             }
         }
 
+        viewModel.showRedeemedTokenWarning.observe2(this) {
+            val dialog = DialogHelper.DialogInstance(
+                requireActivity(),
+                R.string.submission_error_dialog_web_tan_redeemed_title,
+                R.string.submission_error_dialog_web_tan_redeemed_body,
+                R.string.submission_error_dialog_web_tan_redeemed_button_positive
+            )
+
+            DialogHelper.showDialog(dialog)
+            goBack()
+        }
+
         viewModel.registrationState.observe2(this) {
             binding.submissionQrCodeScanSpinner.visibility = when (it) {
                 ApiRequestState.STARTED -> View.VISIBLE
                 else -> View.GONE
             }
-
             if (ApiRequestState.SUCCESS == it) {
                 doNavigate(
                     SubmissionQRCodeScanFragmentDirections
@@ -98,7 +105,7 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co
 
     private fun startDecode() {
         binding.submissionQrCodeScanPreview.decodeSingle {
-            viewModel.validateAndStoreTestGUID(it.text)
+            viewModel.validateTestGUID(it.text)
         }
     }
 
@@ -165,13 +172,14 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co
     ) {
         // if permission was denied
         if (requestCode == REQUEST_CAMERA_PERMISSION_CODE &&
-            (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED)) {
-                if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
-                    showCameraPermissionRationaleDialog()
-                } else {
-                    // user permanently denied access to the camera
-                    showCameraPermissionDeniedDialog()
-                }
+            (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED)
+        ) {
+            if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
+                showCameraPermissionRationaleDialog()
+            } else {
+                // user permanently denied access to the camera
+                showCameraPermissionDeniedDialog()
+            }
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
index b4330c73b3d8596cfcd44f708d814e6e9e786ede..ec4ade17f1b2408ab3fcefd0249b13ec191af7a8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.ui.submission.qrcode.scan
 
-import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.exception.ExceptionCategory
@@ -9,38 +8,41 @@ import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.service.submission.QRScanResult
 import de.rki.coronawarnapp.service.submission.SubmissionService
+import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.ui.submission.ScanStatus
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
-import de.rki.coronawarnapp.util.Event
+import de.rki.coronawarnapp.util.formatter.TestResult
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import timber.log.Timber
 
-class SubmissionQRCodeScanViewModel @AssistedInject constructor() : CWAViewModel() {
-
+class SubmissionQRCodeScanViewModel @AssistedInject constructor() :
+    CWAViewModel() {
     val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent()
-    private val _scanStatus = MutableLiveData(Event(ScanStatus.STARTED))
+    val showRedeemedTokenWarning = SingleLiveEvent<Unit>()
+    val scanStatusValue = SingleLiveEvent<ScanStatus>()
 
-    val scanStatus: LiveData<Event<ScanStatus>> = _scanStatus
+    open class InvalidQRCodeException : Exception("error in qr code")
 
-    fun validateAndStoreTestGUID(rawResult: String) {
+    fun validateTestGUID(rawResult: String) {
         val scanResult = QRScanResult(rawResult)
         if (scanResult.isValid) {
-            SubmissionService.storeTestGUID(scanResult.guid!!)
-            _scanStatus.value = Event(ScanStatus.SUCCESS)
+            scanStatusValue.postValue(ScanStatus.SUCCESS)
+            doDeviceRegistration(scanResult)
         } else {
-            _scanStatus.value = Event(ScanStatus.INVALID)
+            scanStatusValue.postValue(ScanStatus.INVALID)
         }
     }
 
     val registrationState = MutableLiveData(ApiRequestState.IDLE)
     val registrationError = SingleLiveEvent<CwaWebException>()
 
-    fun doDeviceRegistration() = launch {
+    private fun doDeviceRegistration(scanResult: QRScanResult) = launch {
         try {
             registrationState.postValue(ApiRequestState.STARTED)
-            SubmissionService.asyncRegisterDevice()
+            checkTestResult(SubmissionService.asyncRegisterDeviceViaGUID(scanResult.guid!!))
             registrationState.postValue(ApiRequestState.SUCCESS)
         } catch (err: CwaWebException) {
             registrationState.postValue(ApiRequestState.FAILED)
@@ -52,12 +54,34 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor() : CWAViewModel
                 err.report(ExceptionCategory.INTERNAL)
             }
             registrationState.postValue(ApiRequestState.FAILED)
+        } catch (err: InvalidQRCodeException) {
+            registrationState.postValue(ApiRequestState.FAILED)
+            deregisterTestFromDevice()
+            showRedeemedTokenWarning.postValue(Unit)
         } catch (err: Exception) {
             registrationState.postValue(ApiRequestState.FAILED)
             err.report(ExceptionCategory.INTERNAL)
         }
     }
 
+    private fun checkTestResult(testResult: TestResult) {
+        if (testResult == TestResult.REDEEMED) {
+            throw InvalidQRCodeException()
+        }
+    }
+
+    private fun deregisterTestFromDevice() {
+        launch {
+            Timber.d("deregisterTestFromDevice()")
+            SubmissionService.deleteTestGUID()
+            SubmissionService.deleteRegistrationToken()
+            LocalData.isAllowedToSubmitDiagnosisKeys(false)
+            LocalData.initialTestResultReceivedTimestamp(0L)
+
+            routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity)
+        }
+    }
+
     fun onBackPressed() {
         routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRInfo)
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt
index b2fcfe37df70cdd766efa70050c84b350cba7336..c947e09a91c7507945441f918e49abe8306814a2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt
@@ -51,7 +51,7 @@ class SubmissionTanViewModel @AssistedInject constructor(
         launch {
             try {
                 registrationState.postValue(ApiRequestState.STARTED)
-                SubmissionService.asyncRegisterDevice()
+                SubmissionService.asyncRegisterDeviceViaTAN(teletan.value)
                 registrationState.postValue(ApiRequestState.SUCCESS)
             } catch (err: CwaWebException) {
                 registrationState.postValue(ApiRequestState.FAILED)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
index 93ad141a6257c0f04084b90cae08a3463761fcd6..00d62d17067f43ef080520518ba36bf2d4ee5d8f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
@@ -5,8 +5,11 @@ import android.net.wifi.WifiManager
 import android.os.PowerManager
 import androidx.lifecycle.ProcessLifecycleOwner
 import androidx.lifecycle.lifecycleScope
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
 import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
@@ -18,7 +21,8 @@ import javax.inject.Singleton
 
 @Singleton
 class WatchdogService @Inject constructor(
-    @AppContext private val context: Context
+    @AppContext private val context: Context,
+    private val taskController: TaskController
 ) {
 
     private val powerManager by lazy {
@@ -41,16 +45,20 @@ class WatchdogService @Inject constructor(
             val wakeLock = createWakeLock()
             // A wifi lock to wake up the wifi connection in case the device is dozing
             val wifiLock = createWifiLock()
-            try {
-                BackgroundWorkHelper.sendDebugNotification(
-                    "Automatic mode is on", "Check if we have downloaded keys already today"
+            BackgroundWorkHelper.sendDebugNotification(
+                "Automatic mode is on", "Check if we have downloaded keys already today"
+            )
+            val state = taskController.submitBlocking(
+                DefaultTaskRequest(
+                    DownloadDiagnosisKeysTask::class,
+                    DownloadDiagnosisKeysTask.Arguments(null, true)
                 )
-                RetrieveDiagnosisKeysTransaction.startWithConstraints()
-            } catch (e: Exception) {
+            )
+            if (state.isFailed) {
                 BackgroundWorkHelper.sendDebugNotification(
                     "RetrieveDiagnosisKeysTransaction failed",
-                    (e.localizedMessage
-                        ?: "Unknown exception occurred in onCreate") + "\n\n" + (e.cause
+                    (state.error?.localizedMessage
+                        ?: "Unknown exception occurred in onCreate") + "\n\n" + (state.error?.cause
                         ?: "Cause is unknown").toString()
                 )
                 // retry the key retrieval in case of an error with a scheduled work
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 a866c2998e38b8cf77fd4b8e1af84e9fd5b05456..71c8f4f7a8770fc2cee5a1d104f469741ba72b1b 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
@@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.bugreporting.BugReporter
 import de.rki.coronawarnapp.bugreporting.BugReportingModule
 import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule
+import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule
 import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.environment.EnvironmentModule
@@ -28,7 +29,6 @@ import de.rki.coronawarnapp.submission.SubmissionTaskModule
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.internal.TaskModule
 import de.rki.coronawarnapp.test.DeviceForTestersModule
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisInjectionHelper
 import de.rki.coronawarnapp.ui.ActivityBinder
 import de.rki.coronawarnapp.util.ConnectivityHelperInjection
 import de.rki.coronawarnapp.util.UtilModule
@@ -63,6 +63,7 @@ import javax.inject.Singleton
         AppConfigModule::class,
         SubmissionModule::class,
         SubmissionTaskModule::class,
+        DownloadDiagnosisKeysTaskModule::class,
         VerificationModule::class,
         PlaybookModule::class,
         TaskModule::class,
@@ -74,9 +75,6 @@ import javax.inject.Singleton
 )
 interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
 
-    // TODO Remove once Singletons are gone
-    val transRetrieveKeysInjection: RetrieveDiagnosisInjectionHelper
-
     val connectivityHelperInjection: ConnectivityHelperInjection
 
     val settingsRepository: SettingsRepository
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt
index a1f13d193a075b644393c367c7419f89beb67f23..4fd37b49fb823679d637553e239d24b52361c3f0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt
@@ -13,3 +13,7 @@ data class CachedString(val provider: (Context) -> String) : LazyString {
         cached = it
     }
 }
+
+fun String.toLazyString() = object : LazyString {
+    override fun get(context: Context) = this@toLazyString
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
index 51ca8ea55de10fe99bcc1a1abe0ba87a9932df66..59631fb84cbdf88ac69cf8dd916eb2412224fb68 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
@@ -178,10 +178,10 @@ object BackgroundWorkScheduler {
     /**
      * Schedule background noise one time work
      *
-     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK
+     * @see WorkType.BACKGROUND_NOISE_ONE_TIME_WORK
      */
     fun scheduleBackgroundNoiseOneTimeWork() {
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start()
+        WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.start()
     }
 
     /**
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 275708013c4ed13dd391f2e78babab668dece30f..ef5f51077634e914630f793378753b17336e4432 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
@@ -5,7 +5,10 @@ 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.diagnosiskeys.download.DownloadDiagnosisKeysTask
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import timber.log.Timber
 
@@ -17,15 +20,14 @@ import timber.log.Timber
  */
 class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
     @Assisted val context: Context,
-    @Assisted workerParams: WorkerParameters
+    @Assisted workerParams: WorkerParameters,
+    private val taskController: TaskController
 ) : CoroutineWorker(context, workerParams) {
 
     /**
      * Work execution
      *
      * @return Result
-     *
-     * @see RetrieveDiagnosisKeysTransaction
      */
     override suspend fun doWork(): Result {
         Timber.d("$id: doWork() started. Run attempt: $runAttemptCount")
@@ -35,15 +37,18 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
         )
 
         var result = Result.success()
-        try {
-            RetrieveDiagnosisKeysTransaction.startWithConstraints()
-        } catch (e: Exception) {
+        taskController.submitBlocking(
+            DefaultTaskRequest(
+                DownloadDiagnosisKeysTask::class,
+                DownloadDiagnosisKeysTask.Arguments(null, true)
+            )
+        ).error?.also { error: Throwable ->
             Timber.w(
-                e, "$id: Error during RetrieveDiagnosisKeysTransaction.startWithConstraints()."
+                error, "$id: Error during startWithConstraints()."
             )
 
             if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
-                Timber.w(e, "$id: Retry attempts exceeded.")
+                Timber.w(error, "$id: Retry attempts exceeded.")
 
                 BackgroundWorkHelper.sendDebugNotification(
                     "KeyOneTime Executing: Failure",
@@ -52,7 +57,7 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
 
                 return Result.failure()
             } else {
-                Timber.d(e, "$id: Retrying.")
+                Timber.d(error, "$id: Retrying.")
                 result = Result.retry()
             }
         }
diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml
index 4016e191ea1d74a6b97295354813b4d5ece1896a..6856b0f41124fe28851d80b913a5e61e3b017c3c 100644
--- a/Corona-Warn-App/src/main/res/values-de/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/strings.xml
@@ -799,9 +799,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"Zurück"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Test fehlerhaft"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"QR-Code nicht mehr gültig"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"Es gab ein Problem bei der Auswertung Ihres Tests. Ihr QR Code ist bereits abgelaufen."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Ihr Test liegt länger als 21 Tage zurück und kann nicht mehr in der App registriert werden. Bitte stellen Sie bei zukünftigen Tests sicher, dass Sie den QR-Code scannen, sobald er Ihnen vorliegt."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string>
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt
index e94de291f5bca96a1adefb5d57eade3d92760167..5669835c338bbe4d15683b6a62bcff189e66ed36 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt
@@ -50,7 +50,7 @@ class ScanResultTest {
 
     @Test
     fun containsInvalidGUID() {
-        //extra slashes should be invalid.
+        // extra slashes should be invalid.
         buildQRCodeCases("HTTPS:///LOCALHOST/?", guidUpperCase, false)
         buildQRCodeCases("HTTPS://LOCALHOST//?", guidUpperCase, false)
         buildQRCodeCases("HTTPS://LOCALHOST///?", guidUpperCase, false)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
index 553c5c3a6c4de1c6550e402b2220e54d1ffa824f..5645c8f4921f046181893fb317d88e457de7b114 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.service.submission
 
-import de.rki.coronawarnapp.exception.NoGUIDOrTANSetException
 import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException
 import de.rki.coronawarnapp.playbook.BackgroundNoise
 import de.rki.coronawarnapp.playbook.Playbook
@@ -66,13 +65,6 @@ class SubmissionServiceTest {
         clearAllMocks()
     }
 
-    @Test
-    fun registerDeviceWithoutTANOrGUIDFails(): Unit = runBlocking {
-        shouldThrow<NoGUIDOrTANSetException> {
-            SubmissionService.asyncRegisterDevice()
-        }
-    }
-
     @Test
     fun registrationWithGUIDSucceeds() {
         every { LocalData.testGUID() } returns guid
@@ -89,7 +81,7 @@ class SubmissionServiceTest {
         every { backgroundNoise.scheduleDummyPattern() } just Runs
 
         runBlocking {
-            SubmissionService.asyncRegisterDevice()
+            SubmissionService.asyncRegisterDeviceViaGUID(guid)
         }
 
         verify(exactly = 1) {
@@ -117,7 +109,7 @@ class SubmissionServiceTest {
         every { backgroundNoise.scheduleDummyPattern() } just Runs
 
         runBlocking {
-            SubmissionService.asyncRegisterDevice()
+            SubmissionService.asyncRegisterDeviceViaTAN(guid)
         }
 
         verify(exactly = 1) {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt
deleted file mode 100644
index 4cbe2bda9a570500b6b6d56b6b23d3bcf139fc19..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package de.rki.coronawarnapp.transaction
-
-import de.rki.coronawarnapp.appconfig.AppConfigProvider
-import de.rki.coronawarnapp.appconfig.ConfigData
-import de.rki.coronawarnapp.environment.EnvironmentSetup
-import de.rki.coronawarnapp.nearby.ENFClient
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
-import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.util.GoogleAPIVersion
-import de.rki.coronawarnapp.util.di.AppInjector
-import de.rki.coronawarnapp.util.di.ApplicationComponent
-import io.kotest.matchers.shouldBe
-import io.mockk.MockKAnnotations
-import io.mockk.Runs
-import io.mockk.clearAllMocks
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.coVerifyOrder
-import io.mockk.every
-import io.mockk.impl.annotations.MockK
-import io.mockk.just
-import io.mockk.mockk
-import io.mockk.mockkObject
-import kotlinx.coroutines.runBlocking
-import org.joda.time.LocalDate
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import java.io.File
-import java.nio.file.Paths
-import java.util.Date
-import java.util.UUID
-
-/**
- * RetrieveDiagnosisKeysTransaction test.
- */
-class RetrieveDiagnosisKeysTransactionTest {
-
-    @MockK lateinit var mockEnfClient: ENFClient
-    @MockK lateinit var environmentSetup: EnvironmentSetup
-    @MockK lateinit var configProvider: AppConfigProvider
-    @MockK lateinit var configData: ConfigData
-
-    @BeforeEach
-    fun setUp() {
-        MockKAnnotations.init(this)
-
-        mockkObject(AppInjector)
-        val appComponent = mockk<ApplicationComponent>().apply {
-            every { transRetrieveKeysInjection } returns RetrieveDiagnosisInjectionHelper(
-                TransactionCoroutineScope(),
-                GoogleAPIVersion(),
-                mockEnfClient,
-                environmentSetup
-            )
-            every { appConfigProvider } returns configProvider
-        }
-
-        coEvery { configProvider.getAppConfig() } returns configData
-        every { configData.supportedCountries } returns emptyList()
-        every { configData.exposureDetectionConfiguration } returns mockk()
-
-        every { AppInjector.component } returns appComponent
-
-        mockkObject(InternalExposureNotificationClient)
-        mockkObject(RetrieveDiagnosisKeysTransaction)
-        mockkObject(LocalData)
-
-        coEvery { InternalExposureNotificationClient.asyncIsEnabled() } returns true
-
-        every { LocalData.googleApiToken(any()) } just Runs
-        every { LocalData.lastTimeDiagnosisKeysFromServerFetch() } returns Date()
-        every { LocalData.lastTimeDiagnosisKeysFromServerFetch(any()) } just Runs
-        every { LocalData.googleApiToken() } returns UUID.randomUUID().toString()
-
-        every { environmentSetup.useEuropeKeyPackageFiles } returns false
-    }
-
-    @AfterEach
-    fun cleanUp() {
-        clearAllMocks()
-    }
-
-    @Test
-    fun `unsuccessful ENF submission`() {
-        coEvery { mockEnfClient.provideDiagnosisKeys(any(), any(), any()) } returns false
-        val requestedCountries = listOf("DE")
-        coEvery {
-            RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](
-                requestedCountries
-            )
-        } returns listOf<File>()
-
-        runBlocking {
-            RetrieveDiagnosisKeysTransaction.start(requestedCountries)
-        }
-
-        coVerifyOrder {
-            RetrieveDiagnosisKeysTransaction["executeSetup"]()
-            RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]()
-            RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](
-                requestedCountries
-            )
-        }
-        coVerify(exactly = 0) {
-            RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>())
-        }
-    }
-
-    @Test
-    fun `successful submission`() {
-        val file = Paths.get("src", "test", "resources", "keys.bin").toFile()
-        coEvery { mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any()) } returns true
-        val requestedCountries = listOf("DE")
-
-        coEvery {
-            RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](
-                requestedCountries
-            )
-        } returns listOf(file)
-
-        runBlocking {
-            RetrieveDiagnosisKeysTransaction.start(requestedCountries)
-        }
-
-        coVerifyOrder {
-            RetrieveDiagnosisKeysTransaction["executeSetup"]()
-            RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]()
-            RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](
-                requestedCountries
-            )
-            mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any())
-            RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>())
-        }
-    }
-
-    @Test
-    fun `successful submission with EUR`() {
-        every { environmentSetup.useEuropeKeyPackageFiles } returns true
-        val file = Paths.get("src", "test", "resources", "keys.bin").toFile()
-        coEvery { mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any()) } returns true
-        val requestedCountries = listOf("EUR")
-
-        coEvery {
-            RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](
-                requestedCountries
-            )
-        } returns listOf(file)
-
-        runBlocking {
-            RetrieveDiagnosisKeysTransaction.start(requestedCountries)
-        }
-
-        coVerifyOrder {
-            RetrieveDiagnosisKeysTransaction["executeSetup"]()
-            RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]()
-            RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"](
-                requestedCountries
-            )
-            mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any())
-            RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>())
-        }
-    }
-
-    @Test
-    fun `conversion from date to localdate`() {
-        LocalDate.fromDateFields(Date(0)) shouldBe LocalDate.parse("1970-01-01")
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt
deleted file mode 100644
index 5ba62a995d754feedd5966522f036871c2f0da84..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt
+++ /dev/null
@@ -1,113 +0,0 @@
-package de.rki.coronawarnapp.transaction
-
-import de.rki.coronawarnapp.exception.RollbackException
-import de.rki.coronawarnapp.exception.TransactionException
-import de.rki.coronawarnapp.risk.TimeVariables
-import io.kotest.assertions.throwables.shouldThrow
-import io.kotest.matchers.should
-import io.kotest.matchers.types.beInstanceOf
-import io.mockk.clearAllMocks
-import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.mockkObject
-import io.mockk.spyk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.TimeoutCancellationException
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.TestCoroutineScope
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import testhelpers.BaseTest
-import java.io.IOException
-
-@ExperimentalCoroutinesApi
-class TransactionTest : BaseTest() {
-
-    @BeforeEach
-    fun setup() {
-        mockkObject(TimeVariables)
-    }
-
-    @AfterEach
-    fun tearDown() {
-        clearAllMocks()
-    }
-
-    @Suppress("UNREACHABLE_CODE")
-    private class TestTransaction(
-        val errorOnRollBack: Exception? = null
-    ) : Transaction() {
-        override val TAG: String = "TestTag"
-
-        public override suspend fun rollback() {
-            errorOnRollBack?.let { handleRollbackError(it) }
-            super.rollback()
-        }
-
-        public override suspend fun handleTransactionError(error: Throwable): Nothing {
-            return super.handleTransactionError(error)
-        }
-
-        public override fun handleRollbackError(error: Throwable?): Nothing {
-            return super.handleRollbackError(error)
-        }
-    }
-
-    @Test
-    fun `transaction error handler is called`() {
-        val testScope = TestCoroutineScope()
-        val testTransaction = spyk(TestTransaction())
-        shouldThrow<TransactionException> {
-            runBlocking {
-                testTransaction.lockAndExecute(scope = testScope) {
-                    throw IOException()
-                }
-            }
-        }
-
-        coVerify { testTransaction.handleTransactionError(any()) }
-        coVerify { testTransaction.rollback() }
-    }
-
-    @Test
-    fun `rollback error handler is called`() {
-        val testScope = TestCoroutineScope()
-        val testTransaction = spyk(
-            TestTransaction(
-                errorOnRollBack = IllegalAccessException()
-            )
-        )
-        shouldThrow<RollbackException> {
-            runBlocking {
-                testTransaction.lockAndExecute(scope = testScope) {
-                    throw IOException()
-                }
-            }
-        }
-
-        coVerify { testTransaction.handleTransactionError(ofType<IOException>()) }
-        coVerify { testTransaction.rollback() }
-        coVerify { testTransaction.handleRollbackError(ofType<IllegalAccessException>()) }
-    }
-
-    @Test
-    fun `transactions can timeout`() {
-        /**
-         * TODO use runBlockingTest & advanceTime, which currently does not work
-         * https://github.com/Kotlin/kotlinx.coroutines/issues/1204
-         */
-        every { TimeVariables.getTransactionTimeout() } returns 0L
-
-        val testTransaction = TestTransaction()
-        val exception = shouldThrow<TransactionException> {
-            runBlocking {
-                testTransaction.lockAndExecute(scope = this) {
-                    delay(TimeVariables.getTransactionTimeout())
-                }
-            }
-        }
-        exception.cause should beInstanceOf<TimeoutCancellationException>()
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
index b518ae31a0ba845187e3889b415605b4507da462..05edd8a920a6bdf01c7796b68d25d38d59c05243 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt
@@ -40,15 +40,17 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
         val viewModel = createViewModel()
 
         // start
-        viewModel.scanStatus.value!!.getContent() shouldBe ScanStatus.STARTED
+        viewModel.scanStatusValue.value = ScanStatus.STARTED
+
+        viewModel.scanStatusValue.value shouldBe ScanStatus.STARTED
 
         // valid guid
         val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
-        viewModel.validateAndStoreTestGUID("https://localhost/?$guid")
-        viewModel.scanStatus.value?.getContent().let { Assert.assertEquals(ScanStatus.SUCCESS, it) }
+        viewModel.validateTestGUID("https://localhost/?$guid")
+        viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.SUCCESS, it.value) }
 
         // invalid guid
-        viewModel.validateAndStoreTestGUID("https://no-guid-here")
-        viewModel.scanStatus.value?.getContent().let { Assert.assertEquals(ScanStatus.INVALID, it) }
+        viewModel.validateTestGUID("https://no-guid-here")
+        viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.INVALID, it.value) }
     }
 }
diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt
index e626c3e92891f7570e0a386fd23749db6e8a44cb..0413a8515b6db83ef95f76efb6e8af70b46238e7 100644
--- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt
+++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt
@@ -7,7 +7,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.risk.RiskLevels
 import de.rki.coronawarnapp.task.TaskController
-import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
 import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
@@ -15,12 +14,10 @@ import io.mockk.Runs
 import io.mockk.clearAllMocks
 import io.mockk.coEvery
 import io.mockk.coVerify
-import io.mockk.coVerifyOrder
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
 import io.mockk.mockk
-import io.mockk.mockkObject
 import kotlinx.coroutines.flow.flowOf
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
@@ -47,9 +44,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() {
     fun setup() {
         MockKAnnotations.init(this)
 
-        mockkObject(RetrieveDiagnosisKeysTransaction)
-        coEvery { RetrieveDiagnosisKeysTransaction.start() } returns Unit
-
         coEvery { keyCacheRepository.clear() } returns Unit
         every { enfClient.internalClient } returns exposureNotificationClient
         every { tracingCardStateProvider.state } returns flowOf(mockk())
@@ -74,28 +68,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() {
             taskController = taskController
         )
 
-    @Test
-    fun `action retrieveDiagnosisKeys, retieves diagnosis keys and calls risklevel calculation`() {
-        val vm = createViewModel()
-
-        vm.retrieveDiagnosisKeys()
-
-        coVerifyOrder {
-            RetrieveDiagnosisKeysTransaction.start()
-            taskController.submit(any())
-        }
-    }
-
-    @Test
-    fun `action calculateRiskLevel, calls risklevel calculation`() {
-        val vm = createViewModel()
-
-        vm.calculateRiskLevel()
-
-        coVerify(exactly = 1) { taskController.submit(any()) }
-        coVerify(exactly = 0) { RetrieveDiagnosisKeysTransaction.start() }
-    }
-
     @Test
     fun `action clearDiagnosisKeys calls the keyCacheRepo`() {
         val vm = createViewModel()