From 70bf5cf192727d6ffe15e2311cac0c828ee3fbbb Mon Sep 17 00:00:00 2001
From: chris-cwa <69595386+chris-cwa@users.noreply.github.com>
Date: Fri, 23 Apr 2021 17:49:08 +0200
Subject: [PATCH] Rapid Antigene Test Result Polling (EXPOSUREAPP-6517) (#2912)

* + rat worker

* minded mode switching

* fixed tests

* renaming

* renaming

* no skipping for rat workers

* - unused method

* reverted change to vals in order to make it suspend functions in the future

* start worker on rat creation

* renamed confusing method names

* fixed typpos

* moved constants to where they are used

* fixed log statement

* fixed test

* separate scheduler for every test

* use both states

* renamed classes

* fixed logs

Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 .../coronatest/type/pcr/PCRProcessor.kt       |   6 +-
 .../rapidantigen/RapidAntigenProcessor.kt     |   6 ++
 .../worker/PCRResultRetrievalWorker.kt}       |  29 +++--
 .../worker/RAResultRetrievalWorker.kt         | 101 ++++++++++++++++++
 .../execution/PCRResultScheduler.kt}          |  53 +++++----
 .../worker/execution/RAResultScheduler.kt     |  89 +++++++++++++++
 .../coronawarnapp/util/worker/WorkerBinder.kt |  18 +++-
 .../worker/BackgroundWorkHelper.kt            |  13 ---
 .../worker/BackgroundWorkScheduler.kt         |   8 +-
 .../util/worker/WorkerBinderTest.kt           |   8 +-
 .../worker/BackgroundWorkHelperTest.kt        |   3 +-
 ...isTestResultRetrievalPeriodicWorkerTest.kt |  19 ++--
 12 files changed, 279 insertions(+), 74 deletions(-)
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/{worker/DiagnosisTestResultRetrievalPeriodicWorker.kt => coronatest/worker/PCRResultRetrievalWorker.kt} (84%)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/RAResultRetrievalWorker.kt
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/{execution/TestResultScheduler.kt => worker/execution/PCRResultScheduler.kt} (59%)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/RAResultScheduler.kt

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 f7e353433..59848afdd 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
@@ -2,7 +2,6 @@ package de.rki.coronawarnapp.coronatest.type.pcr
 
 import dagger.Reusable
 import de.rki.coronawarnapp.coronatest.TestRegistrationRequest
-import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler
 import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_INVALID
@@ -19,6 +18,7 @@ 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
@@ -37,7 +37,7 @@ class PCRProcessor @Inject constructor(
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector,
     private val testResultDataCollector: TestResultDataCollector,
     private val deadmanNotificationScheduler: DeadmanNotificationScheduler,
-    private val testResultScheduler: TestResultScheduler,
+    private val pcrTestResultScheduler: PCRResultScheduler,
 ) : CoronaTestProcessor {
 
     override val type: CoronaTest.Type = CoronaTest.Type.PCR
@@ -82,7 +82,7 @@ class PCRProcessor @Inject constructor(
         analyticsKeySubmissionCollector.reportTestRegistered()
 
         if (testResult == PCR_OR_RAT_PENDING) {
-            testResultScheduler.setPeriodicTestPolling(enabled = true)
+            pcrTestResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = true)
         }
 
         return PCRCoronaTest(
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 482f6af98..fea68d822 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,6 +17,7 @@ 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
@@ -29,6 +30,7 @@ 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
@@ -41,6 +43,10 @@ class RapidAntigenProcessor @Inject constructor(
 
         val testResult = registrationData.testResult.validOrThrow()
 
+        if (testResult == PCR_OR_RAT_PENDING || testResult == RAT_PENDING) {
+            resultScheduler.setRatResultPeriodicPollingMode(mode = RAResultScheduler.RatPollingMode.PHASE1)
+        }
+
         return RACoronaTest(
             identifier = request.identifier,
             registeredAt = timeStamper.nowUTC,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt
similarity index 84%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt
index dd9ce5a95..4fe1c6e55 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/PCRResultRetrievalWorker.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.worker
+package de.rki.coronawarnapp.coronatest.worker
 
 import android.content.Context
 import androidx.work.CoroutineWorker
@@ -7,16 +7,17 @@ import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler
 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
@@ -24,17 +25,15 @@ import timber.log.Timber
 
 /**
  * Diagnosis test result retrieval by periodic polling
- *
- * @see BackgroundWorkScheduler
  */
-class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
+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: TestResultScheduler,
+    private val testResultScheduler: PCRResultScheduler,
 ) : CoroutineWorker(context, workerParams) {
 
     override suspend fun doWork(): Result {
@@ -43,7 +42,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
         if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
             Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling")
 
-            testResultScheduler.setPeriodicTestPolling(enabled = true)
+            testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = true)
             Timber.tag(TAG).d("$id Rescheduled background worker")
 
             return Result.failure()
@@ -52,7 +51,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
         try {
             if (abortConditionsMet(timeStamper.nowUTC)) {
                 Timber.tag(TAG).d(" $id Stopping worker.")
-                stopWorker()
+                disablePolling()
             } else {
                 Timber.tag(TAG).d(" $id Running worker.")
 
@@ -71,7 +70,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
                     cancelRiskLevelScoreNotification()
                     Timber.tag(TAG)
                         .d("$id: Test Result available - notification sent & risk level notification canceled")
-                    stopWorker()
+                    disablePolling()
                 }
             }
         } catch (e: Exception) {
@@ -87,7 +86,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
     private suspend fun abortConditionsMet(nowUTC: Instant): Boolean {
         val pcrTest = coronaTestRepository.latestPCRT.first()
         if (pcrTest == null) {
-            Timber.tag(TAG).w("There is no PCR test available!?")
+            Timber.tag(TAG).w("There is no PCR available!?")
             return true
         }
 
@@ -123,15 +122,15 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
         )
     }
 
-    private fun stopWorker() {
-        testResultScheduler.setPeriodicTestPolling(enabled = false)
-        Timber.tag(TAG).d("$id: Background worker stopped")
+    private fun disablePolling() {
+        testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false)
+        Timber.tag(TAG).d("$id: polling disabled")
     }
 
     @AssistedFactory
-    interface Factory : InjectedWorkerFactory<DiagnosisTestResultRetrievalPeriodicWorker>
+    interface Factory : InjectedWorkerFactory<PCRResultRetrievalWorker>
 
     companion object {
-        private val TAG = DiagnosisTestResultRetrievalPeriodicWorker::class.java.simpleName
+        private val TAG = PCRResultRetrievalWorker::class.java.simpleName
     }
 }
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/worker/RAResultRetrievalWorker.kt
new file mode 100644
index 000000000..4ee68a0d4
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/RAResultRetrievalWorker.kt
@@ -0,0 +1,101 @@
+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.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.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 timber.log.Timber
+
+/**
+ * RAT result retrieval by periodic polling
+ */
+class RAResultRetrievalWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters,
+    private val coronaTestRepository: CoronaTestRepository,
+    private val timeStamper: TimeStamper,
+    private val ratResultScheduler: RAResultScheduler,
+) : 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")
+
+            ratResultScheduler.setRatResultPeriodicPollingMode(mode = ratResultScheduler.ratResultPeriodicPollingMode)
+            Timber.tag(TAG).d("$id Rescheduled background worker")
+
+            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 {
+            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)
+                }
+            }
+            return Result.success()
+        }
+    }
+
+    private fun disablePolling() {
+        ratResultScheduler.setRatResultPeriodicPollingMode(mode = DISABLED)
+        Timber.tag(TAG).d("$id: polling disabled")
+    }
+
+    @AssistedFactory
+    interface Factory : InjectedWorkerFactory<RAResultRetrievalWorker>
+
+    companion object {
+        private val TAG = RAResultRetrievalWorker::class.java.simpleName
+
+        /**
+         * 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/execution/TestResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt
similarity index 59%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/execution/TestResultScheduler.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt
index a1c7ba36b..c9e83f1f8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/execution/TestResultScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/PCRResultScheduler.kt
@@ -1,48 +1,49 @@
-package de.rki.coronawarnapp.coronatest.execution
+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.storage.TracingSettings
-import de.rki.coronawarnapp.util.TimeStamper
+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 de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker
 import kotlinx.coroutines.runBlocking
 import timber.log.Timber
 import java.util.concurrent.TimeUnit
 import javax.inject.Inject
 
 @Reusable
-class TestResultScheduler @Inject constructor(
-    private val tracingSettings: TracingSettings,
+class PCRResultScheduler @Inject constructor(
     private val workManager: WorkManager,
-    private val timeStamper: TimeStamper,
 ) {
 
-    private suspend fun isScheduled(): Boolean {
-        val workerInfos = workManager.getWorkInfosForUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME).await()
+    private suspend fun isPcrScheduled() =
+        workManager.getWorkInfosForUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME)
+            .await()
+            .any { it.isScheduled }
 
-        return workerInfos.any { it.isScheduled }
-    }
+    private val WorkInfo.isScheduled: Boolean
+        get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED
 
-    fun setPeriodicTestPolling(enabled: Boolean) {
+    fun setPcrPeriodicTestPollingEnabled(enabled: Boolean) {
         if (enabled) {
             // TODO Refactor runBlocking away
-            val isScheduled = runBlocking { isScheduled() }
+            val isScheduled = runBlocking { isPcrScheduled() }
             if (isScheduled) {
                 Timber.tag(TAG).w("Already scheduled, skipping")
                 return
             }
-            Timber.tag(TAG).i("Queueing test result worker (DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER)")
+            Timber.tag(TAG).i("Queueing pcr test result worker (DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER)")
             workManager.enqueueUniquePeriodicWork(
                 PCR_TESTRESULT_WORKER_UNIQUEUNAME,
                 ExistingPeriodicWorkPolicy.REPLACE,
-                buildDiagnosisTestResultRetrievalPeriodicWork()
+                buildPcrTestResultRetrievalPeriodicWork()
             )
         } else {
             Timber.tag(TAG).d("cancelWorker()")
@@ -50,9 +51,9 @@ class TestResultScheduler @Inject constructor(
         }
     }
 
-    private fun buildDiagnosisTestResultRetrievalPeriodicWork() =
-        PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>(
-            BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(),
+    private fun buildPcrTestResultRetrievalPeriodicWork() =
+        PeriodicWorkRequestBuilder<PCRResultRetrievalWorker>(
+            getPcrTestResultRetrievalPeriodicWorkTimeInterval(),
             TimeUnit.MINUTES
         )
             .addTag(PCR_TESTRESULT_WORKER_TAG)
@@ -67,9 +68,6 @@ class TestResultScheduler @Inject constructor(
             )
             .build()
 
-    private 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
@@ -80,6 +78,17 @@ class TestResultScheduler @Inject constructor(
         private const val PCR_TESTRESULT_WORKER_TAG = "DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER"
         private const val PCR_TESTRESULT_WORKER_UNIQUEUNAME = "DiagnosisTestResultBackgroundPeriodicWork"
 
-        private const val TAG = "TestResultScheduler"
+        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
new file mode 100644
index 000000000..f1253c501
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/worker/execution/RAResultScheduler.kt
@@ -0,0 +1,89 @@
+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/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
index 90dc9a5a5..7f1d39bc4 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,18 +5,19 @@ 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.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker
 import de.rki.coronawarnapp.deniability.BackgroundNoiseOneTimeWorker
 import de.rki.coronawarnapp.deniability.BackgroundNoisePeriodicWorker
 import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker
-import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpPeriodicWorker
 import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOutWorker
 import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorker
+import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpPeriodicWorker
 import de.rki.coronawarnapp.submission.auto.SubmissionWorker
-import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker
 
 @Module
 abstract class WorkerBinder {
@@ -51,9 +52,16 @@ abstract class WorkerBinder {
 
     @Binds
     @IntoMap
-    @WorkerKey(DiagnosisTestResultRetrievalPeriodicWorker::class)
-    abstract fun testResultRetrievalPeriodic(
-        factory: DiagnosisTestResultRetrievalPeriodicWorker.Factory
+    @WorkerKey(PCRResultRetrievalWorker::class)
+    abstract fun pcrTestResultRetrievalPeriodic(
+        factory: PCRResultRetrievalWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(RAResultRetrievalWorker::class)
+    abstract fun ratResultRetrievalPeriodic(
+        factory: RAResultRetrievalWorker.Factory
     ): InjectedWorkerFactory<out ListenableWorker>
 
     @Binds
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
index a9e401d30..67a621d61 100644
--- 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
@@ -13,19 +13,6 @@ import kotlin.random.Random
  */
 object BackgroundWorkHelper {
 
-    /**
-     * Calculate the time for diagnosis key retrieval periodic work
-     *
-     * @return Long
-     *
-     * @see BackgroundConstants.MINUTES_IN_DAY
-     */
-    fun getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(): Long =
-        (
-            BackgroundConstants.MINUTES_IN_DAY /
-                BackgroundConstants.DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY
-            ).toLong()
-
     /**
      * Get background noise one time work delay
      * The periodic job is already delayed by MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
index 706db5566..cad840478 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.worker
 
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler
+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
@@ -21,7 +21,7 @@ import javax.inject.Singleton
 class BackgroundWorkScheduler @Inject constructor(
     private val riskWorkScheduler: RiskWorkScheduler,
     private val coronaTestRepository: CoronaTestRepository,
-    private val testResultScheduler: TestResultScheduler,
+    private val testResultScheduler: PCRResultScheduler,
     private val noiseScheduler: NoiseScheduler,
 ) {
 
@@ -36,14 +36,14 @@ class BackgroundWorkScheduler @Inject constructor(
         val hasPendingTests = coronatests.any { !it.isResultAvailableNotificationSent }
 
         if (!isSubmissionSuccessful && hasPendingTests) {
-            testResultScheduler.setPeriodicTestPolling(enabled = true)
+            testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = true)
         }
     }
 
     fun stopWorkScheduler() {
         noiseScheduler.setPeriodicNoise(enabled = false)
         riskWorkScheduler.setPeriodicRiskCalculation(enabled = false)
-        testResultScheduler.setPeriodicTestPolling(enabled = false)
+        testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false)
         Timber.d("All Background Jobs Stopped")
     }
 }
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 ab535e027..34e53717f 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,7 +7,8 @@ import dagger.Component
 import dagger.Module
 import dagger.Provides
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler
+import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
+import de.rki.coronawarnapp.coronatest.worker.execution.RAResultScheduler
 import de.rki.coronawarnapp.datadonation.analytics.Analytics
 import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
@@ -162,5 +163,8 @@ class MockProvider {
     fun noiseScheduler(): NoiseScheduler = mockk()
 
     @Provides
-    fun testResultScheduler(): TestResultScheduler = mockk()
+    fun pcrTestResultScheduler(): PCRResultScheduler = mockk()
+
+    @Provides
+    fun ratResultScheduler(): RAResultScheduler = mockk()
 }
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 0ab0f5e23..d2251ee48 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,6 +1,7 @@
 package de.rki.coronawarnapp.worker
 
 import androidx.work.NetworkType
+import de.rki.coronawarnapp.coronatest.worker.execution.PCRResultScheduler
 import org.junit.Assert
 import org.junit.Test
 
@@ -9,7 +10,7 @@ class BackgroundWorkHelperTest {
     @Test
     fun getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval() {
         Assert.assertEquals(
-            BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(),
+            PCRResultScheduler.getPcrTestResultRetrievalPeriodicWorkTimeInterval(),
             120
         )
     }
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
index ee61927fa..33ec41f79 100644
--- 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
@@ -5,10 +5,11 @@ import androidx.work.ListenableWorker
 import androidx.work.WorkRequest
 import androidx.work.WorkerParameters
 import de.rki.coronawarnapp.coronatest.CoronaTestRepository
-import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler
 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
@@ -48,7 +49,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
     @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool
     @MockK lateinit var timeStamper: TimeStamper
     @MockK lateinit var coronaTestRepository: CoronaTestRepository
-    @MockK lateinit var testResultScheduler: TestResultScheduler
+    @MockK lateinit var testResultScheduler: PCRResultScheduler
 
     @RelaxedMockK lateinit var workerParams: WorkerParameters
     private val currentInstant = Instant.ofEpochSecond(1611764225)
@@ -66,7 +67,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory
         every { appComponent.errorResetTool } returns encryptionErrorResetTool
 
-        every { testResultScheduler.setPeriodicTestPolling(enabled = any()) } just Runs
+        every { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = any()) } just Runs
 
         every { notificationHelper.cancelCurrentNotification(any()) } just Runs
 
@@ -94,7 +95,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         }
     }
 
-    private fun createWorker() = DiagnosisTestResultRetrievalPeriodicWorker(
+    private fun createWorker() = PCRResultRetrievalWorker(
         context = context,
         workerParams = workerParams,
         testResultAvailableNotificationService = testResultAvailableNotificationService,
@@ -111,7 +112,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         val result = createWorker().doWork()
 
         coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        verify(exactly = 1) { testResultScheduler.setPeriodicTestPolling(enabled = false) }
+        verify(exactly = 1) { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false) }
         result shouldBe ListenableWorker.Result.success()
     }
 
@@ -122,7 +123,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         val result = createWorker().doWork()
 
         coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        verify(exactly = 1) { testResultScheduler.setPeriodicTestPolling(enabled = false) }
+        verify(exactly = 1) { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false) }
         result shouldBe ListenableWorker.Result.success()
     }
 
@@ -135,7 +136,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         val result = createWorker().doWork()
 
         coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        verify(exactly = 1) { testResultScheduler.setPeriodicTestPolling(enabled = false) }
+        verify(exactly = 1) { testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false) }
         result shouldBe ListenableWorker.Result.success()
     }
 
@@ -236,7 +237,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
             notificationHelper.cancelCurrentNotification(
                 NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
             )
-            testResultScheduler.setPeriodicTestPolling(enabled = false)
+            testResultScheduler.setPcrPeriodicTestPollingEnabled(enabled = false)
         }
 
         result shouldBe ListenableWorker.Result.success()
@@ -250,7 +251,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         val result = createWorker().doWork()
 
         coVerify(exactly = 1) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) }
-        coVerify(exactly = 0) { testResultScheduler.setPeriodicTestPolling(any()) }
+        coVerify(exactly = 0) { testResultScheduler.setPcrPeriodicTestPollingEnabled(any()) }
         result shouldBe ListenableWorker.Result.retry()
     }
 }
-- 
GitLab