From 63050acabc0e7c1d6a157e4c04de1094bf2a3c6f Mon Sep 17 00:00:00 2001 From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com> Date: Wed, 7 Apr 2021 15:34:27 +0200 Subject: [PATCH] Warning package download & scheduling (EXPOSUREAPP-5695,EXPOSUREAPP-5696) (#2707) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * download and repo * download warning packages * download warning packages * TraceTimeWarning Download, Draft2 * TraceTimeWarning Download, Draft3 * Offer only new warning packages via API. * Remove duplicate test menu entry. * TraceTimeWarning Download, Draft5 * Adapt CheckInWarningMatcher to mark processed packages. * Fix failing unit tests. * Unit Test Skeletons * Back BackgroundScheduler non-static and injected. * Refactored and combined diagnosis and tracing periodic workers. * Fix unit test. * LINTs * Adapt marking packages as processed to upstream CheckInWarningMatcher changes. * Copy .await() from worker library to project due to being scope restricted. * unit tests * klint * klint * add TODO for matching comparison * refactor combination of results * rename * add test for combine * revert unnecessary changes * change initial result to failed * using low risk as default for lastCalculated to maintain the old behavior when no results are available * klint * Resolve merge regressions. * Fix fake check-in generation. * Handle worker refactoring gracefully, catch ClassNotFoundException. * Refactoring packages. * Refactor CheckInWarningMatcher.kt and PresenceTracingWarningTask.kt Move repository/database update calls to task, let the matcher only be responsible for matching. * Adjust test fragment to allow retriggering the download + matching task. * Fix refactoring regression. * Fix flaky time label test. * Fix package import. * Fix another timezone based flaky test. * Reduce nesting level to make the linter happy. * Update ROOM schema files. * Update ROOM schema files. * Refactoring. * Small naming fixes * Shorten worker IDs. * Collect BerndStylePointsâ„¢ * Set tableName explicitly. * * Adjust id comparison within CheckInWarningMatcher.kt * Add tests for traceLocationIdHash calculation. * Calculate traceLocationIdHash on-demand, don't store it. * Improve trace warning task test menu output. * Remove spammy log output. * Remove unused function * Add additional TraceLocation test cases that match mock server. * Improve check-in matching log messages. * Fix duplicate overlaps and check for overlap distinctness. * Add test for config timeout value. * Remove unused test. * Use time measuring function. * Simplify error case handling for revoked metadata packages. * Turn createMatchingLaunchers into runMatchingLaunchers * Use TimeUnit.MINUTES * Remove extra deletion call. If there are no CheckIns, the SyncTool will have deleted all matches already. * Use flatMap instead of flatten. * Use more specific names (toCheckInWarningOverlap/toTraceTimeIntervalMatchEntity) * Revert "Remove extra deletion call. If there are no CheckIns, the SyncTool will have deleted all matches already." This reverts commit 3b0acf3d * Fix merge conflict regressions. Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> Co-authored-by: Alexander Alferov <a.alferov@sap.com> --- .reuse/dep5 | 4 + .../1.json | 64 --- .../1.json | 12 +- .../1.json | 0 .../1.json | 76 ++++ .../storage/CheckInDatabaseData.kt | 2 - ...DiagnosisKeyRetrievalPeriodicWorkerTest.kt | 150 ------- .../risk/storage/DefaultRiskLevelStorage.kt | 2 +- .../risk/storage/DefaultRiskLevelStorage.kt | 2 +- .../ui/EventRegistrationTestFragment.kt | 21 +- .../EventRegistrationTestFragmentViewModel.kt | 84 ++-- .../test/menu/ui/TestMenuFragmentViewModel.kt | 1 - .../fragment_test_eventregistration.xml | 14 +- .../coronawarnapp/CoronaWarnApplication.kt | 3 +- .../appconfig/KeyDownloadConfig.kt | 4 + .../mapping/KeyDownloadParametersMapper.kt | 26 +- .../overview/ContactDiaryOverviewViewModel.kt | 2 +- .../DiagnosisKeyRetrievalWorkBuilder.kt | 34 ++ .../execution/DiagnosisKeyRetrievalWorker.kt} | 48 +-- .../EventRegistrationModule.kt | 15 +- .../eventregistration/checkins/CheckIn.kt | 15 +- .../TraceTimeIntervalWarningPackage.kt | 16 - .../TraceTimeIntervalWarningRepository.kt | 59 --- .../checkins/qrcode/TraceLocation.kt | 15 +- .../entity/TraceLocationCheckInEntity.kt | 2 - .../coronawarnapp/playbook/BackgroundNoise.kt | 5 +- .../checkins/checkout/CheckOutNotification.kt | 4 +- ...ons.kt => PresenceTracingNotifications.kt} | 6 +- .../risk/CheckInWarningMatcher.kt | 156 ------- .../risk/PresenceTracingRiskRepository.kt | 224 ---------- .../presencetracing/risk/PtRiskLevelResult.kt | 2 + .../risk/TraceLocationCheckInRisk.kt | 3 +- .../risk/calculation/CheckInWarningMatcher.kt | 143 +++++++ .../PresenceTracingConversions.kt | 7 +- .../PresenceTracingRiskCalculator.kt | 2 +- .../PresenceTracingRiskMapper.kt | 2 +- .../PresenceTracingRiskModel.kt | 4 +- .../execution/PresenceTracingWarningTask.kt | 158 +++++++ .../PresenceTracingWarningWorkBuilder.kt | 37 ++ .../execution/PresenceTracingWarningWorker.kt | 52 +++ .../PresenceTracingRiskDatabase.kt | 8 +- .../storage/PresenceTracingRiskRepository.kt | 283 +++++++++++++ .../warning/PresenceTracingWarningModule.kt | 43 ++ .../download/TraceWarningPackageDownloader.kt | 164 ++++++++ .../download/TraceWarningPackageSyncTool.kt | 178 ++++++++ .../TraceWarningPackageValidationException.kt | 9 + .../download/server/TraceWarningApiV1.kt | 31 ++ .../server/TraceWarningPackageDownload.kt | 16 + .../download/server/TraceWarningServer.kt | 54 +++ .../warning/storage/TraceWarningPackage.kt | 14 + .../storage/TraceWarningPackageContainer.kt | 23 ++ .../storage/TraceWarningPackageDatabase.kt | 62 +++ .../storage/TraceWarningPackageMetadata.kt | 58 +++ .../warning/storage/TraceWarningRepository.kt | 182 +++++++++ .../warning/storage/WarningPackageId.kt | 3 + .../coronawarnapp/risk/CombinedEwPtRisk.kt | 64 +++ .../risk/RiskLevelChangeDetector.kt | 7 +- .../risk/RiskLevelResultExtensions.kt | 37 +- .../risk/execution/RiskWorkScheduler.kt | 109 +++++ .../risk/storage/BaseRiskLevelStorage.kt | 157 ++++--- .../risk/storage/RiskLevelStorage.kt | 59 +-- .../submission/SubmissionRepository.kt | 5 +- .../submission/task/SubmissionTask.kt | 7 +- .../tracing/states/TracingStateProvider.kt | 11 +- .../TracingDetailsFragmentViewModel.kt | 3 +- .../ui/details/TracingDetailsItemProvider.kt | 3 +- .../items/riskdetails/DetailsLowRiskBox.kt | 1 + .../SettingsTracingFragmentViewModel.kt | 7 +- .../EventRegistrationUIModule.kt | 8 +- .../confirm/ConfirmCheckInViewModel.kt | 1 - .../details/QrCodeDetailViewModel.kt | 6 +- .../rki/coronawarnapp/ui/main/MainActivity.kt | 8 +- .../ui/settings/SettingsResetViewModel.kt | 5 +- .../util/TimeAndDateExtensions.kt | 7 + .../rki/coronawarnapp/util/WatchdogService.kt | 24 +- .../util/coroutine/ListenableFuture.kt | 57 +++ .../util/formatter/FormatterStatistics.kt | 15 +- .../util/worker/CWAWorkerFactory.kt | 18 +- .../coronawarnapp/util/worker/WorkerBinder.kt | 22 +- .../worker/BackgroundConstants.kt | 20 - .../worker/BackgroundNoisePeriodicWorker.kt | 8 +- .../worker/BackgroundWorkBuilder.kt | 193 ++++----- .../worker/BackgroundWorkScheduler.kt | 133 ++---- .../worker/BackgroundWorkSchedulerBase.kt | 11 - .../DiagnosisKeyRetrievalPeriodicWorker.kt | 60 --- ...gnosisTestResultRetrievalPeriodicWorker.kt | 6 +- .../src/main/res/drawable/ic_print.xml | 6 +- .../src/main/res/drawable/ic_share.xml | 6 +- .../ContactDiaryOverviewViewModelTest.kt | 2 +- .../checkins/CheckInRepositoryTest.kt | 5 - .../CheckInTransmissionRiskLevelTest.kt | 1 - .../checkins/CheckInsTransformerTest.kt | 3 - .../checkins/split/CheckInSplitterTest.kt | 1 - .../events/TraceLocationIdTest.kt | 204 +++++++++- .../storage/retention/CheckInCleanerTest.kt | 1 - .../checkins/checkout/CheckOutHandlerTest.kt | 1 - .../checkout/auto/AutoCheckOutTest.kt | 1 - .../common/TraceLocationNotificationsTest.kt | 2 +- .../risk/CheckInWarningMatcherTest.kt | 255 ------------ .../calculation/CheckInWarningMatcherTest.kt | 255 ++++++++++++ .../risk/{ => calculation}/FindMatchesTest.kt | 12 +- .../risk/{ => calculation}/OverlapTest.kt | 94 +++-- .../PresenceTracingRiskCalculatorTest.kt | 2 +- .../PresenceTracingRiskMapperTest.kt | 2 +- .../PresenceTracingWarningTaskTest.kt | 223 ++++++++++ .../PresenceTracingWarningWorkerTest.kt | 95 +++++ .../TraceWarningPackageDownloaderTest.kt | 12 + .../risk/EwRiskLevelResultExtensionsTest.kt | 13 +- .../risk/RiskLevelChangeDetectorTest.kt | 194 +++++++-- .../risk/storage/BaseRiskLevelStorageTest.kt | 385 +++++++++++++++++- .../risk/storage/CombineRiskTest.kt | 110 ++++- .../risk/storage/RiskStorageTestData.kt | 10 +- .../storage/SubmissionRepositoryTest.kt | 9 +- .../submission/task/SubmissionTaskTest.kt | 14 +- .../details/TracingDetailsItemProviderTest.kt | 66 +-- .../poster/QrCodePosterViewModelTest.kt | 2 +- .../util/TimeAndDateExtensionsTest.kt | 13 + .../FormatterStatisticsHelperTest.kt | 10 +- .../util/worker/CWAWorkerFactoryTest.kt | 26 +- .../util/worker/WorkerBinderTest.kt | 4 + .../worker/BackgroundWorkBuilderTest.kt | 16 - ...isTestResultRetrievalPeriodicWorkerTest.kt | 21 +- .../storage/DefaultRiskLevelStorageTest.kt | 4 +- .../storage/DefaultRiskLevelStorageTest.kt | 4 +- 124 files changed, 3934 insertions(+), 1826 deletions(-) delete mode 100644 Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json rename Corona-Warn-App/schemas/{de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase => de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase}/1.json (100%) create mode 100644 Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json delete mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/{worker/DiagnosisKeyRetrievalOneTimeWorker.kt => diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt} (50%) delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/{TraceLocationNotifications.kt => PresenceTracingNotifications.kt} (96%) delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/{ => presencetracing}/risk/TraceLocationCheckInRisk.kt (62%) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/PresenceTracingConversions.kt (73%) rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/PresenceTracingRiskCalculator.kt (97%) rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/PresenceTracingRiskMapper.kt (97%) rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/PresenceTracingRiskModel.kt (90%) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/{ => storage}/PresenceTracingRiskDatabase.kt (85%) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/FindMatchesTest.kt (79%) rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/OverlapTest.kt (72%) rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/PresenceTracingRiskCalculatorTest.kt (98%) rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/{ => calculation}/PresenceTracingRiskMapperTest.kt (98%) create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt diff --git a/.reuse/dep5 b/.reuse/dep5 index 95abf9721..b1088ff6d 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -40,6 +40,10 @@ Files: Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/ViewMod Copyright: 2018 The Android Open Source Project License: Apache-2.0 +Files: Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt +Copyright: 2018 The Android Open Source Project +License: Apache-2.0 + Files: gradlew gradlew.bat Copyright: Copyright 2015 the original author or authors. License: Apache-2.0 diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json deleted file mode 100644 index 1109cd70c..000000000 --- a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 1, - "identityHash": "751c249cb9c836add4b0cb663ed13954", - "entities": [ - { - "tableName": "TraceTimeIntervalMatchEntity", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `checkInId` INTEGER NOT NULL, `traceWarningPackageId` INTEGER NOT NULL, `transmissionRiskLevel` INTEGER NOT NULL, `startTimeMillis` INTEGER NOT NULL, `endTimeMillis` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "checkInId", - "columnName": "checkInId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "traceWarningPackageId", - "columnName": "traceWarningPackageId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "transmissionRiskLevel", - "columnName": "transmissionRiskLevel", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "startTimeMillis", - "columnName": "startTimeMillis", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "endTimeMillis", - "columnName": "endTimeMillis", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "columnNames": [ - "id" - ], - "autoGenerate": true - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '751c249cb9c836add4b0cb663ed13954')" - ] - } -} \ No newline at end of file diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json index 9e78f2a34..befd22943 100644 --- a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "1dc5f8a56361d50b8bb18050bce59d20", + "identityHash": "5117ed4caaa7ecd70051902d844cc665", "entities": [ { "tableName": "checkin", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` TEXT NOT NULL, `traceLocationIdHashBase64` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `traceLocationStart` TEXT, `traceLocationEnd` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `traceLocationStart` TEXT, `traceLocationEnd` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", @@ -20,12 +20,6 @@ "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "traceLocationIdHashBase64", - "columnName": "traceLocationIdHashBase64", - "affinity": "TEXT", - "notNull": true - }, { "fieldPath": "version", "columnName": "version", @@ -192,7 +186,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dc5f8a56361d50b8bb18050bce59d20')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5117ed4caaa7ecd70051902d844cc665')" ] } } \ No newline at end of file diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase/1.json similarity index 100% rename from Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase/1.json rename to Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase/1.json diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json new file mode 100644 index 000000000..71c7f0b1d --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json @@ -0,0 +1,76 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "1775e362b403962906c6d384338b2708", + "entities": [ + { + "tableName": "TraceWarningPackageMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `location` TEXT NOT NULL, `hourInterval` INTEGER NOT NULL, `eTag` TEXT, `downloaded` INTEGER NOT NULL, `emptyPkg` INTEGER NOT NULL, `processed` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "packageId", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hourInterval", + "columnName": "hourInterval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "eTag", + "columnName": "eTag", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isDownloaded", + "columnName": "downloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isEmptyPkg", + "columnName": "emptyPkg", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isProcessed", + "columnName": "processed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1775e362b403962906c6d384338b2708')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt index 500e91485..370276daf 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt @@ -9,7 +9,6 @@ object CheckInDatabaseData { val testCheckIn = TraceLocationCheckInEntity( traceLocationIdBase64 = "traceLocationId1".encode().base64(), - traceLocationIdHashBase64 = "traceLocationIdHash1".encode().base64(), version = 1, type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER.number, description = "testDescription1", @@ -27,7 +26,6 @@ object CheckInDatabaseData { val testCheckInWithoutCheckOutTime = TraceLocationCheckInEntity( traceLocationIdBase64 = "traceLocationId1".encode().base64(), - traceLocationIdHashBase64 = "traceLocationIdHash1".encode().base64(), version = 1, type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER.number, description = "testDescription2", diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt deleted file mode 100644 index bc8b3025f..000000000 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt +++ /dev/null @@ -1,150 +0,0 @@ -package de.rki.coronawarnapp.worker - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.work.WorkInfo -import androidx.work.WorkManager -import androidx.work.WorkRequest -import androidx.work.testing.TestDriver -import androidx.work.testing.WorkManagerTestInitHelper -import io.mockk.Runs -import io.mockk.every -import io.mockk.just -import io.mockk.mockkObject -import org.hamcrest.CoreMatchers.`is` -import org.hamcrest.CoreMatchers.notNullValue -import org.hamcrest.CoreMatchers.nullValue -import org.hamcrest.MatcherAssert.assertThat -import org.junit.After -import org.junit.Before -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith -import testhelpers.BaseTestInstrumentation - -/** - * DiagnosisKeyRetrievalPeriodicWorker test. - */ -@Ignore("FixMe:DiagnosisKeyRetrievalPeriodicWorkerTest") -@RunWith(AndroidJUnit4::class) -class DiagnosisKeyRetrievalPeriodicWorkerTest : BaseTestInstrumentation() { - private lateinit var context: Context - private lateinit var workManager: WorkManager - private lateinit var request: WorkRequest - private lateinit var request2: WorkRequest - - // small delay because WorkManager does not run work instantly when delay is off - private val delay = 500L - - @Before - fun setUp() { - mockkObject(BackgroundWorkScheduler) - // do not init Test WorkManager instance again between tests - // leads to all tests instead of first one to fail - context = ApplicationProvider.getApplicationContext() - if (WorkManager.getInstance(context) !is TestDriver) { - WorkManagerTestInitHelper.initializeTestWorkManager(context) - } - workManager = WorkManager.getInstance(context) - - every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers { - request = this.callOriginal() as WorkRequest - request - } - } - - /** - * Test worker for success. - */ - @Test - fun testDiagnosisKeyRetrievalPeriodicWorkerSuccess() { - every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } just Runs - - BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - - assertThat(request, notNullValue()) - - var workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo, notNullValue()) - assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) - - runPeriodicJobInitialDelayMet() - assertThat(request, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo.runAttemptCount, `is`(0)) - - runPeriodicJobPeriodDelayMet() - assertThat(request, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo.runAttemptCount, `is`(0)) - } - - /** - * Test worker for retries and fail. - */ - @Test - fun testDiagnosisKeyRetrievalPeriodicWorkerRetryAndFail() { - every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } throws Exception("test exception") - - BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - - assertThat(request, notNullValue()) - var workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo, notNullValue()) - assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED))) - - for (i in 1..BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2) { - // run job i times - when (i) { - BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { - every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers { - request2 = this.callOriginal() as WorkRequest - request2 - } - runPeriodicJobInitialDelayMet() - } - else -> { - runPeriodicJobInitialDelayMet() - } - } - - // get job run #i result - when (i) { - BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> { - assertThat(request, notNullValue()) - assertThat(request2, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - val workInfo2 = workManager.getWorkInfoById(request2.id).get() - assertThat(workInfo, nullValue()) - assertThat(workInfo2.state, `is`(WorkInfo.State.ENQUEUED)) - assertThat(workInfo2.runAttemptCount, `is`(0)) - } - else -> { - assertThat(request, notNullValue()) - workInfo = workManager.getWorkInfoById(request.id).get() - assertThat(workInfo.runAttemptCount, `is`(i)) - } - } - } - } - - @After - fun cleanUp() { - workManager.cancelAllWork() - } - - private fun runPeriodicJobInitialDelayMet() { - val testDriver = WorkManagerTestInitHelper.getTestDriver(context) - testDriver?.setAllConstraintsMet(request.id) - testDriver?.setInitialDelayMet(request.id) - Thread.sleep(delay) - } - - private fun runPeriodicJobPeriodDelayMet() { - val testDriver = WorkManagerTestInitHelper.getTestDriver(context) - testDriver?.setAllConstraintsMet(request.id) - testDriver?.setPeriodDelayMet(request.id) - Thread.sleep(delay) - } -} diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index c88e6445b..f8ac9f20d 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.util.coroutine.AppScope diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index ee15ef5ab..7fbb56a67 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt index 70f15828d..95d522e04 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.os.Bundle import android.text.SpannedString import android.view.View -import android.widget.Toast import androidx.core.text.bold import androidx.core.text.buildSpannedString import androidx.core.text.color @@ -35,34 +34,34 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.runMatcher.setOnClickListener { - viewModel.runMatcher() - } - - binding.downloadReportedCheckIns.setOnClickListener { - Toast.makeText(context, "Not implemented", Toast.LENGTH_SHORT).show() + binding.resetProcessedWarningPackages.setOnClickListener { + viewModel.resetProcessedWarningPackages() } binding.calculateRisk.setOnClickListener { viewModel.runRiskCalculationPerCheckInDay() } - viewModel.checkInOverlapsText.observe2(this) { - binding.matchingResultText.text = it + viewModel.presenceTracingWarningTaskResult.observe2(this) { + binding.tracingWarningTaskResult.text = it } viewModel.checkInRiskPerDayText.observe2(this) { binding.riskCalculationResultText.text = it } - viewModel.matchingRuntime.observe2(this) { - binding.matchingRuntimeText.text = "Matching runtime in millis: $it" + viewModel.taskRunTime.observe2(this) { + binding.taskRunTime.text = "Task finished in ${it}ms" } viewModel.riskCalculationRuntime.observe2(this) { binding.riskCalculationRuntimeText.text = "Risk calculation runtime in millis: $it" } + binding.runPtWarningTask.setOnClickListener { + viewModel.runPresenceTracingWarningTask() + } + viewModel.lastOrganiserLocation.observe(viewLifecycleOwner) { binding.lastOrganiserLocationCard.isVisible = it != null it?.let { traceLocation -> diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt index a44796fb2..3913503fe 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt @@ -9,23 +9,31 @@ import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository -import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningMatcher -import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningOverlap -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +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.DispatcherProvider import de.rki.coronawarnapp.util.debug.measureTime import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import timber.log.Timber +import kotlin.system.measureTimeMillis class EventRegistrationTestFragmentViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, traceLocationRepository: TraceLocationRepository, checkInRepository: CheckInRepository, - private val checkInWarningMatcher: CheckInWarningMatcher, - private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator + private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, + private val taskController: TaskController, + private val presenceTracingRiskRepository: PresenceTracingRiskRepository, + private val traceWarningRepository: TraceWarningRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val lastOrganiserLocation: LiveData<TraceLocation?> = @@ -38,42 +46,49 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor( .map { lastAttendeeLocationData(it) } .asLiveData(dispatcherProvider.Default) - private val checkInWarningOverlaps = mutableListOf<CheckInWarningOverlap>() - val checkInOverlapsText = MutableLiveData<String>() - val matchingRuntime = MutableLiveData<Long>() + val presenceTracingWarningTaskResult = MutableLiveData<String>() + val taskRunTime = MutableLiveData<Long>() val riskCalculationRuntime = MutableLiveData<Long>() val checkInRiskPerDayText = MutableLiveData<String>() - fun runMatcher() { - launch { - measureTime( - { - Timber.d("Time to find matches: $it millis") - matchingRuntime.postValue(it) - }, - { - checkInWarningOverlaps.clear() - val matches = checkInWarningMatcher.execute() + fun runPresenceTracingWarningTask() = launch { + Timber.d("runWarningPackageTask()") + presenceTracingWarningTaskResult.postValue("Running") + taskRunTime.postValue(-1L) - checkInWarningOverlaps.addAll(matches) - - if (checkInWarningOverlaps.size < 100) { - val text = checkInWarningOverlaps.fold(StringBuilder()) { stringBuilder, checkInOverlap -> - stringBuilder - .append("CheckIn Id ${checkInOverlap.checkInId}, ") - .append("Date ${checkInOverlap.localDateUtc}, ") - .append("Min. ${checkInOverlap.overlap.standardMinutes}") - .append("\n") - } - if (text.isBlank()) checkInOverlapsText.postValue("No matches found") - else checkInOverlapsText.postValue(text.toString()) - } else { - checkInOverlapsText.postValue("Output too large. ${checkInWarningOverlaps.size} lines") - } - } + val duration = measureTimeMillis { + taskController.submitBlocking( + DefaultTaskRequest( + PresenceTracingWarningTask::class, + originTag = "EventRegistrationTestFragmentViewModel" + ) ) } + taskRunTime.postValue(duration) + + val warningPackages = traceWarningRepository.allMetaData.first() + val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() + val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull() + + val infoText = when { + !lastResult!!.wasSuccessfullyCalculated -> "Last calculation failed" + overlaps.isEmpty() -> "No matches found (${warningPackages.size} warning packages)." + overlaps.size > 100 -> "Output too large. ${overlaps.size} lines" + overlaps.isNotEmpty() -> overlaps.fold(StringBuilder()) { stringBuilder, checkInOverlap -> + stringBuilder + .append("CheckIn Id ${checkInOverlap.checkInId}, ") + .append("Date ${checkInOverlap.localDateUtc}, ") + .append("Min. ${checkInOverlap.overlap.standardMinutes}") + .appendLine() + }.toString() + else -> "Unknown state" + } + presenceTracingWarningTaskResult.postValue(infoText) + } + + fun resetProcessedWarningPackages() = launch { + traceWarningRepository.clear() } fun runRiskCalculationPerCheckInDay() { @@ -84,6 +99,7 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor( riskCalculationRuntime.postValue(it) }, { + val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() val normalizedTimePerCheckInDayList = presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps) val riskStates = diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index 10ba1a3b2..e631969b4 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -37,7 +37,6 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { DataDonationTestFragment.MENU_ITEM, DeltaonboardingFragment.MENU_ITEM, EventRegistrationTestFragment.MENU_ITEM, - DeltaonboardingFragment.MENU_ITEM ).let { MutableLiveData(it) } } val showTestScreenEvent = SingleLiveEvent<TestMenuItem>() diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml index 1f01bcef3..1e4441e00 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml @@ -25,31 +25,30 @@ android:text="Download, matching & risk calculation" /> <com.google.android.material.button.MaterialButton - android:id="@+id/downloadReportedCheckIns" + android:id="@+id/reset_processed_warning_packages" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_tiny" - android:text="download check-ins" /> + android:text="Reset processed packages" /> <com.google.android.material.button.MaterialButton - android:id="@+id/runMatcher" + android:id="@+id/run_pt_warning_task" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" - android:text="Run matcher" /> + android:text="Run task (download+matching)" /> <TextView - android:id="@+id/matchingRuntimeText" + android:id="@+id/task_run_time" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" android:text="Matching runtime:" /> <TextView - android:id="@+id/matchingResultText" + android:id="@+id/tracing_warning_task_result" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" android:text="Matching result:" /> <com.google.android.material.button.MaterialButton @@ -70,7 +69,6 @@ android:id="@+id/riskCalculationResultText" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" android:text="Risk calculation result:" /> </LinearLayout> 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 f364e27d4..cee7752cb 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 @@ -65,6 +65,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var onboardingSettings: OnboardingSettings @Inject lateinit var autoCheckOut: AutoCheckOut @Inject lateinit var traceLocationDbCleanupScheduler: TraceLocationDbCleanUpScheduler + @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree @@ -83,8 +84,6 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { compPreview.inject(this) } - BackgroundWorkScheduler.init(component) - Timber.plant(rollingLogHistory) Timber.v("onCreate(): WorkManager setup done: $workManager") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt index 3c0a31697..d61dd1525 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt @@ -16,6 +16,8 @@ interface KeyDownloadConfig { val revokedHourPackages: Collection<RevokedKeyPackage.Hour> + val revokedTraceWarningPackages: Collection<RevokedKeyPackage.TraceWarning> + interface RevokedKeyPackage { val etag: String val region: LocationCode @@ -27,6 +29,8 @@ interface KeyDownloadConfig { interface Hour : Day, RevokedKeyPackage { val hour: LocalTime } + + interface TraceWarning : RevokedKeyPackage } interface Mapper : ConfigMapper<KeyDownloadConfig> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt index f1a5a2671..f9c95b418 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt @@ -26,7 +26,8 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp individualDownloadTimeout = rawParameters.individualTimeout(), overallDownloadTimeout = rawParameters.overAllTimeout(), revokedDayPackages = rawParameters.mapDayEtags(), - revokedHourPackages = rawParameters.mapHourEtags() + revokedHourPackages = rawParameters.mapHourEtags(), + revokedTraceWarningPackages = rawParameters.mapTraceWarningEtags() ) } @@ -81,11 +82,27 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp } } + private fun KeyDownloadParametersAndroid?.mapTraceWarningEtags(): List<RevokedKeyPackage.TraceWarning> { + if (this == null) return emptyList() + + return this.revokedTraceWarningPackagesList.mapNotNull { + if (it.etag == null) { + Timber.e("TraceWarningPackageMeta data had no ETAG: %s", it) + return@mapNotNull null + } + RevokedKeyPackage.TraceWarning( + etag = it.etag, + region = LocationCode("DE"), + ) + } + } + data class KeyDownloadConfigContainer( override val individualDownloadTimeout: Duration, override val overallDownloadTimeout: Duration, override val revokedDayPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Day>, - override val revokedHourPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Hour> + override val revokedHourPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Hour>, + override val revokedTraceWarningPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.TraceWarning> ) : KeyDownloadConfig companion object { @@ -109,4 +126,9 @@ internal sealed class RevokedKeyPackage : KeyDownloadConfig.RevokedKeyPackage { override val day: LocalDate, override val hour: LocalTime ) : RevokedKeyPackage(), KeyDownloadConfig.RevokedKeyPackage.Hour + + data class TraceWarning( + override val etag: String, + override val region: LocationCode + ) : RevokedKeyPackage(), KeyDownloadConfig.RevokedKeyPackage.TraceWarning } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt index 47e84e294..bb81b5205 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt @@ -21,8 +21,8 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskE import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt new file mode 100644 index 000000000..5e48acc14 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.diagnosiskeys.execution + +import androidx.work.BackoffPolicy +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import dagger.Reusable +import de.rki.coronawarnapp.worker.BackgroundConstants +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Reusable +class DiagnosisKeyRetrievalWorkBuilder @Inject constructor() { + + /** + * This has no network constraints, because even if there is no internet, + * the worker+task will trigger diagnosis key submission to the ENF. + * We don't want to prevent that. + */ + fun createPeriodicWorkRequest(): PeriodicWorkRequest = + PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalWorker>( + 60, + TimeUnit.MINUTES + ) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + BackgroundConstants.BACKOFF_INITIAL_DELAY, + TimeUnit.MINUTES + ) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt similarity index 50% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt index f087a031b..a503798ac 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.worker +package de.rki.coronawarnapp.diagnosiskeys.execution import android.content.Context import androidx.work.CoroutineWorker @@ -6,6 +6,7 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest @@ -13,49 +14,44 @@ import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory import timber.log.Timber -/** - * One time diagnosis key retrieval work - * Executes the retrieve diagnosis key transaction - * - * @see BackgroundWorkScheduler - */ -class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( +class DiagnosisKeyRetrievalWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { - override suspend fun doWork(): Result { + override suspend fun doWork(): Result = try { Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") - var result = Result.success() - taskController.submitBlocking( + val taskState = taskController.submitBlocking( DefaultTaskRequest( DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(), - originTag = "DiagnosisKeyRetrievalOneTimeWorker" + originTag = "DiagnosisKeyRetrievalWorker" ) - ).error?.also { error: Throwable -> - Timber.tag(TAG).w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.") + ) - if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.tag(TAG).w(error, "$id: Retry attempts exceeded.") - - return Result.failure() - } else { - Timber.tag(TAG).d(error, "$id: Retrying.") - result = Result.retry() + when { + taskState.isSuccessful -> { + Timber.tag(TAG).d("$id: DownloadDiagnosisKeysTask finished successfully.") + Result.success() + } + else -> { + taskState.error?.let { + Timber.tag(TAG).w(it, "$id: Error during DownloadDiagnosisKeysTask.") + } + Result.retry() } } - - Timber.tag(TAG).d("$id: doWork() finished with %s", result) - return result + } catch (e: Exception) { + e.reportProblem(TAG, "DownloadDiagnosisKeysTask failed exceptionally, will retry.") + Result.retry() } @AssistedFactory - interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalOneTimeWorker> + interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalWorker> companion object { - private val TAG = DiagnosisKeyRetrievalOneTimeWorker::class.java.simpleName + private val TAG = DiagnosisKeyRetrievalWorker::class.java.simpleName } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt index 2a1be8176..2704dfe26 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt @@ -3,23 +3,20 @@ package de.rki.coronawarnapp.eventregistration import dagger.Binds import dagger.Module import de.rki.coronawarnapp.environment.eventregistration.qrcodeposter.QrCodePosterTemplateModule -import de.rki.coronawarnapp.eventregistration.checkins.download.FakeTraceTimeIntervalWarningRepository -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.presencetracing.warning.PresenceTracingWarningModule @Module( includes = [ - QrCodePosterTemplateModule::class + QrCodePosterTemplateModule::class, + PresenceTracingWarningModule::class, ] ) abstract class EventRegistrationModule { @Binds - abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository): - TraceLocationRepository - - @Binds - abstract fun traceTimeIntervalWarningRepository(repository: FakeTraceTimeIntervalWarningRepository): - TraceTimeIntervalWarningRepository + abstract fun traceLocationRepository( + defaultTraceLocationRepo: DefaultTraceLocationRepository + ): TraceLocationRepository } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt index eae4e6abe..1f755923a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt @@ -1,15 +1,15 @@ package de.rki.coronawarnapp.eventregistration.checkins +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocationIdHash import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity import okio.ByteString -import okio.ByteString.Companion.encode import org.joda.time.Instant @Suppress("LongParameterList") data class CheckIn( val id: Long = 0L, - val traceLocationId: ByteString = "TODO: calculate".encode(), - val traceLocationIdHash: ByteString = "TODO: calculate".encode(), + val traceLocationId: TraceLocationId, val version: Int, val type: Int, val description: String, @@ -24,13 +24,18 @@ data class CheckIn( val completed: Boolean, val createJournalEntry: Boolean ) { - // val locationGuidHash: com.google.protobuf.ByteString by lazy { copyFromUtf8(guid.toSHA256()) } + /** + * Returns SHA-256 hash of [traceLocationId] which itself may also be SHA-256 hash. + * For privacy reasons TraceTimeIntervalWarnings only offer a hash of the actual locationId. + * + * @see [de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation] + */ + val traceLocationIdHash by lazy { traceLocationId.toTraceLocationIdHash() } } fun CheckIn.toEntity() = TraceLocationCheckInEntity( id = id, traceLocationIdBase64 = traceLocationId.base64(), - traceLocationIdHashBase64 = traceLocationIdHash.base64(), version = version, type = type, description = description, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt deleted file mode 100644 index be16290c8..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.download - -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning - -interface TraceTimeIntervalWarningPackage { - - /** - * Hides the file reading - */ - suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> - - /** - * Numeric identifier representing the hour since epoch, used in the Api endpoint - */ - val warningPackageId: String -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt deleted file mode 100644 index 70a8d6332..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt +++ /dev/null @@ -1,59 +0,0 @@ -package de.rki.coronawarnapp.eventregistration.checkins.download - -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import org.joda.time.Duration -import org.joda.time.Instant -import javax.inject.Inject - -interface TraceTimeIntervalWarningRepository { - - val allWarningPackages: Flow<List<TraceTimeIntervalWarningPackage>> - - fun addWarningPackages(list: List<TraceTimeIntervalWarningPackage>) - - fun removeWarningPackages(list: List<TraceTimeIntervalWarningPackage>) -} - -// proprietary dummy implementations -class FakeTraceTimeIntervalWarningRepository @Inject constructor() : TraceTimeIntervalWarningRepository { - override val allWarningPackages: Flow<List<TraceTimeIntervalWarningPackage>> - get() = listOf(listOf<TraceTimeIntervalWarningPackage>(DummyCheckInPackage)).asFlow() - - override fun addWarningPackages(list: List<TraceTimeIntervalWarningPackage>) { - // TODO("Not yet implemented") - } - - override fun removeWarningPackages(list: List<TraceTimeIntervalWarningPackage>) { - // TODO("Not yet implemented") - } -} - -object DummyCheckInPackage : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return warnings - } - - override val warningPackageId: String - get() = "id" -} - -val warnings = (1L..1000L).map { - createWarning( - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) -} - -fun createWarning( - startIntervalDateStr: String, - period: Int, - transmissionRiskLevel: Int -) = TraceWarning.TraceTimeIntervalWarning.newBuilder() - // .locationIdHash = TODO: set location Id hash - .setPeriod(period) - .setStartIntervalNumber((Duration(Instant.parse(startIntervalDateStr).millis).standardMinutes / 10).toInt()) - .setTransmissionRiskLevel(transmissionRiskLevel) - .build() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt index 3bdc275bc..bccfcf4b1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt @@ -41,7 +41,7 @@ data class TraceLocation( * The ID is the byte representation of SHA-256 hash. */ @IgnoredOnParcel - val locationId: ByteString by lazy { + val locationId: TraceLocationId by lazy { val cwaDomain = CWA_GUID.toByteArray() val payloadBytes = qrCodePayload().toByteArray() val totalByteSequence = cwaDomain + payloadBytes @@ -49,12 +49,13 @@ data class TraceLocation( } /** - * Returns SHA-256 hash of [locationId] which itself is SHA-256 hash + * Returns SHA-256 hash of [locationId] which itself may also be SHA-256 hash. + * For privacy reasons TraceTimeIntervalWarnings only offer a hash of the actual locationId. + * + * @see [de.rki.coronawarnapp.eventregistration.checkins.CheckIn] */ @IgnoredOnParcel - val locationIdHash: ByteString by lazy { - locationId.sha256() - } + val locationIdHash: ByteString by lazy { locationId.toTraceLocationIdHash() } fun isBeforeStartTime(now: Instant): Boolean = startDate?.isAfter(now) ?: false @@ -71,6 +72,10 @@ data class TraceLocation( } } +typealias TraceLocationId = ByteString + +fun TraceLocationId.toTraceLocationIdHash() = sha256() + fun List<TraceLocationEntity>.toTraceLocations() = this.map { it.toTraceLocation() } fun TraceLocationEntity.toTraceLocation() = TraceLocation( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt index 2df991c63..2616685c8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt @@ -11,7 +11,6 @@ import org.joda.time.Instant data class TraceLocationCheckInEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0L, @ColumnInfo(name = "traceLocationIdBase64") val traceLocationIdBase64: String, - @ColumnInfo(name = "traceLocationIdHashBase64") val traceLocationIdHashBase64: String, @ColumnInfo(name = "version") val version: Int, @ColumnInfo(name = "type") val type: Int, @ColumnInfo(name = "description") val description: String, @@ -30,7 +29,6 @@ data class TraceLocationCheckInEntity( fun TraceLocationCheckInEntity.toCheckIn() = CheckIn( id = id, traceLocationId = traceLocationIdBase64.decodeBase64()!!, - traceLocationIdHash = traceLocationIdHashBase64.decodeBase64()!!, version = version, type = type, description = description, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt index cce89d650..57791940b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt @@ -10,11 +10,12 @@ import kotlin.random.Random @Singleton class BackgroundNoise @Inject constructor( private val submissionSettings: SubmissionSettings, - private val playbook: Playbook + private val playbook: Playbook, + private val backgroundWorkScheduler: BackgroundWorkScheduler ) { fun scheduleDummyPattern() { if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0) - BackgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork() + backgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork() } suspend fun foregroundScheduleCheck() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt index 0a169254e..419cc7637 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt @@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat import dagger.Reusable import de.rki.coronawarnapp.R import de.rki.coronawarnapp.notification.NotificationConstants -import de.rki.coronawarnapp.presencetracing.common.TraceLocationNotifications +import de.rki.coronawarnapp.presencetracing.common.PresenceTracingNotifications import de.rki.coronawarnapp.ui.launcher.LauncherActivity import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppContext @@ -19,7 +19,7 @@ import javax.inject.Inject class CheckOutNotification @Inject constructor( @AppContext private val context: Context, private val foregroundState: ForegroundState, - private val notificationHelper: TraceLocationNotifications, + private val notificationHelper: PresenceTracingNotifications, private val deepLinkBuilderFactory: NavDeepLinkBuilderFactory, ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt similarity index 96% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt index 349096d2d..7fd3c1f25 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt @@ -19,18 +19,18 @@ import timber.log.Timber import javax.inject.Inject /** - * Helper to send notifications on the notification channel for trace location related events. + * Helper to send notifications on the notification channel for presence tracing related events. * * Also see **[de.rki.coronawarnapp.notification.GeneralNotifications]** */ @Reusable -class TraceLocationNotifications @Inject constructor( +class PresenceTracingNotifications @Inject constructor( @AppContext private val context: Context, private val apiLevel: ApiLevel, private val notificationManagerCompat: NotificationManagerCompat, ) { - private val channelId = "${context.packageName}.notification.traceLocationChannelId" + private val channelId = "${context.packageName}.notification.presenceTracingChannelId" private var isNotificationChannelSetup = false @TargetApi(Build.VERSION_CODES.O) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt deleted file mode 100644 index 66f82291f..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt +++ /dev/null @@ -1,156 +0,0 @@ -package de.rki.coronawarnapp.presencetracing.risk - -import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.eventregistration.checkins.CheckIn -import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository -import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.withContext -import okio.ByteString.Companion.toByteString -import org.joda.time.Instant -import timber.log.Timber -import java.lang.reflect.Modifier.PRIVATE -import javax.inject.Inject -import kotlin.coroutines.CoroutineContext - -class CheckInWarningMatcher @Inject constructor( - private val checkInsRepository: CheckInRepository, - private val traceTimeIntervalWarningRepository: TraceTimeIntervalWarningRepository, - private val presenceTracingRiskRepository: PresenceTracingRiskRepository, - private val dispatcherProvider: DispatcherProvider -) { - suspend fun execute(): List<CheckInWarningOverlap> { - - presenceTracingRiskRepository.deleteStaleData() - - val checkIns = checkInsRepository.allCheckIns.firstOrNull() - if (checkIns.isNullOrEmpty()) { - Timber.i("No check-ins available. Deleting all matches.") - presenceTracingRiskRepository.deleteAllMatches() - presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) - return emptyList() - } - - val warningPackages = traceTimeIntervalWarningRepository.allWarningPackages.firstOrNull() - - if (warningPackages.isNullOrEmpty()) { - // nothing to be done here - return emptyList() - } - - val splitCheckIns = checkIns.flatMap { it.splitByMidnightUTC() } - - val matchLists = createMatchingLaunchers( - splitCheckIns, - warningPackages, - dispatcherProvider.IO - ) - .awaitAll() - - if (matchLists.contains(null)) { - Timber.e("Error occurred during matching. Abort calculation.") - presenceTracingRiskRepository.reportFailedCalculation() - return emptyList() - } - - // delete stale matches from new packages and mark packages as processed - warningPackages.forEach { - presenceTracingRiskRepository.deleteMatchesOfPackage(it.warningPackageId) - presenceTracingRiskRepository.markPackageProcessed(it.warningPackageId) - } - val matches = matchLists.filterNotNull().flatten() - - presenceTracingRiskRepository.reportSuccessfulCalculation(matches) - - return matches - } -} - -@VisibleForTesting(otherwise = PRIVATE) -internal suspend fun createMatchingLaunchers( - checkIns: List<CheckIn>, - warningPackages: List<TraceTimeIntervalWarningPackage>, - coroutineContext: CoroutineContext -): Collection<Deferred<List<CheckInWarningOverlap>?>> { - - val launcher: CoroutineScope.( - List<CheckIn>, - List<TraceTimeIntervalWarningPackage> - ) -> Deferred<List<CheckInWarningOverlap>?> = - { list, packageChunk -> - async { - try { - packageChunk.flatMap { - findMatches(list, it) - } - } catch (e: Throwable) { - Timber.e("Failed to process packages $packageChunk") - null - } - } - } - - // at most 4 parallel processes - val chunkSize = (checkIns.size / 4) + 1 - - return warningPackages.chunked(chunkSize).map { packageChunk -> - withContext(context = coroutineContext) { - launcher(checkIns, packageChunk) - } - } -} - -@VisibleForTesting(otherwise = PRIVATE) -internal suspend fun findMatches( - checkIns: List<CheckIn>, - warningPackage: TraceTimeIntervalWarningPackage -): List<CheckInWarningOverlap> { - return warningPackage - .extractTraceTimeIntervalWarnings() - .flatMap { warning -> - checkIns - .mapNotNull { checkIn -> - checkIn.calculateOverlap(warning, warningPackage.warningPackageId).also { overlap -> - if (overlap == null) { - Timber.d("No match/overlap found for $checkIn and $warning") - } else { - Timber.i("Overlap found $overlap") - } - } - } - } -} - -@VisibleForTesting(otherwise = PRIVATE) -internal fun CheckIn.calculateOverlap( - warning: TraceWarning.TraceTimeIntervalWarning, - traceWarningPackageId: String -): CheckInWarningOverlap? { - - if (warning.locationIdHash.toByteArray().toByteString() != traceLocationIdHash) return null - - val warningStartMillis = warning.startIntervalNumber.tenMinIntervalToMillis() - val warningEndMillis = (warning.startIntervalNumber + warning.period).tenMinIntervalToMillis() - - val overlapStartMillis = kotlin.math.max(checkInStart.millis, warningStartMillis) - val overlapEndMillis = kotlin.math.min(checkInEnd.millis, warningEndMillis) - val overlapMillis = overlapEndMillis - overlapStartMillis - - if (overlapMillis <= 0) return null - - return CheckInWarningOverlap( - checkInId = id, - transmissionRiskLevel = warning.transmissionRiskLevel, - traceWarningPackageId = traceWarningPackageId, - startTime = Instant.ofEpochMilli(overlapStartMillis), - endTime = Instant.ofEpochMilli(overlapEndMillis) - ) -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt deleted file mode 100644 index 0d0f7add0..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt +++ /dev/null @@ -1,224 +0,0 @@ -package de.rki.coronawarnapp.presencetracing.risk - -import androidx.room.ColumnInfo -import androidx.room.Dao -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Insert -import androidx.room.OnConflictStrategy.REPLACE -import androidx.room.PrimaryKey -import androidx.room.Query -import androidx.room.TypeConverter -import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity -import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc -import de.rki.coronawarnapp.util.TimeStamper -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import org.joda.time.Days -import org.joda.time.Instant -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PresenceTracingRiskRepository @Inject constructor( - private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, - private val databaseFactory: PresenceTracingRiskDatabase.Factory, - private val timeStamper: TimeStamper -) { - - private val database by lazy { - databaseFactory.create() - } - - private val traceTimeIntervalMatchDao by lazy { - database.traceTimeIntervalMatchDao() - } - - private val riskLevelResultDao by lazy { - database.presenceTracingRiskLevelResultDao() - } - - private val allMatches = traceTimeIntervalMatchDao.allMatches().map { list -> - list.map { - it.toModel() - } - } - - private val normalizedTime = allMatches.map { - presenceTracingRiskCalculator.calculateNormalizedTime(it) - } - - private val fifteenDaysAgo: Instant - get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) - - val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = - normalizedTime.map { - presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) - } - - val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = - normalizedTime.map { - presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) - } - - internal suspend fun reportSuccessfulCalculation(list: List<CheckInWarningOverlap>) { - traceTimeIntervalMatchDao.insert(list.map { it.toEntity() }) - val last14days = normalizedTime.first().filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } - val risk = presenceTracingRiskCalculator.calculateTotalRisk(last14days) - add( - PtRiskLevelResult( - timeStamper.nowUTC, - risk - ) - ) - } - - internal suspend fun deleteStaleData() { - traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis) - riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis) - } - - internal suspend fun markPackageProcessed(warningPackageId: String) { - // TODO - } - - internal suspend fun deleteMatchesOfPackage(warningPackageId: String) { - traceTimeIntervalMatchDao.deleteMatchesForPackage(warningPackageId) - } - - suspend fun deleteAllMatches() { - traceTimeIntervalMatchDao.deleteAll() - } - - fun latestAndLastSuccessful() = riskLevelResultDao.latestAndLastSuccessful().map { list -> - list.map { entity -> - entity.toModel() - } - } - - fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> - list.map { entity -> - entity.toModel() - } - } - - fun add(riskLevelResult: PtRiskLevelResult) { - riskLevelResultDao.insert(riskLevelResult.toEntity()) - } - - fun reportFailedCalculation() { - add( - PtRiskLevelResult( - timeStamper.nowUTC, - RiskState.CALCULATION_FAILED - ) - ) - } -} - -@Dao -interface TraceTimeIntervalMatchDao { - - @Query("SELECT * FROM TraceTimeIntervalMatchEntity") - fun allMatches(): Flow<List<TraceTimeIntervalMatchEntity>> - - @Query("DELETE FROM TraceTimeIntervalMatchEntity") - suspend fun deleteAll() - - @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE endTimeMillis < :endTimeMillis") - suspend fun deleteOlderThan(endTimeMillis: Long) - - @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE traceWarningPackageId = :warningPackageId") - suspend fun deleteMatchesForPackage(warningPackageId: String) - - @Insert - suspend fun insert(entities: List<TraceTimeIntervalMatchEntity>) -} - -@Entity -data class TraceTimeIntervalMatchEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long? = null, - @ForeignKey( - entity = TraceLocationCheckInEntity::class, - parentColumns = ["id"], - childColumns = ["checkInId"], - onDelete = ForeignKey.CASCADE - ) - @ColumnInfo(name = "checkInId") val checkInId: Long, - @ColumnInfo(name = "traceWarningPackageId") val traceWarningPackageId: String, - @ColumnInfo(name = "transmissionRiskLevel") val transmissionRiskLevel: Int, - @ColumnInfo(name = "startTimeMillis") val startTimeMillis: Long, - @ColumnInfo(name = "endTimeMillis") val endTimeMillis: Long -) - -private fun CheckInWarningOverlap.toEntity() = TraceTimeIntervalMatchEntity( - checkInId = checkInId, - traceWarningPackageId = traceWarningPackageId, - transmissionRiskLevel = transmissionRiskLevel, - startTimeMillis = startTime.millis, - endTimeMillis = endTime.millis -) - -private fun TraceTimeIntervalMatchEntity.toModel() = CheckInWarningOverlap( - checkInId = checkInId, - traceWarningPackageId = traceWarningPackageId, - transmissionRiskLevel = transmissionRiskLevel, - startTime = Instant.ofEpochMilli(startTimeMillis), - endTime = Instant.ofEpochMilli(endTimeMillis) -) - -@Suppress("MaxLineLength") -@Dao -interface PresenceTracingRiskLevelResultDao { - @Query("SELECT * FROM (SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT 1) UNION ALL SELECT * FROM (SELECT * FROM PresenceTracingRiskLevelResultEntity where riskStateCode is not 0 ORDER BY calculatedAtMillis DESC LIMIT 1)") - fun latestAndLastSuccessful(): Flow<List<PresenceTracingRiskLevelResultEntity>> - - @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT :limit") - fun latestEntries(limit: Int): Flow<List<PresenceTracingRiskLevelResultEntity>> - - @Insert(onConflict = REPLACE) - fun insert(entity: PresenceTracingRiskLevelResultEntity) - - @Query("DELETE FROM PresenceTracingRiskLevelResultEntity WHERE calculatedAtMillis < :calculatedAtMillis") - suspend fun deleteOlderThan(calculatedAtMillis: Long) -} - -@Entity -data class PresenceTracingRiskLevelResultEntity( - @PrimaryKey @ColumnInfo(name = "calculatedAtMillis") val calculatedAtMillis: Long, - @ColumnInfo(name = "riskStateCode")val riskState: RiskState -) - -private fun PresenceTracingRiskLevelResultEntity.toModel() = PtRiskLevelResult( - calculatedAt = Instant.ofEpochMilli((calculatedAtMillis)), - riskState = riskState -) - -private fun PtRiskLevelResult.toEntity() = PresenceTracingRiskLevelResultEntity( - calculatedAtMillis = calculatedAt.millis, - riskState = riskState -) - -class RiskStateConverter { - @TypeConverter - fun toRiskStateCode(value: Int?): RiskState? = value?.toRiskState() - - @TypeConverter - fun fromRiskStateCode(code: RiskState?): Int? = code?.toCode() - - private fun RiskState.toCode() = when (this) { - RiskState.CALCULATION_FAILED -> 0 - RiskState.LOW_RISK -> 1 - RiskState.INCREASED_RISK -> 2 - } - - private fun Int.toRiskState() = when (this) { - 0 -> RiskState.CALCULATION_FAILED - 1 -> RiskState.LOW_RISK - 2 -> RiskState.INCREASED_RISK - else -> null - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt index 1fa338aab..62b25b760 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.presencetracing.risk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk import de.rki.coronawarnapp.risk.RiskState import org.joda.time.Instant import org.joda.time.LocalDate @@ -7,6 +8,7 @@ import org.joda.time.LocalDate data class PtRiskLevelResult( val calculatedAt: Instant, val riskState: RiskState, + // only available for the last calculation if successful, otherwise null val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt similarity index 62% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt index a43306755..a9d2d3d01 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt @@ -1,5 +1,6 @@ -package de.rki.coronawarnapp.risk +package de.rki.coronawarnapp.presencetracing.risk +import de.rki.coronawarnapp.risk.RiskState import org.joda.time.LocalDate interface TraceLocationCheckInRisk { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt new file mode 100644 index 000000000..8c318df97 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt @@ -0,0 +1,143 @@ +package de.rki.coronawarnapp.presencetracing.risk.calculation + +import androidx.annotation.VisibleForTesting +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.toOkioByteString +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import org.joda.time.Instant +import timber.log.Timber +import java.lang.reflect.Modifier.PRIVATE +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +class CheckInWarningMatcher @Inject constructor( + private val dispatcherProvider: DispatcherProvider +) { + suspend fun process( + checkIns: List<CheckIn>, + warningPackages: List<TraceWarningPackage> + ): Result { + val splitCheckIns = checkIns.flatMap { it.splitByMidnightUTC() } + + val matchLists: List<List<MatchesPerPackage>?> = runMatchingLaunchers( + splitCheckIns, + warningPackages, + dispatcherProvider.IO + ) + + val successful = if (matchLists.contains(null)) { + Timber.e("Calculation partially failed.") + false + } else { + Timber.d("Matching was successful.") + true + } + + return Result( + successful = successful, + processedPackages = matchLists.filterNotNull().flatten() + ) + } + + data class Result( + val successful: Boolean, + val processedPackages: Collection<MatchesPerPackage> = emptyList(), + ) + + @VisibleForTesting(otherwise = PRIVATE) + internal suspend fun runMatchingLaunchers( + checkIns: List<CheckIn>, + warningPackages: List<TraceWarningPackage>, + coroutineContext: CoroutineContext + ): List<List<MatchesPerPackage>?> { + + val launcher: CoroutineScope.( + List<CheckIn>, + List<TraceWarningPackage> + ) -> Deferred<List<MatchesPerPackage>?> = { list, packageChunk -> + async { + try { + packageChunk.map { + val overlaps = findMatches(list, it) + Timber.d("%d overlaps for %s", overlaps.size, it.packageId) + MatchesPerPackage(warningPackage = it, overlaps = overlaps) + } + } catch (e: Throwable) { + Timber.e(e, "Failed to process packages $packageChunk") + null + } + } + } + + // at most 4 parallel processes + val chunkSize = (checkIns.size / 4) + 1 + + return warningPackages.chunked(chunkSize).map { packageChunk -> + withContext(context = coroutineContext) { + launcher(checkIns, packageChunk) + } + }.awaitAll() + } + + data class MatchesPerPackage( + val warningPackage: TraceWarningPackage, + val overlaps: List<CheckInWarningOverlap>, + ) +} + +@VisibleForTesting(otherwise = PRIVATE) +internal suspend fun findMatches( + checkIns: List<CheckIn>, + warningPackage: TraceWarningPackage +): List<CheckInWarningOverlap> { + return warningPackage + .extractWarnings() + .flatMap { warning -> + checkIns + .mapNotNull { checkIn -> + checkIn.calculateOverlap(warning, warningPackage.packageId).also { overlap -> + if (overlap == null) { + Timber.v("No match found for $checkIn and $warning") + } else { + Timber.w("Overlap was found $overlap") + } + } + } + } +} + +@VisibleForTesting(otherwise = PRIVATE) +internal fun CheckIn.calculateOverlap( + warning: TraceWarning.TraceTimeIntervalWarning, + traceWarningPackageId: String +): CheckInWarningOverlap? { + if (warning.locationIdHash.toOkioByteString() != traceLocationIdHash) return null + + val warningStartMillis = warning.startIntervalNumber.tenMinIntervalToMillis() + val warningEndMillis = (warning.startIntervalNumber + warning.period).tenMinIntervalToMillis() + + val overlapStartMillis = kotlin.math.max(checkInStart.millis, warningStartMillis) + val overlapEndMillis = kotlin.math.min(checkInEnd.millis, warningEndMillis) + val overlapMillis = overlapEndMillis - overlapStartMillis + + if (overlapMillis <= 0) { + Timber.i("No overlap (%dms) with match %s (%s)", overlapMillis, description, traceLocationIdHash) + return null + } + + return CheckInWarningOverlap( + checkInId = id, + transmissionRiskLevel = warning.transmissionRiskLevel, + traceWarningPackageId = traceWarningPackageId, + startTime = Instant.ofEpochMilli(overlapStartMillis), + endTime = Instant.ofEpochMilli(overlapEndMillis) + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt similarity index 73% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt index 1d5c98667..4e529779d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt @@ -1,12 +1,11 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel +import java.util.concurrent.TimeUnit // converts number of 10min intervals into milliseconds -internal fun Int.tenMinIntervalToMillis() = this * MILLIS_IN_MIN - -private const val MILLIS_IN_MIN = 600L * 1000L +internal fun Int.tenMinIntervalToMillis() = this * TimeUnit.MINUTES.toMillis(10L) fun RiskLevel.mapToRiskState(): RiskState { return when (this) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt similarity index 97% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt index 1c2bd002f..568273ff1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.risk.RiskState import javax.inject.Inject diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt similarity index 97% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt index 6558491ca..1cebd0aec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt similarity index 90% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt index 91591e74a..4fc9b892d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt @@ -1,7 +1,7 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.DateTimeConstants import org.joda.time.Duration diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt new file mode 100644 index 000000000..e0d9556af --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt @@ -0,0 +1,158 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.task.Task +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 kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.firstOrNull +import org.joda.time.Duration +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Provider + +class PresenceTracingWarningTask @Inject constructor( + private val timeStamper: TimeStamper, + private val syncTool: TraceWarningPackageSyncTool, + private val checkInWarningMatcher: CheckInWarningMatcher, + private val presenceTracingRiskRepository: PresenceTracingRiskRepository, + private val traceWarningRepository: TraceWarningRepository, + private val checkInsRepository: CheckInRepository, +) : Task<DefaultProgress, PresenceTracingWarningTask.Result> { + + private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() + override val progress: Flow<DefaultProgress> = internalProgress.asFlow() + + private var isCanceled = false + + override suspend fun run(arguments: Task.Arguments): Result = try { + Timber.d("Running with arguments=%s", arguments) + + try { + doWork() + } catch (e: Exception) { + // We need to reported a failed calculation to update the risk card state + presenceTracingRiskRepository.reportCalculation(successful = false) + throw e + } + } catch (error: Exception) { + Timber.tag(TAG).e(error) + error.report(ExceptionCategory.EXPOSURENOTIFICATION) + throw error + } finally { + Timber.i("Finished (isCanceled=$isCanceled).") + internalProgress.close() + } + + private suspend fun doWork(): Result { + val nowUTC = timeStamper.nowUTC + + Timber.tag(TAG).d("Running package sync.") + syncTool.syncPackages() + + checkCancel() + + presenceTracingRiskRepository.deleteStaleData() + + val checkIns = checkInsRepository.allCheckIns.firstOrNull() ?: emptyList() + Timber.tag(TAG).d("There are %d check-ins to match against.", checkIns.size) + + if (checkIns.isEmpty()) { + Timber.tag(TAG).i("No check-ins available. Deleting all matches.") + presenceTracingRiskRepository.deleteAllMatches() + + presenceTracingRiskRepository.reportCalculation(successful = true) + + return Result(calculatedAt = nowUTC) + } + + val unprocessedPackages = traceWarningRepository.unprocessedWarningPackages.firstOrNull() ?: emptyList() + Timber.tag(TAG).d("There are %d unprocessed warning packages.", unprocessedPackages.size) + + if (unprocessedPackages.isEmpty()) { + Timber.tag(TAG).i("No new warning packages available.") + + presenceTracingRiskRepository.reportCalculation(successful = true) + + return Result(calculatedAt = nowUTC) + } + + Timber.tag(TAG).d("Running check-in matcher.") + val matcherResult = checkInWarningMatcher.process( + checkIns = checkIns, + warningPackages = unprocessedPackages, + ) + Timber.tag(TAG).i("Check-in matcher result: %s", matcherResult) + + val overlaps = matcherResult.processedPackages.flatMap { it.overlaps } + val overlapsDistinct = overlaps.distinct() + if (overlaps.size != overlapsDistinct.size) { + IllegalArgumentException("Matched overlaps are not distinct").also { + it.reportProblem(TAG, "CheckInWarningMatcher results are not distinct.") + } + } + + // Partial processing: if calculation was not successful, but some packages were processed, we still save them + presenceTracingRiskRepository.reportCalculation( + successful = matcherResult.successful, + overlaps = overlapsDistinct, + ) + + // markPackagesProcessed only after reportCalculation, if there is an exception, then we can process again. + traceWarningRepository.markPackagesProcessed( + matcherResult.processedPackages.map { it.warningPackage.packageId } + ) + + return Result(calculatedAt = nowUTC) + } + + private fun checkCancel() { + if (isCanceled) throw TaskCancellationException() + } + + override suspend fun cancel() { + Timber.w("cancel() called.") + isCanceled = true + } + + data class Result( + val calculatedAt: Instant + ) : Task.Result + + data class Config( + override val executionTimeout: Duration = Duration.standardMinutes(9), + override val collisionBehavior: TaskFactory.Config.CollisionBehavior = + TaskFactory.Config.CollisionBehavior.SKIP_IF_SIBLING_RUNNING + ) : TaskFactory.Config + + class Factory @Inject constructor( + private val taskByDagger: Provider<PresenceTracingWarningTask>, + private val appConfigProvider: AppConfigProvider + ) : TaskFactory<DefaultProgress, Task.Result> { + + override suspend fun createConfig(): TaskFactory.Config = Config( + executionTimeout = appConfigProvider.getAppConfig().overallDownloadTimeout + ) + + override val taskProvider: () -> Task<DefaultProgress, Task.Result> = { + taskByDagger.get() + } + } + + companion object { + private const val TAG = "TracingWarningTask" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt new file mode 100644 index 000000000..1479650a5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import androidx.work.BackoffPolicy +import androidx.work.Constraints +import androidx.work.NetworkType +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import dagger.Reusable +import de.rki.coronawarnapp.worker.BackgroundConstants +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Reusable +class PresenceTracingWarningWorkBuilder @Inject constructor() { + + fun createPeriodicWorkRequest(): PeriodicWorkRequest = + PeriodicWorkRequestBuilder<PresenceTracingWarningWorker>( + 60, + TimeUnit.MINUTES + ) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .setBackoffCriteria( + BackoffPolicy.EXPONENTIAL, + BackgroundConstants.BACKOFF_INITIAL_DELAY, + TimeUnit.MINUTES + ) + .setConstraints(buildConstraints()) + .build() + + private fun buildConstraints() = + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt new file mode 100644 index 000000000..300cab25a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt @@ -0,0 +1,52 @@ +package de.rki.coronawarnapp.presencetracing.risk.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.bugreporting.reportProblem +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking +import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory +import timber.log.Timber + +class PresenceTracingWarningWorker @AssistedInject constructor( + @Assisted val context: Context, + @Assisted workerParams: WorkerParameters, + private val taskController: TaskController +) : CoroutineWorker(context, workerParams) { + + override suspend fun doWork(): Result = try { + Timber.tag(TAG).v("$id: doWork() started. Run attempt: $runAttemptCount") + + val taskState = taskController.submitBlocking( + DefaultTaskRequest(PresenceTracingWarningTask::class, originTag = TAG) + ) + + when { + taskState.isSuccessful -> { + Timber.tag(TAG).d("$id: PresenceTracingWarningTask finished successfully.") + Result.success() + } + else -> { + taskState.error?.let { + Timber.tag(TAG).w(it, "$id: Error during PresenceTracingWarningTask.") + } + Result.retry() + } + } + } catch (e: Exception) { + e.reportProblem(TAG, "PresenceTracingWarningTask failed exceptionally, will retry.") + Result.retry() + } + + @AssistedFactory + interface Factory : InjectedWorkerFactory<PresenceTracingWarningWorker> + + companion object { + private val TAG = PresenceTracingWarningWorker::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt similarity index 85% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt index 2d39bd5ea..b80b28fbf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.storage import android.content.Context import androidx.room.Database @@ -27,6 +27,8 @@ abstract class PresenceTracingRiskDatabase : RoomDatabase() { .databaseBuilder(context, PresenceTracingRiskDatabase::class.java, DATABASE_NAME) .build() } -} -private const val DATABASE_NAME = "PresenceTracingRisk_db" + companion object { + private const val DATABASE_NAME = "PresenceTracingRisk_db" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt new file mode 100644 index 000000000..daf5c60bc --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt @@ -0,0 +1,283 @@ +package de.rki.coronawarnapp.presencetracing.risk.storage + +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Insert +import androidx.room.OnConflictStrategy.REPLACE +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.TypeConverter +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningOverlap +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import org.joda.time.Days +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class PresenceTracingRiskRepository @Inject constructor( + private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, + private val databaseFactory: PresenceTracingRiskDatabase.Factory, + private val timeStamper: TimeStamper, +) { + + private val database by lazy { + databaseFactory.create() + } + + private val traceTimeIntervalMatchDao by lazy { + database.traceTimeIntervalMatchDao() + } + + private val riskLevelResultDao by lazy { + database.presenceTracingRiskLevelResultDao() + } + + private val matchesOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches() + .map { timeIntervalMatchEntities -> + timeIntervalMatchEntities + .map { it.toCheckInWarningOverlap() } + .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } + } + + val checkInWarningOverlaps: Flow<List<CheckInWarningOverlap>> = + traceTimeIntervalMatchDao.allMatches().map { matchEntities -> + matchEntities.map { + it.toCheckInWarningOverlap() + } + } + + private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.map { + presenceTracingRiskCalculator.calculateNormalizedTime(it) + } + + private val fifteenDaysAgo: Instant + get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) + + val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = + normalizedTimeOfLast14DaysPlusToday.map { + presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) + } + + val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = + normalizedTimeOfLast14DaysPlusToday.map { + presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) + } + + /** + * We delete warning packages after processing, we need to store the latest matches independent of success state + * For a future update we should look into partial processing. + */ + internal suspend fun reportCalculation( + successful: Boolean, + overlaps: List<CheckInWarningOverlap> = emptyList() + ) { + Timber.v("reportCalculation(successful=%b, overlaps=%s)", successful, overlaps) + + val nowUTC = timeStamper.nowUTC + + // delete stale matches from new packages, old matches are superseeded + overlaps.map { it.traceWarningPackageId }.forEach { + traceTimeIntervalMatchDao.deleteMatchesForPackage(it) + } + + if (overlaps.isNotEmpty()) { + traceTimeIntervalMatchDao.insert(overlaps.map { it.toTraceTimeIntervalMatchEntity() }) + } + + val result = if (successful) { + val last14daysPlusToday = normalizedTimeOfLast14DaysPlusToday.first() + val risk = presenceTracingRiskCalculator.calculateTotalRisk(last14daysPlusToday) + PtRiskLevelResult(nowUTC, risk) + } else { + PtRiskLevelResult(nowUTC, RiskState.CALCULATION_FAILED) + } + addResult(result) + } + + internal suspend fun deleteStaleData() { + Timber.d("deleteStaleData()") + traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis) + riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis) + } + + suspend fun deleteAllMatches() { + Timber.d("deleteAllMatches()") + traceTimeIntervalMatchDao.deleteAll() + } + + fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> + var lastSuccessfulFound = false + list.sortedByDescending { + it.calculatedAtMillis + } + .map { entity -> + if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { + lastSuccessfulFound = true + // add risk per day to the last successful result + entity.toCheckInWarningOverlap(presenceTracingDayRisk.first()) + } else { + entity.toCheckInWarningOverlap(null) + } + } + } + + fun allEntries() = riskLevelResultDao.allEntries().map { list -> + var lastSuccessfulFound = false + list.sortedByDescending { + it.calculatedAtMillis + } + .map { entity -> + if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { + lastSuccessfulFound = true + // add risk per day to the last successful result + entity.toCheckInWarningOverlap(presenceTracingDayRisk.first()) + } else { + entity.toCheckInWarningOverlap(null) + } + } + } + + private fun addResult(result: PtRiskLevelResult) { + Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.") + riskLevelResultDao.insert(result.toTraceTimeIntervalMatchEntity()) + } + + suspend fun clearAllTables() { + traceTimeIntervalMatchDao.deleteAll() + riskLevelResultDao.deleteAll() + } +} + +/* +* Stores matches from the last successful execution +* */ +@Dao +interface TraceTimeIntervalMatchDao { + + @Query("SELECT * FROM TraceTimeIntervalMatchEntity") + fun allMatches(): Flow<List<TraceTimeIntervalMatchEntity>> + + @Query("DELETE FROM TraceTimeIntervalMatchEntity") + suspend fun deleteAll() + + @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE endTimeMillis < :endTimeMillis") + suspend fun deleteOlderThan(endTimeMillis: Long) + + @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE traceWarningPackageId = :warningPackageId") + suspend fun deleteMatchesForPackage(warningPackageId: String) + + @Insert + suspend fun insert(entities: List<TraceTimeIntervalMatchEntity>) +} + +@Entity +data class TraceTimeIntervalMatchEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long? = null, + @ForeignKey( + entity = TraceLocationCheckInEntity::class, + parentColumns = ["id"], + childColumns = ["checkInId"], + onDelete = ForeignKey.CASCADE + ) + @ColumnInfo(name = "checkInId") val checkInId: Long, + @ColumnInfo(name = "traceWarningPackageId") val traceWarningPackageId: String, + @ColumnInfo(name = "transmissionRiskLevel") val transmissionRiskLevel: Int, + @ColumnInfo(name = "startTimeMillis") val startTimeMillis: Long, + @ColumnInfo(name = "endTimeMillis") val endTimeMillis: Long +) + +internal fun CheckInWarningOverlap.toTraceTimeIntervalMatchEntity() = TraceTimeIntervalMatchEntity( + checkInId = checkInId, + traceWarningPackageId = traceWarningPackageId, + transmissionRiskLevel = transmissionRiskLevel, + startTimeMillis = startTime.millis, + endTimeMillis = endTime.millis +) + +internal fun TraceTimeIntervalMatchEntity.toCheckInWarningOverlap() = CheckInWarningOverlap( + checkInId = checkInId, + traceWarningPackageId = traceWarningPackageId, + transmissionRiskLevel = transmissionRiskLevel, + startTime = Instant.ofEpochMilli(startTimeMillis), + endTime = Instant.ofEpochMilli(endTimeMillis) +) + +@Suppress("MaxLineLength") +@Dao +interface PresenceTracingRiskLevelResultDao { + + @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT :limit") + fun latestEntries(limit: Int): Flow<List<PresenceTracingRiskLevelResultEntity>> + + @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity") + fun allEntries(): Flow<List<PresenceTracingRiskLevelResultEntity>> + + @Insert(onConflict = REPLACE) + fun insert(entity: PresenceTracingRiskLevelResultEntity) + + @Query("DELETE FROM PresenceTracingRiskLevelResultEntity WHERE calculatedAtMillis < :calculatedAtMillis") + suspend fun deleteOlderThan(calculatedAtMillis: Long) + + @Query("DELETE FROM PresenceTracingRiskLevelResultEntity") + suspend fun deleteAll() +} + +@Entity +data class PresenceTracingRiskLevelResultEntity( + @PrimaryKey @ColumnInfo(name = "calculatedAtMillis") val calculatedAtMillis: Long, + @ColumnInfo(name = "riskStateCode") val riskState: RiskState +) + +private fun PresenceTracingRiskLevelResultEntity.toCheckInWarningOverlap( + presenceTracingDayRisk: List<PresenceTracingDayRisk>? +) = PtRiskLevelResult( + calculatedAt = Instant.ofEpochMilli((calculatedAtMillis)), + riskState = riskState, + presenceTracingDayRisk = presenceTracingDayRisk +) + +private fun PtRiskLevelResult.toTraceTimeIntervalMatchEntity() = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = calculatedAt.millis, + riskState = riskState +) + +class RiskStateConverter { + @TypeConverter + fun toRiskStateCode(value: Int?): RiskState? = value?.toRiskState() + + @TypeConverter + fun fromRiskStateCode(code: RiskState?): Int? = code?.toCode() + + private fun RiskState.toCode() = when (this) { + RiskState.CALCULATION_FAILED -> CALCULATION_FAILED + RiskState.LOW_RISK -> LOW_RISK + RiskState.INCREASED_RISK -> INCREASED_RISK + } + + private fun Int.toRiskState() = when (this) { + CALCULATION_FAILED -> RiskState.CALCULATION_FAILED + LOW_RISK -> RiskState.LOW_RISK + INCREASED_RISK -> RiskState.INCREASED_RISK + else -> null + } + + companion object { + private const val CALCULATION_FAILED = 0 + private const val LOW_RISK = 1 + private const val INCREASED_RISK = 2 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt new file mode 100644 index 000000000..49ced6787 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.presencetracing.warning + +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient +import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningApiV1 +import de.rki.coronawarnapp.task.Task +import de.rki.coronawarnapp.task.TaskFactory +import de.rki.coronawarnapp.task.TaskTypeKey +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +class PresenceTracingWarningModule { + + @Provides + @IntoMap + @TaskTypeKey(PresenceTracingWarningTask::class) + fun taskFactory( + factory: PresenceTracingWarningTask.Factory + ): TaskFactory<out Task.Progress, out Task.Result> = factory + + @Singleton + @Provides + fun api( + @DownloadCDNHttpClient client: OkHttpClient, + @DownloadCDNServerUrl url: String, + gsonConverterFactory: GsonConverterFactory, + ): TraceWarningApiV1 { + + return Retrofit.Builder() + .client(client) + .baseUrl(url) + .addConverterFactory(gsonConverterFactory) + .build() + .create(TraceWarningApiV1::class.java) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt new file mode 100644 index 000000000..581df13ed --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt @@ -0,0 +1,164 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import dagger.Reusable +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningServer +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackageMetadata +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.util.HourInterval +import de.rki.coronawarnapp.util.ZipHelper.readIntoMap +import de.rki.coronawarnapp.util.ZipHelper.unzip +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.security.SignatureValidation +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import org.joda.time.Duration +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class TraceWarningPackageDownloader @Inject constructor( + private val repository: TraceWarningRepository, + private val dispatcherProvider: DispatcherProvider, + private val server: TraceWarningServer, + private val signatureValidation: SignatureValidation, +) { + + data class DownloadResult( + val successful: Boolean, + val newPackages: Collection<TraceWarningPackageMetadata> + ) { + override fun toString(): String { + return "DownloadResult(successful=$successful, newPackages.size=${newPackages.size})" + } + } + + suspend fun launchDownloads( + location: LocationCode, + hourIntervals: List<HourInterval>, + downloadTimeout: Duration + ): DownloadResult { + val launcher: CoroutineScope.(HourInterval) -> Deferred<TraceWarningPackageMetadata?> = { hourInterval -> + async { + val metadata = repository.createMetadata(location, hourInterval) + withTimeout(downloadTimeout.millis) { + downloadPackageForMetaData(metadata) + } + } + } + + Timber.tag(TAG).d("Launching %d downloads.", hourIntervals.size) + + val launchedDownloads: Collection<Deferred<TraceWarningPackageMetadata?>> = + hourIntervals.map { warningPackageId -> + withContext(context = dispatcherProvider.IO) { + launcher(warningPackageId) + } + } + + val successfulDownloads = launchedDownloads.awaitAll() + .filterNotNull() + .also { + Timber.tag(TAG).v("Downloaded keyfile: %s", it.joinToString("\n")) + } + Timber.tag(TAG).i("Download success: ${successfulDownloads.size}/${launchedDownloads.size}") + + return DownloadResult( + successful = launchedDownloads.size == successfulDownloads.size, + newPackages = successfulDownloads + ) + } + + private suspend fun downloadPackageForMetaData( + metaData: TraceWarningPackageMetadata + ): TraceWarningPackageMetadata? = try { + val downloadInfo = server.downloadPackage( + location = metaData.location, + hourInterval = metaData.hourInterval + ) + + if (!downloadInfo.isEmptyPkg) { + val fileMap = downloadInfo.readBody().unzip().readIntoMap() + val rawProtoBuf = getValidatedBinary(metaData, fileMap) + writeProtoBufToFile(metaData, rawProtoBuf) + } else { + Timber.tag(TAG).v("Empty package for %s", metaData) + } + + Timber.tag(TAG).v("Download finished: %s -> %s", metaData, downloadInfo) + + val eTag = requireNotNull(downloadInfo.etag) { "Server provided no ETAG!" } + + repository.markDownloadComplete(metaData, eTag, downloadInfo.isEmptyPkg) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Download failed: %s", metaData) + null + } + + private fun writeProtoBufToFile( + metaData: TraceWarningPackageMetadata, + rawProtoBuf: ByteArray, + ) { + if (rawProtoBuf.isEmpty()) { + Timber.tag(TAG).d("rawProtoBuf was empty for %s", metaData.packageId) + return + } + + val saveTo = repository.getPathForMetaData(metaData) + if (saveTo.exists()) { + Timber.tag(TAG).w("File existed, overwriting: %s", saveTo) + if (saveTo.delete()) { + Timber.tag(TAG).e("%s exists, but can't be deleted.", saveTo) + } + } + try { + saveTo.parentFile?.let { + if (!it.exists() && it.mkdir()) { + Timber.w("Had to create parent dir: %s", it) + } + } + saveTo.writeBytes(rawProtoBuf) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to write %s to %s", metaData, saveTo) + saveTo.delete() + throw e + } + Timber.tag(TAG).v("%d bytes written to %s.", rawProtoBuf.size, saveTo) + } + + private fun getValidatedBinary( + metaData: TraceWarningPackageMetadata, + fileMap: Map<String, ByteArray> + ): ByteArray { + val signature = fileMap[EXPORT_SIGNATURE_NAME] ?: throw TraceWarningPackageValidationException( + message = "Signature was null for ${metaData.packageId}(${metaData.eTag})." + ) + + val binary = fileMap[EXPORT_BINARY_NAME] ?: throw TraceWarningPackageValidationException( + message = "Binary was null for ${metaData.packageId}(${metaData.eTag})." + ) + + val hasValidSignature = signatureValidation.hasValidSignature( + binary, + SignatureValidation.parseTEKStyleSignature(signature) + ) + + if (!hasValidSignature) { + throw TraceWarningPackageValidationException( + message = "Signature didn't match for ${metaData.packageId}(${metaData.eTag})." + ) + } + + return binary + } + + companion object { + private const val TAG = "TraceWarningDownloader" + private const val EXPORT_BINARY_NAME = "export.bin" + private const val EXPORT_SIGNATURE_NAME = "export.sig" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt new file mode 100644 index 000000000..ea2d87f22 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt @@ -0,0 +1,178 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import androidx.annotation.VisibleForTesting +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.KeyDownloadConfig +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningApiV1 +import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningServer +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackageMetadata +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.storage.DeviceStorage +import de.rki.coronawarnapp.util.HourInterval +import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval +import de.rki.coronawarnapp.util.debug.measureTime +import kotlinx.coroutines.flow.first +import timber.log.Timber +import javax.inject.Inject +import kotlin.math.max + +@Reusable +class TraceWarningPackageSyncTool @Inject constructor( + private val deviceStorage: DeviceStorage, + private val server: TraceWarningServer, + private val repository: TraceWarningRepository, + private val configProvider: AppConfigProvider, + private val checkInRepository: CheckInRepository, + private val downloader: TraceWarningPackageDownloader +) { + + suspend fun syncPackages(): SyncResult { + repository.cleanMetadata() + return measureTime( + { Timber.tag(TAG).d("syncPackagesForLocation(DE), took %dms", it) }, + { syncPackagesForLocation(LocationCode("DE")) } + ) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun syncPackagesForLocation(location: LocationCode): SyncResult { + Timber.tag(TAG).d("syncTraceWarningPackages(location=%s)", location) + + val oldestCheckIn = checkInRepository.allCheckIns.first().minByOrNull { it.checkInStart }.also { + Timber.tag(TAG).d("Our oldest check-in is %s", it) + } + + if (oldestCheckIn == null) { + Timber.tag(TAG).w("There were no checkins, cleaning up package metadata, aborting early.") + val metaDataForLocation = repository.getMetaDataForLocation(location) + repository.delete(metaDataForLocation) + return SyncResult(successful = true) + } + + val downloadConfig: KeyDownloadConfig = configProvider.getAppConfig() + + cleanUpRevokedPackages(downloadConfig) + + val intervalDiscovery: TraceWarningApiV1.DiscoveryResult = try { + server.getAvailableIds(location) + } catch (e: Exception) { + Timber.tag(TAG).w(e, "Failed to discover available IDs.") + return SyncResult(successful = false) + } + + val firstRelevantInterval: HourInterval = max( + oldestCheckIn.checkInStart.deriveHourInterval(), + intervalDiscovery.oldest + ) + + cleanUpIrrelevantPackages(location, firstRelevantInterval) + + if (firstRelevantInterval > intervalDiscovery.latest) { + Timber.tag(TAG).d("Known server IDs are older then ours newest, aborting early.") + return SyncResult(successful = true) + } + + val missingHourIntervals = determineIntervalsToDownload( + location = location, + firstRelevant = oldestCheckIn.checkInStart.deriveHourInterval(), + lastRelevant = intervalDiscovery.latest + ) + + if (missingHourIntervals.isEmpty()) { + Timber.tag(TAG).d("There are no missing intervals for %s", location) + return SyncResult(successful = true) + } + + requireStorageSpaceFor(missingHourIntervals.size) + + val downloadResult = downloader.launchDownloads( + location = location, + hourIntervals = missingHourIntervals, + downloadTimeout = downloadConfig.individualDownloadTimeout + ) + Timber.tag(TAG).i("Download result: %s", downloadResult) + + return SyncResult( + successful = downloadResult.successful, + newPackages = downloadResult.newPackages, + ) + } + + /** + * Returns true if any of our cached keys were revoked + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun cleanUpRevokedPackages( + config: KeyDownloadConfig + ): List<TraceWarningPackageMetadata> { + val revokedKeyPackages = config.revokedTraceWarningPackages + + if (revokedKeyPackages.isEmpty()) { + Timber.tag(TAG).d("No revoked key packages to delete.") + return emptyList() + } + + val badEtags = revokedKeyPackages.map { it.etag } + val toDelete = repository.allMetaData.first().filter { badEtags.contains(it.eTag) } + Timber.tag(TAG).d("Revoked key packages matched %s", toDelete) + + repository.delete(toDelete) + + return toDelete.also { + Timber.tag(TAG).d("Cleaned up TraceWarning ids: %s", it) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun cleanUpIrrelevantPackages( + location: LocationCode, + oldestRelevantInterval: HourInterval + ): List<TraceWarningPackageMetadata> { + val downloaded = repository.getMetaDataForLocation(location) + val toDelete = downloaded.filter { it.hourInterval < oldestRelevantInterval } + Timber.tag(TAG).d("Removing irrelevant ids older than %d: %s", oldestRelevantInterval, toDelete) + + repository.delete(toDelete) + + return toDelete.also { + Timber.tag(TAG).d("Removed irrelevant packages: %s", it) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun determineIntervalsToDownload( + location: LocationCode, + firstRelevant: HourInterval, + lastRelevant: HourInterval + ): List<HourInterval> { + val metadatas = repository.getMetaDataForLocation(location) + + return (firstRelevant..lastRelevant).filter { interval -> + // If there is no metadata, it's unknown, so we want to download it + metadatas.none { it.hourInterval == interval } + } + } + + private suspend fun requireStorageSpaceFor(size: Int): DeviceStorage.CheckResult { + val requiredBytes: Long = APPROX_FILE_SIZE * size + Timber.tag(TAG).d("%dB are required for %d files", requiredBytes, size) + return deviceStorage.requireSpacePrivateStorage(requiredBytes).also { + Timber.tag(TAG).d("Storage check result: %s", it) + } + } + + data class SyncResult( + val successful: Boolean, + val newPackages: Collection<TraceWarningPackageMetadata> = emptyList() + ) + + companion object { + private const val TAG = "TraceWarningSyncTool" + + // TODO check size + private const val APPROX_FILE_SIZE = 22 * 1024L // ~22KB + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt new file mode 100644 index 000000000..b05d7ac34 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import de.rki.coronawarnapp.exception.reporting.ErrorCodes +import de.rki.coronawarnapp.util.security.InvalidSignatureException + +class TraceWarningPackageValidationException(message: String) : InvalidSignatureException( + code = ErrorCodes.APPLICATION_CONFIGURATION_INVALID.code, + message = message +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt new file mode 100644 index 000000000..0c99daef7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.presencetracing.warning.download.server + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.util.HourInterval +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Streaming + +interface TraceWarningApiV1 { + + @Keep + data class DiscoveryResult( + @SerializedName("oldest") val oldest: HourInterval, + @SerializedName("latest") val latest: HourInterval + ) + + @GET("/version/v1/twp/country/{region}/hour") + suspend fun getWarningPackageIds( + @Path("region") region: String + ): DiscoveryResult + + @Streaming + @GET("/version/v1/twp/country/{region}/hour/{timeId}") + suspend fun downloadKeyFileForHour( + @Path("region") region: String, + @Path("timeId") timeId: Long + ): Response<ResponseBody> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt new file mode 100644 index 000000000..02e5d4448 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.presencetracing.warning.download.server + +import okhttp3.ResponseBody +import retrofit2.Response +import java.io.InputStream + +data class TraceWarningPackageDownload(val response: Response<ResponseBody>) { + + private val headers = response.headers() + + val etag by lazy { headers.values("ETag").singleOrNull() } + + val isEmptyPkg by lazy { headers.values("cwa-empty-pkg").singleOrNull() == "1" } + + fun readBody(): InputStream = requireNotNull(response.body()) { "Response body was null" }.byteStream() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt new file mode 100644 index 000000000..3cc3c8bb4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt @@ -0,0 +1,54 @@ +package de.rki.coronawarnapp.presencetracing.warning.download.server + +import dagger.Lazy +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.util.HourInterval +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import retrofit2.HttpException +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TraceWarningServer @Inject constructor( + private val traceWarningApi: Lazy<TraceWarningApiV1> +) { + + private val warningApi: TraceWarningApiV1 + get() = traceWarningApi.get() + + suspend fun getAvailableIds( + location: LocationCode + ): TraceWarningApiV1.DiscoveryResult = withContext(Dispatchers.IO) { + warningApi.getWarningPackageIds(location.identifier).also { + Timber.d("getAvailableIds(location=%s): %s", location, it) + } + } + + suspend fun downloadPackage( + location: LocationCode, + hourInterval: HourInterval + ): TraceWarningPackageDownload = withContext(Dispatchers.IO) { + Timber.tag(TAG).v("downloadPackage(location=%s, hourInterval=%s)", location, hourInterval) + + val response = warningApi.downloadKeyFileForHour( + location.identifier, + hourInterval + ) + + val downloadInfo = TraceWarningPackageDownload(response) + + if (response.isSuccessful) { + Timber.tag(TAG).v("TraceTimeWarning download available: %s", downloadInfo) + + return@withContext downloadInfo + } else { + throw HttpException(response) + } + } + + companion object { + private val TAG = TraceWarningServer::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt new file mode 100644 index 000000000..91e655f43 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning + +interface TraceWarningPackage { + + val packageId: WarningPackageId + + /** + * May throw an exception if there is an issue with the protobuf + */ + suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt new file mode 100644 index 000000000..e16436f59 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt @@ -0,0 +1,23 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import java.io.File + +data class TraceWarningPackageContainer( + override val packageId: WarningPackageId, + private val packagePath: File, +) : TraceWarningPackage { + + private val warningPackage by lazy<TraceWarning.TraceWarningPackage> { + if (packagePath.exists()) { + TraceWarning.TraceWarningPackage.parseFrom(packagePath.readBytes()) + } else { + TraceWarning.TraceWarningPackage.getDefaultInstance() + } + } + + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return warningPackage.timeIntervalWarningsList + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt new file mode 100644 index 000000000..5939360ce --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import androidx.room.Update +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.util.database.CommonConverters +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +@Dao +interface TraceWarningPackageDao { + + @Query("SELECT * FROM TraceWarningPackageMetadata") + fun getAllMetaData(): Flow<List<TraceWarningPackageMetadata>> + + @Query("SELECT * FROM TraceWarningPackageMetadata WHERE location = :location") + suspend fun getAllMetaDataForLocation(location: String): List<TraceWarningPackageMetadata> + + @Query("SELECT * FROM TraceWarningPackageMetadata WHERE id = :packageId") + suspend fun get(packageId: WarningPackageId): TraceWarningPackageMetadata? + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(entity: TraceWarningPackageMetadata) + + @Update(entity = TraceWarningPackageMetadata::class) + suspend fun updateMetaData(update: TraceWarningPackageMetadata.UpdateDownload) + + @Update(entity = TraceWarningPackageMetadata::class) + suspend fun updateMetaData(update: TraceWarningPackageMetadata.UpdateProcessed) + + @Query("DELETE FROM TraceWarningPackageMetadata WHERE id in (:packageIds)") + suspend fun deleteByIds(packageIds: List<WarningPackageId>) + + @Query("DELETE FROM TraceWarningPackageMetadata") + suspend fun clear() +} + +@Database( + entities = [TraceWarningPackageMetadata::class], + version = 1, + exportSchema = true +) +@TypeConverters(CommonConverters::class) +abstract class TraceWarningDatabase : RoomDatabase() { + + abstract fun traceWarningPackageDao(): TraceWarningPackageDao + + class Factory @Inject constructor(@AppContext private val context: Context) { + fun create() = Room + .databaseBuilder(context, TraceWarningDatabase::class.java, "TraceWarning_db") + .build() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt new file mode 100644 index 000000000..54725874a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.util.HourInterval +import org.joda.time.Instant + +@Entity(tableName = "TraceWarningPackageMetadata") +data class TraceWarningPackageMetadata( + @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId, + @ColumnInfo(name = "createdAt") val createdAt: Instant, + @ColumnInfo(name = "location") val location: LocationCode, // i.e. "DE" + @ColumnInfo(name = "hourInterval") val hourInterval: HourInterval, + @ColumnInfo(name = "eTag") val eTag: String? = null, + @ColumnInfo(name = "downloaded") val isDownloaded: Boolean = false, + @ColumnInfo(name = "emptyPkg") val isEmptyPkg: Boolean = false, + @ColumnInfo(name = "processed") val isProcessed: Boolean = false +) { + + constructor( + location: LocationCode, + hourInterval: HourInterval, + createdAt: Instant + ) : this( + packageId = calcluateId(location, hourInterval), + location = location, + hourInterval = hourInterval, + createdAt = createdAt, + ) + + @Transient + val fileName: String = "$packageId.bin" + + companion object { + fun calcluateId( + location: LocationCode, + hourInterval: HourInterval + ): WarningPackageId = "${location.identifier}_$hourInterval" + } + + @Entity + data class UpdateDownload( + @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId, + @ColumnInfo(name = "eTag") val eTag: String?, + @ColumnInfo(name = "downloaded") val isDownloaded: Boolean, + @ColumnInfo(name = "emptyPkg") val isEmptyPkg: Boolean, + @ColumnInfo(name = "processed") val isProcessed: Boolean, + ) + + @Entity + data class UpdateProcessed( + @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId, + @ColumnInfo(name = "processed") val isProcessed: Boolean, + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt new file mode 100644 index 000000000..4af922212 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt @@ -0,0 +1,182 @@ +package de.rki.coronawarnapp.presencetracing.warning.storage + +import android.content.Context +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.util.HourInterval +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber +import java.io.File +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TraceWarningRepository @Inject constructor( + @AppContext private val context: Context, + private val factory: TraceWarningDatabase.Factory, + private val timeStamper: TimeStamper +) { + private val database by lazy { factory.create() } + private val dao: TraceWarningPackageDao by lazy { database.traceWarningPackageDao() } + + private val storageDir by lazy { + File(context.cacheDir, "trace_warning_packages").apply { + if (!exists()) { + if (mkdirs()) { + Timber.tag(TAG).d("Trace warning package directory created: %s", this) + } else { + throw IOException("Trace warning package directory creation failed: $this") + } + } + } + } + + val unprocessedWarningPackages: Flow<List<TraceWarningPackage>> = dao.getAllMetaData() + .map { metadatas -> + Timber.tag(TAG).v("Known packages: ${metadatas.size}") + metadatas.filter { !it.isProcessed } + } + .map { unprocessed -> + Timber.tag(TAG).v("Unprocessed packages: ${unprocessed.size}") + unprocessed.filter { !it.isEmptyPkg } + } + .map { metaDatas -> + Timber.tag(TAG).v("There are ${metaDatas.size} unprocessed non-empty warning packages.") + metaDatas.map { metaData -> + TraceWarningPackageContainer( + packageId = metaData.packageId, + packagePath = getPathForMetaData(metaData) + ) + } + } + + fun getPathForMetaData(metaData: TraceWarningPackageMetadata): File { + return File(storageDir, metaData.fileName) + } + + val allMetaData = dao.getAllMetaData() + + suspend fun createMetadata(location: LocationCode, hourInterval: HourInterval): TraceWarningPackageMetadata { + val metadata = TraceWarningPackageMetadata( + location = location, + hourInterval = hourInterval, + createdAt = timeStamper.nowUTC + ) + dao.insert(metadata) + Timber.tag(TAG).d("Inserted new Metadata: %s", metadata) + return metadata + } + + suspend fun getMetaDataForLocation(location: LocationCode): List<TraceWarningPackageMetadata> { + return dao.getAllMetaDataForLocation(location.identifier) + } + + suspend fun markDownloadComplete( + metadata: TraceWarningPackageMetadata, + eTag: String, + isEmptyPkg: Boolean + ): TraceWarningPackageMetadata { + Timber.tag(TAG).d("markDownloadComplete(metaData=%s, eTag=%s)", metadata, eTag) + val update = TraceWarningPackageMetadata.UpdateDownload( + packageId = metadata.packageId, + eTag = eTag, + isDownloaded = true, + isProcessed = false, + isEmptyPkg = isEmptyPkg, + ) + Timber.tag(TAG).d("Metadata marked as complete: %s", update) + dao.updateMetaData(update) + return metadata.copy( + eTag = eTag, + isDownloaded = true, + isProcessed = false, + isEmptyPkg = isEmptyPkg, + ) + } + + suspend fun markPackagesProcessed(packageIds: List<WarningPackageId>) { + Timber.tag(TAG).v("markPackagesProcessed(packageIds=%s)", packageIds) + + packageIds.forEach { packageId -> + Timber.tag(TAG).d("markPackageProcessed(packageId=%s)", packageId) + val update = TraceWarningPackageMetadata.UpdateProcessed( + packageId = packageId, + isProcessed = true, + ) + dao.updateMetaData(update) + + dao.get(packageId)?.also { + val file = getPathForMetaData(it) + if (file.delete()) { + Timber.tag(TAG).v("Deleted processed file: %s", file) + } + } + } + } + + suspend fun delete(metadata: List<TraceWarningPackageMetadata>) { + Timber.tag(TAG).d("delete(metaData=%s)", metadata.map { it.packageId }) + dao.deleteByIds(metadata.map { it.packageId }) + metadata.map { getPathForMetaData(it) }.forEach { + if (it.exists()) { + if (it.delete()) { + Timber.tag(TAG).d("Delete TraceWarningPackage file.") + } else { + Timber.tag(TAG).w("Failed to delete TraceWarningPackage file: %s", it) + } + } + } + } + + suspend fun clear() { + Timber.tag(TAG).d("clear()") + dao.clear() + + if (!storageDir.deleteRecursively()) { + Timber.tag(TAG).e("Failed to delete all TraceWarningPackage files.") + } + } + + suspend fun cleanMetadata() { + Timber.tag(TAG).d("cleanMetadata()") + val allMetadata = allMetaData.first() + + // Lost files, system deleted cache? + run { + val shouldHaveFile = allMetadata.filter { it.isDownloaded && !it.isProcessed && !it.isEmptyPkg } + val toDelete = shouldHaveFile.filter { !getPathForMetaData(it).exists() } + if (toDelete.isNotEmpty()) { + Timber.tag(TAG).w("%d Metadata items lost their file", toDelete.size) + } + delete(toDelete) + } + + // Shouldn't have a file, but has one? Gremlins? + run { + val shouldNotHaveFile = allMetadata.filter { it.isDownloaded && (it.isProcessed || it.isEmptyPkg) } + val toDelete = shouldNotHaveFile.filter { getPathForMetaData(it).exists() } + if (toDelete.isNotEmpty()) { + Timber.tag(TAG).w("%d Metadata items have unexpected files", toDelete.size) + } + delete(toDelete) + } + + // File without owner? + storageDir.listFiles()?.forEach { file -> + val orphan = allMetadata.none { getPathForMetaData(it) == file } + + if (orphan && file.delete()) { + Timber.tag(TAG).w("Deleted orphaned file: %s", file) + } + } + } + + companion object { + private val TAG = TraceWarningRepository::class.java.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt new file mode 100644 index 000000000..af612ce8b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt @@ -0,0 +1,3 @@ +package de.rki.coronawarnapp.presencetracing.warning + +typealias WarningPackageId = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt new file mode 100644 index 000000000..b4e6305fb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt @@ -0,0 +1,64 @@ +package de.rki.coronawarnapp.risk + +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.risk.storage.combine +import de.rki.coronawarnapp.risk.storage.max +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import org.joda.time.Instant +import org.joda.time.LocalDate + +data class CombinedEwPtDayRisk( + val localDate: LocalDate, + val riskState: RiskState +) + +data class CombinedEwPtRiskLevelResult( + val ptRiskLevelResult: PtRiskLevelResult, + val ewRiskLevelResult: EwRiskLevelResult +) { + + val riskState: RiskState by lazy { + combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) + } + + val wasSuccessfullyCalculated: Boolean by lazy { + riskState != RiskState.CALCULATION_FAILED + } + + val calculatedAt: Instant by lazy { + max(ewRiskLevelResult.calculatedAt, ptRiskLevelResult.calculatedAt) + } + + val daysWithEncounters: Int by lazy { + when (riskState) { + RiskState.INCREASED_RISK -> { + (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0) + + ptRiskLevelResult.numberOfDaysWithHighRisk + } + RiskState.LOW_RISK -> { + (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0) + + ptRiskLevelResult.numberOfDaysWithLowRisk + } + else -> 0 + } + } + + val lastRiskEncounterAt: LocalDate? by lazy { + when (riskState) { + RiskState.INCREASED_RISK -> max( + ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithHighRisk?.toLocalDateUtc(), + ptRiskLevelResult.mostRecentDateWithHighRisk + ) + RiskState.LOW_RISK -> max( + ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithLowRisk?.toLocalDateUtc(), + ptRiskLevelResult.mostRecentDateWithLowRisk + ) + else -> null + } + } +} + +data class LastCombinedRiskResults( + val lastCalculated: CombinedEwPtRiskLevelResult, + val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt index d09e184b9..123f45edc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -8,7 +8,6 @@ import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettin import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID -import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionSettings @@ -49,7 +48,7 @@ class RiskLevelChangeDetector @Inject constructor( } .filter { it.size == 2 } .onEach { - Timber.v("Checking for risklevel change.") + Timber.v("Checking for ew risklevel change.") checkEwRiskForStateChanges(it) } .catch { Timber.e(it, "App config change checks failed.") } @@ -61,7 +60,7 @@ class RiskLevelChangeDetector @Inject constructor( } .filter { it.size == 2 } .onEach { - Timber.v("Checking for risklevel change.") + Timber.v("Checking for combined risklevel change.") checkCombinedRiskForStateChanges(it) } .catch { Timber.e(it, "App config change checks failed.") } @@ -69,7 +68,6 @@ class RiskLevelChangeDetector @Inject constructor( } private suspend fun checkCombinedRiskForStateChanges(results: List<CombinedEwPtRiskLevelResult>) { - // TODO refactor val oldResult = results.first() val newResult = results.last() @@ -164,7 +162,6 @@ class RiskLevelChangeDetector @Inject constructor( } else { Timber.d("App is in foreground, not sending notifications") } - Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskState") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt index c47ff8c40..1d0ea0988 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult import org.joda.time.Instant fun List<EwRiskLevelResult>.tryLatestEwResultsWithDefaults(): DisplayableEwRiskResults { @@ -24,41 +22,8 @@ data class DisplayableEwRiskResults( val lastSuccessfullyCalculated: EwRiskLevelResult ) -fun List<CombinedEwPtRiskLevelResult>.tryLatestResultsWithDefaults(): DisplayableRiskResults { - val latestCalculation = this.maxByOrNull { it.calculatedAt } - ?: initialLowLevelEwRiskLevelResult - - val lastSuccessfullyCalculated = this.filter { it.wasSuccessfullyCalculated } - .maxByOrNull { it.calculatedAt } ?: undeterminedEwRiskLevelResult - - return DisplayableRiskResults( - lastCalculated = latestCalculation, - lastSuccessfullyCalculated = lastSuccessfullyCalculated - ) -} - -data class DisplayableRiskResults( - val lastCalculated: CombinedEwPtRiskLevelResult, - val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult -) - -private val undeterminedEwRiskLevelResult = CombinedEwPtRiskLevelResult( - PtRiskLevelResult( - calculatedAt = Instant.EPOCH, - riskState = RiskState.CALCULATION_FAILED - ), - EwUndeterminedRiskLevelResult -) - -private val initialLowLevelEwRiskLevelResult = CombinedEwPtRiskLevelResult( - PtRiskLevelResult( - calculatedAt = Instant.now(), - riskState = RiskState.LOW_RISK - ), - EwInitialLowRiskLevelResult -) - private object EwInitialLowRiskLevelResult : EwRiskLevelResult { + // TODO this causes flaky tests as this is set in memory, once. override val calculatedAt: Instant = Instant.now() override val riskState: RiskState = RiskState.LOW_RISK override val failureReason: EwRiskLevelResult.FailureReason? = null 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 new file mode 100644 index 000000000..ba05ce2a8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt @@ -0,0 +1,109 @@ +package de.rki.coronawarnapp.risk.execution + +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, + private val workManager: WorkManager, + private val taskController: TaskController, + private val presenceWorkBuilder: PresenceTracingWarningWorkBuilder, + private val diagnosisWorkBuilder: DiagnosisKeyRetrievalWorkBuilder, +) { + + 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)") + + if (enabled) { + val diagnosisRequest = diagnosisWorkBuilder.createPeriodicWorkRequest() + queueWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD, diagnosisRequest) + + 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) + workManager.enqueueUniquePeriodicWork( + workerId, + ExistingPeriodicWorkPolicy.KEEP, + request, + ) + } + + private fun cancelWorker(workerId: String) { + Timber.tag(TAG).d("cancelWorker(workerId=$workerId") + workManager.cancelUniqueWork(workerId) + } + + private 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/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index a0844cd0b..d5d0d71f0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -1,14 +1,19 @@ package de.rki.coronawarnapp.risk.storage import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult -import de.rki.coronawarnapp.presencetracing.risk.mapToRiskState +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.mapToRiskState +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult +import de.rki.coronawarnapp.risk.LastCombinedRiskResults import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao @@ -30,7 +35,7 @@ import de.rki.coronawarnapp.util.flow.combine as flowCombine abstract class BaseRiskLevelStorage constructor( private val riskResultDatabaseFactory: RiskResultDatabase.Factory, - presenceTracingRiskRepository: PresenceTracingRiskRepository, + private val presenceTracingRiskRepository: PresenceTracingRiskRepository, scope: CoroutineScope ) : RiskLevelStorage { @@ -185,44 +190,20 @@ abstract class BaseRiskLevelStorage constructor( } .shareLatest(tag = TAG, scope = scope) - private val latestAndLastSuccessfulPtRiskLevelResult: Flow<List<PtRiskLevelResult>> = - presenceTracingRiskRepository - .latestAndLastSuccessful() - .shareLatest(tag = TAG, scope = scope) - - // TODO maybe refactor - override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<List<CombinedEwPtRiskLevelResult>> + // used for risk state in tracing state/details + override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults> get() = combine( - latestAndLastSuccessfulEwRiskLevelResult, - latestAndLastSuccessfulPtRiskLevelResult + allEwRiskLevelResults, + presenceTracingRiskRepository.allEntries() ) { ewRiskLevelResults, ptRiskLevelResults -> - val latestEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt } - val latestPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt } - val combinedList = mutableListOf<CombinedEwPtRiskLevelResult>() - if (latestEwResult != null && latestPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = latestEwResult, - ptRiskLevelResult = latestPtResult - ) - ) - } - val lastSuccessfulEwResult = ewRiskLevelResults - .filter { it.wasSuccessfullyCalculated }.maxByOrNull { it.calculatedAt } - val lastSuccessfulPtResult = ptRiskLevelResults - .filter { it.wasSuccessfullyCalculated }.maxByOrNull { it.calculatedAt } - if (lastSuccessfulEwResult != null && lastSuccessfulPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = lastSuccessfulEwResult, - // current ptDayRiskStates belong to the last successful calculation - ugly - ptRiskLevelResult = lastSuccessfulPtResult.copy( - presenceTracingDayRisk = ptDayRiskStates.first() - ) - ) - ) - } - combinedList + + val combinedResults = combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + .sortedByDescending { it.calculatedAt } + + LastCombinedRiskResults( + lastCalculated = combinedResults.firstOrNull() ?: currentCombinedLowRisk, + lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: initialCombined + ) } private val latestPtRiskLevelResults: Flow<List<PtRiskLevelResult>> = @@ -230,33 +211,15 @@ abstract class BaseRiskLevelStorage constructor( .latestEntries(2) .shareLatest(tag = TAG, scope = scope) + // used for risk level change detector to trigger notification override val latestCombinedEwPtRiskLevelResults: Flow<List<CombinedEwPtRiskLevelResult>> get() = combine( latestEwRiskLevelResults, latestPtRiskLevelResults ) { ewRiskLevelResults, ptRiskLevelResults -> - val latestEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt } - val latestPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt } - val olderEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt } - val olderPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt } - val combinedList = mutableListOf<CombinedEwPtRiskLevelResult>() - if (latestEwResult != null && latestPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = latestEwResult, - ptRiskLevelResult = latestPtResult - ) - ) - } - if (olderEwResult != null && olderPtResult != null) { - combinedList.add( - CombinedEwPtRiskLevelResult( - ewRiskLevelResult = olderEwResult, - ptRiskLevelResult = olderPtResult - ) - ) - } - combinedList + combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + .sortedByDescending { it.calculatedAt } + .take(2) } internal abstract suspend fun storeExposureWindows(storedResultId: String, resultEw: EwRiskLevelResult) @@ -266,6 +229,7 @@ abstract class BaseRiskLevelStorage constructor( override suspend fun clear() { Timber.w("clear() - Clearing stored risklevel/exposure-detection results.") database.clearAllTables() + presenceTracingRiskRepository.clearAllTables() } companion object { @@ -284,7 +248,7 @@ internal fun combineRisk( val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date } CombinedEwPtDayRisk( date, - max( + combine( ptRisk?.riskState, ewRisk?.riskLevel?.mapToRiskState() ) @@ -292,7 +256,7 @@ internal fun combineRisk( } } -internal fun max(left: RiskState?, right: RiskState?): RiskState { +internal fun combine(left: RiskState?, right: RiskState?): RiskState { return if (left == RiskState.INCREASED_RISK || right == RiskState.INCREASED_RISK) RiskState.INCREASED_RISK else if (left == RiskState.LOW_RISK || right == RiskState.LOW_RISK) RiskState.LOW_RISK else RiskState.CALCULATION_FAILED @@ -308,3 +272,66 @@ internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? { return if (left.isAfter(right)) left else right } + +@VisibleForTesting(otherwise = PRIVATE) +internal fun combineEwPtRiskLevelResults( + ptRiskResults: List<PtRiskLevelResult>, + ewRiskResults: List<EwRiskLevelResult> +): List<CombinedEwPtRiskLevelResult> { + val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct() + val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt } + val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt } + return allDates.map { date -> + val ptRisk = sortedPtResults.find { it.calculatedAt <= date } ?: ptInitialRiskLevelResult + val ewRisk = sortedEwResults.find { it.calculatedAt <= date } ?: EwInitialRiskLevelResult + CombinedEwPtRiskLevelResult( + ptRisk, + ewRisk + ) + } +} + +private object EwInitialRiskLevelResult : EwRiskLevelResult { + override val calculatedAt: Instant = Instant.EPOCH + override val riskState: RiskState = RiskState.CALCULATION_FAILED + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} + +private val ptInitialRiskLevelResult: PtRiskLevelResult by lazy { + PtRiskLevelResult( + calculatedAt = Instant.EPOCH, + riskState = RiskState.CALCULATION_FAILED + ) +} + +private val ewCurrentLowRiskLevelResult + get() = object : EwRiskLevelResult { + override val calculatedAt: Instant = Instant.now() + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + +private val ptCurrentLowRiskLevelResult: PtRiskLevelResult + get() = PtRiskLevelResult( + calculatedAt = Instant.now(), + riskState = RiskState.LOW_RISK + ) + +private val initialCombined = CombinedEwPtRiskLevelResult( + ptInitialRiskLevelResult, + EwInitialRiskLevelResult +) + +private val currentCombinedLowRisk: CombinedEwPtRiskLevelResult + get() = CombinedEwPtRiskLevelResult( + ptCurrentLowRiskLevelResult, + ewCurrentLowRiskLevelResult + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt index 46d4366ef..a72b458ba 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt @@ -1,15 +1,13 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk -import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelResult -import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk +import de.rki.coronawarnapp.risk.LastCombinedRiskResults import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import kotlinx.coroutines.flow.Flow -import org.joda.time.Instant -import org.joda.time.LocalDate interface RiskLevelStorage { @@ -51,7 +49,7 @@ interface RiskLevelStorage { * Can be 0-2 entries. * Newest item first. */ - val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<List<CombinedEwPtRiskLevelResult>> + val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults> /** EXPOSURE WINDOW RISK RESULT * Risk level per date/day @@ -82,48 +80,3 @@ interface RiskLevelStorage { suspend fun clear() } - -data class CombinedEwPtDayRisk( - val localDate: LocalDate, - val riskState: RiskState -) - -data class CombinedEwPtRiskLevelResult( - val ptRiskLevelResult: PtRiskLevelResult, - val ewRiskLevelResult: EwRiskLevelResult -) { - - val riskState: RiskState = max(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) - - val wasSuccessfullyCalculated: Boolean - get() = ewRiskLevelResult.ewAggregatedRiskResult != null && - ptRiskLevelResult.riskState != RiskState.CALCULATION_FAILED - - val calculatedAt: Instant = max(ewRiskLevelResult.calculatedAt, ptRiskLevelResult.calculatedAt) - - val daysWithEncounters: Int - get() = when (riskState) { - RiskState.INCREASED_RISK -> { - (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0) + - ptRiskLevelResult.numberOfDaysWithHighRisk - } - RiskState.LOW_RISK -> { - (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0) + - ptRiskLevelResult.numberOfDaysWithLowRisk - } - else -> 0 - } - - val lastRiskEncounterAt: LocalDate? - get() = if (riskState == RiskState.INCREASED_RISK) { - max( - ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithHighRisk?.toLocalDateUtc(), - ptRiskLevelResult.mostRecentDateWithHighRisk - ) - } else { - max( - ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithLowRisk?.toLocalDateUtc(), - ptRiskLevelResult.mostRecentDateWithLowRisk - ) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt index 016ce630f..29a64f316 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt @@ -40,7 +40,8 @@ class SubmissionRepository @Inject constructor( private val backgroundNoise: BackgroundNoise, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, private val tracingSettings: TracingSettings, - private val testResultDataCollector: TestResultDataCollector + private val testResultDataCollector: TestResultDataCollector, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) { private val testResultReceivedDateFlowInternal = MutableStateFlow((submissionSettings.initialTestResultReceivedAt ?: timeStamper.nowUTC).toDate()) @@ -192,7 +193,7 @@ class SubmissionRepository @Inject constructor( submissionSettings.initialTestResultReceivedAt = currentTime testResultReceivedDateFlowInternal.value = currentTime.toDate() if (testResult == TestResult.PENDING) { - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() } } else { testResultReceivedDateFlowInternal.value = initialTestResultReceivedTimestamp.toDate() 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 7265d2bb6..a860a2edb 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 @@ -41,7 +41,8 @@ class SubmissionTask @Inject constructor( private val testResultAvailableNotificationService: TestResultAvailableNotificationService, private val checkInsRepository: CheckInRepository, private val checkInsTransformer: CheckInsTransformer, - private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : Task<DefaultProgress, SubmissionTask.Result> { private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() @@ -176,9 +177,9 @@ class SubmissionTask @Inject constructor( private fun setSubmissionFinished() { Timber.tag(TAG).d("setSubmissionFinished()") - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() submissionSettings.isSubmissionSuccessful = true - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() testResultAvailableNotificationService.cancelTestResultAvailableNotification() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt index c3d9018fc..5aa675aae 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt @@ -8,7 +8,6 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTra import de.rki.coronawarnapp.nearby.modules.detectiontracker.latestSubmission import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress @@ -51,15 +50,13 @@ class TracingStateProvider @AssistedInject constructor( latestSubmission, isBackgroundJobEnabled -> - val ( - latestCalc, - latestSuccessfulCalc - ) = riskLevelResults.tryLatestResultsWithDefaults() + val latestCalc = riskLevelResults.lastCalculated + val lastSuccessfullyCalc = riskLevelResults.lastSuccessfullyCalculated return@combine when { tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE -> TracingDisabled( isInDetailsMode = isDetailsMode, - riskState = latestSuccessfulCalc.riskState, + riskState = lastSuccessfullyCalc.riskState, lastExposureDetectionTime = latestSubmission?.startedAt ) tracingProgress != TracingProgress.Idle -> TracingInProgress( @@ -86,7 +83,7 @@ class TracingStateProvider @AssistedInject constructor( ) else -> TracingFailed( isInDetailsMode = isDetailsMode, - riskState = latestSuccessfulCalc.riskState, + riskState = lastSuccessfullyCalc.riskState, lastExposureDetectionTime = latestSubmission?.startedAt ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt index eba70d84b..dd52cdbc9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys.ConsentResult.AlreadyGiv import de.rki.coronawarnapp.datadonation.survey.Surveys.ConsentResult.Needed import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.states.IncreasedRisk @@ -81,7 +80,7 @@ class TracingDetailsFragmentViewModel @AssistedInject constructor( riskLevelResults, isBackgroundJobEnabled -> - val (latestCalc, _) = riskLevelResults.tryLatestResultsWithDefaults() + val latestCalc = riskLevelResults.lastCalculated val isRestartButtonEnabled = !isBackgroundJobEnabled || latestCalc.riskState == RiskState.CALCULATION_FAILED diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt index f58268644..f47238e68 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt @@ -5,7 +5,6 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.installTime.InstallTimeProvider import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status import de.rki.coronawarnapp.tracing.ui.details.items.DetailsItem @@ -43,7 +42,7 @@ class TracingDetailsItemProvider @Inject constructor( riskLevelResults, availableSurveys -> - val (latestCalc, _) = riskLevelResults.tryLatestResultsWithDefaults() + val latestCalc = riskLevelResults.lastCalculated mutableListOf<DetailsItem>().apply { if (status != Status.TRACING_INACTIVE && diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt index 3628d6add..574698d97 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt @@ -50,6 +50,7 @@ class DetailsLowRiskBox( ) : RiskDetailsStateItem { fun getRiskDetailsRiskLevelBody(c: Context): String { + // TODO consider pt encounters? return c.getString( if (matchedKeyCount > 0) R.string.risk_details_information_body_low_risk_with_encounter else R.string.risk_details_information_body_low_risk 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 76af2958e..571ae77c0 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 @@ -34,7 +34,8 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( tracingStatus: GeneralTracingStatus, installTimeProvider: InstallTimeProvider, private val backgroundStatus: BackgroundModeStatus, - tracingPermissionHelperFactory: TracingPermissionHelper.Factory + tracingPermissionHelperFactory: TracingPermissionHelper.Factory, + private val backgroundWorkScheduler: BackgroundWorkScheduler ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val loggingPeriod: LiveData<PeriodLoggedBox.Item> = @@ -75,7 +76,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( if (!backgroundStatus.isIgnoringBatteryOptimizations.first()) { events.postValue(Event.ManualCheckingDialog) } - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() } isTracingSwitchChecked.postValue(isTracingEnabled) } @@ -109,7 +110,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( launch { if (InternalExposureNotificationClient.asyncIsEnabled()) { InternalExposureNotificationClient.asyncStop() - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt index cd6d537b1..e6f0123e6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -6,10 +6,10 @@ import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsFragm import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsModule import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInModule -import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment -import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingModule import de.rki.coronawarnapp.ui.eventregistration.attendee.edit.EditCheckInFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.edit.EditCheckInModule +import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment +import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingModule import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeModule import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment @@ -18,10 +18,10 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationC import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragmentModule -import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment -import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragmentModule +import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment +import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragmentModule @Module internal abstract class EventRegistrationUIModule { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt index 685f2f245..840682e97 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt @@ -92,7 +92,6 @@ class ConfirmCheckInViewModel @AssistedInject constructor( val traceLocation = verifiedTraceLocation.traceLocation return CheckIn( traceLocationId = traceLocation.locationId, - traceLocationIdHash = traceLocation.locationIdHash, version = traceLocation.version, type = traceLocation.type.number, description = traceLocation.description, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt index 792ff9e53..739078b1e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt @@ -1,15 +1,15 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.details import android.graphics.Bitmap -import androidx.lifecycle.asLiveData -import dagger.assisted.Assisted import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData +import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.DispatcherProvider 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 196029c9f..798abdfcc 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 @@ -80,6 +80,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler @Inject lateinit var dataDonationAnalyticsScheduler: DataDonationAnalyticsScheduler @Inject lateinit var submissionSettings: SubmissionSettings + @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler override fun onCreate(savedInstanceState: Bundle?) { AppInjector.setup(this) @@ -190,7 +191,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { */ override fun onResume() { super.onResume() - scheduleWork() + backgroundWorkScheduler.startWorkScheduler() vm.doBackgroundNoiseCheck() contactDiaryWorkScheduler.schedulePeriodic() dataDonationAnalyticsScheduler.schedulePeriodic() @@ -271,9 +272,4 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { fun goBack() { onBackPressed() } - - /** - * Scheduling for a download of keys every hour. - */ - private fun scheduleWork() = BackgroundWorkScheduler.startWorkScheduler() } 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 e61dfcc8a..144593856 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 @@ -19,7 +19,8 @@ class SettingsResetViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val dataReset: DataReset, private val shareTestResultNotificationService: ShareTestResultNotificationService, - private val shortcutsHelper: AppShortcutsHelper + private val shortcutsHelper: AppShortcutsHelper, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val clickEvent: SingleLiveEvent<SettingsEvents> = SingleLiveEvent() @@ -39,7 +40,7 @@ class SettingsResetViewModel @AssistedInject constructor( // only stop tracing if it is currently enabled if (isTracingEnabled) { InternalExposureNotificationClient.asyncStop() - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() } } catch (apiException: ApiException) { apiException.report( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index 30c03c4a5..f7f9f38cf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -74,6 +74,11 @@ object TimeAndDateExtensions { fun Instant.derive10MinutesInterval(): Long = seconds / TimeUnit.MINUTES.toSeconds(10) // 10 min in seconds + /** + * Derive a UNIX timestamp (in seconds) and returns the corresponding 10-minute interval + */ + fun Instant.deriveHourInterval(): HourInterval = millis / 3600000 + /** * Converts milliseconds to human readable format hh:mm:ss * @@ -110,3 +115,5 @@ object TimeAndDateExtensions { fun Instant.toUserTimeZone() = this.toDateTime(DateTimeZone.forTimeZone(TimeZone.getDefault())) } + +typealias HourInterval = Long 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 6ead4ba58..442df0780 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,11 +5,9 @@ import android.net.wifi.WifiManager import android.os.PowerManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler 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.device.BackgroundModeStatus import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.di.ProcessLifecycle @@ -28,7 +26,9 @@ class WatchdogService @Inject constructor( private val taskController: TaskController, private val backgroundModeStatus: BackgroundModeStatus, @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner, - private val onboardingSettings: OnboardingSettings + private val onboardingSettings: OnboardingSettings, + private val backgroundWorkScheduler: BackgroundWorkScheduler, + private val riskWorkScheduler: RiskWorkScheduler, ) { private val powerManager by lazy { @@ -55,18 +55,8 @@ class WatchdogService @Inject constructor( Timber.tag(TAG).d("Automatic mode is on, check if we have downloaded keys already today") - val state = taskController.submitBlocking( - DefaultTaskRequest( - DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments(), - originTag = "WatchdogService" - ) - ) - if (state.isFailed) { - Timber.tag(TAG).e(state.error, "RetrieveDiagnosisKeysTransaction failed") - // retry the key retrieval in case of an error with a scheduled work - BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - } + val results = riskWorkScheduler.runRiskTasksNow() + Timber.tag(TAG).d("runRiskTasksNow() results: %s", results) if (wifiLock.isHeld) wifiLock.release() if (wakeLock.isHeld) wakeLock.release() @@ -75,7 +65,7 @@ class WatchdogService @Inject constructor( // 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() + backgroundWorkScheduler.startWorkScheduler() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt new file mode 100644 index 000000000..553f18903 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.util.coroutine + +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.suspendCancellableCoroutine +import java.util.concurrent.CancellationException +import java.util.concurrent.ExecutionException +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +/** + * Awaits for the completion of the [ListenableFuture] without blocking a thread. + * + * @return R The result from the [ListenableFuture] + * + */ +@Suppress("BlockingMethodInNonBlockingContext") +suspend inline fun <R> ListenableFuture<R>.await(): R { + // Fast path + if (isDone) { + try { + return get() + } catch (e: ExecutionException) { + throw e.cause ?: e + } + } + return suspendCancellableCoroutine { cancellableContinuation -> + val action = Runnable { + try { + cancellableContinuation.resume(get()) + } catch (throwable: Throwable) { + val cause = throwable.cause ?: throwable + when (throwable) { + is CancellationException -> cancellableContinuation.cancel(cause) + else -> cancellableContinuation.resumeWithException(cause) + } + } + } + addListener(action, MoreExecutors.directExecutor()) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt index 8605f6a5a..377c7491c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt @@ -8,31 +8,32 @@ import de.rki.coronawarnapp.statistics.InfectionStats import de.rki.coronawarnapp.statistics.KeySubmissionsStats import de.rki.coronawarnapp.statistics.SevenDayRValue import de.rki.coronawarnapp.statistics.StatsItem +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone import org.joda.time.LocalDate import org.joda.time.format.DateTimeFormat fun StatsItem.getPrimaryLabel(context: Context): String { val today = LocalDate() val yesterday = today.minusDays(1) - val day = LocalDate(updatedAt) + val updatedAtDate = LocalDate(updatedAt.toUserTimeZone()) val dateTimeFormatter = DateTimeFormat.mediumDate().withLocale(context.getLocale()) return when (this) { is InfectionStats, - is KeySubmissionsStats -> when (day) { + is KeySubmissionsStats -> when (updatedAtDate) { today -> context.getString(R.string.statistics_primary_value_today) yesterday -> context.getString(R.string.statistics_primary_value_yesterday) - else -> dateTimeFormatter.print(day) + else -> dateTimeFormatter.print(updatedAtDate) } - is IncidenceStats -> when (day) { + is IncidenceStats -> when (updatedAtDate) { today -> context.getString(R.string.statistics_primary_value_until_today) yesterday -> context.getString(R.string.statistics_primary_value_until_yesterday) - else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(day)) + else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(updatedAtDate)) } - is SevenDayRValue -> when (day) { + is SevenDayRValue -> when (updatedAtDate) { today -> context.getString(R.string.statistics_primary_value_current) yesterday -> context.getString(R.string.statistics_primary_value_yesterday) - else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(day)) + else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(updatedAtDate)) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt index 2088d2fbe..de0c40995 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt @@ -27,19 +27,29 @@ class CWAWorkerFactory @Inject constructor( ): ListenableWorker? { Timber.v("Checking in known worker factories for %s", workerClassName) val ourWorkerFactories = factories.entries.find { - Class.forName(workerClassName).isAssignableFrom(it.key) + try { + Class.forName(workerClassName).isAssignableFrom(it.key) + } catch (e: ClassNotFoundException) { + Timber.e(e, "Failed to create worker class $workerClassName") + false + } }?.value - return if (ourWorkerFactories != null) { + if (ourWorkerFactories != null) { Timber.v("It's one of ours, creating worker for %s with %s", workerClassName, workerParameters) - ourWorkerFactories.get().create(appContext, workerParameters).also { + return ourWorkerFactories.get().create(appContext, workerParameters).also { Timber.i("Our worker was created: %s", it) } - } else { + } + + return try { Timber.w("Unknown worker class, trying direct instantiation on %s", workerClassName) workerClassName.toNewWorkerInstance(appContext, workerParameters).also { Timber.i("Unknown worker was created: %s", it) } + } catch (e: ClassNotFoundException) { + Timber.w(e, "Failed to create unknown worker class: %s", workerClassName) + null } } 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 3cc59eabf..f93c59b45 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 @@ -8,14 +8,14 @@ import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker +import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker import de.rki.coronawarnapp.eventregistration.storage.retention.TraceLocationDbCleanUpPeriodicWorker import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOutWorker +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorker import de.rki.coronawarnapp.submission.auto.SubmissionWorker import de.rki.coronawarnapp.worker.BackgroundNoiseOneTimeWorker import de.rki.coronawarnapp.worker.BackgroundNoisePeriodicWorker -import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker -import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalPeriodicWorker import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker @Module @@ -44,16 +44,9 @@ abstract class WorkerBinder { @Binds @IntoMap - @WorkerKey(DiagnosisKeyRetrievalOneTimeWorker::class) + @WorkerKey(DiagnosisKeyRetrievalWorker::class) abstract fun diagnosisKeyRetrievalOneTime( - factory: DiagnosisKeyRetrievalOneTimeWorker.Factory - ): InjectedWorkerFactory<out ListenableWorker> - - @Binds - @IntoMap - @WorkerKey(DiagnosisKeyRetrievalPeriodicWorker::class) - abstract fun diagnosisKeyRetrievalPeriodic( - factory: DiagnosisKeyRetrievalPeriodicWorker.Factory + factory: DiagnosisKeyRetrievalWorker.Factory ): InjectedWorkerFactory<out ListenableWorker> @Binds @@ -111,4 +104,11 @@ abstract class WorkerBinder { abstract fun traceLocationCleanUpWorker( factory: TraceLocationDbCleanUpPeriodicWorker.Factory ): InjectedWorkerFactory<out ListenableWorker> + + @Binds + @IntoMap + @WorkerKey(PresenceTracingWarningWorker::class) + abstract fun traceWarningWorker( + factory: PresenceTracingWarningWorker.Factory + ): InjectedWorkerFactory<out ListenableWorker> } 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 75b991b69..7e7340ebb 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 @@ -9,16 +9,6 @@ import java.util.concurrent.TimeUnit */ object BackgroundConstants { - /** - * Tag for diagnosis key retrieval one time work - */ - const val DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG = "DIAGNOSIS_KEY_ONE_TIME_WORKER" - - /** - * Tag for diagnosis key retrieval periodic work - */ - const val DIAGNOSIS_KEY_PERIODIC_WORKER_TAG = "DIAGNOSIS_KEY_PERIODIC_WORKER" - /** * Tag for background polling tp check test result periodic work */ @@ -34,16 +24,6 @@ object BackgroundConstants { */ const val BACKGROUND_NOISE_ONE_TIME_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER" - /** - * Unique name for diagnosis key retrieval one time work - */ - const val DIAGNOSIS_KEY_ONE_TIME_WORK_NAME = "DiagnosisKeyBackgroundOneTimeWork" - - /** - * Unique name for diagnosis key retrieval periodic work - */ - const val DIAGNOSIS_KEY_PERIODIC_WORK_NAME = "DiagnosisKeyBackgroundPeriodicWork" - /** * Unique name for diagnosis test result retrieval periodic work */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt index 31922abf4..2168b0287 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt @@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber @@ -23,7 +22,8 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, private val submissionSettings: SubmissionSettings, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : CoroutineWorker(context, workerParams) { /** @@ -45,7 +45,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( return result } - BackgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork() + backgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork() } catch (e: Exception) { result = if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Result.failure() @@ -58,7 +58,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( } private fun stopWorker() { - BackgroundWorkScheduler.WorkType.BACKGROUND_NOISE_PERIODIC_WORK.stop() + backgroundWorkScheduler.stopBackgroundNoisePeriodicWork() Timber.tag(TAG).d("$id: worker stopped") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt index b109e8ba2..2c03e9544 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt @@ -5,131 +5,84 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.WorkTag import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton -/** - * Build diagnosis key periodic work request - * Set "kind delay" for accessibility reason. - * Backoff criteria set to Linear type. - * - * The launchInterval is 60 minutes as we want to check every hour, for new hour packages on the CDN. - * - * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - * @see BackgroundConstants.BACKOFF_INITIAL_DELAY - * @see BackoffPolicy.LINEAR - */ -fun buildDiagnosisKeyRetrievalPeriodicWork() = - PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalPeriodicWorker>(60, TimeUnit.MINUTES) - .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - BackgroundConstants.BACKOFF_INITIAL_DELAY, - TimeUnit.MINUTES - ) - .build() +@Singleton +class BackgroundWorkBuilder @Inject constructor() { -/** - * Build diagnosis key one time work request - * Set random initial delay for security reason. - * Backoff criteria set to Linear type. - * - * @return OneTimeWorkRequest - * - * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER - * @see buildDiagnosisKeyRetrievalOneTimeWork - * @see BackgroundConstants.BACKOFF_INITIAL_DELAY - * @see BackoffPolicy.LINEAR - */ -fun buildDiagnosisKeyRetrievalOneTimeWork() = - OneTimeWorkRequestBuilder<DiagnosisKeyRetrievalOneTimeWorker>() - .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, - BackgroundConstants.BACKOFF_INITIAL_DELAY, + /** + * Build diagnosis Test Result periodic work request + * Set "kind delay" for accessibility reason. + * + * @return PeriodicWorkRequest + * + * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER + * @see BackgroundConstants.KIND_DELAY + */ + fun buildDiagnosisTestResultRetrievalPeriodicWork() = + PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( + BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), TimeUnit.MINUTES ) - .build() + .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() -/** - * Build diagnosis Test Result periodic work request - * Set "kind delay" for accessibility reason. - * - * @return PeriodicWorkRequest - * - * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - */ -fun buildDiagnosisTestResultRetrievalPeriodicWork() = - PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( - BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), - TimeUnit.MINUTES - ) - .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, - TimeUnit.SECONDS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() + /** + * Build background noise one time work request + * Set BackgroundNoiseOneTimeWorkDelay for timing randomness. + * + * @return PeriodicWorkRequest + * + * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER + * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay + */ + fun buildBackgroundNoiseOneTimeWork() = + OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>() + .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(), + TimeUnit.HOURS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() -/** - * Build background noise one time work request - * Set BackgroundNoiseOneTimeWorkDelay for timing randomness. - * - * @return PeriodicWorkRequest - * - * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER - * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay - */ -fun buildBackgroundNoiseOneTimeWork() = - OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>() - .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(), + /** + * Build background noise periodic work request + * Set "kind delay" for accessibility reason. + * + * @return PeriodicWorkRequest + * + * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION + * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER + * @see BackgroundConstants.KIND_DELAY + */ + fun buildBackgroundNoisePeriodicWork() = + PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>( + BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION, TimeUnit.HOURS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() - -/** - * Build background noise periodic work request - * Set "kind delay" for accessibility reason. - * - * @return PeriodicWorkRequest - * - * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION - * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - */ -fun buildBackgroundNoisePeriodicWork() = - PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>( - BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION, - TimeUnit.HOURS - ) - .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.SECONDS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES ) - .build() + .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .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 index aaf6317d9..4f83979c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt @@ -6,9 +6,13 @@ import androidx.work.Operation import androidx.work.WorkInfo import androidx.work.WorkManager import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.util.di.ApplicationComponent +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings import timber.log.Timber import java.util.concurrent.ExecutionException +import javax.inject.Inject +import javax.inject.Singleton /** * Singleton class for background work handling @@ -17,26 +21,24 @@ import java.util.concurrent.ExecutionException * @see BackgroundConstants * @see BackgroundWorkHelper */ -object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { - - fun init(component: ApplicationComponent) { - component.inject(this) - } +@Singleton +class BackgroundWorkScheduler @Inject constructor( + private val backgroundWorkBuilder: BackgroundWorkBuilder, + private val submissionSettings: SubmissionSettings, + private val tracingSettings: TracingSettings, + private val riskWorkScheduler: RiskWorkScheduler +) { /** * Enum class for work tags * * @param tag the tag of the worker * - * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG - * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG */ enum class WorkTag(val tag: String) { - DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG), - DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG), DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG), BACKGROUND_NOISE_ONE_TIME_WORKER(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG), BACKGROUND_NOISE_PERIODIC_WORKER(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG) @@ -47,15 +49,11 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { * * @param uniqueName the unique name of specified work * - * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME - * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME */ enum class WorkType(val uniqueName: String) { - DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME), - DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME), DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME), BACKGROUND_NOISE_PERIODIC_WORK(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME), BACKGROUND_NOISE_ONE_TIME_WORK(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME) @@ -75,17 +73,9 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { * @see isWorkActive */ fun startWorkScheduler() { - val notificationBody = StringBuilder() - notificationBody.append("Jobs starting: ") - val isPeriodicWorkActive = isWorkActive(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag) - logWorkActiveStatus( - WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag, - isPeriodicWorkActive - ) - if (!isPeriodicWorkActive) { - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() - notificationBody.append("[DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK] ") - } + Timber.d("startWorkScheduler()") + riskWorkScheduler.setPeriodicRiskCalculation(enabled = true) + if (!submissionSettings.isSubmissionSuccessful) { if (!isWorkActive(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) && submissionSettings.registrationToken.value != null && @@ -93,10 +83,22 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { ) { WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() tracingSettings.initialPollingForTestResultTimeStamp = System.currentTimeMillis() - notificationBody.append("[DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER]") + Timber.d("Starting DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER") } } - Timber.d("Background Job Starting: %s", notificationBody) + } + + /** + * Stop work scheduler + * Stops all background work by tag. + */ + fun stopWorkScheduler() { + WorkTag.values().map { workTag: WorkTag -> + workManager.cancelAllWorkByTag(workTag.tag) + .also { it.logOperationCancelByTag(workTag) } + } + riskWorkScheduler.setPeriodicRiskCalculation(enabled = false) + Timber.d("All Background Jobs Stopped") } /** @@ -139,34 +141,12 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { return result } - /** - * Stop work scheduler - * Stops all background work by tag. - */ - fun stopWorkScheduler() { - WorkTag.values().map { workTag: WorkTag -> - workManager.cancelAllWorkByTag(workTag.tag) - .also { it.logOperationCancelByTag(workTag) } - } - Timber.d("All Background Jobs Stopped") + fun scheduleDiagnosisTestResultPeriodicWork() { + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() } - /** - * Schedule diagnosis key periodic time work - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK - */ - fun scheduleDiagnosisKeyPeriodicWork() { - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() - } - - /** - * Schedule diagnosis key one time work - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK - */ - fun scheduleDiagnosisKeyOneTimeWork() { - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start() + fun stopDiagnosisTestResultPeriodicWork() { + WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } /** @@ -178,6 +158,10 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start() } + fun stopBackgroundNoisePeriodicWork() { + WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start() + } + /** * Schedule background noise one time work * @@ -195,41 +179,11 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { * @see WorkType */ private fun WorkType.start(): Operation = when (this) { - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK -> enqueueDiagnosisKeyBackgroundPeriodicWork() - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK -> enqueueDiagnosisKeyBackgroundOneTimeWork() WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER -> enqueueDiagnosisTestResultBackgroundPeriodicWork() WorkType.BACKGROUND_NOISE_PERIODIC_WORK -> enqueueBackgroundNoisePeriodicWork() WorkType.BACKGROUND_NOISE_ONE_TIME_WORK -> enqueueBackgroundNoiseOneTimeWork() } - /** - * Enqueue diagnosis key periodic work and log it - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK - */ - private fun enqueueDiagnosisKeyBackgroundPeriodicWork() = workManager.enqueueUniquePeriodicWork( - WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.uniqueName, - ExistingPeriodicWorkPolicy.REPLACE, - buildDiagnosisKeyRetrievalPeriodicWork() - ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK) } - - /** - * Enqueue diagnosis key one time work and log it - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK - */ - private fun enqueueDiagnosisKeyBackgroundOneTimeWork() = workManager.enqueueUniqueWork( - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.uniqueName, - ExistingWorkPolicy.REPLACE, - buildDiagnosisKeyRetrievalOneTimeWork() - ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK) } - /** * Enqueue diagnosis Test Result periodic * Show a Notification when new Test Results are in. @@ -243,7 +197,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { workManager.enqueueUniquePeriodicWork( WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.uniqueName, ExistingPeriodicWorkPolicy.REPLACE, - buildDiagnosisTestResultRetrievalPeriodicWork() + backgroundWorkBuilder.buildDiagnosisTestResultRetrievalPeriodicWork() ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER) } /** @@ -258,7 +212,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { workManager.enqueueUniquePeriodicWork( WorkType.BACKGROUND_NOISE_PERIODIC_WORK.uniqueName, ExistingPeriodicWorkPolicy.REPLACE, - buildBackgroundNoisePeriodicWork() + backgroundWorkBuilder.buildBackgroundNoisePeriodicWork() ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_PERIODIC_WORK) } /** @@ -273,7 +227,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { workManager.enqueueUniqueWork( WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.uniqueName, ExistingWorkPolicy.REPLACE, - buildBackgroundNoiseOneTimeWork() + backgroundWorkBuilder.buildBackgroundNoiseOneTimeWork() ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_ONE_TIME_WORK) } /** @@ -293,11 +247,4 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { { Timber.d("All work with tag ${workTag.tag} canceled.") }, { it.run() } ).also { Timber.d("Canceling all work with tag ${workTag.tag}") } - - /** - * Log work active status - */ - private fun logWorkActiveStatus(tag: String, active: Boolean) { - Timber.d("Work type $tag is active: $active") - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt deleted file mode 100644 index 9085a1645..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.rki.coronawarnapp.worker - -import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings -import javax.inject.Inject - -@Suppress("UnnecessaryAbstractClass") -abstract class BackgroundWorkSchedulerBase { - @Inject internal lateinit var submissionSettings: SubmissionSettings - @Inject internal lateinit var tracingSettings: TracingSettings -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt deleted file mode 100644 index 5a59a5b2f..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt +++ /dev/null @@ -1,60 +0,0 @@ -package de.rki.coronawarnapp.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.util.worker.InjectedWorkerFactory -import timber.log.Timber - -/** - * Periodic diagnosis key retrieval work - * Executes the scheduling of one time diagnosis key retrieval work - * - * @see BackgroundWorkScheduler - * @see DiagnosisKeyRetrievalOneTimeWorker - */ -class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor( - @Assisted val context: Context, - @Assisted workerParams: WorkerParameters -) : CoroutineWorker(context, workerParams) { - - /** - * @see BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - * @see BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - */ - override suspend fun doWork(): Result { - Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") - - var result = Result.success() - try { - BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() - } catch (e: Exception) { - Timber.tag(TAG).w( - e, - "$id: Error during BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()." - ) - - if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.tag(TAG).w(e, "$id: Retry attempts exceeded.") - - return Result.failure() - } else { - Timber.tag(TAG).d(e, "$id: Retrying.") - result = Result.retry() - } - } - - Timber.tag(TAG).d("$id: doWork() finished with %s", result) - return result - } - - @AssistedFactory - interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalPeriodicWorker> - - companion object { - private val TAG = DiagnosisKeyRetrievalPeriodicWorker::class.java.simpleName - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt index cd80fd710..348777db1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt @@ -17,7 +17,6 @@ import de.rki.coronawarnapp.util.TimeAndDateExtensions import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop import timber.log.Timber /** @@ -34,6 +33,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private val submissionService: SubmissionService, private val timeStamper: TimeStamper, private val tracingSettings: TracingSettings, + private val backgroundWorkScheduler: BackgroundWorkScheduler, ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { @@ -42,7 +42,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling") - BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() + backgroundWorkScheduler.scheduleDiagnosisTestResultPeriodicWork() Timber.tag(TAG).d("$id Rescheduled background worker") return Result.failure() @@ -117,7 +117,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private fun stopWorker() { tracingSettings.initialPollingForTestResultTimeStamp = 0L - BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() + backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() Timber.tag(TAG).d("$id: Background worker stopped") } diff --git a/Corona-Warn-App/src/main/res/drawable/ic_print.xml b/Corona-Warn-App/src/main/res/drawable/ic_print.xml index 1f420bdc3..90691b5cc 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_print.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_print.xml @@ -3,7 +3,7 @@ android:height="18dp" android:viewportWidth="20" android:viewportHeight="18"> - <path - android:pathData="M17,5H3C1.34,5 0,6.34 0,8V14H4V18H16V14H20V8C20,6.34 18.66,5 17,5ZM14,16H6V11H14V16ZM17,9C16.45,9 16,8.55 16,8C16,7.45 16.45,7 17,7C17.55,7 18,7.45 18,8C18,8.55 17.55,9 17,9ZM16,0H4V4H16V0Z" - android:fillColor="#757575"/> + <path + android:pathData="M17,5H3C1.34,5 0,6.34 0,8V14H4V18H16V14H20V8C20,6.34 18.66,5 17,5ZM14,16H6V11H14V16ZM17,9C16.45,9 16,8.55 16,8C16,7.45 16.45,7 17,7C17.55,7 18,7.45 18,8C18,8.55 17.55,9 17,9ZM16,0H4V4H16V0Z" + android:fillColor="#757575" /> </vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_share.xml b/Corona-Warn-App/src/main/res/drawable/ic_share.xml index 2fde22226..2ed6672d4 100644 --- a/Corona-Warn-App/src/main/res/drawable/ic_share.xml +++ b/Corona-Warn-App/src/main/res/drawable/ic_share.xml @@ -3,7 +3,7 @@ android:height="20dp" android:viewportWidth="18" android:viewportHeight="20"> - <path - android:pathData="M15,14.08C14.24,14.08 13.56,14.38 13.04,14.85L5.91,10.7C5.96,10.47 6,10.24 6,10C6,9.76 5.96,9.53 5.91,9.3L12.96,5.19C13.5,5.69 14.21,6 15,6C16.66,6 18,4.66 18,3C18,1.34 16.66,0 15,0C13.34,0 12,1.34 12,3C12,3.24 12.04,3.47 12.09,3.7L5.04,7.81C4.5,7.31 3.79,7 3,7C1.34,7 0,8.34 0,10C0,11.66 1.34,13 3,13C3.79,13 4.5,12.69 5.04,12.19L12.16,16.35C12.11,16.56 12.08,16.78 12.08,17C12.08,18.61 13.39,19.92 15,19.92C16.61,19.92 17.92,18.61 17.92,17C17.92,15.39 16.61,14.08 15,14.08Z" - android:fillColor="#757575"/> + <path + android:pathData="M15,14.08C14.24,14.08 13.56,14.38 13.04,14.85L5.91,10.7C5.96,10.47 6,10.24 6,10C6,9.76 5.96,9.53 5.91,9.3L12.96,5.19C13.5,5.69 14.21,6 15,6C16.66,6 18,4.66 18,3C18,1.34 16.66,0 15,0C13.34,0 12,1.34 12,3C12,3.24 12.04,3.47 12.09,3.7L5.04,7.81C4.5,7.31 3.79,7 3,7C1.34,7 0,8.34 0,10C0,11.66 1.34,13 3,13C3.79,13 4.5,12.69 5.04,12.19L12.16,16.35C12.11,16.56 12.08,16.78 12.08,17C12.08,18.61 13.39,19.92 15,19.92C16.61,19.92 17.92,18.61 17.92,17C17.92,15.39 16.61,14.08 15,14.08Z" + android:fillColor="#757575" /> </vector> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt index 0ad2d0822..7c0754dd1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt @@ -17,8 +17,8 @@ import de.rki.coronawarnapp.contactdiary.util.ContactDiaryData import de.rki.coronawarnapp.contactdiary.util.mockStringsForContactDiaryExporterTests import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt index e869c2869..0298f03ae 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt @@ -48,7 +48,6 @@ class CheckInRepositoryTest : BaseTest() { val checkIn = CheckIn( id = 1L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "brothers birthday", @@ -77,7 +76,6 @@ class CheckInRepositoryTest : BaseTest() { CheckIn( id = 0L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "brothers birthday", @@ -98,7 +96,6 @@ class CheckInRepositoryTest : BaseTest() { TraceLocationCheckInEntity( id = 0L, traceLocationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), - traceLocationIdHashBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), version = 1, type = 2, description = "brothers birthday", @@ -143,7 +140,6 @@ class CheckInRepositoryTest : BaseTest() { TraceLocationCheckInEntity( id = 1L, traceLocationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), - traceLocationIdHashBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(), version = 1, type = 2, description = "sisters birthday", @@ -164,7 +160,6 @@ class CheckInRepositoryTest : BaseTest() { CheckIn( id = 1L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "sisters birthday", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt index bafd28dd1..6439da3b9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt @@ -12,7 +12,6 @@ class CheckInTransmissionRiskLevelTest : BaseTest() { private val checkIn = CheckIn( id = 1L, traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), - traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(), version = 1, type = 2, description = "restaurant_1", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt index 14d50535d..53cf1f3f4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt @@ -42,7 +42,6 @@ class CheckInsTransformerTest : BaseTest() { private val checkIn1 = CheckIn( id = 1L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "restaurant_1", @@ -67,7 +66,6 @@ class CheckInsTransformerTest : BaseTest() { private val checkIn2 = CheckIn( id = 2L, traceLocationId = "traceLocationId2".encode(), - traceLocationIdHash = "traceLocationIdHash2".encode(), version = 1, type = 2, description = "restaurant_2", @@ -87,7 +85,6 @@ class CheckInsTransformerTest : BaseTest() { private val checkIn3 = CheckIn( id = 3L, traceLocationId = "traceLocationId3".encode(), - traceLocationIdHash = "traceLocationIdHash3".encode(), version = 1, type = 3, description = "restaurant_3", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt index dc037d1ca..8c76d64b9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt @@ -16,7 +16,6 @@ class CheckInSplitterTest : BaseTest() { private val defaultCheckIn = CheckIn( id = 1L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "Restaurant", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt index bc1c62af2..f92345e30 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt @@ -1,50 +1,58 @@ package de.rki.coronawarnapp.eventregistration.events import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.qrCodePayload +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocationIdHash import de.rki.coronawarnapp.eventregistration.checkins.qrcode.traceLocation import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant import io.kotest.matchers.shouldBe +import okio.ByteString import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.decodeHex +import okio.ByteString.Companion.toByteString +import org.joda.time.Instant import org.junit.jupiter.api.Test import testhelpers.BaseTest +@Suppress("MaxLineLength") class TraceLocationIdTest : BaseTest() { @Test fun `locationId from qrCodePayloadBase64 - 1`() { val qrCodePayloadBase64 = - "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKo" + - "ZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxe" + - "uFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" + "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg==" val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( qrCodePayloadBase64.decodeBase64()!!.toByteArray() ) - qrCodePayload.traceLocation().locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + qrCodePayload.traceLocation().apply { + locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" + } } @Test fun `locationId from qrCodePayloadBase64 - 2`() { val qrCodePayloadBase64 = - "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIR" + - "cyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" + "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK" val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( qrCodePayloadBase64.decodeBase64()!!.toByteArray() ) - qrCodePayload.traceLocation().locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + qrCodePayload.traceLocation().apply { + locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" + } } @Test fun `locationId from traceLocation - 1`() { val traceLocation = TraceLocation( - id = 1, type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER, description = "My Birthday Party", address = "at my place", startDate = 2687955L.secondsToInstant(), endDate = 2687991L.secondsToInstant(), defaultCheckInLengthInMinutes = null, - cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, - cnPublicKey = PUB_KEY, + cryptographicSeed = "MTIzNA==".decodeBase64()!!, + cnPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==", version = TraceLocation.VERSION ) traceLocation.locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0=" @@ -53,25 +61,185 @@ class TraceLocationIdTest : BaseTest() { @Test fun `locationId from traceLocation - 2`() { val traceLocation = TraceLocation( - id = 2, type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER, description = "Icecream Shop", address = "Main Street 1", startDate = null, endDate = null, defaultCheckInLengthInMinutes = 10, - cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!, - cnPublicKey = PUB_KEY, + cryptographicSeed = "MTIzNA==".decodeBase64()!!, + cnPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==", version = TraceLocation.VERSION ) traceLocation.locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ=" } - companion object { - private const val CRYPTOGRAPHIC_SEED = "MTIzNA==" - private const val PUB_KEY = - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0z" + - "K7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==" + /** + * Match server calculation + * https://github.com/corona-warn-app/cwa-server/blob/5ce7d27a74fbf4f2ed560772f97ac17e2189ad33/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/TraceTimeIntervalWarningServiceTest.java#L141 + */ + @Test + fun `test trace location hash generation`() { + val locationId = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d" + val locationIdByte: ByteString = locationId.decodeHex() + val hashedLocationId: ByteString = locationIdByte.sha256() + + val expectedLocationIdHash = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230" + + hashedLocationId.hex() shouldBe expectedLocationIdHash + hashedLocationId shouldBe expectedLocationIdHash.decodeHex() + } + + @Test + fun `test trace location hash generation - 2`() { + val id: TraceLocationId = "1b02111da7c0799df6ad67deb7b397bdfb07e63da0fdea30fae335762826e34f".decodeHex() + id.toTraceLocationIdHash().hex() shouldBe "394db434a2e9c2ca9f32eed266d30bc037b4314cb3d53249fada68de45450cbb" + } + + @Test + fun `test trace location hash generation - 3`() { + val id: TraceLocationId = "14c7a20ed81ebabdc32c8521382c56b851af15ccd8d13c86cd91a0620e78d664".decodeHex() + id.toTraceLocationIdHash().hex() shouldBe "852475fa271a29c67ad85578bb86ff4922dabf9f2b081353e1b5cdf99442889d" + } + + @Test + fun `turn location ID into location ID hash`() { + val traceLocationIdHex = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d" + val expectedLocationIdHashHex = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230" + + val traceLocationId: TraceLocationId = traceLocationIdHex.decodeHex() + traceLocationId.toTraceLocationIdHash() shouldBe expectedLocationIdHashHex.decodeHex() + } + + @Test + fun `mock server test data - 1`() { + val qrCodePayloadBase64 = + "CAESKAgBEhRBcHBsZSBDb21wdXRlciwgSW5jLhoOMTk1OCBGb2hlIFJvYWQadggBEmA4xNrp5hKJoO_yVbXfF1gS8Yc5nURhOIVLG3nUcSg8IPsI2e8JSIhg-FrHUymQ3RR80KUKb1lZjLQkfTUINUP16r6-jFDURwUlCQQi6NXCgI0rQw0a4MrVrKMbF4NzhQMaENXiVYke5XY0HddkDmj-3HYiBwgBEAQYqQE" + val expectedLocationIdHex = "fc925439f45417a14403b25c95fdc6d8711653f8aa08c0d0967bd30a6348c7fc" + val expectedLocationIdBase64 = "/JJUOfRUF6FEA7Jclf3G2HEWU/iqCMDQlnvTCmNIx/w=" + + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + + // Payload -> locationId (direct from raw) + val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray() + val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes + totalByteSequence.toByteString().sha256().base64() shouldBe expectedLocationIdBase64 + + // TraceLocation -> locationId (manual reconstruction) + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE, + description = "Apple Computer, Inc.", + address = "1958 Fohe Road", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 169, + cryptographicSeed = "1eJViR7ldjQd12QOaP7cdg==".decodeBase64()!!, + cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD", + version = TraceLocation.VERSION + ).apply { + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId + qrCodePayload.traceLocation().apply { + description shouldBe "Apple Computer, Inc." + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // ByteArrays actually match, we construct the data the same way. + qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray() + } + + @Test + fun `mock server test data - 2`() { + val qrCodePayloadBase64 = + "CAESKQgBEhRXYXN0ZSBNYW5hZ2VtZW50IEluYxoPMzc5IE96Zm9jIE1hbm9yGnYIARJgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGhCUpoAjtgZJPU4suuQZ6dVUIgcIARAIGJ4E" + val expectedLocationIdHex = "2c4e7a3be61004ed952cc189e85039e01be65f3d82439c8c7fe0f23b12ffa523" + val expectedLocationIdBase64 = "LE56O+YQBO2VLMGJ6FA54BvmXz2CQ5yMf+DyOxL/pSM=" + + // Payload -> locationId (direct from raw) + val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray() + val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes + totalByteSequence.toByteString().sha256().base64() shouldBe expectedLocationIdBase64 + + // TraceLocation -> locationId (manual reconstruction) + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_PUBLIC_BUILDING, + description = "Waste Management Inc", + address = "379 Ozfoc Manor", + startDate = null, + endDate = null, + defaultCheckInLengthInMinutes = 542, + cryptographicSeed = "lKaAI7YGST1OLLrkGenVVA==".decodeBase64()!!, + cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD", + version = TraceLocation.VERSION + ).apply { + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + + qrCodePayload.traceLocation().apply { + description shouldBe "Waste Management Inc" + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // ByteArrays actually match, we construct the data the same way. + qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray() + } + + @Test + fun `mock server test data - 3`() { + val qrCodePayloadBase64 = + "CAESMggBEg9NZXRhbHMgVVNBIEluYy4aETk2NiBEaXZ1ZCBIZWlnaHRzKLD6qYMGMNDZtoMGGnYIARJgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGhD_mjsnT4b74d8M1P4cUflpIgcIARAKGOkJ" + val expectedLocationIdHex = "7051e04206ef9caf3a3165e82d0fec4cfe7ade770a2e01d9c0e456add760934d" + val expectedLocationIdBase64 = "cFHgQgbvnK86MWXoLQ/sTP563ncKLgHZwORWrddgk00=" + + // Payload -> locationId (direct from raw) + val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray() + val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes + totalByteSequence.toByteString().sha256().apply { + base64() shouldBe expectedLocationIdBase64 + } + + // TraceLocation -> locationId (manual reconstruction) + val traceLocation = TraceLocation( + type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY, + description = "Metals USA Inc.", + address = "966 Divud Heights", + startDate = Instant.ofEpochSecond(1617591600), + endDate = Instant.ofEpochSecond(1617800400), + defaultCheckInLengthInMinutes = 1257, + cryptographicSeed = "/5o7J0+G++HfDNT+HFH5aQ==".decodeBase64()!!, + cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD", + version = TraceLocation.VERSION + ).apply { + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId + val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom( + qrCodePayloadBase64.decodeBase64()!!.toByteArray() + ) + + qrCodePayload.traceLocation().apply { + description shouldBe "Metals USA Inc." + locationId.base64() shouldBe expectedLocationIdBase64 + locationId.hex() shouldBe expectedLocationIdHex + } + + // ByteArrays actually match, we construct the data the same way. + qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt index 0dc18d477..c76619621 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt @@ -71,7 +71,6 @@ internal class CheckInCleanerTest : BaseTest() { private fun createCheckIn(checkOutDate: Instant) = CheckIn( traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt index e420e844c..04fbb3a38 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt @@ -25,7 +25,6 @@ class CheckOutHandlerTest : BaseTest() { private val testCheckIn = CheckIn( id = 42L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 1, description = "Restaurant", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt index 41c1f47c9..4958a8cb2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt @@ -38,7 +38,6 @@ class AutoCheckOutTest : BaseTest() { private val baseCheckin = CheckIn( id = 0L, traceLocationId = "traceLocationId1".encode(), - traceLocationIdHash = "traceLocationIdHash1".encode(), version = 1, type = 2, description = "brothers birthday", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt index 92c141796..f87f49945 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt @@ -37,7 +37,7 @@ class TraceLocationNotificationsTest : BaseTest() { } } - fun createInstance() = TraceLocationNotifications( + fun createInstance() = PresenceTracingNotifications( context = context, apiLevel = apiLevel, notificationManagerCompat = notificationManager diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt deleted file mode 100644 index 1a3dcc048..000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt +++ /dev/null @@ -1,255 +0,0 @@ -package de.rki.coronawarnapp.presencetracing.risk - -import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository -import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import de.rki.coronawarnapp.util.debug.measureTime -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.just -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runBlockingTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import testhelpers.BaseTest -import testhelpers.TestDispatcherProvider -import timber.log.Timber - -class CheckInWarningMatcherTest : BaseTest() { - - @MockK lateinit var checkInsRepository: CheckInRepository - @MockK lateinit var traceTimeIntervalWarningRepository: TraceTimeIntervalWarningRepository - @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - coEvery { presenceTracingRiskRepository.reportSuccessfulCalculation(any()) } just Runs - coEvery { presenceTracingRiskRepository.deleteAllMatches() } just Runs - coEvery { presenceTracingRiskRepository.deleteStaleData() } just Runs - // TODO tests - coEvery { presenceTracingRiskRepository.deleteMatchesOfPackage(any()) } just Runs - coEvery { presenceTracingRiskRepository.markPackageProcessed(any()) } just Runs - } - - @Test - fun `reports new matches`() { - val checkIn1 = createCheckIn( - id = 2L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T10:15+01:00", - endDateStr = "2021-03-04T10:17+01:00" - ) - val checkIn2 = createCheckIn( - id = 3L, - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startDateStr = "2021-03-04T09:15+01:00", - endDateStr = "2021-03-04T10:12+01:00" - ) - - val warning1 = createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - val warning2 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2)) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf(warning1, warning2) - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(any()) } - coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `report empty list if no matches found`() { - val checkIn1 = createCheckIn( - id = 2L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T10:15+01:00", - endDateStr = "2021-03-04T10:17+01:00" - ) - val checkIn2 = createCheckIn( - id = 3L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T09:15+01:00", - endDateStr = "2021-03-04T10:12+01:00" - ) - - val warning1 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - val warning2 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2)) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf(warning1, warning2) - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) } - coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `report empty list if package is empty`() { - val checkIn1 = createCheckIn( - id = 2L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T10:15+01:00", - endDateStr = "2021-03-04T10:17+01:00" - ) - val checkIn2 = createCheckIn( - id = 3L, - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", - startDateStr = "2021-03-04T09:15+01:00", - endDateStr = "2021-03-04T10:12+01:00" - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2)) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf() - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) } - coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `deletes all matches if no check-ins`() { - - val warning1 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - val warning2 = createWarning( - traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - - every { checkInsRepository.allCheckIns } returns flowOf(listOf()) - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return listOf(warning1, warning2) - } - - override val warningPackageId: String - get() = "id" - } - - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - createInstance().execute() - coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) } - coVerify(exactly = 1) { presenceTracingRiskRepository.deleteAllMatches() } - } - } - - @Test - fun `test mass data`() { - val checkIns = (1L..100L).map { - createCheckIn( - id = it, - traceLocationId = it.toString(), - startDateStr = "2021-03-04T09:50+01:00", - endDateStr = "2021-03-04T10:05:15+01:00" - ) - } - val warnings = (1L..1000L).map { - createWarning( - traceLocationId = it.toString(), - startIntervalDateStr = "2021-03-04T10:00+01:00", - period = 6, - transmissionRiskLevel = 8 - ) - } - - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { - return warnings - } - - override val warningPackageId: String - get() = "id" - } - - every { checkInsRepository.allCheckIns } returns flowOf(checkIns) - every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage)) - - runBlockingTest { - measureTime( - { Timber.d("Time to compare 200 checkIns with 1000 warnings: $it millis") }, - { createInstance().execute() } - ) - } - } - - private fun createInstance() = CheckInWarningMatcher( - checkInsRepository, - traceTimeIntervalWarningRepository, - presenceTracingRiskRepository, - TestDispatcherProvider() - ) -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt new file mode 100644 index 000000000..b8beb8b11 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt @@ -0,0 +1,255 @@ +package de.rki.coronawarnapp.presencetracing.risk.calculation + +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.TestDispatcherProvider + +class CheckInWarningMatcherTest : BaseTest() { + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + private fun createInstance() = CheckInWarningMatcher( + TestDispatcherProvider() + ) + + @Test + fun `reports new matches`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warning1 = createWarning( + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warning2 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf(warning1, warning2) + } + + override val packageId: WarningPackageId + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage) + ) + result.apply { + successful shouldBe true + processedPackages.single().warningPackage shouldBe warningPackage + processedPackages.single().apply { + overlaps.size shouldBe 2 + overlaps.any { it.checkInId == 2L } shouldBe true + overlaps.any { it.checkInId == 3L } shouldBe true + } + } + } + } + + @Test + fun `report empty list if no matches found`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warning1 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warning2 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf(warning1, warning2) + } + + override val packageId: WarningPackageId + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage), + ) + + result.apply { + successful shouldBe true + processedPackages.single().warningPackage shouldBe warningPackage + processedPackages.single().overlaps.size shouldBe 0 + } + } + } + + @Test + fun `report empty list if package is empty`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf() + } + + override val packageId: WarningPackageId + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + warningPackages = listOf(warningPackage), + checkIns = listOf(checkIn1, checkIn2), + ) + + result.apply { + successful shouldBe true + processedPackages.single().warningPackage shouldBe warningPackage + processedPackages.single().overlaps.size shouldBe 0 + } + } + } + + @Test + fun `report failure if matching throws exception`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + throw Exception() + } + + override val packageId: String + get() = "id" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage), + ) + + result.apply { + successful shouldBe false + processedPackages shouldBe emptyList() + } + } + } + + @Test + fun `partial processing is possible on exceptions`() { + val checkIn1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val checkIn2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val warningPackage1 = object : TraceWarningPackage { + override suspend fun extractWarnings() = throw Exception() + + override val packageId: WarningPackageId = "id1" + } + val warningPackage2 = object : TraceWarningPackage { + override suspend fun extractWarnings() = listOf( + createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + ) + + override val packageId: WarningPackageId = "id2" + } + + runBlockingTest { + val result = createInstance().process( + checkIns = listOf(checkIn1, checkIn2), + warningPackages = listOf(warningPackage1, warningPackage2), + ) + + result.apply { + successful shouldBe false + processedPackages.single().apply { + warningPackage shouldBe warningPackage2 + overlaps.single().checkInId shouldBe checkIn2.id + } + } + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt similarity index 79% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt index cc9d5e580..3ddd887cd 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt @@ -1,6 +1,7 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation -import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runBlockingTest @@ -36,11 +37,12 @@ class FindMatchesTest { period = 6, transmissionRiskLevel = 8 ) - val warningPackage = object : TraceTimeIntervalWarningPackage { - override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + val warningPackage = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { return listOf(warning1, warning2) } - override val warningPackageId: String + + override val packageId: WarningPackageId get() = "id" } runBlockingTest { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt similarity index 72% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt index fa1af775d..67cdb5a05 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt @@ -1,24 +1,51 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation -import com.google.protobuf.ByteString.copyFromUtf8 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning -import de.rki.coronawarnapp.util.HashExtensions.toSHA256 +import de.rki.coronawarnapp.util.toOkioByteString +import de.rki.coronawarnapp.util.toProtoByteString import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.encode import org.joda.time.Duration import org.joda.time.Instant -import org.junit.Test +import org.junit.jupiter.api.Test import testhelpers.BaseTest class OverlapTest : BaseTest() { - val id = "id" + private val id = "id" + + private val locationId = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d" + private val locationIdHash = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230" + + @Test + fun `test helper method createCheckIn`() { + val checkIn = createCheckIn( + traceLocationId = locationId, + startDateStr = "2021-03-04T09:30+01:00", + endDateStr = "2021-03-04T09:45+01:00" + ) + + checkIn.traceLocationId shouldBe locationId.decodeHex() + checkIn.traceLocationIdHash shouldBe locationIdHash.decodeHex() + } + + @Test + fun `test helper method createWarning`() { + val warning = createWarning( + traceLocationId = locationId, + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + warning.locationIdHash.toOkioByteString().hex() shouldBe locationIdHash + } @Test fun `returns null if guids do not match`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T09:45+01:00" ).calculateOverlap( @@ -35,12 +62,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in precedes warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T09:45+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -52,12 +79,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in is preceded by warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T11:15+01:00", endDateStr = "2021-03-04T11:20+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -69,12 +96,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in meets warning at the start`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T10:00+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -86,12 +113,12 @@ class OverlapTest : BaseTest() { @Test fun `returns null if check-in meets warning at the end`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T11:00+01:00", endDateStr = "2021-03-04T11:10+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -103,12 +130,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in overlaps warning at the start`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:30+01:00", endDateStr = "2021-03-04T10:12+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -120,12 +147,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in overlaps warning at the end`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:45+01:00", endDateStr = "2021-03-04T11:12+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -137,12 +164,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in starts warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:00+01:00", endDateStr = "2021-03-04T10:13+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -154,12 +181,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in during warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:15+01:00", endDateStr = "2021-03-04T10:17+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -171,12 +198,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in finishes warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:30+01:00", endDateStr = "2021-03-04T11:00+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -188,12 +215,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap if check-in equals warning`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T10:00+01:00", endDateStr = "2021-03-04T11:00+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -205,12 +232,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap after rounding (up)`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:50+01:00", endDateStr = "2021-03-04T10:05:45+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -222,12 +249,12 @@ class OverlapTest : BaseTest() { @Test fun `returns overlap after rounding (down)`() { createCheckIn( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startDateStr = "2021-03-04T09:50+01:00", endDateStr = "2021-03-04T10:05:15+01:00" ).calculateOverlap( createWarning( - traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + traceLocationId = locationId, startIntervalDateStr = "2021-03-04T10:00+01:00", period = 6, transmissionRiskLevel = 8 @@ -244,8 +271,7 @@ fun createCheckIn( endDateStr: String ) = CheckIn( id = id, - traceLocationId = traceLocationId.toSHA256().encode(), - traceLocationIdHash = traceLocationId.toSHA256().encode(), + traceLocationId = traceLocationId.decodeHex(), version = 1, type = 2, description = "My birthday party", @@ -266,8 +292,8 @@ fun createWarning( startIntervalDateStr: String, period: Int, transmissionRiskLevel: Int -) = TraceWarning.TraceTimeIntervalWarning.newBuilder() - .setLocationIdHash(copyFromUtf8(traceLocationId.toSHA256())) +): TraceWarning.TraceTimeIntervalWarning = TraceWarning.TraceTimeIntervalWarning.newBuilder() + .setLocationIdHash(traceLocationId.decodeHex().sha256().toProtoByteString()) .setPeriod(period) .setStartIntervalNumber((Duration(Instant.parse(startIntervalDateStr).millis).standardMinutes / 10).toInt()) .setTransmissionRiskLevel(transmissionRiskLevel) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt similarity index 98% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt index 17c8b74c3..f59cd1622 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt similarity index 98% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt index e5334348d..dd07536e3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.presencetracing.risk +package de.rki.coronawarnapp.presencetracing.risk.calculation import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt new file mode 100644 index 000000000..e44656741 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt @@ -0,0 +1,223 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher +import de.rki.coronawarnapp.presencetracing.risk.calculation.createCheckIn +import de.rki.coronawarnapp.presencetracing.risk.calculation.createWarning +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId +import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage +import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.comparables.shouldBeLessThan +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +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 +import java.io.IOException + +class PresenceTracingWarningTaskTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var syncTool: TraceWarningPackageSyncTool + @MockK lateinit var checkInWarningMatcher: CheckInWarningMatcher + @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository + @MockK lateinit var traceWarningRepository: TraceWarningRepository + @MockK lateinit var checkInsRepository: CheckInRepository + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(9000) + coEvery { syncTool.syncPackages() } returns mockk() + coEvery { checkInWarningMatcher.process(any(), any()) } answers { + CheckInWarningMatcher.Result( + successful = true, + processedPackages = listOf( + CheckInWarningMatcher.MatchesPerPackage( + warningPackage = WARNING_PKG, + overlaps = listOf(mockk()) + ) + ) + ) + } + + traceWarningRepository.apply { + coEvery { unprocessedWarningPackages } returns flowOf(listOf(WARNING_PKG)) + coEvery { markPackagesProcessed(any()) } just Runs + } + + coEvery { checkInsRepository.allCheckIns } returns flowOf(listOf(CHECKIN_1, CHECKIN_2)) + + presenceTracingRiskRepository.apply { + coEvery { deleteAllMatches() } just Runs + coEvery { deleteStaleData() } just Runs + coEvery { reportCalculation(any(), any()) } just Runs + } + } + + private fun createInstance() = PresenceTracingWarningTask( + timeStamper = timeStamper, + syncTool = syncTool, + checkInWarningMatcher = checkInWarningMatcher, + presenceTracingRiskRepository = presenceTracingRiskRepository, + traceWarningRepository = traceWarningRepository, + checkInsRepository = checkInsRepository, + ) + + @Test + fun `happy path, match result is reported successfully`() = runBlockingTest { + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + traceWarningRepository.unprocessedWarningPackages + + checkInWarningMatcher.process(any(), any()) + + presenceTracingRiskRepository.reportCalculation( + successful = true, + overlaps = any() + ) + traceWarningRepository.markPackagesProcessed(listOf(WARNING_PKG.packageId)) + } + } + + @Test + fun `overall task errors lead to a reported failed calculation`() = runBlockingTest { + coEvery { syncTool.syncPackages() } throws IOException("Unexpected") + + shouldThrow<IOException> { + createInstance().run(mockk()) + } + + coVerify { + presenceTracingRiskRepository.reportCalculation( + successful = false, + overlaps = emptyList() + ) + } + } + + @Test + fun `there are no check-ins to match against`() = runBlockingTest { + coEvery { checkInsRepository.allCheckIns } returns flowOf(emptyList()) + + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + + presenceTracingRiskRepository.deleteAllMatches() + presenceTracingRiskRepository.reportCalculation(successful = true) + } + } + + @Test + fun `there are no warning packages to process`() = runBlockingTest { + coEvery { traceWarningRepository.unprocessedWarningPackages } returns flowOf(emptyList()) + + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + traceWarningRepository.unprocessedWarningPackages + + presenceTracingRiskRepository.reportCalculation(successful = true) + } + } + + @Test + fun `report failure if matching throws exception`() = runBlockingTest { + coEvery { checkInWarningMatcher.process(any(), any()) } throws IllegalArgumentException() + shouldThrow<IllegalArgumentException> { + createInstance().run(mockk()) shouldNotBe null + } + + coVerifySequence { + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.allCheckIns + traceWarningRepository.unprocessedWarningPackages + + checkInWarningMatcher.process(any(), any()) + + presenceTracingRiskRepository.reportCalculation( + successful = false, + overlaps = any() + ) + } + + coVerify(exactly = 0) { + traceWarningRepository.markPackagesProcessed(any()) + } + } + + @Test + fun `task timeout is constrained to less than 9min`() { + // Worker execution time + val maxDuration = Duration.standardMinutes(9).plus(1) + PresenceTracingWarningTask.Config().executionTimeout shouldBeLessThan maxDuration + } + + companion object { + val CHECKIN_1 = createCheckIn( + id = 2L, + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startDateStr = "2021-03-04T10:15+01:00", + endDateStr = "2021-03-04T10:17+01:00" + ) + val CHECKIN_2 = createCheckIn( + id = 3L, + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startDateStr = "2021-03-04T09:15+01:00", + endDateStr = "2021-03-04T10:12+01:00" + ) + + val WARNING_1 = createWarning( + traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val WARNING_2 = createWarning( + traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060", + startIntervalDateStr = "2021-03-04T10:00+01:00", + period = 6, + transmissionRiskLevel = 8 + ) + + val WARNING_PKG = object : TraceWarningPackage { + override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> { + return listOf(WARNING_1, WARNING_2) + } + + override val packageId: WarningPackageId + get() = "id" + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt new file mode 100644 index 000000000..dee16970f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt @@ -0,0 +1,95 @@ +package de.rki.coronawarnapp.presencetracing.risk.execution + +import android.content.Context +import androidx.work.ListenableWorker +import androidx.work.WorkerParameters +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.TaskRequest +import de.rki.coronawarnapp.task.TaskState +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +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.mockkStatic +import io.mockk.slot +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PresenceTracingWarningWorkerTest : BaseTest() { + + @RelaxedMockK lateinit var workerParams: WorkerParameters + @MockK lateinit var context: Context + @MockK lateinit var taskController: TaskController + @MockK(relaxed = true) lateinit var taskResult: TaskState + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkStatic("de.rki.coronawarnapp.task.TaskControllerExtensionsKt") + + coEvery { taskController.submitBlocking(any()) } returns taskResult + + taskResult.apply { + every { isSuccessful } returns true + every { error } returns null + } + } + + private fun createWorker() = PresenceTracingWarningWorker( + context = context, + workerParams = workerParams, + taskController = taskController + ) + + @Test + fun `worker runs task`() = runBlockingTest { + val slot = slot<TaskRequest>() + coEvery { taskController.submitBlocking(capture(slot)) } returns taskResult + + val worker = createWorker() + + worker.doWork() shouldBe ListenableWorker.Result.success() + + slot.captured shouldBe DefaultTaskRequest( + id = slot.captured.id, + arguments = slot.captured.arguments, + type = PresenceTracingWarningTask::class, + originTag = "PresenceTracingWarningWorker", + ) + } + + @Test + fun `task errors lead to retry`() = runBlockingTest { + every { taskResult.isSuccessful } returns false + every { taskResult.error } returns Exception() + + val worker = createWorker() + + worker.doWork() shouldBe ListenableWorker.Result.retry() + + coVerify { + taskController.submitBlocking(any()) + } + } + + @Test + fun `taskcontroller errors lead to retry`() = runBlockingTest { + coEvery { taskController.submitBlocking(any()) } throws Exception() + + val worker = createWorker() + + worker.doWork() shouldBe ListenableWorker.Result.retry() + + coVerify { + taskController.submitBlocking(any()) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt new file mode 100644 index 000000000..8bd44b79f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.presencetracing.warning.download + +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TraceWarningPackageDownloaderTest : BaseTest() { + + @Test + fun `errors during writeProtoBufToFile cause download to be marked as failed`() { + // TODO + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt index f1b801552..18acf7c63 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt @@ -2,9 +2,9 @@ package de.rki.coronawarnapp.risk import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import io.kotest.matchers.longs.shouldBeInRange import io.kotest.matchers.shouldBe import io.mockk.mockk +import org.joda.time.Duration import org.joda.time.Instant import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -31,11 +31,12 @@ class EwRiskLevelResultExtensionsTest : BaseTest() { emptyResults.tryLatestEwResultsWithDefaults().apply { lastCalculated.apply { riskState shouldBe RiskState.LOW_RISK - val now = Instant.now().millis - calculatedAt.millis shouldBeInRange ((now - 60 * 1000L)..now + 60 * 1000L) - } - lastSuccessfullyCalculated.apply { - riskState shouldBe RiskState.CALCULATION_FAILED + + calculatedAt.isAfter(Instant.EPOCH) shouldBe true + calculatedAt.isBefore(Instant.now().plus(Duration.standardHours(1))) shouldBe true + lastSuccessfullyCalculated.apply { + riskState shouldBe RiskState.CALCULATION_FAILED + } } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt index d391e5cc2..225b325d9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -1,11 +1,14 @@ package de.rki.coronawarnapp.risk +import android.app.Notification import android.content.Context +import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.GeneralNotifications +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK import de.rki.coronawarnapp.risk.RiskState.LOW_RISK @@ -15,6 +18,7 @@ import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.device.ForegroundState +import de.rki.coronawarnapp.util.notifications.setContentTextExpandable import io.kotest.matchers.shouldBe import io.mockk.Called import io.mockk.MockKAnnotations @@ -47,18 +51,24 @@ class RiskLevelChangeDetectorTest : BaseTest() { @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var testResultDonorSettings: TestResultDonorSettings + @MockK lateinit var builder: NotificationCompat.Builder + @MockK lateinit var notification: Notification + @BeforeEach fun setup() { MockKAnnotations.init(this) every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockFlowPreference(false) every { submissionSettings.isSubmissionSuccessful } returns false - every { foregroundState.isInForeground } returns flowOf(true) + every { foregroundState.isInForeground } returns flowOf(false) every { notificationManagerCompat.areNotificationsEnabled() } returns true every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns null + every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp = any() } just Runs + every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp } returns null + every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp = any() } just Runs every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp } returns null @@ -66,10 +76,17 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { testResultDonorSettings.riskLevelTurnedRedTime } returns mockFlowPreference(null) every { testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) - every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns flowOf(listOf()) + + every { builder.build() } returns notification + every { builder.setContentTitle(any()) } returns builder + every { builder.setContentTextExpandable(any()) } returns builder + every { builder.setContentText(any()) } returns builder + every { builder.setStyle(any()) } returns builder + every { notificationHelper.newBaseBuilder() } returns builder + every { context.getString(any()) } returns "" } - private fun createRiskLevel( + private fun createEwRiskLevel( riskState: RiskState, calculatedAt: Instant = Instant.EPOCH, ewAggregatedRiskResult: EwAggregatedRiskResult? = null @@ -83,6 +100,23 @@ class RiskLevelChangeDetectorTest : BaseTest() { override val daysWithEncounters: Int = 0 } + private fun createPtRiskLevel( + riskState: RiskState, + calculatedAt: Instant = Instant.EPOCH + ): PtRiskLevelResult = PtRiskLevelResult( + calculatedAt = calculatedAt, + riskState = riskState + ) + + private fun createCombinedRiskLevel( + riskState: RiskState, + calculatedAt: Instant = Instant.EPOCH, + ewAggregatedRiskResult: EwAggregatedRiskResult? = null + ): CombinedEwPtRiskLevelResult = CombinedEwPtRiskLevelResult( + ewRiskLevelResult = createEwRiskLevel(riskState, calculatedAt, ewAggregatedRiskResult), + ptRiskLevelResult = createPtRiskLevel(riskState, calculatedAt) + ) + private fun createInstance(scope: CoroutineScope) = RiskLevelChangeDetector( context = context, appScope = scope, @@ -99,7 +133,9 @@ class RiskLevelChangeDetectorTest : BaseTest() { @Test fun `nothing happens if there is only one result yet`() { - every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(listOf(createRiskLevel(LOW_RISK))) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(listOf(createEwRiskLevel(LOW_RISK))) runBlockingTest { val instance = createInstance(scope = this) @@ -110,19 +146,28 @@ class RiskLevelChangeDetectorTest : BaseTest() { coVerifySequence { notificationManagerCompat wasNot Called surveys wasNot Called + testResultDonorSettings wasNot Called } } } @Test - fun `no risklevel change, nothing should happen`() { + fun `no risk level change, nothing should happen`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(LOW_RISK), - createRiskLevel(LOW_RISK) + createEwRiskLevel(LOW_RISK), + createEwRiskLevel(LOW_RISK) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(LOW_RISK), + createCombinedRiskLevel(LOW_RISK) + ) + ) + runBlockingTest { val instance = createInstance(scope = this) instance.launch() @@ -136,17 +181,53 @@ class RiskLevelChangeDetectorTest : BaseTest() { } } - // TODO test if risk level change for combined risk triggers notification + @Test + fun `combined risk state change from HIGH to LOW triggers notification`() { + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( + listOf( + createEwRiskLevel(LOW_RISK) + ) + ) + + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + submissionSettings.isSubmissionSuccessful + foregroundState.isInForeground + notificationHelper.newBaseBuilder() + notificationHelper.sendNotification(any(), any()) + } + } + } @Test - fun `risklevel went from HIGH to LOW`() { + fun `combined risk state change from LOW to HIGH triggers notification`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), - createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + runBlockingTest { val instance = createInstance(scope = this) instance.launch() @@ -154,21 +235,46 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) + submissionSettings.isSubmissionSuccessful + foregroundState.isInForeground + notificationHelper.newBaseBuilder() + notificationHelper.sendNotification(any(), any()) } } } - // TODO test if risk level change for combined risk triggers notification + @Test + fun `risk level went from HIGH to LOW resets survey`() { + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( + listOf( + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + ) + ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) + } + } + } @Test - fun `risklevel went from LOW to HIGH`() { + fun `risk level went from LOW to HIGH`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) runBlockingTest { val instance = createInstance(scope = this) @@ -183,13 +289,15 @@ class RiskLevelChangeDetectorTest : BaseTest() { } @Test - fun `risklevel went from LOW to HIGH but it is has already been processed`() { + fun `risk level went from LOW to HIGH but it is has already been processed`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.EPOCH.plus(1) runBlockingTest { @@ -205,23 +313,57 @@ class RiskLevelChangeDetectorTest : BaseTest() { } } + @Test + fun `combined risk level went from LOW to HIGH but it is has already been processed`() { + every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( + listOf( + createEwRiskLevel(LOW_RISK) + ) + ) + + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf( + listOf( + createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp } returns Instant.EPOCH.plus(1) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + notificationManagerCompat wasNot Called + surveys wasNot Called + } + } + } + @Test fun `riskLevelTurnedRedTime is only set once`() { testResultDonorSettings.riskLevelTurnedRedTime.update { Instant.EPOCH.plus(1) } every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel( + createEwRiskLevel( INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(2), ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply { every { isIncreasedRisk() } returns true } ), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) + runBlockingTest { val instance = createInstance(scope = this) instance.launch() @@ -245,7 +387,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { fun `mostRecentDateWithHighOrLowRiskLevel is updated every time`() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel( + createEwRiskLevel( INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1), ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply { @@ -253,9 +395,11 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { isIncreasedRisk() } returns true } ), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) + every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns + flowOf(listOf(createCombinedRiskLevel(LOW_RISK))) runBlockingTest { val instance = createInstance(scope = this) @@ -267,7 +411,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf( listOf( - createRiskLevel( + createEwRiskLevel( INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1), ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply { @@ -275,7 +419,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { every { isIncreasedRisk() } returns false } ), - createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) ) ) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt index 099afd385..03c675890 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -1,7 +1,11 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.ewCalculatedAt import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testAggregatedRiskPerDateResult import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindow import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindowDaoWrapper @@ -14,6 +18,9 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.AggregatedR import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.Factory import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResultsDao +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -32,6 +39,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -70,8 +78,9 @@ class BaseRiskLevelStorageTest : BaseTest() { // TODO proper tests coEvery { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow() coEvery { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow() - coEvery { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow() + coEvery { presenceTracingRiskRepository.allEntries() } returns emptyFlow() coEvery { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow() + coEvery { presenceTracingRiskRepository.clearAllTables() } just Runs } private fun createInstance( @@ -104,7 +113,9 @@ class BaseRiskLevelStorageTest : BaseTest() { val instance = createInstance() val allEntries = instance.aggregatedRiskPerDateResultTables.allEntries() allEntries shouldBe testPersistedAggregatedRiskPerDateResultFlow - allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf(testAggregatedRiskPerDateResult) + allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf( + testAggregatedRiskPerDateResult + ) val aggregatedRiskPerDateResults = instance.ewDayRiskStates.first() aggregatedRiskPerDateResults shouldNotBe listOf(testPersistedAggregatedRiskPerDateResult) @@ -112,6 +123,19 @@ class BaseRiskLevelStorageTest : BaseTest() { } } + @Test + fun `ptDayRiskStates are returned from database`() { + val testPresenceTracingDayRiskFlow = flowOf(listOf(testPresenceTracingDayRisk)) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns testPresenceTracingDayRiskFlow + + runBlockingTest { + val instance = createInstance() + + val states = instance.ptDayRiskStates.first() + states shouldBe listOf(testPresenceTracingDayRisk) + } + } + @Test fun `exposureWindows are returned from database and mapped`() { val testDaoWrappers = flowOf(listOf(testExposureWindowDaoWrapper)) @@ -170,6 +194,142 @@ class BaseRiskLevelStorageTest : BaseTest() { } } + @Test + fun `latestCombinedEwPtRiskLevelResults works when 2 pt result and 1 ew result are available`() { + every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + val calculatedAt = ewCalculatedAt.plus(6000L) + every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(1000L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.INCREASED_RISK + ) + ) + ) + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first() + riskLevelResults.size shouldBe 2 + + riskLevelResults[0].calculatedAt shouldBe calculatedAt + riskLevelResults[0].riskState shouldBe RiskState.INCREASED_RISK + riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt + riskLevelResults[1].riskState shouldBe RiskState.INCREASED_RISK + + verify { + riskResultTables.latestEntries(2) + presenceTracingRiskRepository.latestEntries(2) + } + } + } + + @Test + fun `latestCombinedEwPtRiskLevelResults works when only one calc each is available`() { + every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + val calculatedAt = ewCalculatedAt.minus(400L) + every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ) + ) + ) + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first() + riskLevelResults.size shouldBe 2 + + riskLevelResults[0].calculatedAt shouldBe ewCalculatedAt + riskLevelResults[0].riskState shouldBe RiskState.INCREASED_RISK + + // result from the combination with initial ew low risk result + riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(400L) + riskLevelResults[1].riskState shouldBe RiskState.CALCULATION_FAILED + + verify { + riskResultTables.latestEntries(2) + presenceTracingRiskRepository.latestEntries(2) + } + } + } + + @Test + fun `latestCombinedEwPtRiskLevelResults works when two calc each are available`() { + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(200L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + val calculatedAt = ewCalculatedAt.minus(400L) + every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = calculatedAt.minus(400L), + presenceTracingDayRisk = null, + riskState = RiskState.LOW_RISK + ) + ) + ) + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first() + riskLevelResults.size shouldBe 2 + + riskLevelResults[0].calculatedAt shouldBe ewCalculatedAt + riskLevelResults[0].riskState shouldBe RiskState.LOW_RISK + riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(200L) + riskLevelResults[1].riskState shouldBe RiskState.INCREASED_RISK + + verify { + riskResultTables.latestEntries(2) + presenceTracingRiskRepository.latestEntries(2) + } + } + } + // This just tests the mapping, the correctness of the SQL statement is validated in an instrumentation test @Test fun `latestAndLastSuccessful with exposure windows are returned from database and mapped`() { @@ -190,7 +350,214 @@ class BaseRiskLevelStorageTest : BaseTest() { } @Test - fun `errors when storing risklevel result are rethrown`() = runBlockingTest { + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined`() { + val calculatedAt = ewCalculatedAt.plus(6000L) + every { presenceTracingRiskRepository.allEntries() } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = calculatedAt, + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(1000L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.INCREASED_RISK + ) + ) + ) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk)) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe calculatedAt + riskLevelResult.lastCalculated.riskState shouldBe RiskState.INCREASED_RISK + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe calculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.INCREASED_RISK + + verify { + presenceTracingRiskRepository.allEntries() + presenceTracingRiskRepository.presenceTracingDayRisk + } + } + } + + @Test + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined 2`() { + every { presenceTracingRiskRepository.allEntries() } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.plus(6000L), + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(1000L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.INCREASED_RISK + ) + ) + ) + + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = EwRiskLevelResult.FailureReason.UNKNOWN, + aggregatedRiskResult = null + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(2000L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 0, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(0), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk)) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt.plus(6000L) + riskLevelResult.lastCalculated.riskState shouldBe RiskState.CALCULATION_FAILED + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.INCREASED_RISK + + verify { + presenceTracingRiskRepository.allEntries() + presenceTracingRiskRepository.presenceTracingDayRisk + } + } + } + + @Test + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined 3`() { + every { presenceTracingRiskRepository.allEntries() } returns flowOf( + listOf( + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.plus(6000L), + presenceTracingDayRisk = null, + riskState = RiskState.CALCULATION_FAILED + ), + PtRiskLevelResult( + calculatedAt = ewCalculatedAt.minus(100L), + presenceTracingDayRisk = listOf(testPresenceTracingDayRisk), + riskState = RiskState.LOW_RISK + ) + ) + ) + + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = EwRiskLevelResult.FailureReason.UNKNOWN, + aggregatedRiskResult = null + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(200L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk)) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt.plus(6000L) + riskLevelResult.lastCalculated.riskState shouldBe RiskState.CALCULATION_FAILED + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.LOW_RISK + + verify { + presenceTracingRiskRepository.allEntries() + presenceTracingRiskRepository.presenceTracingDayRisk + } + } + } + + @Test + fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult works when no pt result yet`() { + + every { presenceTracingRiskRepository.allEntries() } returns flowOf(listOf()) + every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf()) + + val ewResultDao1 = PersistedRiskLevelResultDao( + id = "id1", + calculatedAt = ewCalculatedAt, + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + val ewResultDao2 = PersistedRiskLevelResultDao( + id = "id2", + calculatedAt = ewCalculatedAt.minus(200L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + + every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2)) + every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first() + + riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastCalculated.riskState shouldBe RiskState.LOW_RISK + riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt + riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.LOW_RISK + } + } + + @Test + fun `errors when storing risk level result are rethrown`() = runBlockingTest { coEvery { riskResultTables.insertEntry(any()) } throws IllegalStateException("No body expects the...") val instance = createInstance() shouldThrow<java.lang.IllegalStateException> { @@ -242,6 +609,14 @@ class BaseRiskLevelStorageTest : BaseTest() { @Test fun `clear works`() = runBlockingTest { createInstance().clear() - verify { database.clearAllTables() } + coVerify { + database.clearAllTables() + presenceTracingRiskRepository.clearAllTables() + } } } + +private val testPresenceTracingDayRisk = PresenceTracingDayRisk( + Instant.now().toLocalDateUtc(), + RiskState.INCREASED_RISK +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt index 824be93cc..a1099568a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt @@ -1,13 +1,17 @@ package de.rki.coronawarnapp.risk.storage -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel import io.kotest.matchers.shouldBe +import org.joda.time.Instant import org.joda.time.LocalDate import org.junit.jupiter.api.Test -import java.time.Instant class CombineRiskTest { @@ -26,33 +30,33 @@ class CombineRiskTest { riskState = RiskState.CALCULATION_FAILED ) val ewRisk = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-22T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-22T14:00:00.000Z").millis, riskLevel = RiskLevel.HIGH, 0, 0 ) val ewRisk2 = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-19T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-19T14:00:00.000Z").millis, riskLevel = RiskLevel.LOW, 0, 0 ) val ewRisk3 = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-20T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-20T14:00:00.000Z").millis, riskLevel = RiskLevel.UNSPECIFIED, 0, 0 ) val ewRisk4 = ExposureWindowDayRisk( - dateMillisSinceEpoch = Instant.parse("2021-03-15T14:00:00.000Z").toEpochMilli(), + dateMillisSinceEpoch = Instant.parse("2021-03-15T14:00:00.000Z").millis, riskLevel = RiskLevel.UNSPECIFIED, 0, 0 ) - val ptRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk, ptRisk2, ptRisk3) - val exposureWindowDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk, ewRisk2, ewRisk3, ewRisk4) - val result = combineRisk(ptRiskList, exposureWindowDayRiskList) + val ptDayRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk, ptRisk2, ptRisk3) + val ewDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk, ewRisk2, ewRisk3, ewRisk4) + val result = combineRisk(ptDayRiskList, ewDayRiskList) result.size shouldBe 5 result.find { it.localDate == LocalDate(2021, 3, 15) @@ -71,15 +75,87 @@ class CombineRiskTest { }!!.riskState shouldBe RiskState.INCREASED_RISK } + @Test + fun `combineEwPtRiskLevelResults works`() { + val startInstant = Instant.ofEpochMilli(10000) + + val ptResult = PtRiskLevelResult( + calculatedAt = startInstant.plus(1000L), + riskState = RiskState.LOW_RISK + ) + val ptResult2 = PtRiskLevelResult( + calculatedAt = startInstant.plus(3000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ptResult3 = PtRiskLevelResult( + calculatedAt = startInstant.plus(6000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ptResult4 = PtRiskLevelResult( + calculatedAt = startInstant.plus(7000L), + riskState = RiskState.CALCULATION_FAILED + ) + + val ptResults = listOf(ptResult, ptResult2, ptResult4, ptResult3) + val ewResult = createEwRiskLevelResult( + calculatedAt = startInstant.plus(2000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ewResult2 = createEwRiskLevelResult( + calculatedAt = startInstant.plus(4000L), + riskState = RiskState.INCREASED_RISK + ) + val ewResult3 = createEwRiskLevelResult( + calculatedAt = startInstant.plus(5000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ewResult4 = createEwRiskLevelResult( + calculatedAt = startInstant.plus(8000L), + riskState = RiskState.CALCULATION_FAILED + ) + val ewResults = listOf(ewResult, ewResult4, ewResult2, ewResult3) + val result = combineEwPtRiskLevelResults(ptResults, ewResults).sortedByDescending { it.calculatedAt } + result.size shouldBe 8 + result[0].riskState shouldBe RiskState.CALCULATION_FAILED + result[0].calculatedAt shouldBe startInstant.plus(8000L) + result[1].riskState shouldBe RiskState.CALCULATION_FAILED + result[1].calculatedAt shouldBe startInstant.plus(7000L) + result[2].riskState shouldBe RiskState.CALCULATION_FAILED + result[2].calculatedAt shouldBe startInstant.plus(6000L) + result[3].riskState shouldBe RiskState.CALCULATION_FAILED + result[3].calculatedAt shouldBe startInstant.plus(5000L) + result[4].riskState shouldBe RiskState.INCREASED_RISK + result[4].calculatedAt shouldBe startInstant.plus(4000L) + result[5].riskState shouldBe RiskState.CALCULATION_FAILED + result[5].calculatedAt shouldBe startInstant.plus(3000L) + result[6].riskState shouldBe RiskState.LOW_RISK + result[6].calculatedAt shouldBe startInstant.plus(2000L) + result[7].riskState shouldBe RiskState.LOW_RISK + result[7].calculatedAt shouldBe startInstant.plus(1000L) + } + @Test fun `max RiskState works`() { - max(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.INCREASED_RISK - max(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - max(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK - max(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK - max(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED + combine(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.INCREASED_RISK + combine(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK + combine(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK + combine(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK + combine(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED } } + +private fun createEwRiskLevelResult( + calculatedAt: Instant, + riskState: RiskState +): EwRiskLevelResult = object : EwRiskLevelResult { + override val calculatedAt: Instant = calculatedAt + override val riskState: RiskState = riskState + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt index 34c79dac1..5cb8e090c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt @@ -14,9 +14,11 @@ import org.joda.time.Instant object RiskStorageTestData { + val ewCalculatedAt = Instant.ofEpochMilli(9999L) + val testRiskLevelResultDao = PersistedRiskLevelResultDao( id = "riskresult-id", - calculatedAt = Instant.ofEpochMilli(9999L), + calculatedAt = ewCalculatedAt, failureReason = null, aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, @@ -40,7 +42,7 @@ object RiskStorageTestData { ) val testRisklevelResult = EwRiskLevelTaskResult( - calculatedAt = Instant.ofEpochMilli(9999L), + calculatedAt = ewCalculatedAt, ewAggregatedRiskResult = testAggregatedRiskResult, exposureWindows = null ) @@ -76,14 +78,14 @@ object RiskStorageTestData { }.build() val testAggregatedRiskPerDateResult = ExposureWindowDayRisk( - dateMillisSinceEpoch = 9999L, + dateMillisSinceEpoch = ewCalculatedAt.millis, riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, minimumDistinctEncountersWithLowRisk = 0, minimumDistinctEncountersWithHighRisk = 0 ) val testPersistedAggregatedRiskPerDateResult = PersistedAggregatedRiskPerDateResult( - dateMillisSinceEpoch = 9999L, + dateMillisSinceEpoch = ewCalculatedAt.millis, riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, minimumDistinctEncountersWithLowRisk = 0, minimumDistinctEncountersWithHighRisk = 0 diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt index 30de3e989..38282aaf9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt @@ -18,6 +18,7 @@ import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.formatter.TestResult +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -57,6 +58,7 @@ class SubmissionRepositoryTest : BaseTest() { @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var testResultDataCollector: TestResultDataCollector + @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064" @@ -99,6 +101,10 @@ class SubmissionRepositoryTest : BaseTest() { every { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } just Runs coEvery { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } just Runs every { testResultDataCollector.clear() } just Runs + + backgroundWorkScheduler.apply { + every { startWorkScheduler() } just Runs + } } fun createInstance(scope: CoroutineScope) = SubmissionRepository( @@ -111,7 +117,8 @@ class SubmissionRepositoryTest : BaseTest() { backgroundNoise = backgroundNoise, analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, tracingSettings = tracingSettings, - testResultDataCollector = testResultDataCollector + testResultDataCollector = testResultDataCollector, + backgroundWorkScheduler = backgroundWorkScheduler ) @Test 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 0cb6a4ad7..280202997 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 @@ -30,7 +30,6 @@ 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 kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf @@ -63,6 +62,7 @@ class SubmissionTaskTest : BaseTest() { @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector @MockK lateinit var checkInsTransformer: CheckInsTransformer @MockK lateinit var checkInRepository: CheckInRepository + @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler private lateinit var settingSymptomsPreference: FlowPreference<Symptoms?> @@ -80,9 +80,8 @@ class SubmissionTaskTest : BaseTest() { every { submissionSettings.registrationToken } returns registrationToken every { submissionSettings.isSubmissionSuccessful = any() } just Runs - mockkObject(BackgroundWorkScheduler) - every { BackgroundWorkScheduler.stopWorkScheduler() } just Runs - every { BackgroundWorkScheduler.startWorkScheduler() } 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)) @@ -131,7 +130,8 @@ class SubmissionTaskTest : BaseTest() { testResultAvailableNotificationService = testResultAvailableNotificationService, analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, checkInsRepository = checkInRepository, - checkInsTransformer = checkInsTransformer + checkInsTransformer = checkInsTransformer, + backgroundWorkScheduler = backgroundWorkScheduler, ) @Test @@ -183,9 +183,9 @@ class SubmissionTaskTest : BaseTest() { autoSubmission.updateMode(AutoSubmission.Mode.DISABLED) - BackgroundWorkScheduler.stopWorkScheduler() + backgroundWorkScheduler.stopWorkScheduler() submissionSettings.isSubmissionSuccessful = true - BackgroundWorkScheduler.startWorkScheduler() + backgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() testResultAvailableNotificationService.cancelTestResultAvailableNotification() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt index 27175e36d..daf12a721 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt @@ -5,12 +5,11 @@ import android.content.res.Resources import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.installTime.InstallTimeProvider -import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult -import de.rki.coronawarnapp.risk.ProtoRiskLevel +import de.rki.coronawarnapp.risk.LastCombinedRiskResults import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult -import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.ui.details.items.additionalinfos.AdditionalInfoLowRiskBox @@ -46,6 +45,8 @@ class TracingDetailsItemProviderTest : BaseTest() { @MockK lateinit var installTimeProvider: InstallTimeProvider @MockK lateinit var surveys: Surveys + @MockK(relaxed = true) lateinit var combinedResult: CombinedEwPtRiskLevelResult + @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -61,22 +62,23 @@ class TracingDetailsItemProviderTest : BaseTest() { private fun prepare( status: GeneralTracingStatus.Status, - riskLevel: ProtoRiskLevel, + riskState: RiskState, matchedKeyCount: Int, daysSinceInstallation: Long, availableSurveys: List<Surveys.Type> = emptyList() ) { every { tracingStatus.generalStatus } returns flowOf(status) - every { ewAggregatedRiskResult.totalRiskLevel } returns riskLevel every { installTimeProvider.daysSinceInstallation } returns daysSinceInstallation every { surveys.availableSurveys } returns flowOf(availableSurveys) - if (riskLevel == ProtoRiskLevel.LOW) { + if (riskState == RiskState.LOW_RISK) { every { ewAggregatedRiskResult.isLowRisk() } returns true - } else if (riskLevel == ProtoRiskLevel.HIGH) { + } else if (riskState == RiskState.INCREASED_RISK) { every { ewAggregatedRiskResult.isIncreasedRisk() } returns true } + every { combinedResult.riskState } returns riskState + val exposureWindow: ExposureWindow = mockk() val ewRiskLevelTaskResult = EwRiskLevelTaskResult( @@ -85,18 +87,16 @@ class TracingDetailsItemProviderTest : BaseTest() { exposureWindows = listOf(exposureWindow) ) - val ptRiskLevelResult = PtRiskLevelResult( - calculatedAt = Instant.EPOCH, - riskState = RiskState.CALCULATION_FAILED - ) - val combined = CombinedEwPtRiskLevelResult( - ewRiskLevelResult = ewRiskLevelTaskResult, - ptRiskLevelResult = ptRiskLevelResult + every { combinedResult.ewRiskLevelResult } returns ewRiskLevelTaskResult + + val lastCombined = LastCombinedRiskResults( + lastCalculated = combinedResult, + lastSuccessfullyCalculated = combinedResult ) every { ewRiskLevelTaskResult.matchedKeyCount } returns matchedKeyCount - every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns flowOf(listOf(ewRiskLevelTaskResult)) - // TODO tests - every { riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult } returns flowOf(listOf(combined)) + every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns + flowOf(listOf(ewRiskLevelTaskResult)) + every { riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult } returns flowOf(lastCombined) } @Test @@ -104,7 +104,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 1 ) @@ -121,7 +121,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -138,7 +138,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -155,7 +155,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -174,7 +174,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -193,7 +193,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0, ) @@ -213,7 +213,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -233,7 +233,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.UNRECOGNIZED, + riskState = RiskState.CALCULATION_FAILED, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -253,7 +253,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_INACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -273,7 +273,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_INACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -296,7 +296,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.UNRECOGNIZED, + riskState = RiskState.CALCULATION_FAILED, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -319,7 +319,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -342,7 +342,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, daysSinceInstallation = 4, matchedKeyCount = 0 ) @@ -365,7 +365,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.LOW, + riskState = RiskState.LOW_RISK, matchedKeyCount = 0, daysSinceInstallation = 4, availableSurveys = listOf(Surveys.Type.HIGH_RISK_ENCOUNTER) @@ -386,7 +386,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, matchedKeyCount = 0, daysSinceInstallation = 4, availableSurveys = emptyList() @@ -407,7 +407,7 @@ class TracingDetailsItemProviderTest : BaseTest() { prepare( status = GeneralTracingStatus.Status.TRACING_ACTIVE, - riskLevel = ProtoRiskLevel.HIGH, + riskState = RiskState.INCREASED_RISK, matchedKeyCount = 0, daysSinceInstallation = 4, availableSurveys = listOf(Surveys.Type.HIGH_RISK_ENCOUNTER) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt index 78793ae28..7ead8a9e2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt @@ -13,8 +13,8 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK -import org.junit.jupiter.api.Test import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import testhelpers.BaseTest import testhelpers.TestDispatcherProvider diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt index a02df66dd..4c7411bca 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.calculateDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.derive10MinutesInterval +import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval import de.rki.coronawarnapp.util.TimeAndDateExtensions.getCurrentHourUTC import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant @@ -99,4 +100,16 @@ class TimeAndDateExtensionsTest : BaseTest() { Instant.parse("1970-01-01T00:00:00.000Z") .derive10MinutesInterval() shouldBe 0 } + + @Test + fun `derive 1 hour interval should be 0`() { + Instant.parse("1970-01-01T00:00:00.000Z") + .deriveHourInterval() shouldBe 0 + } + + @Test + fun `derive 1 hour interval`() { + Instant.parse("2021-02-15T13:52:05+00:00") + .deriveHourInterval() shouldBe 448165 + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt index 32de83877..9a6302454 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt @@ -13,10 +13,10 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockkStatic import io.mockk.slot -import org.joda.time.DateTime +import org.joda.time.Duration import org.joda.time.Instant -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import testhelpers.BaseTest import java.util.Locale @@ -26,9 +26,9 @@ class FormatterStatisticsHelperTest : BaseTest() { private lateinit var context: Context private val today = Instant() - private val yesterday = DateTime().minusDays(1).toInstant() + private val yesterday = today.minus(Duration.standardDays(1)) - @Before + @BeforeEach fun setUp() { val slot = slot<String>() MockKAnnotations.init(this) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt index 4fdcc4800..ef18b7fd6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt @@ -4,7 +4,7 @@ import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkerParameters import androidx.work.impl.workers.DiagnosticsWorker -import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker +import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.mockk.MockKAnnotations @@ -22,15 +22,15 @@ class CWAWorkerFactoryTest : BaseTest() { @MockK lateinit var context: Context @MockK lateinit var workerParameters: WorkerParameters - @MockK lateinit var ourWorker: DiagnosisKeyRetrievalOneTimeWorker - @MockK lateinit var ourFactory: DiagnosisKeyRetrievalOneTimeWorker.Factory + @MockK lateinit var ourWorker: DiagnosisKeyRetrievalWorker + @MockK lateinit var ourFactory: DiagnosisKeyRetrievalWorker.Factory @BeforeEach fun setup() { MockKAnnotations.init(this) every { ourFactory.create(context, workerParameters) } returns ourWorker - workerFactories[DiagnosisKeyRetrievalOneTimeWorker::class.java] = Provider { ourFactory } + workerFactories[DiagnosisKeyRetrievalWorker::class.java] = Provider { ourFactory } } fun createInstance() = CWAWorkerFactory( @@ -42,7 +42,7 @@ class CWAWorkerFactoryTest : BaseTest() { val instance = createInstance() instance.createWorker( context, - DiagnosisKeyRetrievalOneTimeWorker::class.qualifiedName!!, + DiagnosisKeyRetrievalWorker::class.qualifiedName!!, workerParameters ) shouldBe ourWorker } @@ -55,4 +55,20 @@ class CWAWorkerFactoryTest : BaseTest() { val worker2 = instance.createWorker(context, DiagnosticsWorker::class.qualifiedName!!, workerParameters) worker1 shouldNotBe worker2 } + + /** + * Workers are initialized based on their class name. + * That class name is stored as part of the worker arguments. + * If we refactor a worker, the previously used classname will be unknown. + * Returning null will cause the periodic worker to be dequeued. + */ + @Test + fun `class names that can not be instantiated are treated like an unknown worker`() { + val instance = createInstance() + instance.createWorker( + context, + "abc.im.a.ghost.def", + workerParameters + ) shouldBe null + } } 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 4c7049e9f..67bdaf665 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 @@ -18,6 +18,7 @@ import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutNotification import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.di.AppContext @@ -143,4 +144,7 @@ class MockProvider { @Provides fun checkOutNotification(): CheckOutNotification = mockk() + + @Provides + fun riskWorkScheduler(): RiskWorkScheduler = mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt deleted file mode 100644 index f77fd6a7e..000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.worker - -import io.kotest.matchers.shouldBe -import org.joda.time.Duration -import org.junit.jupiter.api.Test -import testhelpers.BaseTest - -class BackgroundWorkBuilderTest : BaseTest() { - - @Test - fun `worker interval for key retrieval is 60 minutes, once every hour`() { - buildDiagnosisKeyRetrievalPeriodicWork().apply { - workSpec.intervalDuration shouldBe Duration.standardMinutes(60).millis - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt index 499dada21..a59702bd5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt @@ -2,7 +2,6 @@ package de.rki.coronawarnapp.worker import android.content.Context import androidx.work.ListenableWorker -import androidx.work.Operation import androidx.work.WorkRequest import androidx.work.WorkerParameters import de.rki.coronawarnapp.notification.GeneralNotifications @@ -18,7 +17,6 @@ import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -48,9 +46,10 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { @MockK lateinit var appComponent: ApplicationComponent @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool - @MockK lateinit var operation: Operation @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var tracingSettings: TracingSettings + @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler + @RelaxedMockK lateinit var workerParams: WorkerParameters private val currentInstant = Instant.ofEpochSecond(1611764225) private val registrationToken = "test token" @@ -72,8 +71,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { every { submissionSettings.registrationToken } returns mockFlowPreference(registrationToken) - mockkObject(BackgroundWorkScheduler) - every { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } returns operation + every { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } just Runs } @Test @@ -83,7 +81,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -95,7 +93,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -109,7 +107,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -228,7 +226,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID ) } - coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.success() } } @@ -240,7 +238,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } - coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } + coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } result shouldBe ListenableWorker.Result.retry() } } @@ -253,6 +251,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { submissionSettings, submissionService, timeStamper, - tracingSettings + tracingSettings, + backgroundWorkScheduler, ) } diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 8c676d6ab..274a5e5f9 100644 --- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.test.risk.storage import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage @@ -88,7 +88,7 @@ class DefaultRiskLevelStorageTest : BaseTest() { every { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow() every { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow() - every { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow() + every { presenceTracingRiskRepository.allEntries() } returns emptyFlow() every { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow() } diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 24478cb80..72eae2991 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.test.risk.storage import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository +import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage @@ -87,7 +87,7 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() { every { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow() every { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow() - every { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow() + every { presenceTracingRiskRepository.allEntries() } returns emptyFlow() every { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow() } -- GitLab