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