From b874f89cd34e068d6f305b9c394372003982fb36 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Mon, 26 Apr 2021 18:36:46 +0200
Subject: [PATCH] Refactor worker scheduling (EXPOSUREAPP-6007) (#2960)

* Refactor EW&PT RiskWorker scheduling (WIP).
* PresenceTracingRiskWorkScheduler.kt and ExposureWindowRiskWorkScheduler.kt subscribe to different flows and make the decision of when to schedule the workers by themselves.

* Removed BackgroundWorkScheduler.kt

* Refactor RA&PCR test result polling scheduling (WIP)

* Add TODOs

* LINTs

* Add check for RA redeemed state after 60 days.

* Bandaid unittests

* Fix test regressions in CoronaTestStorageTest.kt

* Fix test regressions in RATestResultAvailableNotificationServiceTest.kt and PCRTestResultAvailableNotificationServiceTest.kt

* removed tests for code that was moved

* Implement failing test processor tests.

* LINTs

* updated pcr worker test

* Tweak logging.

* unit tests for pcr scheduler

* 'fixed' ktlint

Co-authored-by: chris-cwa <69595386+chris-cwa@users.noreply.github.com>
Co-authored-by: chris-cwa <chris.cwa.sap@gmail.com>
---
 ...ssionTestResultConsentGivenFragmentTest.kt |   2 +-
 ...ubmissionTestResultNegativeFragmentTest.kt |   2 +-
 ...ionTestResultNoConsentGivenFragmentTest.kt |   2 +-
 .../ui/DeltaOnboardingFragmentViewModel.kt    |  13 +-
 .../coronawarnapp/CoronaWarnApplication.kt    |  26 +-
 .../CoronaTestRepositoryExtensions.kt         |   8 +
 .../coronatest/migration/PCRTestMigration.kt  |   1 +
 .../coronatest/type/CoronaTest.kt             |   7 +
 .../coronatest/type/common/ResultScheduler.kt |  28 ++
 .../TestResultAvailableNotificationService.kt |  20 +-
 .../coronatest/type/pcr/PCRCoronaTest.kt      |   6 +
 .../coronatest/type/pcr/PCRProcessor.kt       |  27 +-
 .../pcr/execution/PCRResultRetrievalWorker.kt |  65 +++++
 .../type/pcr/execution/PCRResultScheduler.kt  | 105 +++++++
 ...RTestResultAvailableNotificationService.kt |  77 ++++++
 .../type/rapidantigen/RACoronaTest.kt         |  12 +-
 .../rapidantigen/RapidAntigenProcessor.kt     |  35 ++-
 .../execution}/RAResultRetrievalWorker.kt     |  69 ++---
 .../execution/RAResultScheduler.kt            | 127 +++++++++
 ...TTestResultAvailableNotificationService.kt |  78 ++++++
 .../worker/PCRResultRetrievalWorker.kt        | 136 ---------
 .../worker/execution/PCRResultScheduler.kt    |  94 -------
 .../worker/execution/RAResultScheduler.kt     |  89 ------
 .../datadonation/analytics/Analytics.kt       |   2 +-
 .../BackgroundNoisePeriodicWorker.kt          |   3 -
 .../deniability/NoiseScheduler.kt             |  28 +-
 ...RTestResultAvailableNotificationService.kt |  26 --
 ...TTestResultAvailableNotificationService.kt |  26 --
 .../presencetracing/TraceLocationSettings.kt  |  26 +-
 .../PresenceTracingRiskWorkScheduler.kt       |  85 ++++++
 .../ExposureWindowRiskWorkScheduler.kt        | 102 +++++++
 .../risk/execution/RiskWorkScheduler.kt       |  93 +------
 .../storage/OnboardingSettings.kt             |  24 +-
 .../storage/TracingRepository.kt              |  18 +-
 .../submission/task/SubmissionTask.kt         |   6 +-
 .../SettingsTracingFragmentViewModel.kt       |   8 +-
 .../rki/coronawarnapp/ui/main/MainActivity.kt |   4 -
 .../ui/onboarding/OnboardingActivity.kt       |   4 +-
 .../onboarding/CheckInOnboardingViewModel.kt  |   6 +-
 .../ui/settings/SettingsResetViewModel.kt     |   4 +-
 .../SubmissionTestResultInvalidViewModel.kt   |   2 +-
 .../SubmissionTestResultNegativeViewModel.kt  |   2 +-
 ...bmissionTestResultConsentGivenViewModel.kt |   2 +-
 .../SubmissionTestResultNoConsentViewModel.kt |   2 +-
 .../rki/coronawarnapp/util/WatchdogService.kt |  29 +-
 .../util/di/ApplicationComponent.kt           |   5 +-
 .../EncryptedPreferencesMigration.kt          |   4 +-
 .../coronawarnapp/util/flow/FlowExtensions.kt |  18 ++
 .../coronawarnapp/util/worker/WorkerBinder.kt |   4 +-
 .../worker/BackgroundConstants.kt             |  13 -
 .../worker/BackgroundWorkHelper.kt            |  46 ----
 .../worker/BackgroundWorkScheduler.kt         |  49 ----
 .../storage/CoronaTestStorageTest.kt          |  10 +-
 .../coronatest/type/pcr/PCRProcessorTest.kt   |  67 ++++-
 .../pcr/execution/PCRResultSchedulerTest.kt   |  78 ++++++
 ...ResultAvailableNotificationServiceTest.kt} |  27 +-
 .../rapidantigen/RapidAntigenProcessorTest.kt |  54 +++-
 ...tResultAvailableNotificationServiceTest.kt | 122 +++++++++
 .../datadonation/analytics/AnalyticsTest.kt   |   4 +-
 .../main/MainActivityViewModelTest.kt         |   5 +-
 .../submission/task/SubmissionTaskTest.kt     |  10 +-
 ...sionTestResultConsentGivenViewModelTest.kt |   2 +-
 ...missionTestResultNoConsentViewModelTest.kt |   2 +-
 .../EncryptedPreferencesMigrationTest.kt      |   3 +-
 .../util/worker/WorkerBinderTest.kt           |   6 +-
 .../worker/BackgroundConstantsTest.kt         |   2 -
 .../worker/BackgroundWorkHelperTest.kt        |   9 +-
 ...isTestResultRetrievalPeriodicWorkerTest.kt | 257 ------------------
 .../worker/PCRResultRetrievalWorkerTest.kt    | 119 ++++++++
 69 files changed, 1423 insertions(+), 1024 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/ResultScheduler.kt
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/{notification => coronatest/type/common}/TestResultAvailableNotificationService.kt (78%)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultRetrievalWorker.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultScheduler.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/{worker => type/rapidantigen/execution}/RAResultRetrievalWorker.kt (50%)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultScheduler.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/RAResultScheduler.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/PCRTestResultAvailableNotificationService.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/RATTestResultAvailableNotificationService.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingRiskWorkScheduler.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/ExposureWindowRiskWorkScheduler.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultSchedulerTest.kt
 rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/{notification/TestResultAvailableNotificationServiceTest.kt => coronatest/type/pcr/notification/PCRTestResultAvailableNotificationServiceTest.kt} (78%)
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATestResultAvailableNotificationServiceTest.kt
 delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/PCRResultRetrievalWorkerTest.kt

diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt
index ee50cc594..0a4616394 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt
@@ -14,8 +14,8 @@ import dagger.android.ContributesAndroidInjector
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt
index 4086ecd6d..5439d1759 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt
@@ -6,7 +6,7 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
 import de.rki.coronawarnapp.ui.submission.testresult.negative.SubmissionTestResultNegativeFragment
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt
index 17de3dcef..8ccf0d97f 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt
@@ -6,8 +6,8 @@ import dagger.Module
 import dagger.android.ContributesAndroidInjector
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
 import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenFragmentArgs
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt
index ee6dd0085..bffe60b06 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt
@@ -6,8 +6,8 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings
 import de.rki.coronawarnapp.environment.BuildConfigWrap
-import de.rki.coronawarnapp.presencetracing.TraceLocationSettings
 import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.presencetracing.TraceLocationSettings
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
@@ -50,14 +50,13 @@ class DeltaOnboardingFragmentViewModel @AssistedInject constructor(
     }
 
     fun isAttendeeOnboardingDone() =
-        traceLocationSettings.onboardingStatus == TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
+        traceLocationSettings.onboardingStatus.value == TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
 
     fun setAttendeeOnboardingDone(value: Boolean) {
-        traceLocationSettings.onboardingStatus =
-            if (value)
-                TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
-            else
-                TraceLocationSettings.OnboardingStatus.NOT_ONBOARDED
+        traceLocationSettings.onboardingStatus.update {
+            if (value) TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
+            else TraceLocationSettings.OnboardingStatus.NOT_ONBOARDED
+        }
     }
 
     @AssistedFactory
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
index b2d67700e..61a3c1136 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
@@ -17,14 +17,20 @@ import de.rki.coronawarnapp.bugreporting.loghistory.LogHistoryTree
 import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService
+import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.notification.RATTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
 import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver
 import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL
 import de.rki.coronawarnapp.notification.GeneralNotifications
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingRiskWorkScheduler
 import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpScheduler
 import de.rki.coronawarnapp.risk.RiskLevelChangeDetector
+import de.rki.coronawarnapp.risk.execution.ExposureWindowRiskWorkScheduler
 import de.rki.coronawarnapp.storage.OnboardingSettings
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.task.TaskController
@@ -33,7 +39,6 @@ import de.rki.coronawarnapp.util.WatchdogService
 import de.rki.coronawarnapp.util.device.ForegroundState
 import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.di.ApplicationComponent
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.launchIn
@@ -68,8 +73,13 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
     @Inject lateinit var onboardingSettings: OnboardingSettings
     @Inject lateinit var autoCheckOut: AutoCheckOut
     @Inject lateinit var traceLocationDbCleanupScheduler: TraceLocationDbCleanUpScheduler
-    @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
     @Inject lateinit var shareTestResultNotificationService: ShareTestResultNotificationService
+    @Inject lateinit var exposureWindowRiskWorkScheduler: ExposureWindowRiskWorkScheduler
+    @Inject lateinit var presenceTracingRiskWorkScheduler: PresenceTracingRiskWorkScheduler
+    @Inject lateinit var pcrTestResultScheduler: PCRResultScheduler
+    @Inject lateinit var raTestResultScheduler: RAResultScheduler
+    @Inject lateinit var pcrTestResultAvailableNotificationService: PCRTestResultAvailableNotificationService
+    @Inject lateinit var raTestResultAvailableNotificationService: RATTestResultAvailableNotificationService
 
     @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree
 
@@ -115,6 +125,18 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
             contactDiaryWorkScheduler.schedulePeriodic()
         }
 
+        Timber.v("Setting up risk work schedulers.")
+        exposureWindowRiskWorkScheduler.setup()
+        presenceTracingRiskWorkScheduler.setup()
+
+        Timber.v("Setting up test result work schedulers.")
+        pcrTestResultScheduler.setup()
+        raTestResultScheduler.setup()
+
+        Timber.v("Setting up test result available notification services.")
+        pcrTestResultAvailableNotificationService.setup()
+        raTestResultAvailableNotificationService.setup()
+
         deviceTimeHandler.launch()
         configChangeDetector.launch()
         riskLevelChangeDetector.launch()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt
index 8237875c2..b7a7d6ae3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt
@@ -24,3 +24,11 @@ val CoronaTestRepository.latestRAT: Flow<RACoronaTest?>
             } as? RACoronaTest
         }
         .distinctUntilChanged()
+
+/**
+ * Should we keep the background workers for our risk results running?
+ */
+val CoronaTestRepository.isRiskCalculationNecessary: Flow<Boolean>
+    get() = coronaTests.map { tests ->
+        tests.none { it.isPositive }
+    }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt
index 851c81cd7..b17da90d5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt
@@ -65,6 +65,7 @@ class PCRTestMigration @Inject constructor(
             identifier = LEGACY_IDENTIFIER,
             registrationToken = token,
             registeredAt = devicePairingSuccessfulAt,
+            lastUpdatedAt = devicePairingSuccessfulAt,
             testResult = when (isAllowedToSubmitKeys) {
                 true -> CoronaTestResult.PCR_POSITIVE
                 else -> CoronaTestResult.PCR_OR_RAT_PENDING
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt
index 2765647ac..02353dcc1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt
@@ -22,9 +22,16 @@ interface CoronaTest {
 
     val isPending: Boolean
 
+    /**
+     * Has this test reached it's final state, i.e. can polling be stopped?
+     */
+    val isFinal: Boolean
+
     val testResultReceivedAt: Instant?
     val testResult: CoronaTestResult
 
+    val lastUpdatedAt: Instant
+
     // TODO why do we need this PER test
     val isAdvancedConsentGiven: Boolean
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/ResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/ResultScheduler.kt
new file mode 100644
index 000000000..ec82a67be
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/ResultScheduler.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.coronatest.type.common
+
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import de.rki.coronawarnapp.util.coroutine.await
+import java.util.concurrent.TimeUnit
+
+open class ResultScheduler(
+    private val workManager: WorkManager,
+) {
+
+    internal suspend fun isScheduled(workerName: String) =
+        workManager.getWorkInfosForUniqueWork(workerName)
+            .await()
+            .any { it.isScheduled }
+
+    internal val WorkInfo.isScheduled: Boolean
+        get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED
+
+    companion object {
+        /**
+         * Kind initial delay in minutes for periodic work for accessibility reason
+         *
+         * @see TimeUnit.SECONDS
+         */
+        internal const val TEST_RESULT_PERIODIC_INITIAL_DELAY = 10L
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt
similarity index 78%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt
index db751afd8..b6e85bebc 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/TestResultAvailableNotificationService.kt
@@ -1,12 +1,14 @@
-package de.rki.coronawarnapp.notification
+package de.rki.coronawarnapp.coronatest.type.common
 
 import android.content.Context
-import androidx.annotation.IdRes
 import androidx.navigation.NavDeepLinkBuilder
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.notification.GeneralNotifications
+import de.rki.coronawarnapp.notification.NotificationId
 import de.rki.coronawarnapp.ui.main.MainActivity
+import de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragmentArgs
 import de.rki.coronawarnapp.util.device.ForegroundState
 import de.rki.coronawarnapp.util.notifications.setContentTextExpandable
 import kotlinx.coroutines.flow.first
@@ -20,11 +22,10 @@ open class TestResultAvailableNotificationService(
     private val notificationHelper: GeneralNotifications,
     private val cwaSettings: CWASettings,
     private val notificationId: NotificationId,
-    @IdRes private val destination: Int
 ) {
 
-    suspend fun showTestResultAvailableNotification(testResult: CoronaTestResult) {
-        Timber.d("showTestResultAvailableNotification(testResult=%s)", testResult)
+    suspend fun showTestResultAvailableNotification(test: CoronaTest) {
+        Timber.d("showTestResultAvailableNotification(test=%s)", test)
 
         if (foregroundState.isInForeground.first()) {
             Timber.d("App in foreground, skipping notification.")
@@ -39,6 +40,11 @@ open class TestResultAvailableNotificationService(
         val pendingIntent = navDeepLinkBuilderProvider.get().apply {
             setGraph(R.navigation.nav_graph)
             setComponentName(MainActivity::class.java)
+            setArguments(
+                SubmissionTestResultPendingFragmentArgs(
+                    testType = test.type
+                ).toBundle()
+            )
             /*
              * The pending result fragment will forward to the correct screen
              * Because we can't save the test result at the moment (legal),
@@ -48,7 +54,7 @@ open class TestResultAvailableNotificationService(
              * By letting the forwarding happen via the PendingResultFragment,
              * we have a common location to retrieve the test result.
              */
-            setDestination(destination)
+            setDestination(R.id.submissionTestResultPendingFragment)
         }.createPendingIntent()
 
         val notification = notificationHelper.newBaseBuilder().apply {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt
index 6c0314767..78c38a1d8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt
@@ -38,6 +38,9 @@ data class PCRCoronaTest(
     @SerializedName("testResult")
     override val testResult: CoronaTestResult,
 
+    @SerializedName("lastUpdatedAt")
+    override val lastUpdatedAt: Instant,
+
     @Transient override val isProcessing: Boolean = false,
     @Transient override val lastError: Throwable? = null,
 ) : CoronaTest {
@@ -45,6 +48,9 @@ data class PCRCoronaTest(
     override val type: CoronaTest.Type
         get() = CoronaTest.Type.PCR
 
+    override val isFinal: Boolean
+        get() = testResult == CoronaTestResult.PCR_REDEEMED
+
     override val isPositive: Boolean
         get() = testResult == CoronaTestResult.PCR_POSITIVE
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt
index 59848afdd..88cd240a6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt
@@ -18,7 +18,6 @@ import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
-import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
@@ -26,6 +25,8 @@ import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import org.joda.time.Duration
 import org.joda.time.Instant
 import timber.log.Timber
 import javax.inject.Inject
@@ -37,7 +38,6 @@ class PCRProcessor @Inject constructor(
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector,
     private val testResultDataCollector: TestResultDataCollector,
     private val deadmanNotificationScheduler: DeadmanNotificationScheduler,
-    private val pcrTestResultScheduler: PCRResultScheduler,
 ) : CoronaTestProcessor {
 
     override val type: CoronaTest.Type = CoronaTest.Type.PCR
@@ -81,13 +81,12 @@ class PCRProcessor @Inject constructor(
 
         analyticsKeySubmissionCollector.reportTestRegistered()
 
-        if (testResult == PCR_OR_RAT_PENDING) {
-            pcrTestResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = true)
-        }
+        val now = timeStamper.nowUTC
 
         return PCRCoronaTest(
             identifier = request.identifier,
-            registeredAt = timeStamper.nowUTC,
+            registeredAt = now,
+            lastUpdatedAt = now,
             registrationToken = response.registrationToken,
             testResult = testResult,
             testResultReceivedAt = determineReceivedDate(null, testResult),
@@ -117,8 +116,9 @@ class PCRProcessor @Inject constructor(
             }
 
             test.copy(
-                testResult = newTestResult,
+                testResult = check60PlusDays(test, newTestResult),
                 testResultReceivedAt = determineReceivedDate(test, newTestResult),
+                lastUpdatedAt = timeStamper.nowUTC,
                 lastError = null
             )
         } catch (e: Exception) {
@@ -130,6 +130,19 @@ class PCRProcessor @Inject constructor(
         }
     }
 
+    // After 60 days, the previously EXPIRED test is deleted from the server, and it will return pending again.
+    private fun check60PlusDays(test: CoronaTest, newResult: CoronaTestResult): CoronaTestResult {
+        val calculateDays = Duration(test.registeredAt, timeStamper.nowUTC).standardDays
+        Timber.tag(TAG).d("Calculated test age: %d days", calculateDays)
+
+        return if (newResult == PCR_OR_RAT_PENDING && calculateDays >= BackgroundConstants.POLLING_VALIDITY_MAX_DAYS) {
+            Timber.tag(TAG).d("$calculateDays is exceeding the maximum polling duration")
+            PCR_REDEEMED
+        } else {
+            newResult
+        }
+    }
+
     private fun determineReceivedDate(oldTest: PCRCoronaTest?, newTestResult: CoronaTestResult): Instant? = when {
         oldTest != null && FINAL_STATES.contains(oldTest.testResult) -> oldTest.testResultReceivedAt
         FINAL_STATES.contains(newTestResult) -> timeStamper.nowUTC
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultRetrievalWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultRetrievalWorker.kt
new file mode 100644
index 000000000..74c5e98d6
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultRetrievalWorker.kt
@@ -0,0 +1,65 @@
+package de.rki.coronawarnapp.coronatest.type.pcr.execution
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.latestPCRT
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import kotlinx.coroutines.flow.first
+import timber.log.Timber
+
+/**
+ * Diagnosis test result retrieval by periodic polling
+ */
+class PCRResultRetrievalWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters,
+    private val coronaTestRepository: CoronaTestRepository,
+) : CoroutineWorker(context, workerParams) {
+
+    override suspend fun doWork(): Result {
+        Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount")
+
+        if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
+            Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Resuming at normal period.")
+            return Result.failure()
+        }
+
+        try {
+            val pcrTest = coronaTestRepository.latestPCRT.first()
+            Timber.tag(TAG).v("Current PCR test: %s", pcrTest)
+
+            if (pcrTest == null) {
+                // PCRResultScheduler will cancel us if test is null or test.isFinal == true
+                Timber.tag(TAG).d(" $id Stopping worker, there is no PCR test.")
+                return Result.success()
+            }
+
+            Timber.tag(TAG).d(" $id Running task.")
+            val coronaTest = coronaTestRepository.refresh(
+                type = CoronaTest.Type.PCR
+            ).single() as PCRCoronaTest
+            val testResult = coronaTest.testResult
+            Timber.tag(TAG).d("$id: Test result retrieved is $testResult")
+
+            return Result.success()
+        } catch (e: Exception) {
+            Timber.tag(TAG).e(e, "Test result retrieval worker failed.")
+            return Result.retry()
+        }
+    }
+
+    @AssistedFactory
+    interface Factory : InjectedWorkerFactory<PCRResultRetrievalWorker>
+
+    companion object {
+        private val TAG = PCRResultRetrievalWorker::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultScheduler.kt
new file mode 100644
index 000000000..32f1e570b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultScheduler.kt
@@ -0,0 +1,105 @@
+package de.rki.coronawarnapp.coronatest.type.pcr.execution
+
+import androidx.annotation.VisibleForTesting
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.latestPCRT
+import de.rki.coronawarnapp.coronatest.type.common.ResultScheduler
+import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class PCRResultScheduler @Inject constructor(
+    @AppScope private val appScope: CoroutineScope,
+    private val workManager: WorkManager,
+    private val coronaTestRepository: CoronaTestRepository,
+) : ResultScheduler(
+    workManager = workManager
+) {
+
+    @VisibleForTesting
+    internal val shouldBePolling = coronaTestRepository.latestPCRT
+        .map { test: PCRCoronaTest? ->
+            if (test == null) return@map false
+            !test.isFinal
+        }
+        .distinctUntilChanged()
+
+    fun setup() {
+        Timber.tag(TAG).i("setup() - PCRResultScheduler")
+        shouldBePolling
+            .onEach { shouldBePolling ->
+                Timber.tag(TAG).d("Polling state change: shouldBePolling=$shouldBePolling")
+                setPcrPeriodicTestPollingEnabled(enabled = shouldBePolling)
+            }
+            .launchIn(appScope)
+    }
+
+    internal suspend fun setPcrPeriodicTestPollingEnabled(enabled: Boolean) {
+        Timber.tag(TAG).i("setPcrPeriodicTestPollingEnabled(enabled=$enabled)")
+        if (enabled) {
+            val isScheduled = isScheduled(PCR_TESTRESULT_WORKER_UNIQUEUNAME)
+            Timber.tag(TAG).d("isScheduled=$isScheduled")
+
+            Timber.tag(TAG).d("enqueueUniquePeriodicWork PCR_TESTRESULT_WORKER_UNIQUEUNAME")
+            workManager.enqueueUniquePeriodicWork(
+                PCR_TESTRESULT_WORKER_UNIQUEUNAME,
+                ExistingPeriodicWorkPolicy.KEEP,
+                buildPcrTestResultRetrievalPeriodicWork()
+            )
+        } else {
+            Timber.tag(TAG).d("cancelWorker()")
+            workManager.cancelUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME)
+        }
+    }
+
+    private fun buildPcrTestResultRetrievalPeriodicWork() =
+        PeriodicWorkRequestBuilder<PCRResultRetrievalWorker>(
+            getPcrTestResultRetrievalPeriodicWorkTimeInterval(),
+            TimeUnit.MINUTES
+        )
+            .addTag(PCR_TESTRESULT_WORKER_TAG)
+            .setConstraints(
+                Constraints.Builder().apply {
+                    setRequiredNetworkType(NetworkType.CONNECTED)
+                }.build()
+            )
+            .setInitialDelay(
+                TEST_RESULT_PERIODIC_INITIAL_DELAY,
+                TimeUnit.SECONDS
+            ).setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .build()
+
+    companion object {
+        private const val PCR_TESTRESULT_WORKER_TAG = "DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER"
+        private const val PCR_TESTRESULT_WORKER_UNIQUEUNAME = "DiagnosisTestResultBackgroundPeriodicWork"
+
+        private const val TAG = "PCRTestResultScheduler"
+
+        private const val MINUTES_IN_DAY = 1440
+        private const val DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY = 12
+
+        @VisibleForTesting
+        internal fun getPcrTestResultRetrievalPeriodicWorkTimeInterval() =
+            (MINUTES_IN_DAY / DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY).toLong()
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt
new file mode 100644
index 000000000..d8d631cb6
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationService.kt
@@ -0,0 +1,77 @@
+package de.rki.coronawarnapp.coronatest.type.pcr.notification
+
+import android.content.Context
+import androidx.navigation.NavDeepLinkBuilder
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.latestPCRT
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.common.TestResultAvailableNotificationService
+import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.notification.GeneralNotifications
+import de.rki.coronawarnapp.notification.NotificationConstants
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.util.device.ForegroundState
+import de.rki.coronawarnapp.util.di.AppContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Provider
+import javax.inject.Singleton
+
+@Singleton
+class PCRTestResultAvailableNotificationService @Inject constructor(
+    @AppContext context: Context,
+    foregroundState: ForegroundState,
+    navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder>,
+    private val notificationHelper: GeneralNotifications,
+    cwaSettings: CWASettings,
+    private val coronaTestRepository: CoronaTestRepository,
+    @AppScope private val appScope: CoroutineScope,
+) : TestResultAvailableNotificationService(
+    context,
+    foregroundState,
+    navDeepLinkBuilderProvider,
+    notificationHelper,
+    cwaSettings,
+    NotificationConstants.PCR_TEST_RESULT_AVAILABLE_NOTIFICATION_ID
+) {
+    fun setup() {
+        Timber.tag(TAG).d("setup() - PCRTestResultAvailableNotificationService")
+
+        coronaTestRepository.latestPCRT
+            .onEach { _ ->
+                val test = coronaTestRepository.latestPCRT.first()
+                if (test == null) {
+                    cancelTestResultAvailableNotification()
+                    return@onEach
+                }
+
+                val alreadySent = test.isResultAvailableNotificationSent
+                val isInteresting = INTERESTING_STATES.contains(test.testResult)
+                Timber.tag(TAG).v("alreadySent=$alreadySent, isInteresting=$isInteresting")
+
+                if (!alreadySent && isInteresting) {
+                    coronaTestRepository.updateResultNotification(identifier = test.identifier, sent = true)
+                    showTestResultAvailableNotification(test)
+                    notificationHelper.cancelCurrentNotification(
+                        NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
+                    )
+                } else {
+                    cancelTestResultAvailableNotification()
+                }
+            }
+            .launchIn(appScope)
+    }
+
+    companion object {
+        private val INTERESTING_STATES = setOf(
+            CoronaTestResult.PCR_NEGATIVE,
+            CoronaTestResult.PCR_POSITIVE,
+            CoronaTestResult.PCR_INVALID,
+        )
+        private val TAG = PCRTestResultAvailableNotificationService::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt
index 8bb272b10..6e353f9b9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt
@@ -43,6 +43,9 @@ data class RACoronaTest(
     @SerializedName("testResultReceivedAt")
     override val testResultReceivedAt: Instant? = null,
 
+    @SerializedName("lastUpdatedAt")
+    override val lastUpdatedAt: Instant,
+
     @SerializedName("testResult")
     override val testResult: CoronaTestResult,
 
@@ -50,13 +53,13 @@ data class RACoronaTest(
     val testedAt: Instant,
 
     @SerializedName("firstName")
-    val firstName: String?,
+    val firstName: String? = null,
 
     @SerializedName("lastName")
-    val lastName: String?,
+    val lastName: String? = null,
 
     @SerializedName("dateOfBirth")
-    val dateOfBirth: LocalDate?,
+    val dateOfBirth: LocalDate? = null,
 
     @Transient override val isProcessing: Boolean = false,
     @Transient override val lastError: Throwable? = null,
@@ -83,6 +86,9 @@ data class RACoronaTest(
             }
         }
 
+    override val isFinal: Boolean
+        get() = testResult == RAT_REDEEMED
+
     override val isPositive: Boolean
         get() = testResult == RAT_POSITIVE
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt
index fea68d822..9454ce480 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt
@@ -17,11 +17,12 @@ import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import org.joda.time.Duration
 import org.joda.time.Instant
 import timber.log.Timber
 import javax.inject.Inject
@@ -30,7 +31,6 @@ import javax.inject.Inject
 class RapidAntigenProcessor @Inject constructor(
     private val timeStamper: TimeStamper,
     private val submissionService: CoronaTestService,
-    private val resultScheduler: RAResultScheduler,
 ) : CoronaTestProcessor {
 
     override val type: CoronaTest.Type = CoronaTest.Type.RAPID_ANTIGEN
@@ -43,13 +43,12 @@ class RapidAntigenProcessor @Inject constructor(
 
         val testResult = registrationData.testResult.validOrThrow()
 
-        if (testResult == PCR_OR_RAT_PENDING || testResult == RAT_PENDING) {
-            resultScheduler.setRatResultPeriodicPollingMode(mode = RAResultScheduler.RatPollingMode.PHASE1)
-        }
+        val now = timeStamper.nowUTC
 
         return RACoronaTest(
             identifier = request.identifier,
-            registeredAt = timeStamper.nowUTC,
+            registeredAt = now,
+            lastUpdatedAt = now,
             registrationToken = registrationData.registrationToken,
             testResult = testResult,
             testResultReceivedAt = determineReceivedDate(null, testResult),
@@ -82,11 +81,13 @@ class RapidAntigenProcessor @Inject constructor(
                 return test
             }
 
-            val testResult = submissionService.asyncRequestTestResult(test.registrationToken)
-            Timber.tag(TAG).d("Test result was %s", testResult)
+            val newTestResult = submissionService.asyncRequestTestResult(test.registrationToken)
+            Timber.tag(TAG).d("Test result was %s", newTestResult)
 
             test.copy(
-                testResult = testResult,
+                testResult = check60PlusDays(test, newTestResult),
+                testResultReceivedAt = determineReceivedDate(test, newTestResult),
+                lastUpdatedAt = timeStamper.nowUTC,
                 lastError = null
             )
         } catch (e: Exception) {
@@ -98,6 +99,22 @@ class RapidAntigenProcessor @Inject constructor(
         }
     }
 
+    // After 60 days, the previously EXPIRED test is deleted from the server, and it will return pending again.
+    private fun check60PlusDays(test: CoronaTest, newResult: CoronaTestResult): CoronaTestResult {
+        val calculateDays = Duration(test.registeredAt, timeStamper.nowUTC).standardDays
+        Timber.tag(TAG).d("Calculated test age: %d days", calculateDays)
+
+        return if (
+            (newResult == PCR_OR_RAT_PENDING || newResult == RAT_PENDING) &&
+            calculateDays >= BackgroundConstants.POLLING_VALIDITY_MAX_DAYS
+        ) {
+            Timber.tag(TAG).d("$calculateDays is exceeding the maximum polling duration")
+            RAT_REDEEMED
+        } else {
+            newResult
+        }
+    }
+
     override suspend fun onRemove(toBeRemoved: CoronaTest) {
         Timber.tag(TAG).v("onRemove(toBeRemoved=%s)", toBeRemoved)
         // Currently nothing to do
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/RAResultRetrievalWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultRetrievalWorker.kt
similarity index 50%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/RAResultRetrievalWorker.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultRetrievalWorker.kt
index 4ee68a0d4..f390af2d9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/RAResultRetrievalWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultRetrievalWorker.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.coronatest.worker
+package de.rki.coronawarnapp.coronatest.type.rapidantigen.execution
 
 import android.content.Context
 import androidx.work.CoroutineWorker
@@ -9,10 +9,8 @@ import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.latestRAT
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler.RatPollingMode.DISABLED
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler.RatPollingMode.PHASE1
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler.RatPollingMode.PHASE2
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler.RatPollingMode.PHASE1
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler.RatPollingMode.PHASE2
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import de.rki.coronawarnapp.worker.BackgroundConstants
@@ -35,56 +33,41 @@ class RAResultRetrievalWorker @AssistedInject constructor(
         Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount")
 
         if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
-            Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling")
-
-            ratResultScheduler.setRatResultPeriodicPollingMode(mode = ratResultScheduler.ratResultPeriodicPollingMode)
-            Timber.tag(TAG).d("$id Rescheduled background worker")
-
+            Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Resuming at normal period.")
             return Result.failure()
         }
 
-        // checking abort conditions
-        val rat = coronaTestRepository.latestRAT.first()
-        if (rat == null) {
-            Timber.tag(TAG).w("There is no RAT test available!?")
-            disablePolling()
-            return Result.success()
-        } else {
+        try {
+            val rat = coronaTestRepository.latestRAT.first()
+            Timber.tag(TAG).v("Current RA test: %s", rat)
+
+            if (rat == null) {
+                // RAResultScheduler will cancel us if the test is null or isFinal==true
+                Timber.tag(TAG).w("There is no RapidAntigen test available!?")
+                return Result.success()
+            }
+
+            coronaTestRepository.refresh(CoronaTest.Type.RAPID_ANTIGEN)
+
             val nowUTC = timeStamper.nowUTC
             val days = Duration(rat.registeredAt, nowUTC).standardDays
             val minutes = Duration(rat.registeredAt, nowUTC).standardMinutes
             val isPhase1 = ratResultScheduler.ratResultPeriodicPollingMode == PHASE1
             Timber.tag(TAG).d("Calculated days: %d", days)
-            when {
-                rat.isResultAvailableNotificationSent -> {
-                    Timber.tag(TAG).d("$id: Notification already sent.")
-                    disablePolling()
-                }
-                rat.isViewed -> {
-                    Timber.tag(TAG).d("$id: Test result has already been viewed.")
-                    disablePolling()
-                }
-                days >= BackgroundConstants.POLLING_VALIDITY_MAX_DAYS -> {
-                    Timber.tag(TAG).d("$id $days is exceeding the maximum polling duration")
-                    disablePolling()
-                }
-                isPhase1 && minutes >= RAT_POLLING_END_OF_PHASE1_MINUTES -> {
-                    Timber.tag(TAG).d("$id $minutes minutes - time for a phase 2!")
-                    ratResultScheduler.setRatResultPeriodicPollingMode(mode = PHASE2)
-                }
-                else -> {
-                    coronaTestRepository.refresh(CoronaTest.Type.RAPID_ANTIGEN)
-                }
+
+            // Time for phase2?
+            if (isPhase1 && minutes >= RAT_POLLING_END_OF_PHASE1_MINUTES) {
+                Timber.tag(TAG).d("$id $minutes minutes - time for a phase 2!")
+                ratResultScheduler.setRatResultPeriodicPollingMode(mode = PHASE2)
             }
+
             return Result.success()
+        } catch (e: Exception) {
+            Timber.tag(TAG).e(e, "Test result retrieval worker failed.")
+            return Result.retry()
         }
     }
 
-    private fun disablePolling() {
-        ratResultScheduler.setRatResultPeriodicPollingMode(mode = DISABLED)
-        Timber.tag(TAG).d("$id: polling disabled")
-    }
-
     @AssistedFactory
     interface Factory : InjectedWorkerFactory<RAResultRetrievalWorker>
 
@@ -93,8 +76,6 @@ class RAResultRetrievalWorker @AssistedInject constructor(
 
         /**
          * The time when rat polling is switched to a larger interval
-         *
-         * @see TimeUnit.MINUTES
          */
         private const val RAT_POLLING_END_OF_PHASE1_MINUTES = 90
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultScheduler.kt
new file mode 100644
index 000000000..0b5c894f2
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/execution/RAResultScheduler.kt
@@ -0,0 +1,127 @@
+package de.rki.coronawarnapp.coronatest.type.rapidantigen.execution
+
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import androidx.work.WorkManager
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.latestRAT
+import de.rki.coronawarnapp.coronatest.type.common.ResultScheduler
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler.RatPollingMode.DISABLED
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler.RatPollingMode.PHASE1
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RAResultScheduler @Inject constructor(
+    @AppScope private val appScope: CoroutineScope,
+    private val workManager: WorkManager,
+    private val coronaTestRepository: CoronaTestRepository,
+) : ResultScheduler(
+    workManager = workManager
+) {
+
+    private var ratWorkerMode = DISABLED
+    val ratResultPeriodicPollingMode
+        get() = ratWorkerMode
+
+    enum class RatPollingMode {
+        DISABLED,
+        PHASE1,
+        PHASE2
+    }
+
+    fun setup() {
+        Timber.tag(TAG).d("setup() - RAResultScheduler")
+        coronaTestRepository.latestRAT
+            .map { test: RACoronaTest? ->
+                if (test == null) return@map false
+                !test.isFinal
+            }
+            .distinctUntilChanged()
+            .onEach { shouldBePolling ->
+                val isScheduled = isScheduled(RAT_RESULT_WORKER_UNIQUEUNAME)
+                Timber.tag(TAG).d("Polling state change: shouldBePolling=$shouldBePolling, isScheduled=$isScheduled")
+
+                if (shouldBePolling && isScheduled) {
+                    Timber.tag(TAG).d("We are already scheduled, no changing MODE.")
+                } else if (shouldBePolling && !isScheduled) {
+                    Timber.tag(TAG).d("We should be polling, but are not scheduled, scheduling...")
+                    setRatResultPeriodicPollingMode(PHASE1)
+                } else {
+                    Timber.tag(TAG).d("We should not be polling, canceling...")
+                    setRatResultPeriodicPollingMode(DISABLED)
+                }
+            }
+            .launchIn(appScope)
+    }
+
+    internal fun setRatResultPeriodicPollingMode(mode: RatPollingMode) {
+        Timber.tag(TAG).i("setRatResultPeriodicPollingMode(mode=%s)", mode)
+        ratWorkerMode = mode
+        if (mode == DISABLED) {
+            Timber.tag(TAG).d("cancelWorker()")
+            workManager.cancelUniqueWork(RAT_RESULT_WORKER_UNIQUEUNAME)
+        } else {
+            // no check for already running workers!
+            // worker must be replaced by next phase instance
+            Timber.tag(TAG).d("Queueing rat result worker (RAT_RESULT_PERIODIC_WORKER)")
+            workManager.enqueueUniquePeriodicWork(
+                RAT_RESULT_WORKER_UNIQUEUNAME,
+                ExistingPeriodicWorkPolicy.REPLACE,
+                buildRatResultRetrievalPeriodicWork(mode)
+            )
+        }
+    }
+
+    private fun buildRatResultRetrievalPeriodicWork(pollingMode: RatPollingMode): PeriodicWorkRequest {
+        val repeatInterval = if (pollingMode == PHASE1) {
+            ratResultRetrievalPeriodicWorkPhase1IntervalInMinutes
+        } else {
+            ratResultRetrievalPeriodicWorkPhase2IntervalInMinutes
+        }
+        return PeriodicWorkRequestBuilder<RAResultRetrievalWorker>(
+            repeatInterval,
+            TimeUnit.MINUTES
+        )
+            .addTag(RAT_RESULT_WORKER_TAG)
+            .setConstraints(
+                Constraints.Builder().apply {
+                    setRequiredNetworkType(NetworkType.CONNECTED)
+                }.build()
+            )
+            .setInitialDelay(
+                TEST_RESULT_PERIODIC_INITIAL_DELAY,
+                TimeUnit.SECONDS
+            ).setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .build()
+    }
+
+    companion object {
+        private const val RAT_RESULT_WORKER_TAG = "RAT_RESULT_PERIODIC_WORKER"
+        private const val RAT_RESULT_WORKER_UNIQUEUNAME = "RatResultRetrievalWorker"
+
+        private const val TAG = "RAResultScheduler"
+
+        private const val ratResultRetrievalPeriodicWorkPhase1IntervalInMinutes = 15L
+
+        private const val ratResultRetrievalPeriodicWorkPhase2IntervalInMinutes = 90L
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt
new file mode 100644
index 000000000..b7d5a0886
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATTestResultAvailableNotificationService.kt
@@ -0,0 +1,78 @@
+package de.rki.coronawarnapp.coronatest.type.rapidantigen.notification
+
+import android.content.Context
+import androidx.navigation.NavDeepLinkBuilder
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.latestPCRT
+import de.rki.coronawarnapp.coronatest.latestRAT
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.common.TestResultAvailableNotificationService
+import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.notification.GeneralNotifications
+import de.rki.coronawarnapp.notification.NotificationConstants
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.util.device.ForegroundState
+import de.rki.coronawarnapp.util.di.AppContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Provider
+import javax.inject.Singleton
+
+@Singleton
+class RATTestResultAvailableNotificationService @Inject constructor(
+    @AppContext context: Context,
+    foregroundState: ForegroundState,
+    navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder>,
+    private val notificationHelper: GeneralNotifications,
+    cwaSettings: CWASettings,
+    private val coronaTestRepository: CoronaTestRepository,
+    @AppScope private val appScope: CoroutineScope,
+) : TestResultAvailableNotificationService(
+    context,
+    foregroundState,
+    navDeepLinkBuilderProvider,
+    notificationHelper,
+    cwaSettings,
+    NotificationConstants.RAT_TEST_RESULT_AVAILABLE_NOTIFICATION_ID,
+) {
+    fun setup() {
+        Timber.tag(TAG).d("setup() - RATTestResultAvailableNotificationService")
+
+        coronaTestRepository.latestPCRT
+            .onEach { _ ->
+                val test = coronaTestRepository.latestRAT.first()
+                if (test == null) {
+                    cancelTestResultAvailableNotification()
+                    return@onEach
+                }
+
+                val alreadySent = test.isResultAvailableNotificationSent
+                val isInteresting = INTERESTING_STATES.contains(test.testResult)
+                Timber.tag(TAG).v("alreadySent=$alreadySent, isInteresting=$isInteresting")
+
+                if (!alreadySent && isInteresting) {
+                    coronaTestRepository.updateResultNotification(identifier = test.identifier, sent = true)
+                    showTestResultAvailableNotification(test)
+                    notificationHelper.cancelCurrentNotification(
+                        NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
+                    )
+                } else {
+                    cancelTestResultAvailableNotification()
+                }
+            }
+            .launchIn(appScope)
+    }
+
+    companion object {
+        private val INTERESTING_STATES = setOf(
+            CoronaTestResult.RAT_NEGATIVE,
+            CoronaTestResult.RAT_POSITIVE,
+            CoronaTestResult.RAT_INVALID,
+        )
+        private val TAG = RATTestResultAvailableNotificationService::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt
deleted file mode 100644
index 4fe1c6e55..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package de.rki.coronawarnapp.coronatest.worker
-
-import android.content.Context
-import androidx.work.CoroutineWorker
-import androidx.work.WorkerParameters
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.latestPCRT
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
-import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
-import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
-import de.rki.coronawarnapp.notification.GeneralNotifications
-import de.rki.coronawarnapp.notification.NotificationConstants
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
-import de.rki.coronawarnapp.util.TimeStamper
-import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
-import de.rki.coronawarnapp.worker.BackgroundConstants
-import kotlinx.coroutines.flow.first
-import org.joda.time.Duration
-import org.joda.time.Instant
-import timber.log.Timber
-
-/**
- * Diagnosis test result retrieval by periodic polling
- */
-class PCRResultRetrievalWorker @AssistedInject constructor(
-    @Assisted val context: Context,
-    @Assisted workerParams: WorkerParameters,
-    private val testResultAvailableNotificationService: PCRTestResultAvailableNotificationService,
-    private val notificationHelper: GeneralNotifications,
-    private val coronaTestRepository: CoronaTestRepository,
-    private val timeStamper: TimeStamper,
-    private val testResultScheduler: PCRResultScheduler,
-) : CoroutineWorker(context, workerParams) {
-
-    override suspend fun doWork(): Result {
-        Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount")
-
-        if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
-            Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling")
-
-            testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = true)
-            Timber.tag(TAG).d("$id Rescheduled background worker")
-
-            return Result.failure()
-        }
-        var result = Result.success()
-        try {
-            if (abortConditionsMet(timeStamper.nowUTC)) {
-                Timber.tag(TAG).d(" $id Stopping worker.")
-                disablePolling()
-            } else {
-                Timber.tag(TAG).d(" $id Running worker.")
-
-                val coronaTest = coronaTestRepository.refresh(
-                    type = CoronaTest.Type.PCR
-                ).single() as PCRCoronaTest
-                val testResult = coronaTest.testResult
-
-                Timber.tag(TAG).d("$id: Test Result retrieved is $testResult")
-
-                if (testResult == CoronaTestResult.PCR_NEGATIVE ||
-                    testResult == CoronaTestResult.PCR_POSITIVE ||
-                    testResult == CoronaTestResult.PCR_INVALID
-                ) {
-                    sendTestResultAvailableNotification(coronaTest)
-                    cancelRiskLevelScoreNotification()
-                    Timber.tag(TAG)
-                        .d("$id: Test Result available - notification sent & risk level notification canceled")
-                    disablePolling()
-                }
-            }
-        } catch (e: Exception) {
-            Timber.tag(TAG).e(e, "Test result retrieval worker failed.")
-            result = Result.retry()
-        }
-
-        Timber.tag(TAG).d("$id: doWork() finished with %s", result)
-
-        return result
-    }
-
-    private suspend fun abortConditionsMet(nowUTC: Instant): Boolean {
-        val pcrTest = coronaTestRepository.latestPCRT.first()
-        if (pcrTest == null) {
-            Timber.tag(TAG).w("There is no PCR available!?")
-            return true
-        }
-
-        if (pcrTest.isResultAvailableNotificationSent) {
-            Timber.tag(TAG).d("$id: Notification already sent.")
-            return true
-        }
-
-        if (pcrTest.isViewed) {
-            Timber.tag(TAG).d("$id: Test result has already been viewed.")
-            return true
-        }
-
-        val calculateDays = Duration(pcrTest.registeredAt, nowUTC).standardDays
-        Timber.tag(TAG).d("Calculated days: %d", calculateDays)
-
-        if (calculateDays >= BackgroundConstants.POLLING_VALIDITY_MAX_DAYS) {
-            Timber.tag(TAG).d("$id $calculateDays is exceeding the maximum polling duration")
-            return true
-        }
-
-        return false
-    }
-
-    private suspend fun sendTestResultAvailableNotification(coronaTest: CoronaTest) {
-        testResultAvailableNotificationService.showTestResultAvailableNotification(coronaTest.testResult)
-        coronaTestRepository.updateResultNotification(identifier = coronaTest.identifier, sent = true)
-    }
-
-    private fun cancelRiskLevelScoreNotification() {
-        notificationHelper.cancelCurrentNotification(
-            NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-        )
-    }
-
-    private fun disablePolling() {
-        testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false)
-        Timber.tag(TAG).d("$id: polling disabled")
-    }
-
-    @AssistedFactory
-    interface Factory : InjectedWorkerFactory<PCRResultRetrievalWorker>
-
-    companion object {
-        private val TAG = PCRResultRetrievalWorker::class.java.simpleName
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt
deleted file mode 100644
index c9e83f1f8..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package de.rki.coronawarnapp.coronatest.worker.execution
-
-import androidx.annotation.VisibleForTesting
-import androidx.work.BackoffPolicy
-import androidx.work.ExistingPeriodicWorkPolicy
-import androidx.work.PeriodicWorkRequestBuilder
-import androidx.work.WorkInfo
-import androidx.work.WorkManager
-import dagger.Reusable
-import de.rki.coronawarnapp.coronatest.worker.PCRResultRetrievalWorker
-import de.rki.coronawarnapp.util.coroutine.await
-import de.rki.coronawarnapp.worker.BackgroundConstants
-import de.rki.coronawarnapp.worker.BackgroundConstants.DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY
-import de.rki.coronawarnapp.worker.BackgroundConstants.MINUTES_IN_DAY
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
-import kotlinx.coroutines.runBlocking
-import timber.log.Timber
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-@Reusable
-class PCRResultScheduler @Inject constructor(
-    private val workManager: WorkManager,
-) {
-
-    private suspend fun isPcrScheduled() =
-        workManager.getWorkInfosForUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME)
-            .await()
-            .any { it.isScheduled }
-
-    private val WorkInfo.isScheduled: Boolean
-        get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED
-
-    fun setPcrPeriodicTestPollingEnabled(enabled: Boolean) {
-        if (enabled) {
-            // TODO Refactor runBlocking away
-            val isScheduled = runBlocking { isPcrScheduled() }
-            if (isScheduled) {
-                Timber.tag(TAG).w("Already scheduled, skipping")
-                return
-            }
-            Timber.tag(TAG).i("Queueing pcr test result worker (DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER)")
-            workManager.enqueueUniquePeriodicWork(
-                PCR_TESTRESULT_WORKER_UNIQUEUNAME,
-                ExistingPeriodicWorkPolicy.REPLACE,
-                buildPcrTestResultRetrievalPeriodicWork()
-            )
-        } else {
-            Timber.tag(TAG).d("cancelWorker()")
-            workManager.cancelUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME)
-        }
-    }
-
-    private fun buildPcrTestResultRetrievalPeriodicWork() =
-        PeriodicWorkRequestBuilder<PCRResultRetrievalWorker>(
-            getPcrTestResultRetrievalPeriodicWorkTimeInterval(),
-            TimeUnit.MINUTES
-        )
-            .addTag(PCR_TESTRESULT_WORKER_TAG)
-            .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
-            .setInitialDelay(
-                DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY,
-                TimeUnit.SECONDS
-            ).setBackoffCriteria(
-                BackoffPolicy.LINEAR,
-                BackgroundConstants.KIND_DELAY,
-                TimeUnit.MINUTES
-            )
-            .build()
-
-    companion object {
-        /**
-         * Kind initial delay in minutes for periodic work for accessibility reason
-         *
-         * @see TimeUnit.SECONDS
-         */
-        private const val DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY = 10L
-        private const val PCR_TESTRESULT_WORKER_TAG = "DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER"
-        private const val PCR_TESTRESULT_WORKER_UNIQUEUNAME = "DiagnosisTestResultBackgroundPeriodicWork"
-
-        private const val TAG = "PCRTestResultScheduler"
-
-        /**
-         * Calculate the time for pcr diagnosis key retrieval periodic work
-         *
-         * @return Long
-         *
-         * @see BackgroundConstants.MINUTES_IN_DAY
-         */
-        @VisibleForTesting
-        internal fun getPcrTestResultRetrievalPeriodicWorkTimeInterval() =
-            (MINUTES_IN_DAY / DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY).toLong()
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/RAResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/RAResultScheduler.kt
deleted file mode 100644
index f1253c501..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/RAResultScheduler.kt
+++ /dev/null
@@ -1,89 +0,0 @@
-package de.rki.coronawarnapp.coronatest.worker.execution
-
-import androidx.work.BackoffPolicy
-import androidx.work.ExistingPeriodicWorkPolicy
-import androidx.work.PeriodicWorkRequest
-import androidx.work.PeriodicWorkRequestBuilder
-import androidx.work.WorkManager
-import dagger.Reusable
-import de.rki.coronawarnapp.coronatest.worker.RAResultRetrievalWorker
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler.RatPollingMode.DISABLED
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler.RatPollingMode.PHASE1
-import de.rki.coronawarnapp.worker.BackgroundConstants
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
-import timber.log.Timber
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-@Reusable
-class RAResultScheduler @Inject constructor(
-    private val workManager: WorkManager,
-) {
-
-    private var ratWorkerMode = DISABLED
-    val ratResultPeriodicPollingMode
-        get() = ratWorkerMode
-
-    enum class RatPollingMode {
-        DISABLED,
-        PHASE1,
-        PHASE2
-    }
-
-    fun setRatResultPeriodicPollingMode(mode: RatPollingMode) {
-        ratWorkerMode = mode
-        if (mode == DISABLED) {
-            Timber.tag(TAG).d("cancelWorker()")
-            workManager.cancelUniqueWork(RAT_RESULT_WORKER_UNIQUEUNAME)
-        } else {
-            // no check for already running workers!
-            // worker must be replaced by next phase instance
-            Timber.tag(TAG).i("Queueing rat result worker (RAT_RESULT_PERIODIC_WORKER)")
-            workManager.enqueueUniquePeriodicWork(
-                RAT_RESULT_WORKER_UNIQUEUNAME,
-                ExistingPeriodicWorkPolicy.REPLACE,
-                buildRatResultRetrievalPeriodicWork(mode)
-            )
-        }
-    }
-
-    private fun buildRatResultRetrievalPeriodicWork(pollingMode: RatPollingMode): PeriodicWorkRequest {
-        val repeatInterval = if (pollingMode == PHASE1) {
-            ratResultRetrievalPeriodicWorkPhase1IntervalInMinutes
-        } else {
-            ratResultRetrievalPeriodicWorkPhase2IntervalInMinutes
-        }
-        return PeriodicWorkRequestBuilder<RAResultRetrievalWorker>(
-            repeatInterval,
-            TimeUnit.MINUTES
-        )
-            .addTag(RAT_RESULT_WORKER_TAG)
-            .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
-            .setInitialDelay(
-                DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY,
-                TimeUnit.SECONDS
-            ).setBackoffCriteria(
-                BackoffPolicy.LINEAR,
-                BackgroundConstants.KIND_DELAY,
-                TimeUnit.MINUTES
-            )
-            .build()
-    }
-
-    companion object {
-        /**
-         * Kind initial delay in minutes for periodic work for accessibility reason
-         *
-         * @see TimeUnit.SECONDS
-         */
-        private const val DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY = 10L
-        private const val RAT_RESULT_WORKER_TAG = "RAT_RESULT_PERIODIC_WORKER"
-        private const val RAT_RESULT_WORKER_UNIQUEUNAME = "RatResultRetrievalWorker"
-
-        private const val TAG = "RatResultScheduler"
-
-        private const val ratResultRetrievalPeriodicWorkPhase1IntervalInMinutes = 15L
-
-        private const val ratResultRetrievalPeriodicWorkPhase2IntervalInMinutes = 90L
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt
index 4c273c3bf..9c4f70657 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt
@@ -173,7 +173,7 @@ class Analytics @Inject constructor(
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     fun stopDueToTimeSinceOnboarding(): Boolean {
-        val onboarding = onboardingSettings.onboardingCompletedTimestamp ?: return true
+        val onboarding = onboardingSettings.onboardingCompletedTimestamp.value ?: return true
         return onboarding.plus(Hours.hours(ONBOARDING_DELAY_HOURS).toStandardDuration()).isAfter(timeStamper.nowUTC)
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt
index fc0852a0d..d67951dd6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt
@@ -10,7 +10,6 @@ import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import de.rki.coronawarnapp.worker.BackgroundConstants
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.flow.first
 import org.joda.time.Duration
 import org.joda.time.Instant
@@ -18,8 +17,6 @@ import timber.log.Timber
 
 /**
  * Periodic background noise worker
- *
- * @see BackgroundWorkScheduler
  */
 class BackgroundNoisePeriodicWorker @AssistedInject constructor(
     @Assisted val context: Context,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt
index c7d8db4da..c41435153 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt
@@ -1,17 +1,19 @@
 package de.rki.coronawarnapp.deniability
 
 import androidx.work.BackoffPolicy
+import androidx.work.Constraints
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
 import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.PeriodicWorkRequestBuilder
 import androidx.work.WorkManager
 import dagger.Reusable
 import de.rki.coronawarnapp.worker.BackgroundConstants
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import timber.log.Timber
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
+import kotlin.random.Random
 
 @Reusable
 class NoiseScheduler @Inject constructor(
@@ -44,9 +46,13 @@ class NoiseScheduler @Inject constructor(
     private fun buildBackgroundNoiseOneTimeWork() =
         OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>()
             .addTag(BACKGROUND_NOISE_ONE_TIME_WORKER_TAG)
-            .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
+            .setConstraints(
+                Constraints.Builder().apply {
+                    setRequiredNetworkType(NetworkType.CONNECTED)
+                }.build()
+            )
             .setInitialDelay(
-                BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(),
+                getBackgroundNoiseOneTimeWorkDelay(),
                 TimeUnit.HOURS
             ).setBackoffCriteria(
                 BackoffPolicy.LINEAR,
@@ -101,5 +107,21 @@ class NoiseScheduler @Inject constructor(
         const val BACKGROUND_NOISE_ONE_TIME_WORK_NAME = "BackgroundNoiseOneTimeWork"
 
         private const val TAG = "NoiseScheduler"
+
+        /**
+         * Get background noise one time work delay
+         * The periodic job is already delayed by MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
+         * so we only need to delay further by the difference between min and max.
+         *
+         * @return Long
+         *
+         * @see BackgroundConstants.MAX_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
+         * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
+         */
+        fun getBackgroundNoiseOneTimeWorkDelay() = Random.nextLong(
+            0,
+            BackgroundConstants.MAX_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION -
+                BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
+        )
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/PCRTestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/PCRTestResultAvailableNotificationService.kt
deleted file mode 100644
index 15eb8d656..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/PCRTestResultAvailableNotificationService.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.rki.coronawarnapp.notification
-
-import android.content.Context
-import androidx.navigation.NavDeepLinkBuilder
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.main.CWASettings
-import de.rki.coronawarnapp.util.device.ForegroundState
-import de.rki.coronawarnapp.util.di.AppContext
-import javax.inject.Inject
-import javax.inject.Provider
-
-class PCRTestResultAvailableNotificationService @Inject constructor(
-    @AppContext context: Context,
-    foregroundState: ForegroundState,
-    navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder>,
-    notificationHelper: GeneralNotifications,
-    cwaSettings: CWASettings
-) : TestResultAvailableNotificationService(
-    context,
-    foregroundState,
-    navDeepLinkBuilderProvider,
-    notificationHelper,
-    cwaSettings,
-    NotificationConstants.PCR_TEST_RESULT_AVAILABLE_NOTIFICATION_ID,
-    R.id.submissionTestResultPendingFragment
-)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/RATTestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/RATTestResultAvailableNotificationService.kt
deleted file mode 100644
index 2d85badd4..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/RATTestResultAvailableNotificationService.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.rki.coronawarnapp.notification
-
-import android.content.Context
-import androidx.navigation.NavDeepLinkBuilder
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.main.CWASettings
-import de.rki.coronawarnapp.util.device.ForegroundState
-import de.rki.coronawarnapp.util.di.AppContext
-import javax.inject.Inject
-import javax.inject.Provider
-
-class RATTestResultAvailableNotificationService @Inject constructor(
-    @AppContext context: Context,
-    foregroundState: ForegroundState,
-    navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder>,
-    notificationHelper: GeneralNotifications,
-    cwaSettings: CWASettings
-) : TestResultAvailableNotificationService(
-    context,
-    foregroundState,
-    navDeepLinkBuilderProvider,
-    notificationHelper,
-    cwaSettings,
-    NotificationConstants.RAT_TEST_RESULT_AVAILABLE_NOTIFICATION_ID,
-    R.id.submissionTestResultPendingFragment // TODO check nav args, after other PRs are merged
-)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/TraceLocationSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/TraceLocationSettings.kt
index 11b0c580f..ecd7764c7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/TraceLocationSettings.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/TraceLocationSettings.kt
@@ -2,7 +2,11 @@ package de.rki.coronawarnapp.presencetracing
 
 import android.content.Context
 import de.rki.coronawarnapp.util.di.AppContext
+import de.rki.coronawarnapp.util.preferences.FlowPreference
 import de.rki.coronawarnapp.util.preferences.clearAndNotify
+import de.rki.coronawarnapp.util.preferences.createFlowPreference
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -15,14 +19,22 @@ class TraceLocationSettings @Inject constructor(
         context.getSharedPreferences(name, Context.MODE_PRIVATE)
     }
 
-    var onboardingStatus: OnboardingStatus
-        get() {
-            val order = preferences.getInt(key_status, OnboardingStatus.NOT_ONBOARDED.order)
-            return OnboardingStatus.values().find { it.order == order } ?: OnboardingStatus.NOT_ONBOARDED
+    val onboardingStatus: FlowPreference<OnboardingStatus> = preferences.createFlowPreference(
+        key = PKEY_ONBOARDING_STATUS,
+        reader = { key ->
+            val order = getInt(key, OnboardingStatus.NOT_ONBOARDED.order)
+            OnboardingStatus.values().find { it.order == order } ?: OnboardingStatus.NOT_ONBOARDED
+        },
+        writer = { key, value ->
+            putInt(key, value.order)
         }
-        set(value) = preferences.edit().putInt(key_status, value.order).apply()
+    )
 
-    inline val isOnboardingDone get() = onboardingStatus == OnboardingStatus.ONBOARDED_2_0
+    inline val isOnboardingDoneFlow: Flow<Boolean>
+        get() = onboardingStatus.flow.map { it == OnboardingStatus.ONBOARDED_2_0 }
+
+    inline val isOnboardingDone: Boolean
+        get() = onboardingStatus.value == OnboardingStatus.ONBOARDED_2_0
 
     fun clear() {
         preferences.clearAndNotify()
@@ -34,7 +46,7 @@ class TraceLocationSettings @Inject constructor(
     }
 
     companion object {
-        private const val key_status = "trace_location_onboardingstatus"
+        private const val PKEY_ONBOARDING_STATUS = "trace_location_onboardingstatus"
         private const val name = "trace_location_localdata"
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingRiskWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingRiskWorkScheduler.kt
new file mode 100644
index 000000000..939213435
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingRiskWorkScheduler.kt
@@ -0,0 +1,85 @@
+package de.rki.coronawarnapp.presencetracing.risk.execution
+
+import android.annotation.SuppressLint
+import androidx.work.WorkManager
+import dagger.Reusable
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.isRiskCalculationNecessary
+import de.rki.coronawarnapp.presencetracing.TraceLocationSettings
+import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.util.coroutine.await
+import de.rki.coronawarnapp.util.device.BackgroundModeStatus
+import de.rki.coronawarnapp.util.flow.combine
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class PresenceTracingRiskWorkScheduler @Inject constructor(
+    @AppScope private val appScope: CoroutineScope,
+    private val workManager: WorkManager,
+    private val taskController: TaskController,
+    private val presenceWorkBuilder: PresenceTracingWarningWorkBuilder,
+    private val backgroundModeStatus: BackgroundModeStatus,
+    private val presenceTracingSettings: TraceLocationSettings,
+    private val coronaTestRepository: CoronaTestRepository,
+) : RiskWorkScheduler(
+    workManager = workManager,
+    logTag = TAG,
+) {
+
+    @SuppressLint("BinaryOperationInTimber")
+    fun setup() {
+        Timber.tag(TAG).i("setup() PresenceTracingRiskWorkScheduler")
+        combine(
+            backgroundModeStatus.isAutoModeEnabled,
+            presenceTracingSettings.isOnboardingDoneFlow,
+            coronaTestRepository.isRiskCalculationNecessary,
+        ) { isAutoMode, isPresenceTracingOnboarded, isRiskCalculationNecessesary ->
+            Timber.tag(TAG).d(
+                "isAutoMode=$isAutoMode, " +
+                    "isPresenceTracingOnboarded=$isPresenceTracingOnboarded, " +
+                    "isRiskCalculationNecessesary=$isRiskCalculationNecessesary"
+            )
+            isAutoMode && isPresenceTracingOnboarded && isRiskCalculationNecessesary
+        }
+            .onEach { runPeriodicWorker ->
+                Timber.tag(TAG).v("runPeriodicWorker=$runPeriodicWorker")
+                setPeriodicRiskCalculation(enabled = runPeriodicWorker)
+            }
+            .launchIn(appScope)
+    }
+
+    fun runRiskTaskNow(sourceTag: String) = taskController.submit(
+        DefaultTaskRequest(
+            PresenceTracingWarningTask::class,
+            originTag = "PresenceTracingRiskWorkScheduler-$sourceTag"
+        )
+    )
+
+    override suspend fun isScheduled(): Boolean = workManager
+        .getWorkInfosForUniqueWork(WORKER_ID_PRESENCE_TRACING)
+        .await()
+        .any { it.isScheduled }
+
+    override fun setPeriodicRiskCalculation(enabled: Boolean) {
+        Timber.tag(TAG).i("setPeriodicRiskCalculation(enabled=$enabled)")
+
+        if (enabled) {
+            val warningRequest = presenceWorkBuilder.createPeriodicWorkRequest()
+            queueWorker(WORKER_ID_PRESENCE_TRACING, warningRequest)
+        } else {
+            cancelWorker(WORKER_ID_PRESENCE_TRACING)
+        }
+    }
+
+    companion object {
+        private const val WORKER_ID_PRESENCE_TRACING = "PresenceTracingWarningWorker"
+        private const val TAG = "PTRiskWorkScheduler"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/ExposureWindowRiskWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/ExposureWindowRiskWorkScheduler.kt
new file mode 100644
index 000000000..e65fa414d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/ExposureWindowRiskWorkScheduler.kt
@@ -0,0 +1,102 @@
+package de.rki.coronawarnapp.risk.execution
+
+import android.annotation.SuppressLint
+import androidx.work.WorkManager
+import dagger.Reusable
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.isRiskCalculationNecessary
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
+import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorkBuilder
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.risk.RiskLevelTask
+import de.rki.coronawarnapp.storage.OnboardingSettings
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.util.coroutine.await
+import de.rki.coronawarnapp.util.device.BackgroundModeStatus
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class ExposureWindowRiskWorkScheduler @Inject constructor(
+    @AppScope private val appScope: CoroutineScope,
+    private val workManager: WorkManager,
+    private val taskController: TaskController,
+    private val diagnosisWorkBuilder: DiagnosisKeyRetrievalWorkBuilder,
+    private val backgroundModeStatus: BackgroundModeStatus,
+    private val onboardingSettings: OnboardingSettings,
+    private val enfClient: ENFClient,
+    private val coronaTestRepository: CoronaTestRepository,
+) : RiskWorkScheduler(
+    workManager = workManager,
+    logTag = TAG,
+) {
+
+    @SuppressLint("BinaryOperationInTimber")
+    fun setup() {
+        Timber.tag(TAG).i("setup() ExposureWindowRiskWorkScheduler")
+        combine(
+            backgroundModeStatus.isAutoModeEnabled,
+            onboardingSettings.isOnboardedFlow,
+            enfClient.isTracingEnabled,
+            coronaTestRepository.isRiskCalculationNecessary,
+        ) { isAutoMode, isOnboarded, isTracing, isRiskCalculationNecessesary ->
+            Timber.tag(TAG).d(
+                "isAutoMode=$isAutoMode, " +
+                    "isOnBoarded=$isOnboarded, " +
+                    "isTracing=$isTracing, " +
+                    "isRiskCalculationNecessesary=$isRiskCalculationNecessesary"
+            )
+            isAutoMode && isOnboarded && isTracing && isRiskCalculationNecessesary
+        }
+            .onEach { runPeriodicWorker ->
+                Timber.tag(TAG).v("runPeriodicWorker=$runPeriodicWorker")
+                setPeriodicRiskCalculation(enabled = runPeriodicWorker)
+            }
+            .launchIn(appScope)
+    }
+
+    suspend fun runRiskTasksNow(sourceTag: String) = appScope.launch {
+        taskController.submitBlocking(
+            DefaultTaskRequest(
+                DownloadDiagnosisKeysTask::class,
+                DownloadDiagnosisKeysTask.Arguments(),
+                originTag = "ExposureWindowRiskWorkScheduler-$sourceTag"
+            )
+        )
+        taskController.submit(
+            DefaultTaskRequest(
+                RiskLevelTask::class,
+                originTag = "ExposureWindowRiskWorkScheduler-$sourceTag"
+            )
+        )
+    }
+
+    override suspend fun isScheduled(): Boolean = workManager
+        .getWorkInfosForUniqueWork(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD)
+        .await()
+        .any { it.isScheduled }
+
+    override fun setPeriodicRiskCalculation(enabled: Boolean) {
+        Timber.tag(TAG).i("setPeriodicRiskCalculation(enabled=$enabled)")
+
+        if (enabled) {
+            val diagnosisRequest = diagnosisWorkBuilder.createPeriodicWorkRequest()
+            queueWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD, diagnosisRequest)
+        } else {
+            cancelWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD)
+        }
+    }
+
+    companion object {
+        private const val WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD = "DiagnosisKeyRetrievalWorker"
+        private const val TAG = "EWRiskWorkScheduler"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt
index ba05ce2a8..37e1eb4a8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt
@@ -4,88 +4,19 @@ import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.PeriodicWorkRequest
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
-import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorkBuilder
-import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask
-import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorkBuilder
-import de.rki.coronawarnapp.task.TaskController
-import de.rki.coronawarnapp.task.TaskState
-import de.rki.coronawarnapp.task.common.DefaultTaskRequest
-import de.rki.coronawarnapp.task.submitBlocking
-import de.rki.coronawarnapp.util.coroutine.AppScope
-import de.rki.coronawarnapp.util.coroutine.await
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
 import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Singleton
 
-@Singleton
-class RiskWorkScheduler @Inject constructor(
-    @AppScope private val appScope: CoroutineScope,
+abstract class RiskWorkScheduler(
     private val workManager: WorkManager,
-    private val taskController: TaskController,
-    private val presenceWorkBuilder: PresenceTracingWarningWorkBuilder,
-    private val diagnosisWorkBuilder: DiagnosisKeyRetrievalWorkBuilder,
+    private val logTag: String
 ) {
 
-    suspend fun runRiskTasksNow(): List<TaskState> {
-        val diagnosisKeysState = appScope.async {
-            Timber.tag(TAG).d("Running DownloadDiagnosisKeysTask")
-            val result = taskController.submitBlocking(
-                DefaultTaskRequest(
-                    DownloadDiagnosisKeysTask::class,
-                    DownloadDiagnosisKeysTask.Arguments(),
-                    originTag = "RiskWorkScheduler-runRiskTasksNow"
-                )
-            )
-            Timber.tag(TAG).d("DownloadDiagnosisKeysTask finished with %s", result)
-            result
-        }
-        val presenceWarningState = appScope.async {
-            Timber.tag(TAG).d("Running PresenceTracingWarningTask")
-            val result = taskController.submitBlocking(
-                DefaultTaskRequest(
-                    PresenceTracingWarningTask::class,
-                    originTag = "RiskWorkScheduler-runRiskTasksNow"
-                )
-            )
-            Timber.tag(TAG).d("PresenceTracingWarningTask finished with %s", result)
-            result
-        }
-        return listOf(diagnosisKeysState, presenceWarningState).awaitAll()
-    }
-
-    suspend fun isScheduled(): Boolean {
-        val diagnosisWorkerInfos = appScope.async {
-            workManager.getWorkInfosForUniqueWork(WORKER_ID_PRESENCE_TRACING).await()
-        }
-        val warningWorkerInfos = appScope.async {
-            workManager.getWorkInfosForUniqueWork(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD).await()
-        }
-        return listOf(diagnosisWorkerInfos, warningWorkerInfos).awaitAll().all { perWorkerInfos ->
-            perWorkerInfos.any { it.isScheduled }
-        }
-    }
-
-    fun setPeriodicRiskCalculation(enabled: Boolean) {
-        Timber.tag(TAG).i("setPeriodicRiskCalculation(enabled=$enabled)")
+    abstract suspend fun isScheduled(): Boolean
 
-        if (enabled) {
-            val diagnosisRequest = diagnosisWorkBuilder.createPeriodicWorkRequest()
-            queueWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD, diagnosisRequest)
+    internal abstract fun setPeriodicRiskCalculation(enabled: Boolean)
 
-            val warningRequest = presenceWorkBuilder.createPeriodicWorkRequest()
-            queueWorker(WORKER_ID_PRESENCE_TRACING, warningRequest)
-        } else {
-            cancelWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD)
-            cancelWorker(WORKER_ID_PRESENCE_TRACING)
-        }
-    }
-
-    private fun queueWorker(workerId: String, request: PeriodicWorkRequest) {
-        Timber.tag(TAG).d("queueWorker(workerId=%s, request=%s)", workerId, request)
+    internal fun queueWorker(workerId: String, request: PeriodicWorkRequest) {
+        Timber.tag(logTag).d("queueWorker(workerId=%s, request=%s)", workerId, request)
         workManager.enqueueUniquePeriodicWork(
             workerId,
             ExistingPeriodicWorkPolicy.KEEP,
@@ -93,17 +24,11 @@ class RiskWorkScheduler @Inject constructor(
         )
     }
 
-    private fun cancelWorker(workerId: String) {
-        Timber.tag(TAG).d("cancelWorker(workerId=$workerId")
+    internal fun cancelWorker(workerId: String) {
+        Timber.tag(logTag).d("cancelWorker(workerId=$workerId")
         workManager.cancelUniqueWork(workerId)
     }
 
-    private val WorkInfo.isScheduled: Boolean
+    internal val WorkInfo.isScheduled: Boolean
         get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED
-
-    companion object {
-        private const val WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD = "DiagnosisKeyRetrievalWorker"
-        private const val WORKER_ID_PRESENCE_TRACING = "PresenceTracingWarningWorker"
-        private const val TAG = "RiskWorkScheduler"
-    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt
index 1f22c31b7..bd7ee3276 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt
@@ -3,7 +3,11 @@ package de.rki.coronawarnapp.storage
 import android.content.Context
 import androidx.core.content.edit
 import de.rki.coronawarnapp.util.di.AppContext
+import de.rki.coronawarnapp.util.preferences.FlowPreference
 import de.rki.coronawarnapp.util.preferences.clearAndNotify
+import de.rki.coronawarnapp.util.preferences.createFlowPreference
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
 import org.joda.time.Instant
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -16,16 +20,24 @@ class OnboardingSettings @Inject constructor(
         context.getSharedPreferences("onboarding_localdata", Context.MODE_PRIVATE)
     }
 
-    var onboardingCompletedTimestamp: Instant?
-        get() = prefs.getLong(ONBOARDING_COMPLETED_TIMESTAMP, 0L).let {
-            if (it != 0L) {
-                Instant.ofEpochMilli(it)
+    val onboardingCompletedTimestamp: FlowPreference<Instant?> = prefs.createFlowPreference(
+        key = ONBOARDING_COMPLETED_TIMESTAMP,
+        reader = {
+            val raw = getLong(it, 0L)
+            if (raw != 0L) {
+                Instant.ofEpochMilli(raw)
             } else null
+        },
+        writer = { key, value ->
+            putLong(key, value?.millis ?: 0L)
         }
-        set(value) = prefs.edit { putLong(ONBOARDING_COMPLETED_TIMESTAMP, value?.millis ?: 0L) }
+    )
 
     val isOnboarded: Boolean
-        get() = onboardingCompletedTimestamp != null
+        get() = onboardingCompletedTimestamp.value != null
+
+    val isOnboardedFlow: Flow<Boolean>
+        get() = onboardingCompletedTimestamp.flow.map { it != null }
 
     var isBackgroundCheckDone: Boolean
         get() = prefs.getBoolean(BACKGROUND_CHECK_DONE, false)
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 b69db70bb..bd5aaa78b 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
@@ -7,10 +7,11 @@ import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.lastSubmission
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingRiskWorkScheduler
 import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask
 import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTaskProgress
 import de.rki.coronawarnapp.risk.RiskLevelTask
-import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
+import de.rki.coronawarnapp.risk.execution.ExposureWindowRiskWorkScheduler
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.TaskInfo
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
@@ -47,7 +48,8 @@ class TracingRepository @Inject constructor(
     private val timeStamper: TimeStamper,
     private val exposureDetectionTracker: ExposureDetectionTracker,
     private val backgroundModeStatus: BackgroundModeStatus,
-    private val riskWorkScheduler: RiskWorkScheduler,
+    private val exposureWindowRiskWorkScheduler: ExposureWindowRiskWorkScheduler,
+    private val presenceTracingRiskWorkScheduler: PresenceTracingRiskWorkScheduler,
 ) {
 
     @SuppressLint("BinaryOperationInTimber")
@@ -96,14 +98,8 @@ class TracingRepository @Inject constructor(
     fun refreshRiskResult() = scope.launch {
         Timber.tag(TAG).d("refreshRiskResults()")
 
-        riskWorkScheduler.runRiskTasksNow()
-
-        taskController.submit(
-            DefaultTaskRequest(
-                RiskLevelTask::class,
-                originTag = "TracingRepository.refreshRiskResult()"
-            )
-        )
+        exposureWindowRiskWorkScheduler.runRiskTasksNow(sourceTag = TAG)
+        presenceTracingRiskWorkScheduler.runRiskTaskNow(sourceTag = TAG)
     }
 
     /**
@@ -164,6 +160,6 @@ class TracingRepository @Inject constructor(
     }
 
     companion object {
-        private val TAG: String? = TracingRepository::class.simpleName
+        private const val TAG: String = "TracingRepository"
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt
index aacefbebc..8e0551005 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt
@@ -7,8 +7,8 @@ import de.rki.coronawarnapp.coronatest.CoronaTestRepository
 import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationService
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository
 import de.rki.coronawarnapp.presencetracing.checkins.CheckInsTransformer
@@ -23,7 +23,6 @@ import de.rki.coronawarnapp.task.TaskCancellationException
 import de.rki.coronawarnapp.task.TaskFactory
 import de.rki.coronawarnapp.task.common.DefaultProgress
 import de.rki.coronawarnapp.util.TimeStamper
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.channels.ConflatedBroadcastChannel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.asFlow
@@ -47,7 +46,6 @@ class SubmissionTask @Inject constructor(
     private val checkInsRepository: CheckInRepository,
     private val checkInsTransformer: CheckInsTransformer,
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector,
-    private val backgroundWorkScheduler: BackgroundWorkScheduler,
     private val coronaTestRepository: CoronaTestRepository,
 ) : Task<DefaultProgress, SubmissionTask.Result> {
 
@@ -201,9 +199,7 @@ class SubmissionTask @Inject constructor(
 
     private suspend fun setSubmissionFinished(coronaTest: CoronaTest) {
         Timber.tag(TAG).d("setSubmissionFinished()")
-        backgroundWorkScheduler.stopWorkScheduler()
         coronaTestRepository.markAsSubmitted(coronaTest.identifier)
-        backgroundWorkScheduler.startWorkScheduler()
 
         testResultAvailableNotificationService.cancelTestResultAvailableNotification()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt
index 571ae77c0..420e2179b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt
@@ -13,6 +13,7 @@ import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.installTime.InstallTimeProvider
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
 import de.rki.coronawarnapp.nearby.TracingPermissionHelper
+import de.rki.coronawarnapp.risk.execution.ExposureWindowRiskWorkScheduler
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus
 import de.rki.coronawarnapp.tracing.ui.details.items.periodlogged.PeriodLoggedBox
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
@@ -21,7 +22,6 @@ import de.rki.coronawarnapp.util.flow.shareLatest
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
@@ -35,7 +35,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
     installTimeProvider: InstallTimeProvider,
     private val backgroundStatus: BackgroundModeStatus,
     tracingPermissionHelperFactory: TracingPermissionHelper.Factory,
-    private val backgroundWorkScheduler: BackgroundWorkScheduler
+    private val exposureWindowRiskWorkScheduler: ExposureWindowRiskWorkScheduler
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val loggingPeriod: LiveData<PeriodLoggedBox.Item> =
@@ -76,7 +76,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
                             if (!backgroundStatus.isIgnoringBatteryOptimizations.first()) {
                                 events.postValue(Event.ManualCheckingDialog)
                             }
-                            backgroundWorkScheduler.startWorkScheduler()
+                            exposureWindowRiskWorkScheduler.setPeriodicRiskCalculation(enabled = true)
                         }
                         isTracingSwitchChecked.postValue(isTracingEnabled)
                     }
@@ -110,7 +110,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
                 launch {
                     if (InternalExposureNotificationClient.asyncIsEnabled()) {
                         InternalExposureNotificationClient.asyncStop()
-                        backgroundWorkScheduler.stopWorkScheduler()
+                        exposureWindowRiskWorkScheduler.setPeriodicRiskCalculation(enabled = false)
                     }
                 }
             }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
index 9a6eda1d2..1b669bb18 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
@@ -37,7 +37,6 @@ import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper.Companion.getShort
 import de.rki.coronawarnapp.util.ui.findNavController
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import org.joda.time.LocalDate
 import timber.log.Timber
 import javax.inject.Inject
@@ -47,7 +46,6 @@ import javax.inject.Inject
  * connectivity and bluetooth to update the ui.
  *
  * @see ConnectivityHelper
- * @see BackgroundWorkScheduler
  */
 class MainActivity : AppCompatActivity(), HasAndroidInjector {
     companion object {
@@ -79,7 +77,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
     @Inject lateinit var powerManagement: PowerManagement
     @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler
     @Inject lateinit var dataDonationAnalyticsScheduler: DataDonationAnalyticsScheduler
-    @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
 
     override fun onCreate(savedInstanceState: Bundle?) {
         AppInjector.setup(this)
@@ -195,7 +192,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
      */
     override fun onResume() {
         super.onResume()
-        backgroundWorkScheduler.startWorkScheduler()
         vm.doBackgroundNoiseCheck()
         contactDiaryWorkScheduler.schedulePeriodic()
         dataDonationAnalyticsScheduler.schedulePeriodic()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt
index 6ad6c7e9f..6723bf329 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt
@@ -68,7 +68,9 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj
     }
 
     fun completeOnboarding() {
-        onboardingSettings.onboardingCompletedTimestamp = timeStamper.nowUTC
+        onboardingSettings.onboardingCompletedTimestamp.update {
+            timeStamper.nowUTC
+        }
         settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE }
         settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE }
         MainActivity.start(this, intent)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/onboarding/CheckInOnboardingViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/onboarding/CheckInOnboardingViewModel.kt
index afbffab4c..f364d30e9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/onboarding/CheckInOnboardingViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/onboarding/CheckInOnboardingViewModel.kt
@@ -13,7 +13,9 @@ class CheckInOnboardingViewModel @AssistedInject constructor(
     val events = SingleLiveEvent<CheckInOnboardingNavigation>()
 
     fun onAcknowledged() {
-        settings.onboardingStatus = TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
+        settings.onboardingStatus.update {
+            TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
+        }
         events.value = CheckInOnboardingNavigation.AcknowledgedNavigation
     }
 
@@ -25,7 +27,7 @@ class CheckInOnboardingViewModel @AssistedInject constructor(
         events.value = CheckInOnboardingNavigation.AcknowledgedNavigation
     }
 
-    val isOnboardingComplete = settings.onboardingStatus == TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
+    val isOnboardingComplete = settings.onboardingStatus.value == TraceLocationSettings.OnboardingStatus.ONBOARDED_2_0
 
     @AssistedFactory
     interface Factory : SimpleCWAViewModelFactory<CheckInOnboardingViewModel>
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt
index aac4f54a8..9260aefa1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt
@@ -12,13 +12,11 @@ import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 
 class SettingsResetViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
     private val dataReset: DataReset,
     private val shortcutsHelper: AppShortcutsHelper,
-    private val backgroundWorkScheduler: BackgroundWorkScheduler,
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val clickEvent: SingleLiveEvent<SettingsEvents> = SingleLiveEvent()
@@ -34,11 +32,11 @@ class SettingsResetViewModel @AssistedInject constructor(
     fun deleteAllAppContent() {
         launch {
             try {
+                // TODO Remove static access
                 val isTracingEnabled = InternalExposureNotificationClient.asyncIsEnabled()
                 // only stop tracing if it is currently enabled
                 if (isTracingEnabled) {
                     InternalExposureNotificationClient.asyncStop()
-                    backgroundWorkScheduler.stopWorkScheduler()
                 }
             } catch (apiException: ApiException) {
                 apiException.report(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt
index 163264bb6..913da481b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt
@@ -7,7 +7,7 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt
index d4b683833..98ed23aee 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt
@@ -7,7 +7,7 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt
index 5033e1905..022d9310f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt
@@ -6,9 +6,9 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt
index 69a3092f8..3c21410fa 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt
@@ -7,9 +7,9 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
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 442df0780..e657ddd7d 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,13 +5,11 @@ import android.net.wifi.WifiManager
 import android.os.PowerManager
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
-import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
-import de.rki.coronawarnapp.storage.OnboardingSettings
-import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingRiskWorkScheduler
+import de.rki.coronawarnapp.risk.execution.ExposureWindowRiskWorkScheduler
 import de.rki.coronawarnapp.util.device.BackgroundModeStatus
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.di.ProcessLifecycle
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
@@ -23,12 +21,10 @@ import javax.inject.Singleton
 @Singleton
 class WatchdogService @Inject constructor(
     @AppContext private val context: Context,
-    private val taskController: TaskController,
     private val backgroundModeStatus: BackgroundModeStatus,
     @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner,
-    private val onboardingSettings: OnboardingSettings,
-    private val backgroundWorkScheduler: BackgroundWorkScheduler,
-    private val riskWorkScheduler: RiskWorkScheduler,
+    private val exposureWindowRiskWorkScheduler: ExposureWindowRiskWorkScheduler,
+    private val presenceTracingRiskRepository: PresenceTracingRiskWorkScheduler,
 ) {
 
     private val powerManager by lazy {
@@ -46,6 +42,10 @@ class WatchdogService @Inject constructor(
             return
         }
 
+        // TODO it's unclear whether this really has any effect
+        // If we are being bound by Google Play Services (which is only a few seconds)
+        // and don't have a worker or foreground service, the system may still kill us and the tasks
+        // before they have finished executing.
         Timber.tag(TAG).v("Acquiring wakelocks for watchdog routine.")
         processLifecycleOwner.lifecycleScope.launch {
             // A wakelock as the OS does not handle this for us like in the background job execution
@@ -55,18 +55,15 @@ class WatchdogService @Inject constructor(
 
             Timber.tag(TAG).d("Automatic mode is on, check if we have downloaded keys already today")
 
-            val results = riskWorkScheduler.runRiskTasksNow()
-            Timber.tag(TAG).d("runRiskTasksNow() results: %s", results)
+            Timber.tag(TAG).d("Running EW risk tasks now.")
+            exposureWindowRiskWorkScheduler.runRiskTasksNow(TAG)
+
+            Timber.tag(TAG).d("Rnuning PT risk tasks now.")
+            presenceTracingRiskRepository.runRiskTaskNow(TAG)
 
             if (wifiLock.isHeld) wifiLock.release()
             if (wakeLock.isHeld) wakeLock.release()
         }
-
-        // if the user is onboarded we will schedule period background jobs
-        // in case the app was force stopped and woken up again by the Google WakeUpService
-        if (onboardingSettings.isOnboarded) {
-            backgroundWorkScheduler.startWorkScheduler()
-        }
     }
 
     private fun createWakeLock(): PowerManager.WakeLock = powerManager
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 fafb7e29d..574ed5dbc 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
@@ -18,12 +18,12 @@ import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule
 import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.environment.EnvironmentModule
-import de.rki.coronawarnapp.presencetracing.PresenceTracingModule
 import de.rki.coronawarnapp.http.HttpModule
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.nearby.ENFModule
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.playbook.PlaybookModule
+import de.rki.coronawarnapp.presencetracing.PresenceTracingModule
 import de.rki.coronawarnapp.receiver.ReceiverBinder
 import de.rki.coronawarnapp.risk.RiskModule
 import de.rki.coronawarnapp.service.ServiceBinder
@@ -44,7 +44,6 @@ import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool
 import de.rki.coronawarnapp.util.security.SecurityModule
 import de.rki.coronawarnapp.util.serialization.SerializationModule
 import de.rki.coronawarnapp.util.worker.WorkerBinder
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import javax.inject.Singleton
 
 @Singleton
@@ -103,8 +102,6 @@ interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
 
     fun inject(logger: DebugLogger)
 
-    fun inject(backgroundWorkScheduler: BackgroundWorkScheduler)
-
     val encryptedMigration: EncryptedPreferencesMigration
 
     @Component.Factory
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt
index b2eea5c60..9479266cd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt
@@ -59,8 +59,8 @@ class EncryptedPreferencesMigration @Inject constructor(
         }
 
         OnboardingLocalData(encryptedSharedPreferences).apply {
-            onboardingSettings.onboardingCompletedTimestamp = onboardingCompletedTimestamp()?.let {
-                Instant.ofEpochMilli(it)
+            onboardingSettings.onboardingCompletedTimestamp.update {
+                onboardingCompletedTimestamp()?.let { Instant.ofEpochMilli(it) }
             }
             onboardingSettings.isBackgroundCheckDone = isBackgroundCheckDone()
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt
index 1abf9aded..09fc597d5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt
@@ -57,6 +57,24 @@ inline fun <T1, T2, R> combine(
     )
 }
 
+@Suppress("UNCHECKED_CAST", "LongParameterList")
+inline fun <T1, T2, T3, R> combine(
+    flow: Flow<T1>,
+    flow2: Flow<T2>,
+    flow3: Flow<T3>,
+    crossinline transform: suspend (T1, T2, T3) -> R
+): Flow<R> = combine(
+    flow,
+    flow2,
+    flow3,
+) { args: Array<*> ->
+    transform(
+        args[0] as T1,
+        args[1] as T2,
+        args[2] as T3,
+    )
+}
+
 @Suppress("UNCHECKED_CAST", "LongParameterList")
 inline fun <T1, T2, T3, T4, T5, R> combine(
     flow: Flow<T1>,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
index 7f1d39bc4..3b07716c5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
@@ -5,8 +5,8 @@ import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoMap
 import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker
-import de.rki.coronawarnapp.coronatest.worker.PCRResultRetrievalWorker
-import de.rki.coronawarnapp.coronatest.worker.RAResultRetrievalWorker
+import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultRetrievalWorker
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultRetrievalWorker
 import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
index b250c13b7..32bec142a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
@@ -4,22 +4,9 @@ import java.util.concurrent.TimeUnit
 
 /**
  * The background work constants are used inside the BackgroundWorkScheduler
- *
- * @see BackgroundWorkScheduler
  */
 object BackgroundConstants {
 
-    /**
-     * Total minutes in one day
-     */
-    const val MINUTES_IN_DAY = 1440
-
-    /**
-     * Total tries count for diagnosis key retrieval per day
-     * Internal requirement
-     */
-    const val DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY = 12
-
     /**
      * Kind initial delay in minutes for periodic work for accessibility reason
      *
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt
deleted file mode 100644
index 67a621d61..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import androidx.work.Constraints
-import androidx.work.NetworkType
-import kotlin.random.Random
-
-/**
- * Singleton class for background work helper functions
- * The helper uses externalised constants for readability.
- *
- * @see BackgroundConstants
- * @see BackgroundWorkScheduler
- */
-object BackgroundWorkHelper {
-
-    /**
-     * Get background noise one time work delay
-     * The periodic job is already delayed by MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
-     * so we only need to delay further by the difference between min and max.
-     *
-     * @return Long
-     *
-     * @see BackgroundConstants.MAX_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
-     * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
-     */
-    fun getBackgroundNoiseOneTimeWorkDelay() = Random.nextLong(
-        0,
-        BackgroundConstants.MAX_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION -
-            BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
-    )
-
-    /**
-     * Constraints for diagnosis key one time work
-     * Requires battery not low and any network connection
-     * Mobile data usage is handled on OS level in application settings
-     *
-     * @return Constraints
-     *
-     * @see NetworkType.CONNECTED
-     */
-    fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork() =
-        Constraints
-            .Builder()
-            .setRequiredNetworkType(NetworkType.CONNECTED)
-            .build()
-}
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
deleted file mode 100644
index cad840478..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
-import de.rki.coronawarnapp.deniability.NoiseScheduler
-import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.runBlocking
-import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Singleton
-
-/**
- * Singleton class for background work handling
- * The helper uses externalised constants and helper for readability.
- *
- * @see BackgroundConstants
- * @see BackgroundWorkHelper
- */
-@Singleton
-class BackgroundWorkScheduler @Inject constructor(
-    private val riskWorkScheduler: RiskWorkScheduler,
-    private val coronaTestRepository: CoronaTestRepository,
-    private val testResultScheduler: PCRResultScheduler,
-    private val noiseScheduler: NoiseScheduler,
-) {
-
-    fun startWorkScheduler() {
-        Timber.d("startWorkScheduler()")
-        riskWorkScheduler.setPeriodicRiskCalculation(enabled = true)
-
-        // TODO Blocking isn't very nice here...
-        val coronatests = runBlocking { coronaTestRepository.coronaTests.first() }
-
-        val isSubmissionSuccessful = coronatests.any { it.isSubmitted }
-        val hasPendingTests = coronatests.any { !it.isResultAvailableNotificationSent }
-
-        if (!isSubmissionSuccessful && hasPendingTests) {
-            testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = true)
-        }
-    }
-
-    fun stopWorkScheduler() {
-        noiseScheduler.setPeriodicNoise(enabled = false)
-        riskWorkScheduler.setPeriodicRiskCalculation(enabled = false)
-        testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false)
-        Timber.d("All Background Jobs Stopped")
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt
index 4f026681c..c3e2537a4 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt
@@ -50,7 +50,8 @@ class CoronaTestStorageTest : BaseTest() {
         isJournalEntryCreated = false,
         isResultAvailableNotificationSent = false,
         testResult = CoronaTestResult.PCR_POSITIVE,
-        testResultReceivedAt = Instant.ofEpochMilli(2000)
+        testResultReceivedAt = Instant.ofEpochMilli(2000),
+        lastUpdatedAt = Instant.ofEpochMilli(2001),
     )
     private val raTest = RACoronaTest(
         identifier = "identifier-ra",
@@ -66,7 +67,8 @@ class CoronaTestStorageTest : BaseTest() {
         firstName = "firstname",
         lastName = "lastname",
         dateOfBirth = LocalDate.parse("2021-12-24"),
-        testedAt = Instant.ofEpochMilli(3000)
+        testedAt = Instant.ofEpochMilli(3000),
+        lastUpdatedAt = Instant.ofEpochMilli(2001),
     )
 
     @Test
@@ -110,7 +112,8 @@ class CoronaTestStorageTest : BaseTest() {
                     "isJournalEntryCreated": false,
                     "isResultAvailableNotificationSent": false,
                     "testResultReceivedAt": 2000,
-                    "testResult": 2
+                    "testResult": 2,
+                    "lastUpdatedAt": 2001
                 }
             ]
         """.toComparableJsonPretty()
@@ -148,6 +151,7 @@ class CoronaTestStorageTest : BaseTest() {
                     "isJournalEntryCreated": false,
                     "isResultAvailableNotificationSent": false,
                     "testResultReceivedAt": 2000,
+                    "lastUpdatedAt": 2001,
                     "testResult": 7,
                     "testedAt": 3000,
                     "firstName": "firstname",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt
index 39ee2b0fd..4692cc48a 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt
@@ -1,12 +1,75 @@
 package de.rki.coronawarnapp.coronatest.type.pcr
 
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.CoronaTestService
+import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
+import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector
+import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Duration
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 
 class PCRProcessorTest : BaseTest() {
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var submissionService: CoronaTestService
+    @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
+    @MockK lateinit var testResultDataCollector: TestResultDataCollector
+    @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler
+
+    private val nowUTC = Instant.parse("2021-03-15T05:45:00.000Z")
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { timeStamper.nowUTC } returns nowUTC
+
+        submissionService.apply {
+            coEvery { asyncRequestTestResult(any()) } answers { CoronaTestResult.PCR_OR_RAT_PENDING }
+        }
+
+        testResultDataCollector.apply {
+            coEvery { updatePendingTestResultReceivedTime(any()) } just Runs
+        }
+    }
+
+    fun createInstance() = PCRProcessor(
+        timeStamper = timeStamper,
+        submissionService = submissionService,
+        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
+        testResultDataCollector = testResultDataCollector,
+        deadmanNotificationScheduler = deadmanNotificationScheduler,
+    )
 
     @Test
-    fun todo() {
-        // TODO
+    fun `if we receive a pending result 60 days after registration, we map to REDEEMED`() = runBlockingTest {
+        val instance = createInstance()
+
+        val pcrTest = PCRCoronaTest(
+            identifier = "identifier",
+            lastUpdatedAt = Instant.EPOCH,
+            registeredAt = nowUTC,
+            registrationToken = "regtoken",
+            testResult = CoronaTestResult.PCR_POSITIVE
+        )
+
+        instance.pollServer(pcrTest).testResult shouldBe CoronaTestResult.PCR_OR_RAT_PENDING
+
+        val past60DaysTest = pcrTest.copy(
+            registeredAt = nowUTC.minus(Duration.standardDays(21))
+        )
+
+        instance.pollServer(past60DaysTest).testResult shouldBe CoronaTestResult.PCR_REDEEMED
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultSchedulerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultSchedulerTest.kt
new file mode 100644
index 000000000..3034188cc
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/execution/PCRResultSchedulerTest.kt
@@ -0,0 +1,78 @@
+package de.rki.coronawarnapp.coronatest.type.pcr.execution
+
+import androidx.work.WorkManager
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class PCRResultSchedulerTest : BaseTest() {
+
+    @MockK lateinit var workManager: WorkManager
+    @MockK lateinit var coronaTestRepository: CoronaTestRepository
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { workManager.enqueueUniquePeriodicWork(any(), any(), any()) } returns mockk()
+    }
+
+    private fun createInstance() = PCRResultScheduler(
+        appScope = TestCoroutineScope(),
+        coronaTestRepository = coronaTestRepository,
+        workManager = workManager
+    )
+
+    @Test
+    fun `final worker doesn't need to be scheduled`() {
+        every { coronaTestRepository.coronaTests } returns flowOf(
+            setOf(
+                mockk<PCRCoronaTest>().apply {
+                    every { isFinal } returns true
+                    every { type } returns CoronaTest.Type.PCR
+                }
+            )
+        )
+
+        runBlockingTest {
+            createInstance().shouldBePolling.first() shouldBe false
+        }
+    }
+
+    @Test
+    fun `not final worker needs to be scheduled`() {
+        every { coronaTestRepository.coronaTests } returns flowOf(
+            setOf(
+                mockk<PCRCoronaTest>().apply {
+                    every { isFinal } returns false
+                    every { type } returns CoronaTest.Type.PCR
+                }
+            )
+        )
+
+        runBlockingTest {
+            createInstance().shouldBePolling.first() shouldBe true
+        }
+    }
+
+    @Test
+    fun `no worker needed without test`() {
+        every { coronaTestRepository.coronaTests } returns flowOf(emptySet())
+
+        runBlockingTest {
+            createInstance().shouldBePolling.first() shouldBe false
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationServiceTest.kt
similarity index 78%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationServiceTest.kt
index e81162622..0a44ab4c0 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/notification/PCRTestResultAvailableNotificationServiceTest.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.notification
+package de.rki.coronawarnapp.coronatest.type.pcr.notification
 
 import android.app.NotificationManager
 import android.app.PendingIntent
@@ -6,8 +6,10 @@ import android.content.Context
 import androidx.navigation.NavDeepLinkBuilder
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.notification.GeneralNotifications
 import de.rki.coronawarnapp.util.device.ForegroundState
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -19,14 +21,16 @@ import io.mockk.mockk
 import io.mockk.mockkObject
 import io.mockk.verify
 import io.mockk.verifyOrder
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.TestCoroutineScope
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 import javax.inject.Provider
 
-class TestResultAvailableNotificationServiceTest : BaseTest() {
+class PCRTestResultAvailableNotificationServiceTest : BaseTest() {
 
     @MockK(relaxed = true) lateinit var context: Context
     @MockK lateinit var foregroundState: ForegroundState
@@ -36,6 +40,7 @@ class TestResultAvailableNotificationServiceTest : BaseTest() {
     @MockK lateinit var notificationManager: NotificationManager
     @MockK lateinit var notificationHelper: GeneralNotifications
     @MockK lateinit var cwaSettings: CWASettings
+    @MockK lateinit var coronaTestRepository: CoronaTestRepository
 
     @BeforeEach
     fun setUp() {
@@ -52,19 +57,21 @@ class TestResultAvailableNotificationServiceTest : BaseTest() {
         every { notificationHelper.newBaseBuilder() } returns mockk(relaxed = true)
     }
 
-    fun createInstance() = PCRTestResultAvailableNotificationService(
+    fun createInstance(scope: CoroutineScope = TestCoroutineScope()) = PCRTestResultAvailableNotificationService(
         context = context,
         foregroundState = foregroundState,
         navDeepLinkBuilderProvider = navDeepLinkBuilderProvider,
         notificationHelper = notificationHelper,
-        cwaSettings = cwaSettings
+        cwaSettings = cwaSettings,
+        coronaTestRepository = coronaTestRepository,
+        appScope = scope,
     )
 
     @Test
     fun `test notification in foreground`() = runBlockingTest {
         coEvery { foregroundState.isInForeground } returns flow { emit(true) }
 
-        createInstance().showTestResultAvailableNotification(CoronaTestResult.PCR_POSITIVE)
+        createInstance().showTestResultAvailableNotification(mockk())
 
         verify(exactly = 0) { navDeepLinkBuilderProvider.get() }
     }
@@ -80,8 +87,10 @@ class TestResultAvailableNotificationServiceTest : BaseTest() {
         } just Runs
 
         val instance = createInstance()
-
-        instance.showTestResultAvailableNotification(CoronaTestResult.PCR_POSITIVE)
+        val coronaTest = mockk<CoronaTest>().apply {
+            every { type } returns CoronaTest.Type.PCR
+        }
+        instance.showTestResultAvailableNotification(coronaTest)
 
         verifyOrder {
             navDeepLinkBuilderProvider.get()
@@ -100,7 +109,7 @@ class TestResultAvailableNotificationServiceTest : BaseTest() {
         every { cwaSettings.isNotificationsTestEnabled.value } returns false
 
         createInstance().apply {
-            showTestResultAvailableNotification(CoronaTestResult.PCR_POSITIVE)
+            showTestResultAvailableNotification(mockk())
 
             verify(exactly = 0) {
                 notificationHelper.sendNotification(
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt
index 2396194a9..b2b3693be 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt
@@ -1,12 +1,62 @@
 package de.rki.coronawarnapp.coronatest.type.rapidantigen
 
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.CoronaTestService
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Duration
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 
 class RapidAntigenProcessorTest : BaseTest() {
 
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var submissionService: CoronaTestService
+
+    private val nowUTC = Instant.parse("2021-03-15T05:45:00.000Z")
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { timeStamper.nowUTC } returns nowUTC
+
+        submissionService.apply {
+            coEvery { asyncRequestTestResult(any()) } answers { CoronaTestResult.PCR_OR_RAT_PENDING }
+        }
+    }
+
+    fun createInstance() = RapidAntigenProcessor(
+        timeStamper = timeStamper,
+        submissionService = submissionService,
+    )
+
     @Test
-    fun todo() {
-        // TODO
+    fun `if we receive a pending result 60 days after registration, we map to REDEEMED`() = runBlockingTest {
+        val instance = createInstance()
+
+        val pcrTest = RACoronaTest(
+            identifier = "identifier",
+            lastUpdatedAt = Instant.EPOCH,
+            registeredAt = nowUTC,
+            registrationToken = "regtoken",
+            testResult = CoronaTestResult.RAT_POSITIVE,
+            testedAt = Instant.EPOCH,
+        )
+
+        instance.pollServer(pcrTest).testResult shouldBe CoronaTestResult.PCR_OR_RAT_PENDING
+
+        val past60DaysTest = pcrTest.copy(
+            registeredAt = nowUTC.minus(Duration.standardDays(21))
+        )
+
+        instance.pollServer(past60DaysTest).testResult shouldBe CoronaTestResult.RAT_REDEEMED
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATestResultAvailableNotificationServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATestResultAvailableNotificationServiceTest.kt
new file mode 100644
index 000000000..3878e2e2e
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/notification/RATestResultAvailableNotificationServiceTest.kt
@@ -0,0 +1,122 @@
+package de.rki.coronawarnapp.coronatest.type.rapidantigen.notification
+
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import androidx.navigation.NavDeepLinkBuilder
+import de.rki.coronawarnapp.CoronaWarnApplication
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.main.CWASettings
+import de.rki.coronawarnapp.notification.GeneralNotifications
+import de.rki.coronawarnapp.util.device.ForegroundState
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.verify
+import io.mockk.verifyOrder
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import javax.inject.Provider
+
+class RATestResultAvailableNotificationServiceTest : BaseTest() {
+
+    @MockK(relaxed = true) lateinit var context: Context
+    @MockK lateinit var foregroundState: ForegroundState
+    @MockK(relaxed = true) lateinit var navDeepLinkBuilder: NavDeepLinkBuilder
+    @MockK lateinit var pendingIntent: PendingIntent
+    @MockK lateinit var navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder>
+    @MockK lateinit var notificationManager: NotificationManager
+    @MockK lateinit var notificationHelper: GeneralNotifications
+    @MockK lateinit var cwaSettings: CWASettings
+    @MockK lateinit var coronaTestRepository: CoronaTestRepository
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this)
+
+        mockkObject(CoronaWarnApplication)
+
+        every { CoronaWarnApplication.getAppContext() } returns context
+        every { context.getSystemService(Context.NOTIFICATION_SERVICE) } returns notificationManager
+        every { navDeepLinkBuilderProvider.get() } returns navDeepLinkBuilder
+        every { navDeepLinkBuilder.createPendingIntent() } returns pendingIntent
+        every { cwaSettings.isNotificationsTestEnabled.value } returns true
+
+        every { notificationHelper.newBaseBuilder() } returns mockk(relaxed = true)
+    }
+
+    fun createInstance(scope: CoroutineScope = TestCoroutineScope()) = RATTestResultAvailableNotificationService(
+        context = context,
+        foregroundState = foregroundState,
+        navDeepLinkBuilderProvider = navDeepLinkBuilderProvider,
+        notificationHelper = notificationHelper,
+        cwaSettings = cwaSettings,
+        coronaTestRepository = coronaTestRepository,
+        appScope = scope,
+    )
+
+    @Test
+    fun `test notification in foreground`() = runBlockingTest {
+        coEvery { foregroundState.isInForeground } returns flow { emit(true) }
+
+        createInstance().showTestResultAvailableNotification(mockk())
+
+        verify(exactly = 0) { navDeepLinkBuilderProvider.get() }
+    }
+
+    @Test
+    fun `test notification in background`() = runBlockingTest {
+        coEvery { foregroundState.isInForeground } returns flow { emit(false) }
+        every {
+            notificationHelper.sendNotification(
+                notificationId = any(),
+                notification = any()
+            )
+        } just Runs
+
+        val instance = createInstance()
+        val coronaTest = mockk<CoronaTest>().apply {
+            every { type } returns CoronaTest.Type.RAPID_ANTIGEN
+        }
+        instance.showTestResultAvailableNotification(coronaTest)
+
+        verifyOrder {
+            navDeepLinkBuilderProvider.get()
+            context.getString(R.string.notification_headline_test_result_ready)
+            context.getString(R.string.notification_body_test_result_ready)
+            notificationHelper.sendNotification(
+                notificationId = any(),
+                notification = any()
+            )
+        }
+    }
+
+    @Test
+    fun `test notification in background disabled`() = runBlockingTest {
+        coEvery { foregroundState.isInForeground } returns flow { emit(false) }
+        every { cwaSettings.isNotificationsTestEnabled.value } returns false
+
+        createInstance().apply {
+            showTestResultAvailableNotification(mockk())
+
+            verify(exactly = 0) {
+                notificationHelper.sendNotification(
+                    notificationId = any(),
+                    notification = any()
+                )
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt
index 6cc0f482d..c15f04bc3 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt
@@ -71,7 +71,7 @@ class AnalyticsTest : BaseTest() {
 
         val twoDaysAgo = baseTime.minus(Days.TWO.toStandardDuration())
         every { settings.lastSubmittedTimestamp } returns mockFlowPreference(twoDaysAgo)
-        every { onboardingSettings.onboardingCompletedTimestamp } returns twoDaysAgo
+        every { onboardingSettings.onboardingCompletedTimestamp } returns mockFlowPreference(twoDaysAgo)
 
         every { analyticsConfig.safetyNetRequirements } returns SafetyNetRequirementsContainer()
 
@@ -195,7 +195,7 @@ class AnalyticsTest : BaseTest() {
 
     @Test
     fun `abort due to time since onboarding`() {
-        every { onboardingSettings.onboardingCompletedTimestamp } returns baseTime
+        every { onboardingSettings.onboardingCompletedTimestamp } returns mockFlowPreference(baseTime)
 
         val analytics = createInstance()
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt
index fa2a64983..ea587edec 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt
@@ -24,6 +24,7 @@ import testhelpers.BaseTest
 import testhelpers.TestDispatcherProvider
 import testhelpers.extensions.CoroutinesTestExtension
 import testhelpers.extensions.InstantExecutorExtension
+import testhelpers.preferences.mockFlowPreference
 
 @ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class)
 class MainActivityViewModelTest : BaseTest() {
@@ -46,7 +47,9 @@ class MainActivityViewModelTest : BaseTest() {
 
         every { onboardingSettings.isOnboarded } returns true
         every { environmentSetup.currentEnvironment } returns EnvironmentSetup.Type.WRU
-        every { traceLocationSettings.onboardingStatus } returns TraceLocationSettings.OnboardingStatus.NOT_ONBOARDED
+        every { traceLocationSettings.onboardingStatus } returns mockFlowPreference(
+            TraceLocationSettings.OnboardingStatus.NOT_ONBOARDED
+        )
         every { onboardingSettings.isBackgroundCheckDone } returns true
         every { checkInRepository.checkInsWithinRetention } returns MutableStateFlow(listOf())
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt
index f58738ff6..de0bc5cb2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt
@@ -8,8 +8,8 @@ import de.rki.coronawarnapp.coronatest.notification.ShareTestResultNotificationS
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.presencetracing.checkins.CheckIn
 import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository
@@ -22,7 +22,6 @@ import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.preferences.FlowPreference
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.assertions.throwables.shouldThrowMessage
 import io.kotest.matchers.shouldBe
@@ -68,7 +67,6 @@ class SubmissionTaskTest : BaseTest() {
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
     @MockK lateinit var checkInsTransformer: CheckInsTransformer
     @MockK lateinit var checkInRepository: CheckInRepository
-    @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
     @MockK lateinit var coronaTestRepository: CoronaTestRepository
 
     private lateinit var settingSymptomsPreference: FlowPreference<Symptoms?>
@@ -124,9 +122,6 @@ class SubmissionTaskTest : BaseTest() {
             coEvery { markAsSubmitted("coronatest-identifier") } just Runs
         }
 
-        every { backgroundWorkScheduler.stopWorkScheduler() } just Runs
-        every { backgroundWorkScheduler.startWorkScheduler() } just Runs
-
         every { tekBatch.keys } returns listOf(tek)
         every { tekHistoryStorage.tekData } returns flowOf(listOf(tekBatch))
         coEvery { tekHistoryStorage.clear() } just Runs
@@ -183,7 +178,6 @@ class SubmissionTaskTest : BaseTest() {
         analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
         checkInsRepository = checkInRepository,
         checkInsTransformer = checkInsTransformer,
-        backgroundWorkScheduler = backgroundWorkScheduler,
         coronaTestRepository = coronaTestRepository,
     )
 
@@ -233,9 +227,7 @@ class SubmissionTaskTest : BaseTest() {
 
             autoSubmission.updateMode(AutoSubmission.Mode.DISABLED)
 
-            backgroundWorkScheduler.stopWorkScheduler()
             coronaTestRepository.markAsSubmitted(any())
-            backgroundWorkScheduler.startWorkScheduler()
 
             testResultAvailableNotificationService.cancelTestResultAvailableNotification()
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultConsentGivenViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultConsentGivenViewModelTest.kt
index f504e2a83..9c717c081 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultConsentGivenViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultConsentGivenViewModelTest.kt
@@ -1,9 +1,9 @@
 package de.rki.coronawarnapp.ui.submission.testresult
 
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenViewModel
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModelTest.kt
index cd5efd3f5..e47dff84d 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModelTest.kt
@@ -3,9 +3,9 @@ package de.rki.coronawarnapp.ui.submission.testresult.positive
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import io.mockk.MockKAnnotations
 import io.mockk.impl.annotations.MockK
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt
index f3585f5d8..f10be3cd8 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt
@@ -109,7 +109,8 @@ class EncryptedPreferencesMigrationTest : BaseIOTest() {
         every { cwaSettings.numberOfRemainingSharePositiveTestResultReminders = Int.MAX_VALUE } just Runs
 
         // OnboardingLocalData
-        every { onboardingSettings.onboardingCompletedTimestamp = Instant.ofEpochMilli(10101010L) } just Runs
+        val mockOnboardingCompletedTimestamp = mockFlowPreference(Instant.ofEpochMilli(10101010L))
+        every { onboardingSettings.onboardingCompletedTimestamp } returns mockOnboardingCompletedTimestamp
         every { onboardingSettings.isBackgroundCheckDone = true } just Runs
 
         // TracingLocalData
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
index 34e53717f..812d53b02 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
@@ -7,8 +7,9 @@ import dagger.Component
 import dagger.Module
 import dagger.Provides
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
-import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler
+import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler
+import de.rki.coronawarnapp.coronatest.type.pcr.notification.PCRTestResultAvailableNotificationService
+import de.rki.coronawarnapp.coronatest.type.rapidantigen.execution.RAResultScheduler
 import de.rki.coronawarnapp.datadonation.analytics.Analytics
 import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
@@ -16,7 +17,6 @@ import de.rki.coronawarnapp.deadman.DeadmanNotificationSender
 import de.rki.coronawarnapp.deniability.NoiseScheduler
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.notification.GeneralNotifications
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutNotification
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt
index 899fc4623..c932ef5a2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt
@@ -7,8 +7,6 @@ class BackgroundConstantsTest {
 
     @Test
     fun allBackgroundConstants() {
-        Assert.assertEquals(BackgroundConstants.MINUTES_IN_DAY, 1440)
-        Assert.assertEquals(BackgroundConstants.DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY, 12)
         Assert.assertEquals(BackgroundConstants.KIND_DELAY, 1L)
         Assert.assertEquals(BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD, 2)
         Assert.assertEquals(BackgroundConstants.POLLING_VALIDITY_MAX_DAYS, 21)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkHelperTest.kt
index d2251ee48..411335d17 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkHelperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkHelperTest.kt
@@ -1,7 +1,6 @@
 package de.rki.coronawarnapp.worker
 
-import androidx.work.NetworkType
-import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
+import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler
 import org.junit.Assert
 import org.junit.Test
 
@@ -14,10 +13,4 @@ class BackgroundWorkHelperTest {
             120
         )
     }
-
-    @Test
-    fun getConstraintsForDiagnosisKeyOneTimeBackgroundWork() {
-        val constraints = BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()
-        Assert.assertEquals(constraints.requiredNetworkType, NetworkType.CONNECTED)
-    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt
deleted file mode 100644
index 33ec41f79..000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt
+++ /dev/null
@@ -1,257 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import android.content.Context
-import androidx.work.ListenableWorker
-import androidx.work.WorkRequest
-import androidx.work.WorkerParameters
-import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
-import de.rki.coronawarnapp.coronatest.type.CoronaTest
-import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
-import de.rki.coronawarnapp.coronatest.worker.PCRResultRetrievalWorker
-import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
-import de.rki.coronawarnapp.notification.GeneralNotifications
-import de.rki.coronawarnapp.notification.NotificationConstants
-import de.rki.coronawarnapp.notification.PCRTestResultAvailableNotificationService
-import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds
-import de.rki.coronawarnapp.util.TimeStamper
-import de.rki.coronawarnapp.util.di.AppInjector
-import de.rki.coronawarnapp.util.di.ApplicationComponent
-import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory
-import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool
-import io.kotest.matchers.shouldBe
-import io.mockk.MockKAnnotations
-import io.mockk.Runs
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.impl.annotations.MockK
-import io.mockk.impl.annotations.RelaxedMockK
-import io.mockk.just
-import io.mockk.mockk
-import io.mockk.mockkObject
-import io.mockk.verify
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.runBlockingTest
-import org.joda.time.Instant
-import org.junit.Before
-import org.junit.Test
-import testhelpers.BaseTest
-
-class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
-    @MockK lateinit var context: Context
-    @MockK lateinit var request: WorkRequest
-    @MockK lateinit var testResultAvailableNotificationService: PCRTestResultAvailableNotificationService
-    @MockK lateinit var notificationHelper: GeneralNotifications
-    @MockK lateinit var appComponent: ApplicationComponent
-    @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory
-    @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool
-    @MockK lateinit var timeStamper: TimeStamper
-    @MockK lateinit var coronaTestRepository: CoronaTestRepository
-    @MockK lateinit var testResultScheduler: PCRResultScheduler
-
-    @RelaxedMockK lateinit var workerParams: WorkerParameters
-    private val currentInstant = Instant.ofEpochSecond(1611764225)
-    private val testToken = "test token"
-
-    private val coronaTestFlow = MutableStateFlow(emptySet<CoronaTest>())
-
-    @Before
-    fun setUp() {
-        MockKAnnotations.init(this)
-        every { timeStamper.nowUTC } returns currentInstant
-
-        mockkObject(AppInjector)
-        every { AppInjector.component } returns appComponent
-        every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory
-        every { appComponent.errorResetTool } returns encryptionErrorResetTool
-
-        every { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = any()) } just Runs
-
-        every { notificationHelper.cancelCurrentNotification(any()) } just Runs
-
-        coronaTestRepository.apply {
-            every { coronaTests } answers { coronaTestFlow }
-            coEvery { refresh(any()) } coAnswers { coronaTestFlow.first() }
-            coEvery { updateResultNotification(identifier = any(), sent = any()) } just Runs
-        }
-    }
-
-    private fun newCoronaTest(
-        registered: Instant = currentInstant,
-        viewed: Boolean = false,
-        result: CoronaTestResult = CoronaTestResult.PCR_POSITIVE,
-        isNotificationSent: Boolean = false,
-    ): CoronaTest {
-        return mockk<PCRCoronaTest>().apply {
-            every { identifier } returns ""
-            every { type } returns CoronaTest.Type.PCR
-            every { registeredAt } returns registered
-            every { isViewed } returns viewed
-            every { testResult } returns result
-            every { registrationToken } returns testToken
-            every { isResultAvailableNotificationSent } returns isNotificationSent
-        }
-    }
-
-    private fun createWorker() = PCRResultRetrievalWorker(
-        context = context,
-        workerParams = workerParams,
-        testResultAvailableNotificationService = testResultAvailableNotificationService,
-        notificationHelper = notificationHelper,
-        coronaTestRepository = coronaTestRepository,
-        testResultScheduler = testResultScheduler,
-        timeStamper = timeStamper,
-    )
-
-    @Test
-    fun testStopWorkerWhenResultHasBeenViewed() = runBlockingTest {
-        coronaTestFlow.value = setOf(newCoronaTest(viewed = true))
-
-        val result = createWorker().doWork()
-
-        coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        verify(exactly = 1) { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false) }
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testStopWorkerWhenNotificationSent() = runBlockingTest {
-        coronaTestFlow.value = setOf(newCoronaTest(isNotificationSent = true))
-
-        val result = createWorker().doWork()
-
-        coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        verify(exactly = 1) { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false) }
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testStopWorkerWhenMaxDaysExceeded() = runBlockingTest {
-        val past =
-            currentInstant - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() + 1).daysToMilliseconds()
-        coronaTestFlow.value = setOf(newCoronaTest(registered = past))
-
-        val result = createWorker().doWork()
-
-        coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        verify(exactly = 1) { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false) }
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testSendNotificationWhenPositive() = runBlockingTest {
-        val testResult = CoronaTestResult.PCR_POSITIVE
-        coronaTestFlow.value = setOf(newCoronaTest(result = testResult))
-
-        coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs
-        coEvery {
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-        } just Runs
-
-        val result = createWorker().doWork()
-
-        coVerify {
-            coronaTestRepository.refresh(type = CoronaTest.Type.PCR)
-            testResultAvailableNotificationService.showTestResultAvailableNotification(testResult)
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-            coronaTestRepository.updateResultNotification(any(), sent = true)
-        }
-
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testSendNotificationWhenNegative() = runBlockingTest {
-        val testResult = CoronaTestResult.PCR_NEGATIVE
-        coronaTestFlow.value = setOf(newCoronaTest(result = testResult))
-        coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs
-        coEvery {
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-        } just Runs
-
-        val result = createWorker().doWork()
-
-        coVerify {
-            coronaTestRepository.refresh(type = CoronaTest.Type.PCR)
-            testResultAvailableNotificationService.showTestResultAvailableNotification(testResult)
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-            coronaTestRepository.updateResultNotification(any(), sent = true)
-        }
-
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testSendNotificationWhenInvalid() = runBlockingTest {
-        val testResult = CoronaTestResult.PCR_INVALID
-        coronaTestFlow.value = setOf(newCoronaTest(result = testResult))
-        coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs
-        coEvery {
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-        } just Runs
-
-        val result = createWorker().doWork()
-
-        coVerify {
-            coronaTestRepository.refresh(type = CoronaTest.Type.PCR)
-            testResultAvailableNotificationService.showTestResultAvailableNotification(testResult)
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-            coronaTestRepository.updateResultNotification(any(), sent = true)
-        }
-
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testSendNoNotificationWhenPending() = runBlockingTest {
-        val testResult = CoronaTestResult.PCR_OR_RAT_PENDING
-        coronaTestFlow.value = setOf(newCoronaTest(result = testResult))
-        coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs
-        coEvery {
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-        } just Runs
-
-        val result = createWorker().doWork()
-
-        coVerify { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        coVerify(exactly = 0) {
-            testResultAvailableNotificationService.showTestResultAvailableNotification(
-                testResult
-            )
-            notificationHelper.cancelCurrentNotification(
-                NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-            )
-            testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false)
-        }
-
-        result shouldBe ListenableWorker.Result.success()
-    }
-
-    @Test
-    fun testRetryWhenExceptionIsThrown() = runBlockingTest {
-        coronaTestFlow.value = setOf(newCoronaTest())
-        coEvery { coronaTestRepository.refresh(any()) } throws Exception()
-
-        val result = createWorker().doWork()
-
-        coVerify(exactly = 1) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        coVerify(exactly = 0) { testResultScheduler.setPcrPeriodicTestPollingEnabled(any()) }
-        result shouldBe ListenableWorker.Result.retry()
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/PCRResultRetrievalWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/PCRResultRetrievalWorkerTest.kt
new file mode 100644
index 000000000..605a6d920
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/PCRResultRetrievalWorkerTest.kt
@@ -0,0 +1,119 @@
+package de.rki.coronawarnapp.worker
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkRequest
+import androidx.work.WorkerParameters
+import de.rki.coronawarnapp.coronatest.CoronaTestRepository
+import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
+import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest
+import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultRetrievalWorker
+import de.rki.coronawarnapp.coronatest.type.pcr.execution.PCRResultScheduler
+import de.rki.coronawarnapp.notification.GeneralNotifications
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.di.AppInjector
+import de.rki.coronawarnapp.util.di.ApplicationComponent
+import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory
+import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
+import org.junit.Before
+import org.junit.Test
+import testhelpers.BaseTest
+
+class PCRResultRetrievalWorkerTest : BaseTest() {
+    @MockK lateinit var context: Context
+    @MockK lateinit var request: WorkRequest
+    @MockK lateinit var notificationHelper: GeneralNotifications
+    @MockK lateinit var appComponent: ApplicationComponent
+    @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory
+    @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var coronaTestRepository: CoronaTestRepository
+    @MockK lateinit var testResultScheduler: PCRResultScheduler
+
+    @RelaxedMockK lateinit var workerParams: WorkerParameters
+    private val currentInstant = Instant.ofEpochSecond(1611764225)
+    private val testToken = "test token"
+
+    private val coronaTestFlow = MutableStateFlow(emptySet<CoronaTest>())
+
+    @Before
+    fun setUp() {
+        MockKAnnotations.init(this)
+        every { timeStamper.nowUTC } returns currentInstant
+
+        mockkObject(AppInjector)
+        every { AppInjector.component } returns appComponent
+        every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory
+        every { appComponent.errorResetTool } returns encryptionErrorResetTool
+
+        coEvery { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = any()) } just Runs
+
+        every { notificationHelper.cancelCurrentNotification(any()) } just Runs
+
+        coronaTestRepository.apply {
+            every { coronaTests } answers { coronaTestFlow }
+            coEvery { refresh(any()) } coAnswers { coronaTestFlow.first() }
+            coEvery { updateResultNotification(identifier = any(), sent = any()) } just Runs
+        }
+    }
+
+    private fun newCoronaTest(
+        registered: Instant = currentInstant,
+        viewed: Boolean = false,
+        result: CoronaTestResult = CoronaTestResult.PCR_POSITIVE,
+        isNotificationSent: Boolean = false,
+    ): CoronaTest {
+        return mockk<PCRCoronaTest>().apply {
+            every { identifier } returns ""
+            every { type } returns CoronaTest.Type.PCR
+            every { registeredAt } returns registered
+            every { isViewed } returns viewed
+            every { testResult } returns result
+            every { registrationToken } returns testToken
+            every { isResultAvailableNotificationSent } returns isNotificationSent
+        }
+    }
+
+    private fun createWorker() = PCRResultRetrievalWorker(
+        context = context,
+        workerParams = workerParams,
+        coronaTestRepository = coronaTestRepository,
+    )
+
+    @Test
+    fun testSuccess() = runBlockingTest {
+        coronaTestFlow.value = setOf(newCoronaTest())
+
+        val result = createWorker().doWork()
+
+        coVerify(exactly = 1) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
+        result shouldBe ListenableWorker.Result.success()
+    }
+
+    @Test
+    fun testRetryWhenExceptionIsThrown() = runBlockingTest {
+        coronaTestFlow.value = setOf(newCoronaTest())
+        coEvery { coronaTestRepository.refresh(any()) } throws Exception()
+
+        val result = createWorker().doWork()
+
+        coVerify(exactly = 1) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
+        result shouldBe ListenableWorker.Result.retry()
+    }
+}
-- 
GitLab