diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 255c61a8b5e1c2bfb4ad40b161fb60b148ff5e3b..d4b62b61449d2525d9e6ff7af9bde7bc282ca84c 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -125,7 +125,6 @@ android { device { dimension "version" resValue "string", "app_name", "Corona-Warn" - resValue "bool", "extract_native_libs", "false" ext { envTypeDefault = [debug: "INT", release: "PROD"] @@ -139,7 +138,6 @@ android { // Contains test fragments dimension "version" resValue "string", "app_name", "CWA TEST" - resValue "bool", "extract_native_libs", "true" applicationIdSuffix '.test' ext { @@ -305,10 +303,10 @@ configurations.all { dependencies { // KOTLIN - def coroutineVersion = "1.4.0-M1" + def coroutineVersion = "1.4.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutineVersion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // ANDROID STANDARD diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase/1.json new file mode 100644 index 0000000000000000000000000000000000000000..97a2838616902f518cb855cd557fe9555ec8f46c --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase/1.json @@ -0,0 +1,203 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "8d5e82a0429a20bd137235b7cc055b1a", + "entities": [ + { + "tableName": "riskresults", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `calculatedAt` TEXT NOT NULL, `failureReason` TEXT, `totalRiskLevel` INTEGER, `totalMinimumDistinctEncountersWithLowRisk` INTEGER, `totalMinimumDistinctEncountersWithHighRisk` INTEGER, `mostRecentDateWithLowRisk` TEXT, `mostRecentDateWithHighRisk` TEXT, `numberOfDaysWithLowRisk` INTEGER, `numberOfDaysWithHighRisk` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "calculatedAt", + "columnName": "calculatedAt", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "failureReason", + "columnName": "failureReason", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.totalRiskLevel", + "columnName": "totalRiskLevel", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.totalMinimumDistinctEncountersWithLowRisk", + "columnName": "totalMinimumDistinctEncountersWithLowRisk", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.totalMinimumDistinctEncountersWithHighRisk", + "columnName": "totalMinimumDistinctEncountersWithHighRisk", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.mostRecentDateWithLowRisk", + "columnName": "mostRecentDateWithLowRisk", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.mostRecentDateWithHighRisk", + "columnName": "mostRecentDateWithHighRisk", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.numberOfDaysWithLowRisk", + "columnName": "numberOfDaysWithLowRisk", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "aggregatedRiskResult.numberOfDaysWithHighRisk", + "columnName": "numberOfDaysWithHighRisk", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "exposurewindows", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `riskLevelResultId` TEXT NOT NULL, `dateMillisSinceEpoch` INTEGER NOT NULL, `calibrationConfidence` INTEGER NOT NULL, `infectiousness` INTEGER NOT NULL, `reportType` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "riskLevelResultId", + "columnName": "riskLevelResultId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateMillisSinceEpoch", + "columnName": "dateMillisSinceEpoch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "calibrationConfidence", + "columnName": "calibrationConfidence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "infectiousness", + "columnName": "infectiousness", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reportType", + "columnName": "reportType", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "scaninstances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exposureWindowId` INTEGER NOT NULL, `minAttenuationDb` INTEGER NOT NULL, `secondsSinceLastScan` INTEGER NOT NULL, `typicalAttenuationDb` INTEGER NOT NULL, FOREIGN KEY(`exposureWindowId`) REFERENCES `exposurewindows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "exposureWindowId", + "columnName": "exposureWindowId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minAttenuationDb", + "columnName": "minAttenuationDb", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "secondsSinceLastScan", + "columnName": "secondsSinceLastScan", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typicalAttenuationDb", + "columnName": "typicalAttenuationDb", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_scaninstances_exposureWindowId", + "unique": false, + "columnNames": [ + "exposureWindowId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_scaninstances_exposureWindowId` ON `${TABLE_NAME}` (`exposureWindowId`)" + } + ], + "foreignKeys": [ + { + "table": "exposurewindows", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "exposureWindowId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, '8d5e82a0429a20bd137235b7cc055b1a')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt similarity index 100% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt rename to Corona-Warn-App/src/device/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt 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 new file mode 100644 index 0000000000000000000000000000000000000000..e1131726c36f35eb79041d6058ddae315e85e782 --- /dev/null +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultRiskLevelStorage @Inject constructor( + riskResultDatabaseFactory: RiskResultDatabase.Factory, + riskLevelResultMigrator: RiskLevelResultMigrator +) : BaseRiskLevelStorage(riskResultDatabaseFactory, riskLevelResultMigrator) { + + // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation + // Taken from TimeVariables.MAX_STALE_EXPOSURE_RISK_RANGE + override val storedResultLimit: Int = 2 * 6 + + override suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) { + Timber.d("storeExposureWindows(): NOOP") + // NOOP + } + + override suspend fun deletedOrphanedExposureWindows() { + Timber.d("deletedOrphanedExposureWindows(): NOOP") + // NOOP + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml b/Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml new file mode 100644 index 0000000000000000000000000000000000000000..ca1b20da31c951adc75614ac532bda6681a4d23e --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/AndroidManifest.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="LockedOrientationActivity" + package="de.rki.coronawarnapp"> + + <application> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.fileProvider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/provider_paths"/> + </provider> + + </application> + +</manifest> \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json new file mode 100644 index 0000000000000000000000000000000000000000..544b74346319309e35780eccde28274cb969fd73 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json @@ -0,0 +1,690 @@ +[ + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 299, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 73, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 73, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 72, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 72, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 299, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 3, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 4, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 1, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 3, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 4, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 1, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 0, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 70, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 70, + "secondsSinceLastScan": 0, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 70, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 70, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + } +] \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json new file mode 100644 index 0000000000000000000000000000000000000000..7bd59ff8a9564b7a65aee151ea3bf6714e18cefa --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json @@ -0,0 +1,25 @@ +[ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 299 + } + ] + } +] \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..dd7306b0b1f347ba356fbbbca101f5abd95dc827 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.storage.TestSettings +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@Singleton +class DefaultExposureWindowProvider @Inject constructor( + private val client: ExposureNotificationClient, + private val testSettings: TestSettings, + private val fakeExposureWindowProvider: FakeExposureWindowProvider +) : ExposureWindowProvider { + + override suspend fun exposureWindows(): List<ExposureWindow> = suspendCoroutine { cont -> + when (val fakeSetting = testSettings.fakeExposureWindows.value) { + TestSettings.FakeExposureWindowTypes.DISABLED -> { + client.exposureWindows + .addOnSuccessListener { cont.resume(it) } + .addOnFailureListener { cont.resumeWithException(it) } + } + else -> { + fakeExposureWindowProvider.getExposureWindows(fakeSetting).let { cont.resume(it) } + } + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..ce213e8e03432873ea37ac457889cc65c92da657 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt @@ -0,0 +1,63 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import android.content.Context +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import dagger.Reusable +import de.rki.coronawarnapp.storage.TestSettings.FakeExposureWindowTypes +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.fromJson +import org.joda.time.Duration +import javax.inject.Inject + +@Reusable +class FakeExposureWindowProvider @Inject constructor( + @AppContext val context: Context, + @BaseGson val gson: Gson, + val timeStamper: TimeStamper +) { + + fun getExposureWindows(testSettings: FakeExposureWindowTypes): List<ExposureWindow> { + val jsonInput = when (testSettings) { + FakeExposureWindowTypes.INCREASED_RISK_DEFAULT -> "exposure-windows-increased-risk-random.json" + FakeExposureWindowTypes.LOW_RISK_DEFAULT -> "exposure-windows-lowrisk-random.json" + else -> throw NotImplementedError() + }.let { context.assets.open(it) }.readBytes().toString(Charsets.UTF_8) + val jsonWindows: List<JsonWindow> = gson.fromJson(jsonInput) + val nowUTC = timeStamper.nowUTC + return jsonWindows.map { jWindow -> + ExposureWindow.Builder().apply { + setDateMillisSinceEpoch(nowUTC.minus(Duration.standardDays(jWindow.ageInDays.toLong())).millis) + setCalibrationConfidence(jWindow.calibrationConfidence) + setInfectiousness(jWindow.infectiousness) + setReportType(jWindow.reportType) + + jWindow.scanInstances.map { jScanInstance -> + ScanInstance.Builder().apply { + setMinAttenuationDb(jScanInstance.minAttenuation) + setSecondsSinceLastScan(jScanInstance.secondsSinceLastScan) + setTypicalAttenuationDb(jScanInstance.typicalAttenuation) + }.build() + }.let { setScanInstances(it) } + }.build() + } + } +} + +private data class JsonScanInstance( + @SerializedName("minAttenuation") val minAttenuation: Int, + @SerializedName("secondsSinceLastScan") val secondsSinceLastScan: Int, + @SerializedName("typicalAttenuation") val typicalAttenuation: Int +) + +private data class JsonWindow( + @SerializedName("ageInDays") val ageInDays: Int, + @SerializedName("calibrationConfidence") val calibrationConfidence: Int, + @SerializedName("infectiousness") val infectiousness: Int, + @SerializedName("reportType") val reportType: Int, + @SerializedName("scanInstances") val scanInstances: List<JsonScanInstance> +) 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 new file mode 100644 index 0000000000000000000000000000000000000000..0ce4d0e7ae795152f84e8fd33fd6e7a210cea4ba --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedScanInstances +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import kotlinx.coroutines.flow.firstOrNull +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultRiskLevelStorage @Inject constructor( + riskResultDatabaseFactory: RiskResultDatabase.Factory, + riskLevelResultMigrator: RiskLevelResultMigrator +) : BaseRiskLevelStorage(riskResultDatabaseFactory, riskLevelResultMigrator) { + + // 14 days, 6 times per day + // For testers keep all the results! + override val storedResultLimit: Int = 14 * 6 + + override suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) { + Timber.d("Storing exposure windows for storedResultId=%s", storedResultId) + try { + val startTime = System.currentTimeMillis() + val exposureWindows = result.exposureWindows ?: emptyList() + val windowIds = exposureWindows + .map { it.toPersistedExposureWindow(riskLevelResultId = storedResultId) } + .let { exposureWindowsTables.insertWindows(it) } + + require(windowIds.size == exposureWindows.size) { + Timber.e("Inserted ${windowIds.size}, but wanted ${exposureWindows.size}") + } + + val persistedScanInstances: List<PersistedScanInstance> = windowIds.flatMapIndexed { index, id -> + val scanInstances = exposureWindows[index].scanInstances + scanInstances.toPersistedScanInstances(exposureWindowId = id) + } + exposureWindowsTables.insertScanInstances(persistedScanInstances) + + Timber.d("Storing ExposureWindows took %dms.", (System.currentTimeMillis() - startTime)) + } catch (e: Exception) { + Timber.e(e, "Failed to save exposure windows") + } + } + + override suspend fun deletedOrphanedExposureWindows() { + Timber.d("deletedOrphanedExposureWindows() running...") + val currentRiskResultIds = riskResultsTables.allEntries().firstOrNull()?.map { it.id } ?: emptyList() + + exposureWindowsTables.deleteByRiskResultId(currentRiskResultIds).also { + Timber.d("$it orphaned exposure windows were deleted.") + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt index c03b84f39023d1999f7d0e38b93fa25b1e6d0d10..ec7778acfa0c13c4117156c77776a7c9c6f7d7c0 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt @@ -26,6 +26,7 @@ import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentResult import com.google.zxing.qrcode.QRCodeWriter import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.databinding.FragmentTestForAPIBinding import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL @@ -34,8 +35,8 @@ import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.receiver.ExposureStateUpdateReceiver -import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.TimeVariables +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange import de.rki.coronawarnapp.sharing.ExposureSharingService import de.rki.coronawarnapp.storage.AppDatabase @@ -65,7 +66,10 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory @Inject lateinit var enfClient: ENFClient - @Inject lateinit var exposureResultStore: ExposureResultStore + + // TODO: This is ugly, remove when refactoring the fragment + @Inject lateinit var appConfigProvider: AppConfigProvider + @Inject lateinit var riskLevelStorage: RiskLevelStorage private val vm: TestForApiFragmentViewModel by cwaViewModels { viewModelFactory } companion object { @@ -159,7 +163,9 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), buttonRetrieveExposureSummary.setOnClickListener { vm.launch { - val summary = exposureResultStore.entities.first().exposureWindows.toString() + val summary = riskLevelStorage.riskLevelResults.first().maxByOrNull { + it.calculatedAt + }?.toString() ?: "No results yet." withContext(Dispatchers.Main) { showToast(summary) @@ -291,7 +297,8 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), try { // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API enfClient.provideDiagnosisKeys( - googleFileList + googleFileList, + appConfigProvider.getAppConfig().diagnosisKeysDataMapping ) showToast("Provided ${appleKeyList.size} keys to Google API") } catch (e: Exception) { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt new file mode 100644 index 0000000000000000000000000000000000000000..ad35b645400a8d63be689db16a780cc7252992ad --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ExposureWindowJson.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.test.risklevel.entities + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow + +data class ExposureWindowJson( + val dateMillisSinceEpoch: Long, + val reportType: Int, + val infectiousness: Int, + val calibrationConfidence: Int, + val scanInstances: List<ScanInstanceJson> +) + +fun ExposureWindow.toExposureWindowJson(): ExposureWindowJson = ExposureWindowJson( + dateMillisSinceEpoch = dateMillisSinceEpoch, + reportType = reportType, + infectiousness = infectiousness, + calibrationConfidence = calibrationConfidence, + scanInstances = scanInstances.map { it.toScanInstanceJson() } +) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt new file mode 100644 index 0000000000000000000000000000000000000000..0b42a6bc738cd7ddac4ca957145075bccf5b6d86 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/entities/ScanInstanceJson.kt @@ -0,0 +1,15 @@ +package de.rki.coronawarnapp.test.risklevel.entities + +import com.google.android.gms.nearby.exposurenotification.ScanInstance + +data class ScanInstanceJson( + val typicalAttenuationDb: Int, + val minAttenuationDb: Int, + val secondsSinceLastScan: Int +) + +fun ScanInstance.toScanInstanceJson(): ScanInstanceJson = ScanInstanceJson( + typicalAttenuationDb = typicalAttenuationDb, + minAttenuationDb = minAttenuationDb, + secondsSinceLastScan = secondsSinceLastScan +) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt index b0e40407ce9b0895cc283865456e43f7303882d0..d6c542fe27fa24962e8e1f7cecd46ba6dfde2302 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt @@ -1,13 +1,21 @@ package de.rki.coronawarnapp.test.risklevel.ui +import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.Toast +import android.widget.RadioButton +import android.widget.RadioGroup +import androidx.core.app.ShareCompat +import androidx.core.content.FileProvider +import androidx.core.view.ViewCompat +import androidx.core.view.children import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding +import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.util.di.AutoInject @@ -15,6 +23,8 @@ import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted +import timber.log.Timber +import java.io.File import javax.inject.Inject @Suppress("LongMethod") @@ -37,52 +47,88 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - vm.tracingCardState.observe2(this) { binding.tracingCard = it } - binding.settingsViewModel = settingsViewModel - vm.showRiskStatusCard.observe2(this) { binding.showRiskStatusCard = it } - binding.buttonRetrieveDiagnosisKeys.setOnClickListener { vm.retrieveDiagnosisKeys() } binding.buttonCalculateRiskLevel.setOnClickListener { vm.calculateRiskLevel() } binding.buttonClearDiagnosisKeyCache.setOnClickListener { vm.clearKeyCache() } - binding.buttonResetRiskLevel.setOnClickListener { vm.resetRiskLevel() } - vm.riskLevelResetEvent.observe2(this) { - Toast.makeText( - requireContext(), "Reset done, please fetch diagnosis keys from server again", - Toast.LENGTH_SHORT - ).show() - } + binding.buttonExposureWindowsShare.setOnClickListener { vm.shareExposureWindows() } + + vm.dataResetEvent.observe2(this) { Snackbar.make(requireView(), it, Snackbar.LENGTH_SHORT).show() } vm.additionalRiskCalcInfo.observe2(this) { binding.labelRiskAdditionalInfo.text = it } - vm.aggregatedRiskResult.observe2(this) { binding.labelAggregatedRiskResult.text = it } - - vm.exposureWindowCountString.observe2(this) { - binding.labelExposureWindowCount.text = it + vm.backendParameters.observe2(this) { + binding.labelBackendParameters.text = it } - - vm.exposureWindows.observe2(this) { - binding.labelExposureWindows.text = it + vm.exposureWindowCount.observe2(this) { exposureWindowCount -> + binding.labelExposureWindowCount.text = "Retrieved $exposureWindowCount Exposure Windows" + binding.buttonExposureWindowsShare.visibility = when (exposureWindowCount > 0) { + true -> View.VISIBLE + false -> View.GONE + } + } + vm.shareFileEvent.observe2(this) { + shareExposureWindowsFile(it) + } + vm.fakeWindowsState.observe2(this) { currentType -> + binding.apply { + if (fakeWindowsToggleGroup.childCount != TestSettings.FakeExposureWindowTypes.values().size) { + fakeWindowsToggleGroup.removeAllViews() + TestSettings.FakeExposureWindowTypes.values().forEach { type -> + RadioButton(requireContext()).apply { + id = ViewCompat.generateViewId() + text = type.name + layoutParams = RadioGroup.LayoutParams( + RadioGroup.LayoutParams.MATCH_PARENT, + RadioGroup.LayoutParams.WRAP_CONTENT + ) + fakeWindowsToggleGroup.addView(this) + } + } + } + fakeWindowsToggleGroup.children.forEach { + it as RadioButton + it.isChecked = it.text == currentType.name + } + } } + binding.fakeWindowsToggleGroup.apply { + setOnCheckedChangeListener { group, checkedId -> + val chip = group.findViewById<RadioButton>(checkedId) + if (!chip.isPressed) return@setOnCheckedChangeListener + vm.selectFakeExposureWindowMode(TestSettings.FakeExposureWindowTypes.valueOf(chip.text.toString())) + } + } + } - vm.backendParameters.observe2(this) { - binding.labelBackendParameters.text = it + private fun shareExposureWindowsFile(file: File) { + Timber.d("Opening Share-Intent for Exposure Windows") + val shareFileUri = + FileProvider.getUriForFile(requireContext(), requireContext().packageName + ".fileProvider", file) + val shareIntent = ShareCompat.IntentBuilder + .from(requireActivity()) + .setStream(shareFileUri) + .setType("text/plain") + .intent + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + if (shareIntent.resolveActivity(requireActivity().packageManager) != null) { + startActivity(shareIntent) } } companion object { - val TAG: String = TestRiskLevelCalculationFragment::class.simpleName!! + private val TAG = TestRiskLevelCalculationFragment::class.java.simpleName val MENU_ITEM = TestMenuItem( title = "ENF v2 Calculation", description = "Window Mode related overview.", diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt index b65f3240c90e9789e20bbc407d30d285eddf968c..c082b5b64e30e99ac9d2229f7db75ef77c8121b5 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -3,67 +3,81 @@ package de.rki.coronawarnapp.test.risklevel.ui import android.content.Context import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData +import com.google.gson.Gson +import com.google.gson.GsonBuilder import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.diagnosiskeys.download.KeyPackageSyncSettings import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.risk.ExposureResult -import de.rki.coronawarnapp.risk.ExposureResultStore -import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.latestSubmission import de.rki.coronawarnapp.risk.RiskLevelTask +import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.risk.result.AggregatedRiskResult -import de.rki.coronawarnapp.storage.AppDatabase -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking +import de.rki.coronawarnapp.test.risklevel.entities.toExposureWindowJson import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider +import de.rki.coronawarnapp.ui.tracing.common.tryLatestResultsWithDefaults import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.withContext import org.joda.time.Instant +import org.joda.time.format.DateTimeFormat import timber.log.Timber -import java.util.Date +import java.io.File import java.util.concurrent.TimeUnit class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( @Assisted private val handle: SavedStateHandle, @Assisted private val exampleArg: String?, @AppContext private val context: Context, // App context - dispatcherProvider: DispatcherProvider, + private val dispatcherProvider: DispatcherProvider, private val taskController: TaskController, private val keyCacheRepository: KeyCacheRepository, private val appConfigProvider: AppConfigProvider, tracingCardStateProvider: TracingCardStateProvider, - private val exposureResultStore: ExposureResultStore, + private val riskLevelStorage: RiskLevelStorage, + private val testSettings: TestSettings, + private val timeStamper: TimeStamper, + private val exposureDetectionTracker: ExposureDetectionTracker, + private val keyPackageSyncSettings: KeyPackageSyncSettings, private val submissionRepository: SubmissionRepository ) : CWAViewModel( dispatcherProvider = dispatcherProvider ) { + // Use unique instance for pretty output + private val gson: Gson by lazy { + GsonBuilder().setPrettyPrinting().create() + } + + val fakeWindowsState = testSettings.fakeExposureWindows.flow.asLiveData() + init { Timber.d("CWAViewModel: %s", this) Timber.d("SavedStateHandle: %s", handle) Timber.d("Example arg: %s", exampleArg) } - val riskLevelResetEvent = SingleLiveEvent<Unit>() + val dataResetEvent = SingleLiveEvent<String>() + val shareFileEvent = SingleLiveEvent<File>() val showRiskStatusCard = submissionRepository.deviceUIStateFlow.map { it.withSuccess(false) { true } @@ -73,19 +87,27 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( .sample(150L) .asLiveData(dispatcherProvider.Default) - val exposureWindowCountString = exposureResultStore - .entities - .map { "Retrieved ${it.exposureWindows.size} Exposure Windows" } - .asLiveData() + private val lastRiskResult = riskLevelStorage.riskLevelResults.map { results -> + results.maxByOrNull { it.calculatedAt } + } - val exposureWindows = exposureResultStore - .entities - .map { if (it.exposureWindows.isEmpty()) "Exposure windows list is empty" else it.exposureWindows.toString() } + val exposureWindowCount = lastRiskResult + .map { it?.exposureWindows?.size ?: 0 } .asLiveData() - val aggregatedRiskResult = exposureResultStore - .entities - .map { if (it.aggregatedRiskResult != null) it.aggregatedRiskResult.toReadableString() else "Aggregated risk result is not available" } + val aggregatedRiskResult = lastRiskResult + .map { + if (it == null) return@map "No results yet." + + if (it.wasSuccessfullyCalculated) { + // wasSuccessfullyCalculated check for aggregatedRiskResult != null + it.aggregatedRiskResult!!.toReadableString() + } else { + var notAvailable = "Aggregated risk result is not available" + it.failureReason?.let { failureReason -> notAvailable += " because ${failureReason.failureCode}" } + notAvailable + } + } .asLiveData() private fun AggregatedRiskResult.toReadableString(): String = StringBuilder() @@ -126,44 +148,38 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( .toString() val additionalRiskCalcInfo = combine( - RiskLevelRepository.riskLevelScore, - RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated, - exposureResultStore.matchedKeyCount, - exposureResultStore.daysSinceLastExposure, - LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() - ) { riskLevelScore, - riskLevelScoreLastSuccessfulCalculated, - matchedKeyCount, - daysSinceLastExposure, - lastTimeDiagnosisKeysFromServerFetch -> + riskLevelStorage.riskLevelResults, + exposureDetectionTracker.latestSubmission() + ) { riskLevelResults, latestSubmission -> + + val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults() + createAdditionalRiskCalcInfo( - riskLevelScore = riskLevelScore, - riskLevelScoreLastSuccessfulCalculated = riskLevelScoreLastSuccessfulCalculated, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, - lastTimeDiagnosisKeysFromServerFetch = lastTimeDiagnosisKeysFromServerFetch + latestCalc.calculatedAt, + riskLevel = latestCalc.riskState, + riskLevelLastSuccessfulCalculated = latestSuccessfulCalc.riskState, + matchedKeyCount = latestCalc.matchedKeyCount, + daysSinceLastExposure = latestCalc.daysWithEncounters, + lastKeySubmission = latestSubmission?.startedAt ) }.asLiveData() private suspend fun createAdditionalRiskCalcInfo( - riskLevelScore: Int, - riskLevelScoreLastSuccessfulCalculated: Int, + lastTimeRiskLevelCalculation: Instant, + riskLevel: RiskState, + riskLevelLastSuccessfulCalculated: RiskState, matchedKeyCount: Int, daysSinceLastExposure: Int, - lastTimeDiagnosisKeysFromServerFetch: Date? + lastKeySubmission: Instant? ): String = StringBuilder() - .appendLine("Risk Level: ${RiskLevel.forValue(riskLevelScore)}") - .appendLine("Last successful Risk Level: ${RiskLevel.forValue(riskLevelScoreLastSuccessfulCalculated)}") + .appendLine("Risk Level: $riskLevel") + .appendLine("Last successful Risk Level: $riskLevelLastSuccessfulCalculated") .appendLine("Matched key count: $matchedKeyCount") .appendLine("Days since last Exposure: $daysSinceLastExposure days") - .appendLine("Last Time Server Fetch: ${lastTimeDiagnosisKeysFromServerFetch?.time?.let { Instant.ofEpochMilli(it) }}") + .appendLine("Last key submission: $lastKeySubmission") .appendLine("Tracing Duration: ${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days") .appendLine("Tracing Duration in last 14 days: ${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days") - .appendLine( - "Last time risk level calculation ${ - LocalData.lastTimeRiskLevelCalculation()?.let { Instant.ofEpochMilli(it) } - }" - ) + .appendLine("Last time risk level calculation $lastTimeRiskLevelCalculation") .toString() fun retrieveDiagnosisKeys() { @@ -192,32 +208,48 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( fun resetRiskLevel() { Timber.d("Resetting risk level") launch { - withContext(Dispatchers.IO) { - try { - // Preference reset - SecurityHelper.resetSharedPrefs() - // Database Reset - AppDatabase.reset(context) - // Export File Reset - keyCacheRepository.clear() - - exposureResultStore.entities.value = ExposureResult(emptyList(), null) - - LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) - LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) - LocalData.lastTimeDiagnosisKeysFromServerFetch(null) - } catch (e: Exception) { - e.report(ExceptionCategory.INTERNAL) + riskLevelStorage.clear() + dataResetEvent.postValue("Risk level calculation related data reset.") + } + } + + fun shareExposureWindows() { + Timber.d("Creating text file for Exposure Windows") + launch(dispatcherProvider.IO) { + val exposureWindows = lastRiskResult.firstOrNull()?.exposureWindows?.map { it.toExposureWindowJson() } + val fileNameCompatibleTimestamp = timeStamper.nowUTC.toString( + DateTimeFormat.forPattern("yyyy-MM-DD-HH-mm-ss") + ) + + val path = File(context.cacheDir, "share/") + path.mkdirs() + + val file = File(path, "exposureWindows-$fileNameCompatibleTimestamp.json") + file.bufferedWriter() + .use { writer -> + if (exposureWindows.isNullOrEmpty()) { + writer.appendLine("Exposure windows list was empty") + } else { + writer.appendLine(gson.toJson(exposureWindows)) + } } - } - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) - riskLevelResetEvent.postValue(Unit) + shareFileEvent.postValue(file) } } fun clearKeyCache() { Timber.d("Clearing key cache") - launch { keyCacheRepository.clear() } + launch { + keyCacheRepository.clear() + keyPackageSyncSettings.clear() + exposureDetectionTracker.clear() + + dataResetEvent.postValue("Download & Submission related data reset.") + } + } + + fun selectFakeExposureWindowMode(newMode: TestSettings.FakeExposureWindowTypes) { + testSettings.fakeExposureWindows.update { newMode } } @AssistedInject.Factory diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml index 7d94e7c3dca3a8e044acb4aac523109b7266aa7c..b11dc97e7c883c490333a4e70fde9e1bc1c03323 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml @@ -23,6 +23,7 @@ type="de.rki.coronawarnapp.ui.tracing.card.TracingCardState" /> </data> + <ScrollView android:layout_width="match_parent" android:layout_height="match_parent" @@ -34,23 +35,45 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <TextView - style="@style/headline6" - android:accessibilityHeading="true" + <LinearLayout + android:id="@+id/environment_container" + style="@style/card" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Preview (no interaction possible)" /> + android:orientation="vertical"> + + <TextView + android:id="@+id/fake_windows_title" + style="@style/headline6" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Fake exposure windows" /> + + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Takes effect the next time `ExposureNotificationClient.exposureWindows` is called, i.e. on risk level calculation." /> + + <RadioGroup + android:id="@+id/fake_windows_toggle_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:orientation="vertical" /> + </LinearLayout> <FrameLayout android:id="@+id/test_risk_card" style="@style/card" + gone="@{showRiskStatusCard == null || !showRiskStatusCard}" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - gone="@{showRiskStatusCard == null || !showRiskStatusCard}" - android:focusable="true" android:backgroundTint="@{tracingCard.getRiskInfoContainerBackgroundTint(context)}" - android:backgroundTintMode="src_over"> + android:backgroundTintMode="src_over" + android:focusable="true"> <include android:id="@+id/risk_card_content" @@ -60,28 +83,48 @@ </FrameLayout> <Button - android:id="@+id/button_retrieve_diagnosis_keys" + android:id="@+id/button_calculate_risk_level" style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Retrieve Diagnosis Keys" /> + android:text="Calculate Risk Level" /> + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Start the task that gets the latest exposure windows and calculates a current risk state." /> <Button - android:id="@+id/button_calculate_risk_level" + android:id="@+id/button_reset_risk_level" style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Calculate Risk Level" /> + android:text="Reset Risk Level" /> + + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Delete the all stored calculated risk level results." /> <Button - android:id="@+id/button_reset_risk_level" + android:id="@+id/button_retrieve_diagnosis_keys" style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Reset Risk Level" /> + android:text="Download Diagnosis Keys" /> + + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Start the task syncs the local diagnosis key cache with the server and submits them to the exposure notification framework for detection (if constraints allow). " /> <Button android:id="@+id/button_clear_diagnosis_key_cache" @@ -89,15 +132,21 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Clear Diagnosis-Key cache" /> + android:text="Reset Diagnosis-Keys" /> + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Restore download task conditions to initial state. Remove cached keys, delete last download logs, reset tracked exposure detections. " /> <TextView android:id="@+id/label_aggregated_risk_result_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" android:text="Aggregated Risk Result" /> <TextView @@ -109,9 +158,9 @@ <TextView android:id="@+id/label_risk_additional_info_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" + android:accessibilityHeading="true" android:text="Risk Calculation Additional Information" /> <TextView @@ -123,9 +172,9 @@ <TextView android:id="@+id/label_backend_parameters_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" + android:accessibilityHeading="true" android:text="Backend Parameters" /> <TextView @@ -137,9 +186,9 @@ <TextView android:id="@+id/label_exposure_window_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" + android:accessibilityHeading="true" android:text="Exposure Windows" /> <TextView @@ -148,13 +197,17 @@ android:layout_height="wrap_content" android:text="-" /> - <TextView - android:id="@+id/label_exposure_windows" - android:layout_width="wrap_content" + <Button + android:id="@+id/buttonExposureWindowsShare" + style="@style/buttonPrimary" + android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingTop="5dp" - android:text="-" /> + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginBottom="@dimen/spacing_normal" + android:text="Share ExposureWindows" /> </LinearLayout> </ScrollView> -</layout> \ No newline at end of file + + +</layout> diff --git a/Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml b/Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml new file mode 100644 index 0000000000000000000000000000000000000000..b6522dfad14436380412cde73b661d34d7a1eef7 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/xml/provider_paths.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths xmlns:android="http://schemas.android.com/apk/res/android"> + <cache-path name="share" path="share/" /> +</paths> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml index 6556197e9545e30520b7ac4d2027f8a5d6772781..23b0a591b2ae07771449a46d1eb4e5cc4e68ce37 100644 --- a/Corona-Warn-App/src/main/AndroidManifest.xml +++ b/Corona-Warn-App/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:extractNativeLibs="@bool/extract_native_libs" + android:extractNativeLibs="true" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.bin b/Corona-Warn-App/src/main/assets/default_app_config_android.bin index 72e76a3f1f0b4b5fe7275c9d0052477df4b0a129..e94bea0e57dec2d532f9642e5f52f9426e52d028 100644 Binary files a/Corona-Warn-App/src/main/assets/default_app_config_android.bin and b/Corona-Warn-App/src/main/assets/default_app_config_android.bin differ diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 index ce41e9e2b98f97be0e8e4fb9b2ccd4d7cce1bf6b..f352a96630405e9b9b44ae0e7a5d3fb960223c66 100644 --- a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 +++ b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 @@ -1 +1 @@ -3713298c705ee867f0b12cd2a05bc6209442baa156d8e38e19856a3a6b91a48e \ No newline at end of file +827fb746a1128e465d65ec77030fdf38c823dec593ae18aed55195069cf8b701 \ No newline at end of file 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 941a2b4e1890e8c0f69d4804f7c9f454313a7242..7bdbcd80dbf9c81bb829df92e6371d3a3ac33dbc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -17,6 +17,7 @@ import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL import de.rki.coronawarnapp.notification.NotificationHelper +import de.rki.coronawarnapp.risk.RiskLevelChangeDetector import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.CWADebug @@ -45,6 +46,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var foregroundState: ForegroundState @Inject lateinit var workManager: WorkManager @Inject lateinit var configChangeDetector: ConfigChangeDetector + @Inject lateinit var riskLevelChangeDetector: RiskLevelChangeDetector @Inject lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree @@ -78,6 +80,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { } configChangeDetector.launch() + riskLevelChangeDetector.launch() } private val activityLifecycleCallback = object : ActivityLifecycleCallbacks { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt index 89532fb14ebdc44427de454e33ce6f4de4994de2..e0a59c5c81491acaa305699c9cea9e26cda9c88c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt @@ -11,7 +11,7 @@ interface CWAConfig { val supportedCountries: List<String> - val appFeatures: AppFeaturesOuterClass.AppFeatures + val appFeatures: List<AppFeaturesOuterClass.AppFeature> interface Mapper : ConfigMapper<CWAConfig> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt index 588451c9dec8b5393a6c55e1aaf9fbb00bc2317b..9ba3aef2945746ad3eb62b44777b806aff810454 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt @@ -1,10 +1,9 @@ package de.rki.coronawarnapp.appconfig import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.risk.RiskLevelData +import de.rki.coronawarnapp.risk.RiskLevelSettings import de.rki.coronawarnapp.risk.RiskLevelTask -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.util.coroutine.AppScope @@ -20,44 +19,41 @@ class ConfigChangeDetector @Inject constructor( private val appConfigProvider: AppConfigProvider, private val taskController: TaskController, @AppScope private val appScope: CoroutineScope, - private val riskLevelData: RiskLevelData + private val riskLevelSettings: RiskLevelSettings, + private val riskLevelStorage: RiskLevelStorage ) { fun launch() { - Timber.v("Monitoring config changes.") + Timber.tag(TAG).v("Monitoring config changes.") appConfigProvider.currentConfig .distinctUntilChangedBy { it.identifier } .onEach { - Timber.v("Running app config change checks.") + Timber.tag(TAG).v("Running app config change checks.") check(it.identifier) } - .catch { Timber.e(it, "App config change checks failed.") } + .catch { Timber.tag(TAG).e(it, "App config change checks failed.") } .launchIn(appScope) } @VisibleForTesting - internal fun check(newIdentifier: String) { - if (riskLevelData.lastUsedConfigIdentifier == null) { + internal suspend fun check(newIdentifier: String) { + if (riskLevelSettings.lastUsedConfigIdentifier == null) { // No need to reset anything if we didn't calculate a risklevel yet. - Timber.d("Config changed, but no previous identifier is available.") + Timber.tag(TAG).d("Config changed, but no previous identifier is available.") return } - val oldConfigId = riskLevelData.lastUsedConfigIdentifier + val oldConfigId = riskLevelSettings.lastUsedConfigIdentifier if (newIdentifier != oldConfigId) { - Timber.i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.") - RiskLevelRepositoryDeferrer.resetRiskLevel() + Timber.tag(TAG).i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.") + riskLevelStorage.clear() taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector")) } else { - Timber.v("Config identifier ($oldConfigId) didn't change, NOOP.") + Timber.tag(TAG).v("Config identifier ($oldConfigId) didn't change, NOOP.") } } - @VisibleForTesting - internal object RiskLevelRepositoryDeferrer { - - fun resetRiskLevel() { - RiskLevelRepository.setRiskLevelScore(RiskLevel.UNDETERMINED) - } + companion object { + private const val TAG = "ConfigChangeDetector" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt index 80eadd589d6bf68fc19648513195ab5e36841036..cf72d970a6f1ea04e86bf7530fe85b156f976322 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.appconfig +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass @@ -13,6 +14,7 @@ interface ExposureWindowRiskCalculationConfig { List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> val normalizedTimePerDayToRiskLevelMappingList: List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> + val diagnosisKeysDataMapping: DiagnosisKeysDataMapping interface Mapper { fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt index 8c2e9502ffe4d60f99dfab4d11b24ea62e67a4b1..47b827428122cb17950186207ffd39ad2890791a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt @@ -1,26 +1,24 @@ package de.rki.coronawarnapp.appconfig.mapping -import androidx.annotation.VisibleForTesting import dagger.Reusable import de.rki.coronawarnapp.appconfig.CWAConfig -import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid.ApplicationConfigurationAndroid import de.rki.coronawarnapp.server.protocols.internal.v2.AppFeaturesOuterClass import timber.log.Timber import javax.inject.Inject @Reusable class CWAConfigMapper @Inject constructor() : CWAConfig.Mapper { - override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): CWAConfig { + override fun map(rawConfig: ApplicationConfigurationAndroid): CWAConfig { return CWAConfigContainer( latestVersionCode = rawConfig.latestVersionCode, minVersionCode = rawConfig.minVersionCode, supportedCountries = rawConfig.getMappedSupportedCountries(), - appFeatures = rawConfig.appFeatures + appFeatures = rawConfig.mapAppFeatures() ) } - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun AppConfigAndroid.ApplicationConfigurationAndroid.getMappedSupportedCountries(): List<String> = + private fun ApplicationConfigurationAndroid.getMappedSupportedCountries(): List<String> = when { supportedCountriesList == null -> emptyList() supportedCountriesList.size == 1 && !VALID_CC.matches(supportedCountriesList.single()) -> { @@ -30,11 +28,22 @@ class CWAConfigMapper @Inject constructor() : CWAConfig.Mapper { else -> supportedCountriesList } + private fun ApplicationConfigurationAndroid.mapAppFeatures(): List<AppFeaturesOuterClass.AppFeature> = + if (hasAppFeatures()) { + val parsedFeatures = mutableListOf<AppFeaturesOuterClass.AppFeature>() + for (index in 0 until appFeatures.appFeaturesCount) { + parsedFeatures.add(appFeatures.getAppFeatures(index)) + } + parsedFeatures + } else { + emptyList() + } + data class CWAConfigContainer( override val latestVersionCode: Long, override val minVersionCode: Long, override val supportedCountries: List<String>, - override val appFeatures: AppFeaturesOuterClass.AppFeatures + override val appFeatures: List<AppFeaturesOuterClass.AppFeature> ) : CWAConfig companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt index ab55b97ee827ef27a091e1575c0eae048e50ee7a..607c0d8d0ed2019165a2705693411f70c4ef9d7f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.appconfig.mapping +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping import dagger.Reusable import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException @@ -18,6 +19,12 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() : ) } + if (!rawConfig.hasDiagnosisKeysDataMapping()) { + throw ApplicationConfigurationInvalidException( + message = "Diagnosis Keys Data Mapping is missing" + ) + } + val riskCalculationParameters = rawConfig.riskCalculationParameters return ExposureWindowRiskCalculationContainer( @@ -34,10 +41,24 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() : normalizedTimePerExposureWindowToRiskLevelMapping = riskCalculationParameters .normalizedTimePerEWToRiskLevelMappingList, normalizedTimePerDayToRiskLevelMappingList = riskCalculationParameters - .normalizedTimePerDayToRiskLevelMappingList + .normalizedTimePerDayToRiskLevelMappingList, + diagnosisKeysDataMapping = rawConfig.diagnosisKeysDataMapping() ) } + private fun AppConfigAndroid.ApplicationConfigurationAndroid.diagnosisKeysDataMapping(): + DiagnosisKeysDataMapping { + val diagnosisKeyDataMapping = this.diagnosisKeysDataMapping + return DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder() + .apply { + setDaysSinceOnsetToInfectiousness(diagnosisKeyDataMapping.daysSinceOnsetToInfectiousnessMap) + setInfectiousnessWhenDaysSinceOnsetMissing( + diagnosisKeysDataMapping.infectiousnessWhenDaysSinceOnsetMissing + ) + setReportTypeWhenMissing(diagnosisKeysDataMapping.reportTypeWhenMissing) + }.build() + } + data class ExposureWindowRiskCalculationContainer( override val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>, override val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>, @@ -47,6 +68,7 @@ class ExposureWindowRiskCalculationConfigMapper @Inject constructor() : override val normalizedTimePerExposureWindowToRiskLevelMapping: List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>, override val normalizedTimePerDayToRiskLevelMappingList: - List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> + List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>, + override val diagnosisKeysDataMapping: DiagnosisKeysDataMapping ) : ExposureWindowRiskCalculationConfig } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt index dda995f9b17ddd06d498c0292e125490eed35eeb..4f0db33f244672f31344b0217142d6ef1305ac21 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt @@ -61,8 +61,9 @@ class AppConfigStorage @Inject constructor( } return@withLock try { - gson.fromJson<InternalConfigData>(configFile).also { + gson.fromJson<InternalConfigData>(configFile)?.also { requireNotNull(it.rawData) + Timber.v("Loaded stored config, serverTime=%s", it.serverTime) } } catch (e: Exception) { Timber.e(e, "Couldn't load config.") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt index e5006f0279a83676f128d16bb1b1a5f68d7caa92..f77c2a8de1ef49852e4af005ce8eacbc09669c97 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt @@ -19,7 +19,7 @@ class RemoteAppConfigSource @Inject constructor( ) { suspend fun getConfigData(): ConfigData? = withContext(dispatcherProvider.IO) { - Timber.tag(TAG).v("retrieveConfig()") + Timber.tag(TAG).v("getConfigData()") val configDownload = try { server.downloadAppConfig() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationOneTimeWorker.kt index 039330a1ca998c927852a16cf9d0a0465b072503..9eec045cc1776734cbad047b9c822b3db65cbe65 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationOneTimeWorker.kt @@ -22,10 +22,10 @@ class DeadmanNotificationOneTimeWorker @AssistedInject constructor( CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { - Timber.d("Background job started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("Background job started. Run attempt: $runAttemptCount") if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.d("Background job failed after $runAttemptCount attempts. Rescheduling") + Timber.tag(TAG).d("Background job failed after $runAttemptCount attempts. Rescheduling") return Result.failure() } @@ -41,4 +41,8 @@ class DeadmanNotificationOneTimeWorker @AssistedInject constructor( @AssistedInject.Factory interface Factory : InjectedWorkerFactory<DeadmanNotificationOneTimeWorker> + + companion object { + private val TAG = DeadmanNotificationOneTimeWorker::class.java.simpleName + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationPeriodicWorker.kt index 53e5d06ca010c8e45f40be61ab9219f0141d0381..efd968e15ea379950f0a7b1dacc8e6a39d6ee66f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deadman/DeadmanNotificationPeriodicWorker.kt @@ -22,10 +22,10 @@ class DeadmanNotificationPeriodicWorker @AssistedInject constructor( CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { - Timber.d("Background job started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("Background job started. Run attempt: $runAttemptCount") if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.d("Background job failed after $runAttemptCount attempts. Rescheduling") + Timber.tag(TAG).d("Background job failed after $runAttemptCount attempts. Rescheduling") return Result.failure() } @@ -34,7 +34,7 @@ class DeadmanNotificationPeriodicWorker @AssistedInject constructor( // Schedule one time deadman notification send work scheduler.scheduleOneTime() } catch (e: Exception) { - Timber.d(e) + Timber.tag(TAG).d(e) result = Result.retry() } @@ -43,4 +43,8 @@ class DeadmanNotificationPeriodicWorker @AssistedInject constructor( @AssistedInject.Factory interface Factory : InjectedWorkerFactory<DeadmanNotificationPeriodicWorker> + + companion object { + private val TAG = DeadmanNotificationPeriodicWorker::class.java.simpleName + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt index 9474d942db8ec51dbc6d405e493886940d2bde59..586d8a9510e5b1b3cfcbacbc1d757bd72b02e440 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DayPackageSyncTool.kt @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.exception.http.CwaUnknownHostException import de.rki.coronawarnapp.storage.DeviceStorage import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate import de.rki.coronawarnapp.util.TimeStamper @@ -46,9 +47,15 @@ class DayPackageSyncTool @Inject constructor( val downloadConfig: KeyDownloadConfig = configProvider.getAppConfig() val keysWereRevoked = revokeCachedKeys(downloadConfig.revokedDayPackages) - val missingDays = targetLocations.mapNotNull { - determineMissingDayPackages(it, forceIndexLookup || keysWereRevoked) + val missingDays = try { + targetLocations.mapNotNull { + determineMissingDayPackages(it, forceIndexLookup || keysWereRevoked) + } + } catch (e: CwaUnknownHostException) { + Timber.tag(TAG).w(e, "Failed to sync with day index.") + return SyncResult(successful = false, newPackages = emptyList()) } + if (missingDays.isEmpty()) { Timber.tag(TAG).i("There were no missing day packages.") return SyncResult(successful = true, newPackages = emptyList()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt index d04dc5137d1dc33c3eb31577ef33fade451f4594..cd5bc298faa82904ddbf2c2610ed954ed4a98750 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.diagnosiskeys.download import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.environment.EnvironmentSetup @@ -8,7 +9,6 @@ import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.risk.RollbackItem -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -63,7 +63,7 @@ class DownloadDiagnosisKeysTask @Inject constructor( throwIfCancelled() // RETRIEVE RISK SCORE PARAMETERS - val exposureConfig: ExposureDetectionConfig = appConfigProvider.getAppConfig() + val exposureConfig: ConfigData = appConfigProvider.getAppConfig() internalProgress.send(Progress.ApiSubmissionStarted) internalProgress.send(Progress.KeyFilesDownloadStarted) @@ -102,15 +102,12 @@ class DownloadDiagnosisKeysTask @Inject constructor( ) Timber.tag(TAG).d("Attempting submission to ENF") - val isSubmissionSuccessful = enfClient.provideDiagnosisKeys(availableKeyFiles) + val isSubmissionSuccessful = enfClient.provideDiagnosisKeys( + availableKeyFiles, + exposureConfig.diagnosisKeysDataMapping + ) Timber.tag(TAG).d("Diagnosis Keys provided (success=%s)", isSubmissionSuccessful) - // EXPOSUREAPP-3878 write timestamp immediately after submission, - // so that progress observers can rely on a clean app state - if (isSubmissionSuccessful) { - saveTimestamp(currentDate, rollbackItems) - } - internalProgress.send(Progress.ApiSubmissionFinished) return object : Task.Result {} @@ -155,18 +152,6 @@ class DownloadDiagnosisKeysTask @Inject constructor( } } - private fun saveTimestamp( - currentDate: Date, - rollbackItems: MutableList<RollbackItem> - ) { - val lastFetchDateForRollback = LocalData.lastTimeDiagnosisKeysFromServerFetch() - rollbackItems.add { - LocalData.lastTimeDiagnosisKeysFromServerFetch(lastFetchDateForRollback) - } - Timber.tag(TAG).d("dateUpdate(currentDate=%s)", currentDate) - LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate) - } - private fun rollback(rollbackItems: MutableList<RollbackItem>) { try { Timber.tag(TAG).d("Initiate Rollback") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt index a15a2fa6969f165ac04109a4a55e3022a8bf878a..6cd2b736817058246ad9515cab0c3e09309f776c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.exception.http.NetworkConnectTimeoutException import de.rki.coronawarnapp.storage.DeviceStorage import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate import de.rki.coronawarnapp.util.TimeStamper @@ -51,7 +52,12 @@ class HourPackageSyncTool @Inject constructor( val keysWereRevoked = revokeCachedKeys(downloadConfig.revokedHourPackages) val missingHours = targetLocations.mapNotNull { - determineMissingHours(it, forceIndexLookup || keysWereRevoked) + try { + determineMissingHours(it, forceIndexLookup || keysWereRevoked) + } catch (e: NetworkConnectTimeoutException) { + Timber.tag(TAG).i("missing hours sync failed due to network timeout") + return SyncResult(successful = false, newPackages = emptyList()) + } } if (missingHours.isEmpty()) { Timber.tag(TAG).i("There were no missing hours.") @@ -142,6 +148,9 @@ class HourPackageSyncTool @Inject constructor( val availableHours = run { val hoursToday = try { keyServer.getHourIndex(location, today) + } catch (e: NetworkConnectTimeoutException) { + Timber.tag(TAG).e(e, "Failed to get today's hour due - not going to delete the cache.") + throw e } catch (e: IOException) { Timber.tag(TAG).e(e, "failed to get today's hour index.") emptyList() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt index fc89eff4627de3bd0fb3069f8937876563fb0341..7f8d7b4ae755482bc1d80ca7a1d4ce85bd243010 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt @@ -3,46 +3,77 @@ package de.rki.coronawarnapp.exception.http import de.rki.coronawarnapp.exception.reporting.ErrorCodes import de.rki.coronawarnapp.exception.reporting.ReportedIOException -open class CwaWebException(val statusCode: Int) : ReportedIOException( - ErrorCodes.CWA_WEB_REQUEST_PROBLEM.code, "error during web request, http status $statusCode" +open class CwaWebException( + val statusCode: Int, + message: String?, + cause: Throwable? = null +) : ReportedIOException( + code = ErrorCodes.CWA_WEB_REQUEST_PROBLEM.code, + message = "Error during web request: code=$statusCode message=$message", + cause = cause ) -open class CwaServerError(statusCode: Int) : CwaWebException(statusCode) { +open class CwaServerError( + statusCode: Int, + message: String?, + cause: Throwable? = null +) : CwaWebException( + statusCode = statusCode, + message = message, + cause = cause +) { init { - if (statusCode !in 500..599) - throw IllegalArgumentException("a server error has to have code 5xx") + if (statusCode !in 500..599) { + throw IllegalArgumentException("Invalid HTTP server error code $statusCode (!= 5xx)") + } } } -open class CwaClientError(statusCode: Int) : CwaWebException(statusCode) { +open class CwaClientError( + statusCode: Int, + message: String?, + cause: Throwable? = null +) : CwaWebException( + statusCode = statusCode, + message = message, + cause = cause +) { init { - if (statusCode !in 400..499) - throw IllegalArgumentException("a client error has to have code 4xx") + if (statusCode !in 400..499) { + throw IllegalArgumentException("Invalid HTTP client error code $statusCode (!= 4xx)") + } } } -open class CwaSuccessResponseWithCodeMismatchNotSupportedError(statusCode: Int) : - CwaWebException(statusCode) - -open class CwaInformationalNotSupportedError(statusCode: Int) : CwaWebException(statusCode) -open class CwaRedirectNotSupportedError(statusCode: Int) : CwaWebException(statusCode) - -class BadRequestException : CwaClientError(400) -class UnauthorizedException : CwaClientError(401) -class ForbiddenException : CwaClientError(403) -class NotFoundException : CwaClientError(404) -class ConflictException : CwaClientError(409) -class GoneException : CwaClientError(410) -class UnsupportedMediaTypeException : CwaClientError(415) -class TooManyRequestsException : CwaClientError(429) - -class InternalServerErrorException : CwaServerError(500) -class NotImplementedException : CwaServerError(501) -class BadGatewayException : CwaServerError(502) -class ServiceUnavailableException : CwaServerError(503) -class GatewayTimeoutException : CwaServerError(504) -class HTTPVersionNotSupported : CwaServerError(505) -class NetworkAuthenticationRequiredException : CwaServerError(511) -class CwaUnknownHostException : CwaServerError(597) -class NetworkReadTimeoutException : CwaServerError(598) -class NetworkConnectTimeoutException : CwaServerError(599) +open class CwaSuccessResponseWithCodeMismatchNotSupportedError(statusCode: Int, message: String?) : + CwaWebException(statusCode, message) + +open class CwaInformationalNotSupportedError(statusCode: Int, message: String?) : CwaWebException(statusCode, message) +open class CwaRedirectNotSupportedError(statusCode: Int, message: String?) : CwaWebException(statusCode, message) + +class BadRequestException(message: String?) : CwaClientError(400, message) +class UnauthorizedException(message: String?) : CwaClientError(401, message) +class ForbiddenException(message: String?) : CwaClientError(403, message) +class NotFoundException(message: String?) : CwaClientError(404, message) +class ConflictException(message: String?) : CwaClientError(409, message) +class GoneException(message: String?) : CwaClientError(410, message) +class UnsupportedMediaTypeException(message: String?) : CwaClientError(415, message) +class TooManyRequestsException(message: String?) : CwaClientError(429, message) + +class InternalServerErrorException(message: String?) : CwaServerError(500, message) +class NotImplementedException(message: String?) : CwaServerError(501, message) +class BadGatewayException(message: String?) : CwaServerError(502, message) +class ServiceUnavailableException(message: String?) : CwaServerError(503, message) +class GatewayTimeoutException(message: String?) : CwaServerError(504, message) +class HTTPVersionNotSupported(message: String?) : CwaServerError(505, message) +class NetworkAuthenticationRequiredException(message: String?) : CwaServerError(511, message) +class CwaUnknownHostException( + message: String? = null, + cause: Throwable? +) : CwaServerError(597, message, cause) + +class NetworkReadTimeoutException(message: String?) : CwaServerError(598, message) +class NetworkConnectTimeoutException( + message: String? = null, + cause: Throwable? = null +) : CwaServerError(599, message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt index 1061abc7e164af4ff5ac41ec2e63776004ebcce3..40765f7fa89c1dbc9f350737c818cbf97900a35d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt @@ -26,6 +26,7 @@ import de.rki.coronawarnapp.exception.http.UnauthorizedException import de.rki.coronawarnapp.exception.http.UnsupportedMediaTypeException import okhttp3.Interceptor import okhttp3.Response +import timber.log.Timber import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection @@ -34,43 +35,54 @@ class HttpErrorParser : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { try { val response = chain.proceed(chain.request()) + + val message: String? = try { + if (response.isSuccessful) { + null + } else { + response.message + } + } catch (e: Exception) { + Timber.w("Failed to get http error message.") + null + } return when (val code = response.code) { HttpsURLConnection.HTTP_OK -> response HttpsURLConnection.HTTP_CREATED -> response HttpsURLConnection.HTTP_ACCEPTED -> response HttpsURLConnection.HTTP_NO_CONTENT -> response - HttpsURLConnection.HTTP_BAD_REQUEST -> throw BadRequestException() - HttpsURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() - HttpsURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() - HttpsURLConnection.HTTP_NOT_FOUND -> throw NotFoundException() - HttpsURLConnection.HTTP_CONFLICT -> throw ConflictException() - HttpsURLConnection.HTTP_GONE -> throw GoneException() - HttpsURLConnection.HTTP_UNSUPPORTED_TYPE -> throw UnsupportedMediaTypeException() - 429 -> throw TooManyRequestsException() - HttpsURLConnection.HTTP_INTERNAL_ERROR -> throw InternalServerErrorException() - HttpsURLConnection.HTTP_NOT_IMPLEMENTED -> throw NotImplementedException() - HttpsURLConnection.HTTP_BAD_GATEWAY -> throw BadGatewayException() - HttpsURLConnection.HTTP_UNAVAILABLE -> throw ServiceUnavailableException() - HttpsURLConnection.HTTP_GATEWAY_TIMEOUT -> throw GatewayTimeoutException() - HttpsURLConnection.HTTP_VERSION -> throw HTTPVersionNotSupported() - 511 -> throw NetworkAuthenticationRequiredException() - 598 -> throw NetworkReadTimeoutException() - 599 -> throw NetworkConnectTimeoutException() + HttpsURLConnection.HTTP_BAD_REQUEST -> throw BadRequestException(message) + HttpsURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException(message) + HttpsURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException(message) + HttpsURLConnection.HTTP_NOT_FOUND -> throw NotFoundException(message) + HttpsURLConnection.HTTP_CONFLICT -> throw ConflictException(message) + HttpsURLConnection.HTTP_GONE -> throw GoneException(message) + HttpsURLConnection.HTTP_UNSUPPORTED_TYPE -> throw UnsupportedMediaTypeException(message) + 429 -> throw TooManyRequestsException(message) + HttpsURLConnection.HTTP_INTERNAL_ERROR -> throw InternalServerErrorException(message) + HttpsURLConnection.HTTP_NOT_IMPLEMENTED -> throw NotImplementedException(message) + HttpsURLConnection.HTTP_BAD_GATEWAY -> throw BadGatewayException(message) + HttpsURLConnection.HTTP_UNAVAILABLE -> throw ServiceUnavailableException(message) + HttpsURLConnection.HTTP_GATEWAY_TIMEOUT -> throw GatewayTimeoutException(message) + HttpsURLConnection.HTTP_VERSION -> throw HTTPVersionNotSupported(message) + 511 -> throw NetworkAuthenticationRequiredException(message) + 598 -> throw NetworkReadTimeoutException(message) + 599 -> throw NetworkConnectTimeoutException(message) else -> { - if (code in 100..199) throw CwaInformationalNotSupportedError(code) + if (code in 100..199) throw CwaInformationalNotSupportedError(code, message) if (code in 200..299) throw CwaSuccessResponseWithCodeMismatchNotSupportedError( - code + code, message ) - if (code in 300..399) throw CwaRedirectNotSupportedError(code) - if (code in 400..499) throw CwaClientError(code) - if (code in 500..599) throw CwaServerError(code) - throw CwaWebException(code) + if (code in 300..399) throw CwaRedirectNotSupportedError(code, message) + if (code in 400..499) throw CwaClientError(code, message) + if (code in 500..599) throw CwaServerError(code, message) + throw CwaWebException(code, message) } } } catch (err: SocketTimeoutException) { - throw NetworkConnectTimeoutException() + throw NetworkConnectTimeoutException(cause = err) } catch (err: UnknownHostException) { - throw CwaUnknownHostException() + throw CwaUnknownHostException(cause = err) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt index 587603da757c029bf13e0a553538130c9737fa96..4838e9ceeac1340f3a7bd07671e84e8b6dd712bc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.nearby +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker @@ -36,7 +37,10 @@ class ENFClient @Inject constructor( internal val internalClient: ExposureNotificationClient get() = googleENFClient - override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean { + override suspend fun provideDiagnosisKeys( + keyFiles: Collection<File>, + newDiagnosisKeysDataMapping: DiagnosisKeysDataMapping + ): Boolean { Timber.d("asyncProvideDiagnosisKeys(keyFiles=$keyFiles)") return if (keyFiles.isEmpty()) { @@ -45,7 +49,7 @@ class ENFClient @Inject constructor( } else { Timber.d("Forwarding %d key files to our DiagnosisKeyProvider.", keyFiles.size) exposureDetectionTracker.trackNewExposureDetection(UUID.randomUUID().toString()) - diagnosisKeyProvider.provideDiagnosisKeys(keyFiles) + diagnosisKeyProvider.provideDiagnosisKeys(keyFiles, newDiagnosisKeysDataMapping) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt index 1d4220ee0df1f0640c4d5587ddd56bfdbd669e77..cbf0c96de676d53d0f59d28aad11c4ea4ee121e5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt @@ -9,6 +9,8 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.DefaultExposureDetec import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DefaultDiagnosisKeyProvider import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider +import de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper.DefaultDiagnosisKeysDataMapper +import de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper.DiagnosisKeysDataMapper import de.rki.coronawarnapp.nearby.modules.exposurewindow.DefaultExposureWindowProvider import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider import de.rki.coronawarnapp.nearby.modules.locationless.DefaultScanningSupport @@ -48,6 +50,11 @@ class ENFModule { fun exposureWindowProvider(exposureWindowProvider: DefaultExposureWindowProvider): ExposureWindowProvider = exposureWindowProvider + @Singleton + @Provides + fun diagnosisKeysDataMapper(diagnosisKeysDataMapper: DefaultDiagnosisKeysDataMapper): + DiagnosisKeysDataMapper = diagnosisKeysDataMapper + @Singleton @Provides fun calculationTracker(exposureDetectionTracker: DefaultExposureDetectionTracker): ExposureDetectionTracker = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt index f0f0ed5c5ff56d8758407875de879465d2e79560..5332793bffb9cff398fb6bae06816085bd8a7086 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt @@ -8,8 +8,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.risk.ExposureResult -import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest @@ -19,23 +17,15 @@ import timber.log.Timber class ExposureStateUpdateWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, - private val exposureResultStore: ExposureResultStore, - private val enfClient: ENFClient, private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { try { - Timber.v("worker to persist exposure summary started") - enfClient.exposureWindows().let { - exposureResultStore.entities.value = ExposureResult(it, null) - Timber.v("exposure summary state updated: $it") - } - taskController.submit( DefaultTaskRequest(RiskLevelTask::class, originTag = "ExposureStateUpdateWorker") ) - Timber.v("risk level calculation triggered") + Timber.tag(TAG).v("Risk level calculation triggered") } catch (e: ApiException) { e.report(ExceptionCategory.EXPOSURENOTIFICATION) } @@ -45,4 +35,8 @@ class ExposureStateUpdateWorker @AssistedInject constructor( @AssistedInject.Factory interface Factory : InjectedWorkerFactory<ExposureStateUpdateWorker> + + companion object { + private val TAG = ExposureStateUpdateWorker::class.java.simpleName + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt index 909a66247fd7405a3239631942052de2892add21..0375e974226341dae587918b107e4e51374622d0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt @@ -88,7 +88,8 @@ class DefaultExposureDetectionTracker @Inject constructor( mutate { this[identifier] = TrackedExposureDetection( identifier = identifier, - startedAt = timeStamper.nowUTC + startedAt = timeStamper.nowUTC, + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..a05ccdf39d2d6e6ce63cd0b147d4c2f2ce89abfb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.nearby.modules.detectiontracker + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +suspend fun ExposureDetectionTracker.lastSubmission( + onlyFinished: Boolean = true +): TrackedExposureDetection? = calculations + .first().values + .filter { it.isSuccessful || !onlyFinished } + .maxByOrNull { it.startedAt } + +fun ExposureDetectionTracker.latestSubmission( + onlySuccessful: Boolean = true +): Flow<TrackedExposureDetection?> = calculations + .map { entries -> + entries.values.filter { it.isSuccessful || !onlySuccessful } + } + .map { detections -> + detections.maxByOrNull { it.startedAt } + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt index c4299cafe56f85562c86d276a10d910bb5e39287..8ca647c1d71529065afd20c959a659986bf94c38 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt @@ -43,13 +43,11 @@ class ExposureDetectionTrackerStorage @Inject constructor( suspend fun load(): Map<String, TrackedExposureDetection> = mutex.withLock { return@withLock try { - if (!storageFile.exists()) return@withLock emptyMap() - - gson.fromJson<Map<String, TrackedExposureDetection>>(storageFile).also { + gson.fromJson<Map<String, TrackedExposureDetection>>(storageFile)?.also { require(it.size >= 0) Timber.v("Loaded detection data: %s", it) lastCalcuationData = it - } + } ?: emptyMap() } catch (e: Exception) { Timber.e(e, "Failed to load tracked detections.") if (storageFile.delete()) Timber.w("Storage file was deleted.") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt index 20fdddefe71875201448539b662f89f782f986dd..e5ecf15fb0fbaa6ba111ed922c022c4cce5cf25a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt @@ -9,7 +9,8 @@ data class TrackedExposureDetection( @SerializedName("identifier") val identifier: String, @SerializedName("startedAt") val startedAt: Instant, @SerializedName("result") val result: Result? = null, - @SerializedName("finishedAt") val finishedAt: Instant? = null + @SerializedName("finishedAt") val finishedAt: Instant? = null, + @SerializedName("enfVersion") val enfVersion: EnfVersion? = null ) { val isCalculating: Boolean @@ -28,4 +29,11 @@ data class TrackedExposureDetection( @SerializedName("TIMEOUT") TIMEOUT } + + enum class EnfVersion { + @SerializedName("V1_LEGACY_MODE") + V1_LEGACY_MODE, + @SerializedName("V2_WINDOW_MODE") + V2_WINDOW_MODE + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt index 64f2e75d7addfca50ab691aa7d42bc7d8cd3f4cd..3f22ca8519ee4791d8639b77d794ed21ec81ab1c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt @@ -1,8 +1,11 @@ package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider import com.google.android.gms.common.api.ApiException +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeyFileProvider import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.exception.reporting.ReportingConstants +import de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper.DiagnosisKeysDataMapper import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import timber.log.Timber import java.io.File @@ -16,10 +19,16 @@ import kotlin.coroutines.suspendCoroutine class DefaultDiagnosisKeyProvider @Inject constructor( private val enfVersion: ENFVersion, private val submissionQuota: SubmissionQuota, - private val enfClient: ExposureNotificationClient + private val enfClient: ExposureNotificationClient, + private val diagnosisKeysDataMapper: DiagnosisKeysDataMapper ) : DiagnosisKeyProvider { - override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean { + override suspend fun provideDiagnosisKeys( + keyFiles: Collection<File>, + newDiagnosisKeysDataMapping: DiagnosisKeysDataMapping + ): Boolean { + diagnosisKeysDataMapper.updateDiagnosisKeysDataMapping(newDiagnosisKeysDataMapping) + if (keyFiles.isEmpty()) { Timber.d("No key files submitted, returning early.") return true @@ -35,10 +44,19 @@ class DefaultDiagnosisKeyProvider @Inject constructor( // return false } + val keyFilesList = keyFiles.toList() + val provideDiagnosisKeysTask = if (enfVersion.isAtLeast(ENFVersion.V1_7)) { + Timber.i("Provide diagnosis keys with DiagnosisKeyFileProvider") + val diagnosisKeyFileProvider = DiagnosisKeyFileProvider(keyFilesList) + enfClient.provideDiagnosisKeys(diagnosisKeyFileProvider) + } else { + Timber.i("Provide diagnosis keys as list") + enfClient.provideDiagnosisKeys(keyFilesList) + } + return suspendCoroutine { cont -> Timber.d("Performing key submission.") - enfClient - .provideDiagnosisKeys(keyFiles.toList()) + provideDiagnosisKeysTask .addOnSuccessListener { cont.resume(true) } .addOnFailureListener { val wrappedException = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt index b3339619f5b4e96635d1326a43cfb8d1cc09951e..14e67c266290a30290e3bab27b0a8c413c8fc4e7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping import java.io.File interface DiagnosisKeyProvider { @@ -15,5 +16,8 @@ interface DiagnosisKeyProvider { * @return */ - suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean + suspend fun provideDiagnosisKeys( + keyFiles: Collection<File>, + newDiagnosisKeysDataMapping: DiagnosisKeysDataMapping + ): Boolean } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DefaultDiagnosisKeysDataMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DefaultDiagnosisKeysDataMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..7f018e89cac0b2f5ecf64a4faacfe2779547fbfe --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DefaultDiagnosisKeysDataMapper.kt @@ -0,0 +1,64 @@ +package de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper + +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.FAILED_RATE_LIMITED +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@Singleton +class DefaultDiagnosisKeysDataMapper @Inject constructor( + private val client: ExposureNotificationClient +) : DiagnosisKeysDataMapper { + private suspend fun getDiagnosisKeysDataMapping(): DiagnosisKeysDataMapping? = + suspendCoroutine { cont -> + client.diagnosisKeysDataMapping + .addOnSuccessListener { cont.resume(it) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + private suspend fun setDiagnosisKeysDataMapping(diagnosisKeysDataMapping: DiagnosisKeysDataMapping) = + suspendCoroutine<Unit> { cont -> + client.setDiagnosisKeysDataMapping(diagnosisKeysDataMapping) + .addOnSuccessListener { cont.resume(Unit) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + override suspend fun updateDiagnosisKeysDataMapping(newDiagnosisKeysDataMapping: DiagnosisKeysDataMapping) { + val currentDiagnosisKeysDataMapping = + try { + getDiagnosisKeysDataMapping() + } catch (e: Exception) { + Timber.e("Failed to get the current DiagnosisKeysDataMapping assuming none present") + null + } + + if (newDiagnosisKeysDataMapping.hasChanged(currentDiagnosisKeysDataMapping)) { + try { + Timber.i( + "Current DiagnosisKeysDataMapping: %s vs new: %s, applying new version.", + currentDiagnosisKeysDataMapping, + newDiagnosisKeysDataMapping + ) + setDiagnosisKeysDataMapping(newDiagnosisKeysDataMapping) + } catch (e: ApiException) { + if (e.statusCode == FAILED_RATE_LIMITED) { + Timber.e(e, "Failed to setDiagnosisKeysDataMapping due to rate limit ") + } else { + throw e + } + } + } + } + + companion object { + fun DiagnosisKeysDataMapping?.hasChanged(old: DiagnosisKeysDataMapping?): Boolean { + return old == null || old.hashCode() != hashCode() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DiagnosisKeysDataMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DiagnosisKeysDataMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..99cdec102fa88402e49c66f18e62e9553d30858b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DiagnosisKeysDataMapper.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper + +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping + +interface DiagnosisKeysDataMapper { + suspend fun updateDiagnosisKeysDataMapping(newDiagnosisKeysDataMapping: DiagnosisKeysDataMapping) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt index c2319109f5914784469d885563b9d82fb7511809..d8ef3330e9fda35f2935660f6ce4ffef0094f8c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt @@ -5,16 +5,16 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.flow.shareLatest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.isActive import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -28,24 +28,26 @@ class DefaultTracingStatus @Inject constructor( @AppScope val scope: CoroutineScope ) : TracingStatus { - override val isTracingEnabled: Flow<Boolean> = callbackFlow<Boolean> { + override val isTracingEnabled: Flow<Boolean> = flow { while (true) { try { - send(pollIsEnabled()) - } catch (e: Exception) { - Timber.w(e, "ENF isEnabled failed.") - send(false) - e.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null) - cancel("ENF isEnabled failed", e) + emit(pollIsEnabled()) + delay(POLLING_DELAY_MS) + } catch (e: CancellationException) { + Timber.d("isBackgroundRestricted was cancelled") + break } - if (!isActive) break - delay(POLLING_DELAY_MS) } } .distinctUntilChanged() .onStart { Timber.v("isTracingEnabled FLOW start") } .onEach { Timber.v("isTracingEnabled FLOW emission: %b", it) } - .onCompletion { Timber.v("isTracingEnabled FLOW completed.") } + .onCompletion { if (it == null) Timber.v("isTracingEnabled FLOW completed.") } + .catch { + Timber.w(it, "ENF isEnabled failed.") + it.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null) + emit(false) + } .shareLatest( tag = TAG, scope = scope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt index 7652fb055bdb64e4cbd76d4528a7047ffbfeef91..3ce93d4823d6b61d3c6a2634937087ee742f9a31 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt @@ -9,6 +9,7 @@ import javax.inject.Singleton import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +import kotlin.math.abs @Singleton class DefaultENFVersion @Inject constructor( @@ -39,9 +40,28 @@ class DefaultENFVersion @Inject constructor( } } + override suspend fun isAtLeast(compareVersion: Long): Boolean { + if (!compareVersion.isCorrectVersionLength) throw IllegalArgumentException("given version has incorrect length") + + getENFClientVersion()?.let { currentENFClientVersion -> + Timber.i("Comparing current ENF client version $currentENFClientVersion with $compareVersion") + return currentENFClientVersion >= compareVersion + } + + return false + } + private suspend fun internalGetENFClientVersion(): Long = suspendCoroutine { cont -> client.version .addOnSuccessListener { cont.resume(it) } .addOnFailureListener { cont.resumeWithException(it) } } + + // check if a raw long has the correct length to be considered an API version + private val Long.isCorrectVersionLength + get(): Boolean = abs(this).toString().length == GOOGLE_API_VERSION_FIELD_LENGTH + + companion object { + private const val GOOGLE_API_VERSION_FIELD_LENGTH = 8 + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt index b7d16994a91e0742d3910f3c22fd7323b31b6857..7b1b8d8303560d5d1fd2e93dd5a4d90dc301e23f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt @@ -12,7 +12,15 @@ interface ENFVersion { */ suspend fun requireMinimumVersion(required: Long) + /** + * Indicates if the client runs above a certain version + * + * @return isAboveVersion, if connected to an old unsupported version, return false + */ + suspend fun isAtLeast(compareVersion: Long): Boolean + companion object { const val V1_6 = 16000000L + const val V1_7 = 17000000L } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt index 01b785fe65070177f3bd47080b2fd1fbc0c3712e..c8e44982d9912472c91bdf9fc27c194990435c68 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationConstants.kt @@ -17,6 +17,9 @@ object NotificationConstants { val POSITIVE_RESULT_NOTIFICATION_INITIAL_OFFSET: Duration = Duration.standardHours(2) val POSITIVE_RESULT_NOTIFICATION_INTERVAL: Duration = Duration.standardHours(2) + const val NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID = 110 + const val NEW_MESSAGE_TEST_RESULT_NOTIFICATION_ID = 120 + /** * Notification channel id String.xml path */ @@ -36,14 +39,4 @@ object NotificationConstants { * Notification channel description String.xml path */ const val CHANNEL_DESCRIPTION = R.string.notification_description - - /** - * Risk changed notification title String.xml path - */ - const val NOTIFICATION_CONTENT_TITLE_RISK_CHANGED = R.string.notification_headline - - /** - * Risk changed notification content text String.xml path - */ - const val NOTIFICATION_CONTENT_TEXT_RISK_CHANGED = R.string.notification_body } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt index c642a8a4b7788ec131309bd8704549889abffebc..d33f520cdde49538a824e513bcd03a0e3e340ac7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt @@ -16,12 +16,12 @@ import androidx.core.app.NotificationCompat.PRIORITY_HIGH import androidx.core.app.NotificationManagerCompat import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.R import de.rki.coronawarnapp.notification.NotificationConstants.NOTIFICATION_ID import de.rki.coronawarnapp.ui.main.MainActivity import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber -import kotlin.random.Random /** * Singleton class for notification handling @@ -94,6 +94,11 @@ object NotificationHelper { Timber.v("Canceled future notifications with id: %s", notificationId) } + fun cancelCurrentNotification(notificationId: Int) { + NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).cancel(notificationId) + Timber.v("Canceled notifications with id: %s", notificationId) + } + fun scheduleRepeatingNotification( initialTime: Instant, interval: Duration, @@ -189,19 +194,18 @@ object NotificationHelper { * * @param title: String * @param content: String - * @param visibility: Int * @param expandableLongText: Boolean * @param notificationId: NotificationId * @param pendingIntent: PendingIntent */ - fun sendNotification( - title: String, + title: String = CoronaWarnApplication.getAppContext().getString(R.string.notification_name), content: String, + notificationId: NotificationId, expandableLongText: Boolean = false, - notificationId: NotificationId = Random.nextInt(), pendingIntent: PendingIntent = createPendingIntentToMainActivity() ) { + Timber.d("Sending notification with id: %s | title: %s | content: %s", notificationId, title, content) val notification = buildNotification(title, content, PRIORITY_HIGH, expandableLongText, pendingIntent) ?: return with(NotificationManagerCompat.from(CoronaWarnApplication.getAppContext())) { @@ -215,10 +219,16 @@ object NotificationHelper { * Notification is only sent if app is not in foreground. * * @param content: String + * @param notificationId: NotificationId */ - fun sendNotification(content: String) { + fun sendNotificationIfAppIsNotInForeground(content: String, notificationId: NotificationId) { if (!CoronaWarnApplication.isAppInForeground) { - sendNotification("", content, true) + sendNotification( + content = content, + notificationId = notificationId, + expandableLongText = true) + } else { + Timber.d("App is in foreground - not sending the notification with id: %s", notificationId) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt index ab39dda9e64d0849d655603e9c720ff53819c7f8..3d8dc7205d3755f631f6bdc1aae85a4835c84f20 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt @@ -14,6 +14,7 @@ import org.joda.time.Instant import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton +import kotlin.math.max @Singleton class DefaultRiskLevels @Inject constructor() : RiskLevels { @@ -37,7 +38,7 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels { // Get total seconds at attenuation in exposure window val secondsAtAttenuation: Double = scanInstances .filter { attenuationFilter.attenuationRange.inRange(it.minAttenuationDb) } - .fold(.0) { acc, scanInstance -> acc + scanInstance.secondsSinceLastScan } + .fold(.0) { acc, scanInstance -> acc + max(scanInstance.secondsSinceLastScan, 0) } val minutesAtAttenuation = secondsAtAttenuation / 60 return attenuationFilter.dropIfMinutesInRange.inRange(minutesAtAttenuation) @@ -86,7 +87,7 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels { .filter { it.attenuationRange.inRange(scanInstance.minAttenuationDb) } .map { it.weight } .firstOrNull() ?: .0 - seconds + scanInstance.secondsSinceLastScan * weight + seconds + max(scanInstance.secondsSinceLastScan, 0) * weight } private fun determineRiskLevel( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt deleted file mode 100644 index 3b26fc49702912b12b519529d3df6e2eced4c749..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt +++ /dev/null @@ -1,30 +0,0 @@ -package de.rki.coronawarnapp.risk - -import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.risk.result.AggregatedRiskResult -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ExposureResultStore @Inject constructor() { - - val entities = MutableStateFlow( - ExposureResult( - exposureWindows = emptyList(), - aggregatedRiskResult = null - ) - ) - - internal val internalMatchedKeyCount = MutableStateFlow(0) - val matchedKeyCount: Flow<Int> = internalMatchedKeyCount - - internal val internalDaysSinceLastExposure = MutableStateFlow(0) - val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure -} - -data class ExposureResult( - val exposureWindows: List<ExposureWindow>, - val aggregatedRiskResult: AggregatedRiskResult? -) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt deleted file mode 100644 index 05267394d6cabd8aa691caf1ae5c8a8deeba455e..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt +++ /dev/null @@ -1,84 +0,0 @@ -package de.rki.coronawarnapp.risk - -enum class RiskLevel(val raw: Int) { - // mapped to: unknown risk - initial - // the risk score is not yet calculated - // This score is set if the application was freshly installed without running the tracing - UNKNOWN_RISK_INITIAL(RiskLevelConstants.UNKNOWN_RISK_INITIAL), - - // mapped to: no calculation possible - // the ExposureNotification Framework or Bluetooth is not active - // This risk score level has the highest priority and can oversteer the other risk score levels. - NO_CALCULATION_POSSIBLE_TRACING_OFF(RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF), - - // mapped to: low risk - // the risk score is higher than the riskScoreMinimumThreshold and lower than the riskScoreLevelThreshold - // and the timeActivateTracing is higher than notEnoughDataTimeRange - LOW_LEVEL_RISK(RiskLevelConstants.LOW_LEVEL_RISK), - - // mapped to: increased risk - // the risk score is higher than the riskScoreLevelThreshold - // The notEnoughDataTimeRange must not be not considered. - INCREASED_RISK(RiskLevelConstants.INCREASED_RISK), - - // mapped to: unknown risk - outdated results - // This risk status is shown if timeSinceLastExposureCalculation > maxStaleExposureRiskRange - // and background jobs are enabled - UNKNOWN_RISK_OUTDATED_RESULTS(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS), - - // mapped to: unknown risk - outdated results manual - // This risk status is shown if timeSinceLastExposureCalculation > maxStaleExposureRiskRange - // and background jobs are disabled - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL), - - // mapped to no UI state - // this should never happen - UNDETERMINED(RiskLevelConstants.UNDETERMINED); - - companion object { - fun forValue(value: Int): RiskLevel { - return when (value) { - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> UNKNOWN_RISK_INITIAL - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> NO_CALCULATION_POSSIBLE_TRACING_OFF - RiskLevelConstants.LOW_LEVEL_RISK -> LOW_LEVEL_RISK - RiskLevelConstants.INCREASED_RISK -> INCREASED_RISK - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> UNKNOWN_RISK_OUTDATED_RESULTS - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -> UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - else -> UNDETERMINED - } - } - - // risk level categories - val UNSUCCESSFUL_RISK_LEVELS = - arrayOf( - UNDETERMINED, - NO_CALCULATION_POSSIBLE_TRACING_OFF, - UNKNOWN_RISK_OUTDATED_RESULTS, - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - ) - private val HIGH_RISK_LEVELS = arrayOf(INCREASED_RISK) - private val LOW_RISK_LEVELS = arrayOf( - UNKNOWN_RISK_INITIAL, - NO_CALCULATION_POSSIBLE_TRACING_OFF, - LOW_LEVEL_RISK, - UNKNOWN_RISK_OUTDATED_RESULTS, - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL, - UNDETERMINED - ) - - /** - * Checks if the RiskLevel has change from a high to low or from low to high - * - * @param previousRiskLevel previously persisted RiskLevel - * @param currentRiskLevel newly calculated RiskLevel - * @return - */ - fun riskLevelChangedBetweenLowAndHigh( - previousRiskLevel: RiskLevel, - currentRiskLevel: RiskLevel - ): Boolean { - return HIGH_RISK_LEVELS.contains(previousRiskLevel) && LOW_RISK_LEVELS.contains(currentRiskLevel) || - LOW_RISK_LEVELS.contains(previousRiskLevel) && HIGH_RISK_LEVELS.contains(currentRiskLevel) - } - } -} 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 new file mode 100644 index 0000000000000000000000000000000000000000..e2d403a1d01aa91a5dbc1866bbe85ebae24197c8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -0,0 +1,101 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.core.app.NotificationManagerCompat +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID +import de.rki.coronawarnapp.notification.NotificationHelper +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.ForegroundState +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class RiskLevelChangeDetector @Inject constructor( + @AppContext private val context: Context, + @AppScope private val appScope: CoroutineScope, + private val riskLevelStorage: RiskLevelStorage, + private val riskLevelSettings: RiskLevelSettings, + private val notificationManagerCompat: NotificationManagerCompat, + private val foregroundState: ForegroundState +) { + + fun launch() { + Timber.v("Monitoring risk level changes.") + riskLevelStorage.riskLevelResults + .map { results -> + results.sortedBy { it.calculatedAt }.takeLast(2) + } + .filter { it.size == 2 } + .onEach { + Timber.v("Checking for risklevel change.") + check(it) + } + .catch { Timber.e(it, "App config change checks failed.") } + .launchIn(appScope) + } + + private suspend fun check(changedLevels: List<RiskLevelResult>) { + val oldResult = changedLevels.first() + val newResult = changedLevels.last() + + val lastCheckedResult = riskLevelSettings.lastChangeCheckedRiskLevelTimestamp + if (lastCheckedResult == newResult.calculatedAt) { + Timber.d("We already checked this risk level change, skipping further checks.") + return + } + riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = newResult.calculatedAt + + val oldRiskState = oldResult.riskState + val newRiskState = newResult.riskState + + Timber.d("Last state was $oldRiskState and current state is $newRiskState") + + if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !LocalData.submissionWasSuccessful()) { + Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}") + + if (!foregroundState.isInForeground.first()) { + NotificationHelper.sendNotification( + content = context.getString(R.string.notification_body), + notificationId = NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } else { + Timber.d("App is in foreground, not sending notifications") + } + + Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskState") + } + + if (oldRiskState == RiskState.INCREASED_RISK && newRiskState == RiskState.LOW_RISK) { + LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true + + Timber.d("Risk level changed LocalData is updated. Current Risk level is $newRiskState") + } + } + + companion object { + /** + * Checks if the RiskLevel has change from a high to low or from low to high + * + * @param previous previously persisted RiskLevel + * @param current newly calculated RiskLevel + * @return + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun hasHighLowLevelChanged(previous: RiskState, current: RiskState) = + previous.isIncreasedRisk != current.isIncreasedRisk + + private val RiskState.isIncreasedRisk: Boolean + get() = this == RiskState.INCREASED_RISK + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelConstants.kt deleted file mode 100644 index cf752e7f4e0361e13f58490741f6f16179bed094..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelConstants.kt +++ /dev/null @@ -1,11 +0,0 @@ -package de.rki.coronawarnapp.risk - -object RiskLevelConstants { - const val UNKNOWN_RISK_INITIAL = 0 - const val NO_CALCULATION_POSSIBLE_TRACING_OFF = 1 - const val LOW_LEVEL_RISK = 2 - const val INCREASED_RISK = 3 - const val UNKNOWN_RISK_OUTDATED_RESULTS = 4 - const val UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL = 5 - const val UNDETERMINED = 9001 -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..00327c9e458452010032a38dbf3f8928f2989bf9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt @@ -0,0 +1,59 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import org.joda.time.Instant + +interface RiskLevelResult { + val calculatedAt: Instant + + val riskState: RiskState + get() = when { + aggregatedRiskResult?.isIncreasedRisk() == true -> RiskState.INCREASED_RISK + aggregatedRiskResult?.isLowRisk() == true -> RiskState.LOW_RISK + else -> RiskState.CALCULATION_FAILED + } + + val failureReason: FailureReason? + val aggregatedRiskResult: AggregatedRiskResult? + + /** + * This will only be filled in deviceForTester builds + */ + val exposureWindows: List<ExposureWindow>? + + val wasSuccessfullyCalculated: Boolean + get() = aggregatedRiskResult != null + + val isIncreasedRisk: Boolean + get() = aggregatedRiskResult?.isIncreasedRisk() ?: false + + val matchedKeyCount: Int + get() = if (isIncreasedRisk) { + aggregatedRiskResult?.totalMinimumDistinctEncountersWithHighRisk ?: 0 + } else { + aggregatedRiskResult?.totalMinimumDistinctEncountersWithLowRisk ?: 0 + } + + val daysWithEncounters: Int + get() = if (isIncreasedRisk) { + aggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0 + } else { + aggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0 + } + + val lastRiskEncounterAt: Instant? + get() = if (isIncreasedRisk) { + aggregatedRiskResult?.mostRecentDateWithHighRisk + } else { + aggregatedRiskResult?.mostRecentDateWithLowRisk + } + + enum class FailureReason(val failureCode: String) { + UNKNOWN("unknown"), + TRACING_OFF("tracingOff"), + NO_INTERNET("noInternet"), + OUTDATED_RESULTS("outDatedResults"), + OUTDATED_RESULTS_MANUAL("outDatedResults.manual") + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt similarity index 63% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt index 83372c3f5d5d671f21c29ceb1c478e23df6fed79..4e2eb9cf9883e8bf9d5e557df24e9dca5123a89f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt @@ -3,11 +3,12 @@ package de.rki.coronawarnapp.risk import android.content.Context import androidx.core.content.edit import de.rki.coronawarnapp.util.di.AppContext +import org.joda.time.Instant import javax.inject.Inject import javax.inject.Singleton @Singleton -class RiskLevelData @Inject constructor( +class RiskLevelSettings @Inject constructor( @AppContext private val context: Context ) { @@ -24,8 +25,17 @@ class RiskLevelData @Inject constructor( putString(PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID, value) } + var lastChangeCheckedRiskLevelTimestamp: Instant? + get() = prefs.getLong(PKEY_LAST_CHANGE_CHECKED_RISKLEVEL_TIMESTAMP, 0L).let { + if (it != 0L) Instant.ofEpochMilli(it) else null + } + set(value) = prefs.edit { + putLong(PKEY_LAST_CHANGE_CHECKED_RISKLEVEL_TIMESTAMP, value?.millis ?: 0L) + } + companion object { private const val NAME_SHARED_PREFS = "risklevel_localdata" private const val PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID = "risklevel.config.identifier.last" + private const val PKEY_LAST_CHANGE_CHECKED_RISKLEVEL_TIMESTAMP = "PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt index 8069454c2b5225e5b157f831777d184519d483d7..92e4a94222a210531e67fb8b27ae96096c9f9f51 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt @@ -1,34 +1,23 @@ package de.rki.coronawarnapp.risk import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.core.app.NotificationManagerCompat -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.R import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig +import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.RiskLevelCalculationException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.notification.NotificationHelper -import de.rki.coronawarnapp.risk.RiskLevel.INCREASED_RISK -import de.rki.coronawarnapp.risk.RiskLevel.LOW_LEVEL_RISK -import de.rki.coronawarnapp.risk.RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF -import de.rki.coronawarnapp.risk.RiskLevel.UNDETERMINED -import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL -import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS -import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection +import de.rki.coronawarnapp.risk.RiskLevelResult.FailureReason +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage 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.BackgroundModeStatus import de.rki.coronawarnapp.util.ConnectivityHelper.isNetworkEnabled -import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.channels.ConflatedBroadcastChannel @@ -36,197 +25,132 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.first import org.joda.time.Duration +import org.joda.time.Instant import timber.log.Timber import javax.inject.Inject import javax.inject.Provider +@Suppress("ReturnCount") class RiskLevelTask @Inject constructor( private val riskLevels: RiskLevels, @AppContext private val context: Context, private val enfClient: ENFClient, private val timeStamper: TimeStamper, private val backgroundModeStatus: BackgroundModeStatus, - private val riskLevelData: RiskLevelData, + private val riskLevelSettings: RiskLevelSettings, private val appConfigProvider: AppConfigProvider, - private val exposureResultStore: ExposureResultStore -) : Task<DefaultProgress, RiskLevelTask.Result> { + private val riskLevelStorage: RiskLevelStorage, + private val keyCacheRepository: KeyCacheRepository +) : Task<DefaultProgress, RiskLevelTaskResult> { 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) - // If there is no connectivity the transaction will set the last calculated risk level - if (!isNetworkEnabled(context)) { - RiskLevelRepository.setLastCalculatedRiskLevelAsCurrent() - return Result(UNDETERMINED) - } - - if (!enfClient.isTracingEnabled.first()) { - return Result(NO_CALCULATION_POSSIBLE_TRACING_OFF) - } - - val configData: ConfigData = appConfigProvider.getAppConfig() + @Suppress("LongMethod") + override suspend fun run(arguments: Task.Arguments): RiskLevelTaskResult = try { + Timber.d("Running with arguments=%s", arguments) - return Result( - when { - calculationNotPossibleBecauseOfNoKeys().also { - checkCancel() - } -> UNKNOWN_RISK_INITIAL + val configData: ConfigData = appConfigProvider.getAppConfig() - calculationNotPossibleBecauseOfOutdatedResults().also { - checkCancel() - } -> if (backgroundJobsEnabled()) { - UNKNOWN_RISK_OUTDATED_RESULTS - } else { - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - } + determineRiskLevelResult(configData).also { + Timber.i("Risklevel determined: %s", it) - isIncreasedRisk(configData).also { - checkCancel() - } -> INCREASED_RISK + checkCancel() - !isActiveTracingTimeAboveThreshold().also { - checkCancel() - } -> UNKNOWN_RISK_INITIAL + Timber.tag(TAG).d("storeTaskResult(...)") + riskLevelStorage.storeResult(it) - else -> LOW_LEVEL_RISK - }.also { - checkCancel() - updateRepository(it, timeStamper.nowUTC.millis) - riskLevelData.lastUsedConfigIdentifier = configData.identifier - } - ) - } catch (error: Exception) { - Timber.tag(TAG).e(error) - error.report(ExceptionCategory.EXPOSURENOTIFICATION) - throw error - } finally { - Timber.i("Finished (isCanceled=$isCanceled).") - internalProgress.close() + riskLevelSettings.lastUsedConfigIdentifier = configData.identifier } + } catch (error: Exception) { + Timber.tag(TAG).e(error) + error.report(ExceptionCategory.EXPOSURENOTIFICATION) + throw error + } finally { + Timber.i("Finished (isCanceled=$isCanceled).") + internalProgress.close() } - private fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean { - // if the last calculation is longer in the past as the defined threshold we return the stale state - val timeSinceLastDiagnosisKeyFetchFromServer = - TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer() - ?: throw RiskLevelCalculationException( - IllegalArgumentException("Time since last exposure calculation is null") - ) - /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the - defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */ - return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() > - TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold() - } - - private fun calculationNotPossibleBecauseOfNoKeys() = - (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also { - if (it) { - Timber.tag(TAG) - .v("No last time diagnosis keys from server fetch timestamp was found") - } + private suspend fun determineRiskLevelResult(configData: ConfigData): RiskLevelTaskResult { + val nowUTC = timeStamper.nowUTC.also { + Timber.d("The current time is %s", it) } - private fun isActiveTracingTimeAboveThreshold(): Boolean { - val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration() - val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours() - val durationTracingIsActiveThreshold = TimeVariables.getMinActivatedTracingTime().toLong() + if (!isNetworkEnabled(context)) { + Timber.i("Risk not calculated, internet unavailable.") + return RiskLevelTaskResult( + calculatedAt = nowUTC, + failureReason = FailureReason.NO_INTERNET + ) + } - return (activeTracingDurationInHours >= durationTracingIsActiveThreshold).also { - Timber.tag(TAG).v( - "Active tracing time ($activeTracingDurationInHours h) is above threshold " + - "($durationTracingIsActiveThreshold h): $it" + if (!enfClient.isTracingEnabled.first()) { + Timber.i("Risk not calculated, tracing is disabled.") + return RiskLevelTaskResult( + calculatedAt = nowUTC, + failureReason = FailureReason.TRACING_OFF ) } - } - private suspend fun isIncreasedRisk(configData: ExposureWindowRiskCalculationConfig): Boolean { - val exposureWindows = enfClient.exposureWindows() + if (areKeyPkgsOutDated(nowUTC)) { + Timber.i("Risk not calculated, results are outdated.") + return RiskLevelTaskResult( + calculatedAt = nowUTC, + failureReason = when (backgroundJobsEnabled()) { + true -> FailureReason.OUTDATED_RESULTS + false -> FailureReason.OUTDATED_RESULTS_MANUAL + } + ) + } + checkCancel() - return riskLevels.determineRisk(configData, exposureWindows).apply { - // TODO This should be solved differently, by saving a more specialised result object - if (isIncreasedRisk()) { - exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithHighRisk - exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithHighRisk - } else { - exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithLowRisk - exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithLowRisk - } - exposureResultStore.entities.value = ExposureResult(exposureWindows, this) - }.isIncreasedRisk() + return calculateRiskLevel(configData) } - private fun updateRepository(riskLevel: RiskLevel, time: Long) { - val rollbackItems = mutableListOf<RollbackItem>() - try { - Timber.tag(TAG).v("Update the risk level with $riskLevel") - val lastCalculatedRiskLevelScoreForRollback = RiskLevelRepository.getLastCalculatedScore() - updateRiskLevelScore(riskLevel) - rollbackItems.add { - updateRiskLevelScore(lastCalculatedRiskLevelScoreForRollback) - } + private suspend fun areKeyPkgsOutDated(nowUTC: Instant): Boolean { + Timber.tag(TAG).d("Evaluating areKeyPkgsOutDated(nowUTC=%s)", nowUTC) - // risk level calculation date update - val lastCalculatedRiskLevelDate = LocalData.lastTimeRiskLevelCalculation() - LocalData.lastTimeRiskLevelCalculation(time) - rollbackItems.add { - LocalData.lastTimeRiskLevelCalculation(lastCalculatedRiskLevelDate) - } - } catch (error: Exception) { - Timber.tag(TAG).e(error, "Updating the RiskLevelRepository failed.") + val latestDownload = keyCacheRepository.getAllCachedKeys().maxByOrNull { + it.info.toDateTime() + } + if (latestDownload == null) { + Timber.w("areKeyPkgsOutDated(): No downloads available, why is the RiskLevelTask running? Aborting!") + return true + } - try { - Timber.tag(TAG).d("Initiate Rollback") - for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke() - } catch (rollbackException: Exception) { - Timber.tag(TAG).e(rollbackException, "RiskLevelRepository rollback failed.") - } + val downloadAge = Duration(latestDownload.info.toDateTime(), nowUTC).also { + Timber.d("areKeyPkgsOutDated(): Age is %dh for latest key package: %s", it.standardHours, latestDownload) + } - throw error + return (downloadAge.isLongerThan(STALE_DOWNLOAD_LIMIT)).also { + if (it) { + Timber.tag(TAG).i("areKeyPkgsOutDated(): Calculation was not possible because results are outdated.") + } else { + Timber.tag(TAG).d("areKeyPkgsOutDated(): Key pkgs are fresh :), continuing evaluation.") + } } } - /** - * Updates the Risk Level Score in the repository with the calculated Risk Level - * - * @param riskLevel - */ - @VisibleForTesting - internal fun updateRiskLevelScore(riskLevel: RiskLevel) { - val lastCalculatedScore = RiskLevelRepository.getLastCalculatedScore() - Timber.d("last CalculatedS core is ${lastCalculatedScore.raw} and Current Risk Level is ${riskLevel.raw}") + private suspend fun calculateRiskLevel(configData: ExposureWindowRiskCalculationConfig): RiskLevelTaskResult { + Timber.tag(TAG).d("Calculating risklevel") + val exposureWindows = enfClient.exposureWindows() - if (RiskLevel.riskLevelChangedBetweenLowAndHigh(lastCalculatedScore, riskLevel) && - !LocalData.submissionWasSuccessful() - ) { - Timber.d( - "Notification Permission = ${ - NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled() - }" - ) + return riskLevels.determineRisk(configData, exposureWindows).let { + Timber.tag(TAG).d("Risklevel calculated: %s", it) + if (it.isIncreasedRisk()) { + Timber.tag(TAG).i("Risk is increased!") + } else { + Timber.tag(TAG).d("Risk is not increased, continuing evaluating.") + } - NotificationHelper.sendNotification( - CoronaWarnApplication.getAppContext().getString(R.string.notification_body) + RiskLevelTaskResult( + calculatedAt = timeStamper.nowUTC, + aggregatedRiskResult = it, + exposureWindows = exposureWindows ) - - Timber.d("Risk level changed and notification sent. Current Risk level is ${riskLevel.raw}") - } - if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK && - riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK - ) { - LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true - - Timber.d("Risk level changed LocalData is updated. Current Risk level is ${riskLevel.raw}") } - RiskLevelRepository.setRiskLevelScore(riskLevel) - } - - private fun checkCancel() { - if (isCanceled) throw TaskCancellationException() } private suspend fun backgroundJobsEnabled() = @@ -240,37 +164,44 @@ class RiskLevelTask @Inject constructor( } } + private fun checkCancel() { + if (isCanceled) throw TaskCancellationException() + } + override suspend fun cancel() { Timber.w("cancel() called.") isCanceled = true } - class Result(val riskLevel: RiskLevel) : Task.Result { - override fun toString(): String { - return "Result(riskLevel=${riskLevel.name})" - } - } - data class Config( - // TODO unit-test that not > 9 min + private val exposureDetectionTracker: ExposureDetectionTracker, override val executionTimeout: Duration = Duration.standardMinutes(8), - override val collisionBehavior: TaskFactory.Config.CollisionBehavior = TaskFactory.Config.CollisionBehavior.SKIP_IF_SIBLING_RUNNING + ) : TaskFactory.Config { - ) : TaskFactory.Config + override val preconditions: List<suspend () -> Boolean> + get() = listOf { + // check whether we already have a successful v2 exposure + exposureDetectionTracker.calculations.first().values.any { + it.enfVersion == TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE && it.isSuccessful + } + } + } class Factory @Inject constructor( - private val taskByDagger: Provider<RiskLevelTask> - ) : TaskFactory<DefaultProgress, Result> { + private val taskByDagger: Provider<RiskLevelTask>, + private val exposureDetectionTracker: ExposureDetectionTracker + ) : TaskFactory<DefaultProgress, RiskLevelTaskResult> { - override suspend fun createConfig(): TaskFactory.Config = Config() - override val taskProvider: () -> Task<DefaultProgress, Result> = { + override suspend fun createConfig(): TaskFactory.Config = Config(exposureDetectionTracker) + override val taskProvider: () -> Task<DefaultProgress, RiskLevelTaskResult> = { taskByDagger.get() } } companion object { private val TAG: String? = RiskLevelTask::class.simpleName + private val STALE_DOWNLOAD_LIMIT = Duration.standardHours(48) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..7c29ce690f4e497e9e1dd6b8d001a71be44dff9e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.task.Task +import org.joda.time.Instant + +data class RiskLevelTaskResult( + override val calculatedAt: Instant, + override val failureReason: RiskLevelResult.FailureReason?, + override val aggregatedRiskResult: AggregatedRiskResult?, + override val exposureWindows: List<ExposureWindow>? +) : Task.Result, RiskLevelResult { + + constructor( + calculatedAt: Instant, + aggregatedRiskResult: AggregatedRiskResult, + exposureWindows: List<ExposureWindow>? + ) : this( + calculatedAt = calculatedAt, + aggregatedRiskResult = aggregatedRiskResult, + exposureWindows = exposureWindows, + failureReason = null + ) + + constructor( + calculatedAt: Instant, + failureReason: RiskLevelResult.FailureReason + ) : this( + calculatedAt = calculatedAt, + failureReason = failureReason, + aggregatedRiskResult = null, + exposureWindows = null + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt index ca97d2dc37ee251a74321e42c620dc0b49c90a2c..ecf7818a7796c39a206ba779e00e0b7118e46d71 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt @@ -1,26 +1,34 @@ package de.rki.coronawarnapp.risk -import dagger.Binds import dagger.Module +import dagger.Provides import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskFactory import de.rki.coronawarnapp.task.TaskTypeKey import javax.inject.Singleton @Module -abstract class RiskModule { +class RiskModule { - @Binds + @Provides @IntoMap @TaskTypeKey(RiskLevelTask::class) - abstract fun riskLevelTaskFactory( + fun riskLevelTaskFactory( factory: RiskLevelTask.Factory - ): TaskFactory<out Task.Progress, out Task.Result> + ): TaskFactory<out Task.Progress, out Task.Result> = factory - @Binds + @Provides @Singleton - abstract fun bindRiskLevelCalculation( + fun bindRiskLevelCalculation( riskLevelCalculation: DefaultRiskLevels - ): RiskLevels + ): RiskLevels = riskLevelCalculation + + @Provides + @Singleton + fun riskLevelStorage( + storage: DefaultRiskLevelStorage + ): RiskLevelStorage = storage } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskState.kt new file mode 100644 index 0000000000000000000000000000000000000000..e71d6f7a47b0ebce7445f3615ba4d8777a60af21 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskState.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.risk + +enum class RiskState { + LOW_RISK, + INCREASED_RISK, + CALCULATION_FAILED +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt index dc90f38b4cc4d9daacf006ed8cc8296f64f29f54..b1f46c2a894f9f7360bd2497a46fad1f5d581ef6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds import de.rki.coronawarnapp.util.TimeAndDateExtensions.roundUpMsToDays +import timber.log.Timber object TimeVariables { @@ -79,20 +80,7 @@ object TimeVariables { */ fun getMinActivatedTracingTime(): Int = MIN_ACTIVATED_TRACING_TIME - /** - * The timeRange until the calculated exposure figures are rated as stale. - * In hours. - */ - private const val MAX_STALE_EXPOSURE_RISK_RANGE = 48 - - /** - * Getter function for [MAX_STALE_EXPOSURE_RISK_RANGE] - * - * @return stale threshold in hours - */ - fun getMaxStaleExposureRiskRange(): Int = MAX_STALE_EXPOSURE_RISK_RANGE - - private const val MILISECONDS_IN_A_SECOND = 1000 + private const val MILLISECONDS_IN_A_SECOND = 1000 private const val SECONDS_IN_A_MINUTES = 60 private const val MINUTES_IN_AN_HOUR = 60 private const val HOURS_IN_AN_DAY = 24 @@ -106,9 +94,9 @@ object TimeVariables { */ fun getManualKeyRetrievalDelay() = if (CWADebug.buildFlavor == CWADebug.BuildFlavor.DEVICE_FOR_TESTERS) { - MILISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES + MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES } else { - MILISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES * MINUTES_IN_AN_HOUR * HOURS_IN_AN_DAY + MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES * MINUTES_IN_AN_HOUR * HOURS_IN_AN_DAY } /** @@ -139,33 +127,10 @@ object TimeVariables { fun getInitialExposureTracingActivationTimestamp(): Long? = LocalData.initialTracingActivationTimestamp() - /** - * timestamp when the last successful exposureRisk calculation happened read from the mobile device storage. - * Last time when the transaction was successfully executed - * - * @return last time in milliseconds [de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction] - * was run successfully - */ - // because we have risk level calculation and key retrieval calculation - fun getLastTimeDiagnosisKeysFromServerFetch(): Long? = - LocalData.lastTimeDiagnosisKeysFromServerFetch()?.time - /**************************************************** * CALCULATED TIME VARIABLES ****************************************************/ - /** - * The time since the last successful exposure calculation ran in foreground or background. - * In milliseconds - * - * @return time in milliseconds since the exposure calculation was run successfully - */ - fun getTimeSinceLastDiagnosisKeyFetchFromServer(): Long? { - val lastTimeDiagnosisKeysFromServerFetch = - getLastTimeDiagnosisKeysFromServerFetch() ?: return null - return System.currentTimeMillis() - lastTimeDiagnosisKeysFromServerFetch - } - /** * The time the tracing is active. * @@ -212,7 +177,13 @@ object TimeVariables { // because we delete periods that are past 14 days but tracingActiveMS counts from first // ever activation, there are edge cases where tracingActiveMS gets to be > 14 days - return (minOf(tracingActiveMS, retentionPeriodInMS) - inactiveTracingMS).roundUpMsToDays() + val activeTracingDays = (minOf(tracingActiveMS, retentionPeriodInMS) - inactiveTracingMS).roundUpMsToDays() + return if (activeTracingDays >= 0) { + activeTracingDays + } else { + Timber.w("Negative active tracing days: %d", activeTracingDays) + 0 + } } /**************************************************** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt index 07595cd56e098af5055339c2ce3cbfcbf07ed19b..1a6d0c6ccd2d06f0925464843cb924b45acfe241 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt @@ -15,4 +15,6 @@ data class AggregatedRiskResult( ) { fun isIncreasedRisk(): Boolean = totalRiskLevel == ProtoRiskLevel.HIGH + + fun isLowRisk(): Boolean = totalRiskLevel == ProtoRiskLevel.LOW } 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 new file mode 100644 index 0000000000000000000000000000000000000000..6f543da716ebf9db4fc649b4bfb22ce94e24d0ce --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -0,0 +1,92 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedRiskResult +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import de.rki.coronawarnapp.util.flow.combine +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import timber.log.Timber + +abstract class BaseRiskLevelStorage constructor( + private val riskResultDatabaseFactory: RiskResultDatabase.Factory, + private val riskLevelResultMigrator: RiskLevelResultMigrator +) : RiskLevelStorage { + + private val database by lazy { riskResultDatabaseFactory.create() } + internal val riskResultsTables by lazy { database.riskResults() } + internal val exposureWindowsTables by lazy { database.exposureWindows() } + + abstract val storedResultLimit: Int + + final override val riskLevelResults: Flow<List<RiskLevelResult>> = combine( + riskResultsTables.allEntries(), + exposureWindowsTables.allEntries() + ) { allRiskResults, allWindows -> + Timber.v("Mapping ${allWindows.size} windows to ${allRiskResults.size} risk results.") + allRiskResults.map { result -> + val matchingWindows = allWindows.filter { it.exposureWindowDao.riskLevelResultId == result.id } + if (matchingWindows.isEmpty()) { + result.toRiskResult() + } else { + result.toRiskResult(matchingWindows) + } + } + } + .map { results -> + if (results.isEmpty()) { + riskLevelResultMigrator.getLegacyResults() + } else { + results + } + } + + override suspend fun storeResult(result: RiskLevelResult) { + Timber.d("Storing result (exposureWindows.size=%s)", result.exposureWindows?.size) + + val storedResultId = try { + val startTime = System.currentTimeMillis() + + require(result.aggregatedRiskResult == null || result.failureReason == null) { + "A result needs to have either an aggregatedRiskResult or a failureReason, not both!" + } + + val resultToPersist = result.toPersistedRiskResult() + riskResultsTables.insertEntry(resultToPersist).also { + Timber.d("Storing RiskLevelResult took %dms.", (System.currentTimeMillis() - startTime)) + } + + resultToPersist.id + } catch (e: Exception) { + Timber.e(e, "Failed to store latest result: %s", result) + throw e + } + + try { + Timber.d("Cleaning up old results.") + + riskResultsTables.deleteOldest(storedResultLimit).also { + Timber.d("$it old results were deleted.") + } + } catch (e: Exception) { + Timber.e(e, "Failed to clean up old results.") + throw e + } + + Timber.d("Storing exposure windows.") + storeExposureWindows(storedResultId = storedResultId, result) + + Timber.d("Deleting orphaned exposure windows.") + deletedOrphanedExposureWindows() + } + + internal abstract suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) + + internal abstract suspend fun deletedOrphanedExposureWindows() + + override suspend fun clear() { + Timber.w("clear() - Clearing stored riskleve/exposure-detection results.") + database.clearAllTables() + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..2e011c76282c3fefa4b31606bdea60cccfd504e5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +import kotlinx.coroutines.flow.Flow + +interface RiskLevelStorage { + + val riskLevelResults: Flow<List<RiskLevelResult>> + + suspend fun storeResult(result: RiskLevelResult) + + suspend fun clear() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9f1252fa4931ed3014424929357d46e2cc7f656 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt @@ -0,0 +1,87 @@ +package de.rki.coronawarnapp.risk.storage.internal + +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 de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.util.database.CommonConverters +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.flow.Flow +import timber.log.Timber +import javax.inject.Inject + +@Suppress("MaxLineLength") +@Database( + entities = [ + PersistedRiskLevelResultDao::class, + PersistedExposureWindowDao::class, + PersistedExposureWindowDao.PersistedScanInstance::class + ], + version = 1, + exportSchema = true +) +@TypeConverters( + CommonConverters::class, + PersistedRiskLevelResultDao.Converter::class, + PersistedRiskLevelResultDao.PersistedAggregatedRiskResult.Converter::class +) +abstract class RiskResultDatabase : RoomDatabase() { + + abstract fun riskResults(): RiskResultsDao + + abstract fun exposureWindows(): ExposureWindowsDao + + @Dao + interface RiskResultsDao { + @Query("SELECT * FROM riskresults") + fun allEntries(): Flow<List<PersistedRiskLevelResultDao>> + + @Insert(onConflict = OnConflictStrategy.ABORT) + suspend fun insertEntry(riskResultDao: PersistedRiskLevelResultDao) + + @Query( + "DELETE FROM riskresults where id NOT IN (SELECT id from riskresults ORDER BY calculatedAt DESC LIMIT :keep)" + ) + suspend fun deleteOldest(keep: Int): Int + } + + @Dao + interface ExposureWindowsDao { + @Query("SELECT * FROM exposurewindows") + fun allEntries(): Flow<List<PersistedExposureWindowDaoWrapper>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertWindows(exposureWindows: List<PersistedExposureWindowDao>): List<Long> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertScanInstances(scanInstances: List<PersistedExposureWindowDao.PersistedScanInstance>) + + @Query( + "DELETE FROM exposurewindows where riskLevelResultId NOT IN (:riskResultIds)" + ) + suspend fun deleteByRiskResultId(riskResultIds: List<String>): Int + } + + class Factory @Inject constructor(@AppContext private val context: Context) { + + fun create(): RiskResultDatabase { + Timber.d("Instantiating risk result database.") + return Room + .databaseBuilder(context, RiskResultDatabase::class.java, DATABASE_NAME) + .fallbackToDestructiveMigrationFrom() + .build() + } + } + + companion object { + private const val DATABASE_NAME = "riskresults.db" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..51a596d47376a04e95a941f6177d565969312e84 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt @@ -0,0 +1,90 @@ +package de.rki.coronawarnapp.risk.storage.internal.riskresults + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import de.rki.coronawarnapp.risk.RiskLevelResult.FailureReason +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping +import org.joda.time.Instant +import timber.log.Timber + +@Entity(tableName = "riskresults") +data class PersistedRiskLevelResultDao( + @PrimaryKey @ColumnInfo(name = "id") val id: String, + @ColumnInfo(name = "calculatedAt") val calculatedAt: Instant, + @ColumnInfo(name = "failureReason") val failureReason: FailureReason?, + @Embedded val aggregatedRiskResult: PersistedAggregatedRiskResult? +) { + + fun toRiskResult(exposureWindows: List<PersistedExposureWindowDaoWrapper>? = null) = when { + aggregatedRiskResult != null -> { + RiskLevelTaskResult( + calculatedAt = calculatedAt, + aggregatedRiskResult = aggregatedRiskResult.toAggregatedRiskResult(), + exposureWindows = exposureWindows?.map { it.toExposureWindow() } + ) + } + else -> { + if (failureReason == null) { + Timber.e("Entry contained no aggregateResult and no failure reason, shouldn't happen.") + } + RiskLevelTaskResult( + calculatedAt = calculatedAt, + failureReason = failureReason ?: FailureReason.UNKNOWN + ) + } + } + + data class PersistedAggregatedRiskResult( + @ColumnInfo(name = "totalRiskLevel") + val totalRiskLevel: NormalizedTimeToRiskLevelMapping.RiskLevel, + @ColumnInfo(name = "totalMinimumDistinctEncountersWithLowRisk") + val totalMinimumDistinctEncountersWithLowRisk: Int, + @ColumnInfo(name = "totalMinimumDistinctEncountersWithHighRisk") + val totalMinimumDistinctEncountersWithHighRisk: Int, + @ColumnInfo(name = "mostRecentDateWithLowRisk") + val mostRecentDateWithLowRisk: Instant?, + @ColumnInfo(name = "mostRecentDateWithHighRisk") + val mostRecentDateWithHighRisk: Instant?, + @ColumnInfo(name = "numberOfDaysWithLowRisk") + val numberOfDaysWithLowRisk: Int, + @ColumnInfo(name = "numberOfDaysWithHighRisk") + val numberOfDaysWithHighRisk: Int + ) { + + fun toAggregatedRiskResult() = AggregatedRiskResult( + totalRiskLevel = totalRiskLevel, + totalMinimumDistinctEncountersWithLowRisk = totalMinimumDistinctEncountersWithLowRisk, + totalMinimumDistinctEncountersWithHighRisk = totalMinimumDistinctEncountersWithHighRisk, + mostRecentDateWithLowRisk = mostRecentDateWithLowRisk, + mostRecentDateWithHighRisk = mostRecentDateWithHighRisk, + numberOfDaysWithLowRisk = numberOfDaysWithLowRisk, + numberOfDaysWithHighRisk = numberOfDaysWithHighRisk + ) + + class Converter { + @TypeConverter + fun toType(value: Int?): NormalizedTimeToRiskLevelMapping.RiskLevel? = value?.let { + NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(value) + } + + @TypeConverter + fun fromType(type: NormalizedTimeToRiskLevelMapping.RiskLevel?): Int? = type?.number + } + } + + class Converter { + @TypeConverter + fun toType(value: String?): FailureReason? = value?.let { + FailureReason.values().singleOrNull { it.failureCode == value } ?: FailureReason.UNKNOWN + } + + @TypeConverter + fun fromType(type: FailureReason?): String? = type?.failureCode + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskResultDaoExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskResultDaoExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..75d9d80c869a7f954caff5c62d03dd6dc9066a37 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskResultDaoExtensions.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.risk.storage.internal.riskresults + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import java.util.UUID + +fun RiskLevelResult.toPersistedRiskResult( + id: String = UUID.randomUUID().toString() +) = PersistedRiskLevelResultDao( + id = id, + calculatedAt = calculatedAt, + aggregatedRiskResult = aggregatedRiskResult?.toPersistedAggregatedRiskResult(), + failureReason = failureReason +) + +fun AggregatedRiskResult.toPersistedAggregatedRiskResult() = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = totalRiskLevel, + totalMinimumDistinctEncountersWithLowRisk = totalMinimumDistinctEncountersWithLowRisk, + totalMinimumDistinctEncountersWithHighRisk = totalMinimumDistinctEncountersWithHighRisk, + mostRecentDateWithLowRisk = mostRecentDateWithLowRisk, + mostRecentDateWithHighRisk = mostRecentDateWithHighRisk, + numberOfDaysWithLowRisk = numberOfDaysWithLowRisk, + numberOfDaysWithHighRisk = numberOfDaysWithHighRisk +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..c82caf8dd075bc00a6d6a8dc492b8e53d62ce814 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDao.kt @@ -0,0 +1,39 @@ +package de.rki.coronawarnapp.risk.storage.internal.windows + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "exposurewindows") +data class PersistedExposureWindowDao( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "riskLevelResultId") val riskLevelResultId: String, + @ColumnInfo(name = "dateMillisSinceEpoch") val dateMillisSinceEpoch: Long, + @ColumnInfo(name = "calibrationConfidence") val calibrationConfidence: Int, + @ColumnInfo(name = "infectiousness") val infectiousness: Int, + @ColumnInfo(name = "reportType") val reportType: Int +) { + + @Entity( + tableName = "scaninstances", + foreignKeys = [ + ForeignKey( + onDelete = CASCADE, + entity = PersistedExposureWindowDao::class, + parentColumns = ["id"], + childColumns = ["exposureWindowId"] + ) + ], + indices = [Index("exposureWindowId")] + ) + data class PersistedScanInstance( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "exposureWindowId") val exposureWindowId: Long, + @ColumnInfo(name = "minAttenuationDb") val minAttenuationDb: Int, + @ColumnInfo(name = "secondsSinceLastScan") val secondsSinceLastScan: Int, + @ColumnInfo(name = "typicalAttenuationDb") val typicalAttenuationDb: Int + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d43fe68d27727125aeb684c8c507aa95a0c5a0d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoExtensions.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.risk.storage.internal.windows + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance + +fun ExposureWindow.toPersistedExposureWindow( + riskLevelResultId: String +) = PersistedExposureWindowDao( + riskLevelResultId = riskLevelResultId, + dateMillisSinceEpoch = this.dateMillisSinceEpoch, + calibrationConfidence = this.calibrationConfidence, + infectiousness = this.infectiousness, + reportType = this.reportType +) + +fun List<ExposureWindow>.toPersistedExposureWindows( + riskLevelResultId: String +) = this.map { it.toPersistedExposureWindow(riskLevelResultId) } + +fun ScanInstance.toPersistedScanInstance(exposureWindowId: Long) = PersistedExposureWindowDao.PersistedScanInstance( + exposureWindowId = exposureWindowId, + minAttenuationDb = minAttenuationDb, + secondsSinceLastScan = secondsSinceLastScan, + typicalAttenuationDb = typicalAttenuationDb +) + +fun List<ScanInstance>.toPersistedScanInstances( + exposureWindowId: Long +) = this.map { it.toPersistedScanInstance(exposureWindowId) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoWrapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3b8fb13dd6f83a18275de716ade67841292aa95 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoWrapper.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.risk.storage.internal.windows + +import androidx.room.Embedded +import androidx.room.Relation +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance + +/** + * Helper class for Room @Relation + */ +data class PersistedExposureWindowDaoWrapper( + @Embedded + val exposureWindowDao: PersistedExposureWindowDao, + @Relation(parentColumn = "id", entityColumn = "exposureWindowId") + val scanInstances: List<PersistedExposureWindowDao.PersistedScanInstance> +) { + fun toExposureWindow(): ExposureWindow = + ExposureWindow.Builder().apply { + setDateMillisSinceEpoch(exposureWindowDao.dateMillisSinceEpoch) + setCalibrationConfidence(exposureWindowDao.calibrationConfidence) + setInfectiousness(exposureWindowDao.infectiousness) + setReportType(exposureWindowDao.reportType) + setScanInstances(scanInstances.map { it.toScanInstance() }) + }.build() + + private fun PersistedExposureWindowDao.PersistedScanInstance.toScanInstance(): ScanInstance = ScanInstance.Builder() + .apply { + setMinAttenuationDb(minAttenuationDb) + setSecondsSinceLastScan(secondsSinceLastScan) + setTypicalAttenuationDb(typicalAttenuationDb) + }.build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt new file mode 100644 index 0000000000000000000000000000000000000000..0f60e67edfc2b0e1533ad3325acee636ef1aa86f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt @@ -0,0 +1,98 @@ +package de.rki.coronawarnapp.risk.storage.legacy + +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import dagger.Lazy +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.storage.EncryptedPreferences +import de.rki.coronawarnapp.util.TimeStamper +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * TODO Remove this in the future + * Once a significant portion of the user base has already been running 1.8.x, + * this class can be removed to reduce access to the EncryptedPreferences. + */ +@Singleton +class RiskLevelResultMigrator @Inject constructor( + @EncryptedPreferences encryptedPreferences: Lazy<SharedPreferences>, + private val timeStamper: TimeStamper +) { + + private val prefs by lazy { encryptedPreferences.get() } + + private fun lastTimeRiskLevelCalculation(): Instant? { + prefs.getLong("preference_timestamp_risk_level_calculation", -1L).also { + return if (it < 0) null else Instant.ofEpochMilli(it) + } + } + + private fun lastCalculatedRiskLevel(): RiskState? { + val rawRiskLevel = prefs.getInt("preference_risk_level_score", -1) + return if (rawRiskLevel != -1) mapRiskLevelConstant(rawRiskLevel) else null + } + + private fun lastSuccessfullyCalculatedRiskLevel(): RiskState? { + val rawRiskLevel = prefs.getInt("preference_risk_level_score_successful", -1) + return if (rawRiskLevel != -1) mapRiskLevelConstant(rawRiskLevel) else null + } + + fun getLegacyResults(): List<RiskLevelResult> = try { + val legacyResults = mutableListOf<RiskLevelResult>() + lastCalculatedRiskLevel()?.let { + legacyResults.add( + LegacyResult( + riskState = it, + calculatedAt = lastTimeRiskLevelCalculation() ?: timeStamper.nowUTC + ) + ) + } + + lastSuccessfullyCalculatedRiskLevel()?.let { + legacyResults.add(LegacyResult(riskState = it, calculatedAt = timeStamper.nowUTC)) + } + + legacyResults + } catch (e: Exception) { + Timber.e(e, "Failed to parse legacy risklevel data.") + emptyList() + } + + data class LegacyResult( + override val riskState: RiskState, + override val calculatedAt: Instant + ) : RiskLevelResult { + override val failureReason: RiskLevelResult.FailureReason? = null + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + companion object { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun mapRiskLevelConstant(value: Int): RiskState = when (value) { + MigrationRiskLevelConstants.LOW_LEVEL_RISK -> RiskState.LOW_RISK + MigrationRiskLevelConstants.INCREASED_RISK -> RiskState.INCREASED_RISK + else -> RiskState.CALCULATION_FAILED + } + } +} + +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +internal object MigrationRiskLevelConstants { + const val NO_CALCULATION_POSSIBLE_TRACING_OFF = 1 + const val LOW_LEVEL_RISK = 2 + const val INCREASED_RISK = 3 + const val UNKNOWN_RISK_OUTDATED_RESULTS = 4 + const val UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL = 5 + const val UNKNOWN_RISK_NO_INTERNET = 6 + const val UNDETERMINED = 9001 +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt new file mode 100644 index 0000000000000000000000000000000000000000..63733ba49fa6471e67ab037f2835dbcf677a8381 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.storage + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class EncryptedPreferences diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt index fd533993e2ecbee697a3ec5fc5c2215ce056b7c1..1c2adaaf16b39eb53644cb8744ce02ec6f49a55e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt @@ -4,13 +4,10 @@ import android.content.SharedPreferences import androidx.core.content.edit import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.util.preferences.createFlowPreference import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import java.util.Date +import timber.log.Timber /** * LocalData is responsible for all access to the shared preferences. Each preference is accessible @@ -261,78 +258,6 @@ object LocalData { ) } - /**************************************************** - * RISK LEVEL - ****************************************************/ - - /** - * Gets the last calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @return - */ - fun lastCalculatedRiskLevel(): RiskLevel { - val rawRiskLevel = getSharedPreferenceInstance().getInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score), - RiskLevel.UNDETERMINED.raw - ) - return RiskLevel.forValue(rawRiskLevel) - } - - /** - * Sets the last calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @param rawRiskLevel - */ - fun lastCalculatedRiskLevel(rawRiskLevel: Int) = - getSharedPreferenceInstance().edit(true) { - putInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score), - rawRiskLevel - ) - } - - /** - * Gets the last successfully calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @return - */ - fun lastSuccessfullyCalculatedRiskLevel(): RiskLevel { - val rawRiskLevel = getSharedPreferenceInstance().getInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score_successful), - RiskLevel.UNDETERMINED.raw - ) - return RiskLevel.forValue(rawRiskLevel) - } - - /** - * Sets the last calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @param rawRiskLevel - */ - fun lastSuccessfullyCalculatedRiskLevel(rawRiskLevel: Int) = - getSharedPreferenceInstance().edit(true) { - putInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score_successful), - rawRiskLevel - ) - } - /** * Gets the boolean if the user has seen the explanation dialog for the * risk level tracing days @@ -379,59 +304,6 @@ object LocalData { .edit(commit = true) { putBoolean(PREFERENCE_HAS_RISK_STATUS_LOWERED, value) } .also { isUserToBeNotifiedOfLoweredRiskLevelFlowInternal.value = value } - /**************************************************** - * SERVER FETCH DATA - ****************************************************/ - - private val dateMapperForFetchTime: (Long) -> Date? = { - if (it != 0L) Date(it) else null - } - - private val lastTimeDiagnosisKeysFetchedFlowPref by lazy { - getSharedPreferenceInstance() - .createFlowPreference<Long>(key = "preference_timestamp_diagnosis_keys_fetch", 0L) - } - - fun lastTimeDiagnosisKeysFromServerFetchFlow() = lastTimeDiagnosisKeysFetchedFlowPref.flow - .map { dateMapperForFetchTime(it) } - - fun lastTimeDiagnosisKeysFromServerFetch() = - dateMapperForFetchTime(lastTimeDiagnosisKeysFetchedFlowPref.value) - - fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) = - lastTimeDiagnosisKeysFetchedFlowPref.update { value?.time ?: 0L } - - /** - * Gets the last time of successful risk level calculation as long - * from the EncryptedSharedPrefs - * - * @return Long - */ - fun lastTimeRiskLevelCalculation(): Long? { - val time = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_risk_level_calculation), - 0L - ) - return Date(time).time - } - - /** - * Sets the last time of successful risk level calculation as long - * from the EncryptedSharedPrefs - * - * @param value timestamp as Long - */ - fun lastTimeRiskLevelCalculation(value: Long?) { - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_risk_level_calculation), - value ?: 0L - ) - } - } - /**************************************************** * SETTINGS DATA ****************************************************/ @@ -687,6 +559,6 @@ object LocalData { } fun clear() { - lastTimeDiagnosisKeysFetchedFlowPref.update { 0L } + Timber.w("LocalData.clear()") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt deleted file mode 100644 index a4d580da04fefd742dce2756adef1343e28f14ff..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt +++ /dev/null @@ -1,104 +0,0 @@ -package de.rki.coronawarnapp.storage - -import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.risk.RiskLevelConstants -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -object RiskLevelRepository { - - private val internalRisklevelScore = MutableStateFlow(getLastSuccessfullyCalculatedScore().raw) - val riskLevelScore: Flow<Int> = internalRisklevelScore - - private val internalRiskLevelScoreLastSuccessfulCalculated = - MutableStateFlow(LocalData.lastSuccessfullyCalculatedRiskLevel().raw) - val riskLevelScoreLastSuccessfulCalculated: Flow<Int> = - internalRiskLevelScoreLastSuccessfulCalculated - - /** - * Set the new calculated [RiskLevel] - * Calculation happens in the [de.rki.coronawarnapp.transaction.RiskLevelTransaction] - * - * @see de.rki.coronawarnapp.transaction.RiskLevelTransaction - * @see de.rki.coronawarnapp.risk.RiskLevels - * - * @param riskLevel - */ - fun setRiskLevelScore(riskLevel: RiskLevel) { - val rawRiskLevel = riskLevel.raw - internalRisklevelScore.value = rawRiskLevel - - setLastCalculatedScore(rawRiskLevel) - setLastSuccessfullyCalculatedScore(riskLevel) - } - - /** - * Resets the data in the [RiskLevelRepository] - * - */ - fun reset() { - internalRisklevelScore.value = RiskLevelConstants.UNKNOWN_RISK_INITIAL - } - - /** - * Set the current risk level from the last calculated risk level. - * This is necessary if the app has no connectivity and the risk level transaction - * fails. - * - * @see de.rki.coronawarnapp.transaction.RiskLevelTransaction - * - */ - fun setLastCalculatedRiskLevelAsCurrent() { - var lastRiskLevelScore = getLastCalculatedScore() - if (lastRiskLevelScore == RiskLevel.UNDETERMINED) { - lastRiskLevelScore = RiskLevel.UNKNOWN_RISK_INITIAL - } - internalRisklevelScore.value = lastRiskLevelScore.raw - } - - /** - * Get the last calculated RiskLevel - * - * @return - */ - fun getLastCalculatedScore(): RiskLevel = LocalData.lastCalculatedRiskLevel() - - /** - * Set the last calculated RiskLevel - * - * @param rawRiskLevel - */ - private fun setLastCalculatedScore(rawRiskLevel: Int) = - LocalData.lastCalculatedRiskLevel(rawRiskLevel) - - /** - * Get the last successfully calculated [RiskLevel] - * - * @see RiskLevel - * - * @return - */ - fun getLastSuccessfullyCalculatedScore(): RiskLevel = - LocalData.lastSuccessfullyCalculatedRiskLevel() - - /** - * Refreshes repository variable with local data - * - */ - fun refreshLastSuccessfullyCalculatedScore() { - internalRiskLevelScoreLastSuccessfulCalculated.value = - getLastSuccessfullyCalculatedScore().raw - } - - /** - * Set the last successfully calculated [RiskLevel] - * - * @param riskLevel - */ - private fun setLastSuccessfullyCalculatedScore(riskLevel: RiskLevel) { - if (!RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(riskLevel)) { - LocalData.lastSuccessfullyCalculatedRiskLevel(riskLevel.raw) - internalRiskLevelScoreLastSuccessfulCalculated.value = riskLevel.raw - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt index 9a202f476782694b1b338429897d4158288494e5..6610cee87208fd7060a4e6a0715c1d1e15e9096a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SettingsRepository.kt @@ -4,7 +4,6 @@ import android.content.Context import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import de.rki.coronawarnapp.util.BackgroundPrioritization -import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -26,7 +25,6 @@ class SettingsRepository @Inject constructor( ) { val isConnectionEnabled = MutableLiveData(true) - val isBackgroundJobEnabled = MutableLiveData(true) private val internalIsBackgroundPriorityEnabled = MutableStateFlow(false) val isBackgroundPriorityEnabledFlow: Flow<Boolean> = internalIsBackgroundPriorityEnabled @@ -43,46 +41,10 @@ class SettingsRepository @Inject constructor( isConnectionEnabled.postValue(value) } - /** - * Refresh global bluetooth state to point out that tracing isn't working - * - * @see ConnectivityHelper - */ - fun updateBackgroundJobEnabled(value: Boolean) { - isBackgroundJobEnabled.postValue(value) - } - - private val internalIsManualKeyRetrievalEnabled = MutableStateFlow(true) - val isManualKeyRetrievalEnabledFlow: Flow<Boolean> = internalIsManualKeyRetrievalEnabled - - @Deprecated("Please use isManualKeyRetrievalEnabledFlow") - val isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabledFlow.asLiveData() - - /** - * Refresh manual key retrieval button status - */ - fun updateManualKeyRetrievalEnabled(value: Boolean) { - internalIsManualKeyRetrievalEnabled.value = value - } - - private val internalManualKeyRetrievalTime = MutableStateFlow(0L) - val manualKeyRetrievalTimeFlow: Flow<Long> = internalManualKeyRetrievalTime - - @Deprecated("Please use manualKeyRetrievalTimeFlow") - val manualKeyRetrievalTime = manualKeyRetrievalTimeFlow.asLiveData() - - /** - * Refresh manual key retrieval button status - */ - fun updateManualKeyRetrievalTime(value: Long) { - internalManualKeyRetrievalTime.value = value - } - /** * Refresh the current background priority state. */ fun refreshBackgroundPriorityEnabled() { - internalIsBackgroundPriorityEnabled.value = - backgroundPrioritization.isBackgroundActivityPrioritized + internalIsBackgroundPriorityEnabled.value = backgroundPrioritization.isBackgroundActivityPrioritized } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt index 261fd5fcacf20a08176f1db87dbc42bb1e44b8a1..5f9b38fcb3cf3949be5d8fbd5af640560911d62c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt @@ -1,14 +1,19 @@ package de.rki.coronawarnapp.storage import android.content.Context +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.FlowPreference import de.rki.coronawarnapp.util.preferences.createFlowPreference +import de.rki.coronawarnapp.util.serialization.BaseGson import javax.inject.Inject import javax.inject.Singleton @Singleton class TestSettings @Inject constructor( - @AppContext private val context: Context + @AppContext private val context: Context, + @BaseGson private val gson: Gson ) { private val prefs by lazy { context.getSharedPreferences("test_settings", Context.MODE_PRIVATE) @@ -18,4 +23,22 @@ class TestSettings @Inject constructor( key = "connections.metered.fake", defaultValue = false ) + + val fakeExposureWindows = FlowPreference( + preferences = prefs, + key = "riskleve.exposurewindows.fake", + reader = FlowPreference.gsonReader<FakeExposureWindowTypes>(gson, FakeExposureWindowTypes.DISABLED), + writer = FlowPreference.gsonWriter(gson) + ) + + enum class FakeExposureWindowTypes { + @SerializedName("DISABLED") + DISABLED, + + @SerializedName("INCREASED_RISK_DEFAULT") + INCREASED_RISK_DEFAULT, + + @SerializedName("LOW_RISK_DEFAULT") + LOW_RISK_DEFAULT + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt index 75be4b8cf6503bb73bdf1a9dbda5c92e1fad2206..e107f9754d38e32023ecfdab4cb7a2ce6d525eb0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt @@ -4,13 +4,14 @@ import android.content.Context import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.lastSubmission import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPeriod import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskInfo import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking -import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.TimeStamper @@ -25,7 +26,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.joda.time.Duration import timber.log.Timber -import java.util.Date import java.util.NoSuchElementException import javax.inject.Inject import javax.inject.Singleton @@ -44,24 +44,21 @@ class TracingRepository @Inject constructor( @AppScope private val scope: CoroutineScope, private val taskController: TaskController, enfClient: ENFClient, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val exposureDetectionTracker: ExposureDetectionTracker ) { - val lastTimeDiagnosisKeysFetched: Flow<Date?> = LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() - private val internalActiveTracingDaysInRetentionPeriod = MutableStateFlow(0L) val activeTracingDaysInRetentionPeriod: Flow<Long> = internalActiveTracingDaysInRetentionPeriod - private val internalIsRefreshing = - taskController.tasks.map { it.isDownloadDiagnosisKeysTaskRunning() || it.isRiskLevelTaskRunning() } - val tracingProgress: Flow<TracingProgress> = combine( - internalIsRefreshing, - enfClient.isPerformingExposureDetection() - ) { isDownloading, isCalculating -> + taskController.tasks.map { it.isDownloadDiagnosisKeysTaskRunning() }, + enfClient.isPerformingExposureDetection(), + taskController.tasks.map { it.isRiskLevelTaskRunning() } + ) { isDownloading, isExposureDetecting, isRiskLeveling -> when { isDownloading -> TracingProgress.Downloading - isCalculating -> TracingProgress.ENFIsCalculating + isExposureDetecting || isRiskLeveling -> TracingProgress.ENFIsCalculating else -> TracingProgress.Idle } } @@ -95,7 +92,6 @@ class TracingRepository @Inject constructor( RiskLevelTask::class, originTag = "TracingRepository.refreshDiagnosisKeys()" ) ) - TimerHelper.startManualKeyRetrievalTimer() } } @@ -125,15 +121,15 @@ class TracingRepository @Inject constructor( // model the keys are only fetched on button press of the user val isBackgroundJobEnabled = ConnectivityHelper.autoModeEnabled(context) - val wasNotYetFetched = LocalData.lastTimeDiagnosisKeysFromServerFetch() == null - Timber.tag(TAG).v("Network is enabled $isNetworkEnabled") Timber.tag(TAG).v("Background jobs are enabled $isBackgroundJobEnabled") - Timber.tag(TAG).v("Was not yet fetched from server $wasNotYetFetched") if (isNetworkEnabled && isBackgroundJobEnabled) { scope.launch { - if (wasNotYetFetched || downloadDiagnosisKeysTaskDidNotRunRecently()) { + val lastSubmission = exposureDetectionTracker.lastSubmission(onlyFinished = false) + Timber.tag(TAG).v("Last submission was %s", lastSubmission) + + if (lastSubmission == null || downloadDiagnosisKeysTaskDidNotRunRecently()) { Timber.tag(TAG).v("Start the fetching and submitting of the diagnosis keys") taskController.submitBlocking( @@ -143,7 +139,6 @@ class TracingRepository @Inject constructor( originTag = "TracingRepository.refreshRisklevel()" ) ) - TimerHelper.checkManualKeyRetrievalTimer() taskController.submit( DefaultTaskRequest(RiskLevelTask::class, originTag = "TracingRepository.refreshRiskLevel()") @@ -172,10 +167,6 @@ class TracingRepository @Inject constructor( } } - fun refreshLastSuccessfullyCalculatedScore() { - RiskLevelRepository.refreshLastSuccessfullyCalculatedScore() - } - companion object { private val TAG: String? = TracingRepository::class.simpleName } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt index f14a53f5ff4656b76205d9e1e847d695e4cf3e76..b0c972544a74ef5442d0fa6da7173d55f9e3ff82 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt @@ -5,9 +5,12 @@ import androidx.lifecycle.asLiveData import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.launch import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -15,13 +18,11 @@ import javax.inject.Singleton @Singleton class InteroperabilityRepository @Inject constructor( - private val appConfigProvider: AppConfigProvider + private val appConfigProvider: AppConfigProvider, + @AppScope private val appScope: CoroutineScope, + private val dispatcherProvider: DefaultDispatcherProvider ) { - fun saveInteroperabilityUsed() { - LocalData.isInteroperabilityShownAtLeastOnce = true - } - private val countryListFlowInternal = MutableStateFlow(listOf<Country>()) val countryListFlow: Flow<List<Country>> = countryListFlowInternal @@ -32,12 +33,9 @@ class InteroperabilityRepository @Inject constructor( getAllCountries() } - /** - * Gets all countries from @see ApplicationConfigurationService.asyncRetrieveApplicationConfiguration - * Also changes every country code to lower case - */ fun getAllCountries() { - runBlocking { + // TODO Make this reactive, the AppConfigProvider should refresh itself on network changes. + appScope.launch(context = dispatcherProvider.IO) { try { val countries = appConfigProvider.getAppConfig() .supportedCountries @@ -60,4 +58,8 @@ class InteroperabilityRepository @Inject constructor( fun clear() { countryListFlowInternal.value = emptyList() } + + fun saveInteroperabilityUsed() { + LocalData.isInteroperabilityShownAtLeastOnce = true + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt index 932fb9bebc875c253a4e8bbae26b8bbbb5f91c7c..6db01b71292562462a2db3d0cd7202e68f772f29 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt @@ -123,24 +123,40 @@ class TaskController @Inject constructor( private suspend fun processMap() = internalTaskData.updateSafely { Timber.tag(TAG).d("Processing task data (count=%d)", size) - Timber.tag(TAG).v("Tasks before processing: %s", this.values) - // Procress all unprocessed finished tasks - procressFinishedTasks(this).let { + // Process all unprocessed finished tasks + processFinishedTasks(this).let { this.clear() this.putAll(it) } // Start new tasks - procressPendingTasks(this).let { + processPendingTasks(this).let { this.clear() this.putAll(it) } - Timber.tag(TAG).v("Tasks after processing: %s", this.values) + if (size > TASK_HISTORY_LIMIT) { + Timber.v("Enforcing history limits (%d), need to remove %d.", TASK_HISTORY_LIMIT, size - TASK_HISTORY_LIMIT) + values + .filter { it.isFinished } + .sortedBy { it.finishedAt } + .take(size - TASK_HISTORY_LIMIT) + .forEach { + Timber.v("Removing from history: %s", get(it.id)) + remove(it.id) + } + } + + Timber.tag(TAG).v( + "Tasks after processing (count=%d):\n%s", + size, values.sortedBy { it.finishedAt }.joinToString("\n") { + it.toLogString() + } + ) } - private fun procressFinishedTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { + private fun processFinishedTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { val workMap = data.toMutableMap() workMap.values .filter { it.job.isCompleted && it.executionState != TaskState.ExecutionState.FINISHED } @@ -165,7 +181,7 @@ class TaskController @Inject constructor( return workMap } - private fun procressPendingTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { + private suspend fun processPendingTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { val workMap = data.toMutableMap() workMap.values .filter { it.executionState == TaskState.ExecutionState.PENDING } @@ -178,16 +194,22 @@ class TaskController @Inject constructor( it.id != state.id } Timber.tag(TAG).d("Task has %d siblings", siblingTasks.size) - Timber.tag(TAG).v( - "Sibling are:\n%s", siblingTasks.joinToString("\n") - ) + if (siblingTasks.isNotEmpty()) { + Timber.tag(TAG).v("Sibling are:\n%s", siblingTasks.joinToString("\n")) + } + + Timber.tag(TAG).v("Checking preconditions for request: %s", state.config) + val arePreconditionsMet = state.config.preconditions.fold(true) { allPreConditionsMet, precondition -> + allPreConditionsMet && precondition() + } // Handle collision behavior for tasks of same type when { siblingTasks.isEmpty() -> { workMap[state.id] = state.toRunningState() } - state.config.collisionBehavior == CollisionBehavior.SKIP_IF_SIBLING_RUNNING -> { + !arePreconditionsMet || + state.config.collisionBehavior == CollisionBehavior.SKIP_IF_SIBLING_RUNNING -> { workMap[state.id] = state.toSkippedState() } state.config.collisionBehavior == CollisionBehavior.ENQUEUE -> { @@ -237,5 +259,6 @@ class TaskController @Inject constructor( companion object { private const val TAG = "TaskController" + private const val TASK_HISTORY_LIMIT = 50 } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt index 652bf1b58f63604bcc2aaa5cd17f895ea7f55ac4..25dfd34f5f19af7d6a7ebbfc0f055dd0936dd5de 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt @@ -15,6 +15,9 @@ interface TaskFactory< val collisionBehavior: CollisionBehavior + val preconditions: List<suspend () -> Boolean> + get() = emptyList() + enum class CollisionBehavior { ENQUEUE, SKIP_IF_SIBLING_RUNNING diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/InternalTaskState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/InternalTaskState.kt index 1eb0c52be55c8d78ca18f61ffba1b754316056ae..8ac4fd5df832c7f4bdec509a9a09d11c0fb86de8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/InternalTaskState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/InternalTaskState.kt @@ -32,4 +32,10 @@ internal data class InternalTaskState( startedAt != null -> ExecutionState.RUNNING else -> ExecutionState.PENDING } + + fun toLogString(): String = """ + ${request.type.simpleName} state=${executionState.name} id=$id + startedAt=$startedAt finishedAt=$finishedAt result=${result != null} error=$error + arguments=${request.arguments} config=$config + """.trimIndent() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt deleted file mode 100644 index 96c90c0f6beb744030269b9f6268694af52c425c..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/timer/TimerHelper.kt +++ /dev/null @@ -1,164 +0,0 @@ -package de.rki.coronawarnapp.timer - -import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.SettingsRepository -import de.rki.coronawarnapp.util.di.AppInjector -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Instant -import timber.log.Timber -import java.util.Timer -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.concurrent.fixedRateTimer - -/** - * Singleton class for timer handling - */ -object TimerHelper { - - private val TAG: String? = TimerHelper::class.simpleName - - /** - * Atomic boolean for timer existence check - * - * @see AtomicBoolean - */ - private val isManualKeyRetrievalOnTimer = AtomicBoolean(false) - - /** - * A timer for manual key retrieval button - * - * @see Timer - */ - private var manualKeyRetrievalTimer: Timer? = null - - /** - * Manual key retrieval button timer unique name - */ - private const val MANUAL_KEY_RETRIEVAL_TIMER_NAME = "ManualKeyRetrievalTimer" - - /** - * Timer tick in milliseconds - */ - private const val TIMER_TICK = 1000L - - /** - * Initial timer delay in milliseconds - */ - private const val INITIAL_TIMER_DELAY = 0L - - private val settingsRepository by lazy { - AppInjector.component.settingsRepository - } - - /** - * Get cooldown time left between last time update button was triggered and current time - * - * @return Long - * - * @see LocalData.lastTimeDiagnosisKeysFromServerFetch - * @see TimeVariables.getManualKeyRetrievalDelay - */ - private fun getManualKeyRetrievalTimeLeft(): Long { - if (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null) return 0 - - val currentDate = DateTime(Instant.now(), DateTimeZone.getDefault()) - val lastFetch = - DateTime(LocalData.lastTimeDiagnosisKeysFromServerFetch(), DateTimeZone.getDefault()) - - return TimeVariables.getManualKeyRetrievalDelay() - (currentDate.millis - lastFetch.millis) - } - - /** - * Start manual key retrieval timer - * Update last call time with current time in shared preferences, set the enable flag to false - * and starts the cooldown timer. - * - * @see SettingsRepository.isManualKeyRetrievalEnabled - */ - fun startManualKeyRetrievalTimer() { - checkManualKeyRetrievalTimer() - } - - /** - * Start manual key retrieval timer if not yet started - * Every timer tick refresh manual key retrieval button status and text - * - * @see isManualKeyRetrievalOnTimer - * @see MANUAL_KEY_RETRIEVAL_TIMER_NAME - * @see TIMER_TICK - */ - fun checkManualKeyRetrievalTimer() { - if (!isManualKeyRetrievalOnTimer.get() && getManualKeyRetrievalTimeLeft() > 0) { - try { - isManualKeyRetrievalOnTimer.set(true) - manualKeyRetrievalTimer = - fixedRateTimer( - MANUAL_KEY_RETRIEVAL_TIMER_NAME, - true, - INITIAL_TIMER_DELAY, - TIMER_TICK - ) { - onManualKeyRetrievalTimerTick() - }.also { it.logTimerStart() } - } catch (e: Exception) { - logTimerException(e) - } - } - if (!isManualKeyRetrievalOnTimer.get()) { - settingsRepository.updateManualKeyRetrievalEnabled(true) - } - } - - /** - * Process manual key retrieval timer tick - * If no cooldown time left - stop timer, change text and enable update button - * Else - update text with timer HMS format - * - * @see getManualKeyRetrievalTimeLeft - * @see SettingsRepository.updateManualKeyRetrievalEnabled - * @see SettingsRepository.updateManualKeyRetrievalTime - */ - private fun onManualKeyRetrievalTimerTick() { - val timeDifference = getManualKeyRetrievalTimeLeft() - val result = timeDifference <= 0 - settingsRepository.updateManualKeyRetrievalEnabled(result) - settingsRepository.updateManualKeyRetrievalTime(timeDifference) - if (result) stopManualKeyRetrievalTimer() - } - - /** - * Stop manual key retrieval timer and set timer flag to false - * - * @see isManualKeyRetrievalOnTimer - * @see MANUAL_KEY_RETRIEVAL_TIMER_NAME - */ - private fun stopManualKeyRetrievalTimer() { - manualKeyRetrievalTimer?.cancel() - isManualKeyRetrievalOnTimer.set(false) - logTimerStop(MANUAL_KEY_RETRIEVAL_TIMER_NAME) - } - - /** - * Log timer start - */ - private fun Timer.logTimerStart() { - if (BuildConfig.DEBUG) Timber.d("Timer started: $this") - } - - /** - * Log timer stop - */ - private fun logTimerStop(timerName: String) { - if (BuildConfig.DEBUG) Timber.d("Timer stopped: $timerName") - } - - /** - * Log timer exception - */ - private fun logTimerException(exception: java.lang.Exception) { - Timber.e("Timer exception: $exception") - } -} 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 fd0e01406f0c89425fac3b81ffb0a5bc0bbcbc8e..c9f7e3d3c7c61c11ad3da2467d1379924055d917 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 @@ -99,7 +99,6 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { override fun onResume() { super.onResume() ConnectivityHelper.registerNetworkStatusCallback(this, callbackNetwork) - settingsViewModel.updateBackgroundJobEnabled(ConnectivityHelper.autoModeEnabled(this)) scheduleWork() checkShouldDisplayBackgroundWarning() vm.doBackgroundNoiseCheck() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt index 87280672027294bb39746d42b6fcf752a3def860..18bffeac946353d8c4779d4b3f9405c5a7ebea94 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -131,7 +131,6 @@ class HomeFragment : Fragment(R.layout.fragment_home), AutoInject { binding.riskCardContent.apply { riskCardButtonUpdate.setOnClickListener { vm.refreshDiagnosisKeys() - vm.settingsViewModel.updateManualKeyRetrievalEnabled(false) } riskCardButtonEnableTracing.setOnClickListener { doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsTracingFragment()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index e1f8d5f6098612b854678ffbb93b89cf3c675b8d..bdcdbc0aefff08b393fca5ecc4b920b66dd6b5e6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -8,7 +8,6 @@ import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.TracingRepository -import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowErrorResetDialog import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowInteropDeltaOnboarding @@ -105,8 +104,6 @@ class HomeFragmentViewModel @AssistedInject constructor( // TODO the ordering here is weird, do we expect these to run in sequence? tracingRepository.refreshRiskLevel() tracingRepository.refreshActiveTracingDaysInRetentionPeriod() - TimerHelper.checkManualKeyRetrievalTimer() - tracingRepository.refreshLastSuccessfullyCalculatedScore() } fun tracingExplanationWasShown() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt index 2deb4e445f9a1be7f1711ef41665dd2f9b5e72d0..c35d1beed7a3b70ac031d8477cab8c95cb4c0187 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt @@ -13,9 +13,7 @@ import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionTask import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.task.TaskController -import de.rki.coronawarnapp.task.TaskState import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -24,6 +22,7 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import timber.log.Timber import java.util.UUID @@ -35,48 +34,35 @@ class SubmissionResultPositiveOtherWarningViewModel @AssistedInject constructor( interoperabilityRepository: InteroperabilityRepository, private val testResultNotificationService: TestResultNotificationService ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - private var currentSubmissionRequestId: UUID? = null + private val currentSubmission = taskController.tasks - .map { it.find { taskInfo -> taskInfo.taskState.type == SubmissionTask::class }?.taskState } - private val submissionState = currentSubmission - .map { taskState -> + .map { it.find { taskInfo -> taskInfo.taskState.request.id == currentSubmissionRequestId }?.taskState } + .onEach { + it?.let { when { - taskState == null -> ApiRequestState.IDLE - taskState.isFailed -> ApiRequestState.FAILED.also { updateUI(taskState) } - taskState.isFinished -> ApiRequestState.SUCCESS.also { updateUI(taskState) } - else -> ApiRequestState.STARTED + it.isFailed -> submissionError.postValue(it.error) + it.isSuccessful -> routeToScreen.postValue(SubmissionNavigationEvents.NavigateToSubmissionDone) } } - val submissionError = SingleLiveEvent<Throwable>() + } val uiState = combineTransform( - submissionState, - interoperabilityRepository.countryListFlow + currentSubmission, + interoperabilityRepository.countryListFlow ) { state, countries -> WarnOthersState( - apiRequestState = state, - countryList = countries + submitTaskState = state, + countryList = countries ).also { emit(it) } }.asLiveData(context = dispatcherProvider.Default) + val submissionError = SingleLiveEvent<Throwable>() val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() val requestKeySharing = SingleLiveEvent<Unit>() val showEnableTracingEvent = SingleLiveEvent<Unit>() - private fun updateUI(taskState: TaskState) { - if (taskState.request.id == currentSubmissionRequestId) { - currentSubmissionRequestId = null - when { - taskState.isFailed -> - submissionError.postValue(taskState.error ?: return) - taskState.isSuccessful -> - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToSubmissionDone) - } - } - } - fun onBackPressed() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToTestResult) } @@ -104,10 +90,10 @@ class SubmissionResultPositiveOtherWarningViewModel @AssistedInject constructor( private fun submitDiagnosisKeys(keys: List<TemporaryExposureKey>) { Timber.d("submitDiagnosisKeys(keys=%s, symptoms=%s)", keys, symptoms) val registrationToken = - LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() + LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() val taskRequest = DefaultTaskRequest( - SubmissionTask::class, - SubmissionTask.Arguments(registrationToken, keys, symptoms) + SubmissionTask::class, + SubmissionTask.Arguments(registrationToken, keys, symptoms) ) currentSubmissionRequestId = taskRequest.id taskController.submit(taskRequest) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/WarnOthersState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/WarnOthersState.kt index 33809f6b7780c42c014d3c0bfb0698deee4aab2b..e224181fb79770ca69535a7630325574f703a6d8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/WarnOthersState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/WarnOthersState.kt @@ -1,17 +1,16 @@ package de.rki.coronawarnapp.ui.submission.warnothers +import de.rki.coronawarnapp.task.TaskState import de.rki.coronawarnapp.ui.Country -import de.rki.coronawarnapp.ui.submission.ApiRequestState data class WarnOthersState( - val apiRequestState: ApiRequestState, + val submitTaskState: TaskState?, val countryList: List<Country> ) { fun isSubmitButtonEnabled(): Boolean = - apiRequestState == ApiRequestState.IDLE || apiRequestState == ApiRequestState.FAILED + submitTaskState == null || submitTaskState.isFailed - fun isSubmitSpinnerVisible(): Boolean { - return apiRequestState == ApiRequestState.STARTED - } + fun isSubmitSpinnerVisible(): Boolean = + submitTaskState != null && submitTaskState.isActive } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt index ceb286194df6b309f4b4138964e573721ddb3bc1..a2f71aeb12586afed13237c5b390e337bcbb5e91 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt @@ -5,25 +5,29 @@ import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.text.format.DateUtils import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.ui.tracing.common.BaseTracingState -import java.util.Date +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate +import org.joda.time.Instant +import org.joda.time.format.DateTimeFormat +@Suppress("TooManyFunctions") data class TracingCardState( override val tracingStatus: GeneralTracingStatus.Status, - override val riskLevelScore: Int, + override val riskState: RiskState, override val tracingProgress: TracingProgress, - override val lastRiskLevelScoreCalculated: Int, - override val matchedKeyCount: Int, - override val daysSinceLastExposure: Int, - override val activeTracingDaysInRetentionPeriod: Long, - override val lastTimeDiagnosisKeysFetched: Date?, - override val isBackgroundJobEnabled: Boolean, + val lastSuccessfulRiskState: RiskState, + val daysWithEncounters: Int, + val lastEncounterAt: Instant?, + val activeTracingDays: Long, + val lastExposureDetectionTime: Instant?, override val isManualKeyRetrievalEnabled: Boolean, - override val manualKeyRetrievalTime: Long, override val showDetails: Boolean = false ) : BaseTracingState() { @@ -32,27 +36,25 @@ data class TracingCardState( * This special handling is required due to light / dark mode differences and switches * between colored / light / dark background */ - fun getStableIconColor(c: Context): Int = c.getColor( - if (!isTracingOffRiskLevel()) R.color.colorStableLight else R.color.colorTextSemanticNeutral - ) + fun getStableIconColor(c: Context): Int = when { + isTracingOff() -> R.color.colorTextSemanticNeutral + riskState == INCREASED_RISK || riskState == LOW_RISK -> R.color.colorStableLight + else -> R.color.colorTextSemanticNeutral + }.let { c.getColor(it) } /** * Formats the risk card text display depending on risk level * for general information when no definite risk level * can be calculated */ - fun getRiskBody(c: Context): String { - return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - when (riskLevelScore) { - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> R.string.risk_card_outdated_risk_body - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> R.string.risk_card_body_tracing_off - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> R.string.risk_card_unknown_risk_body - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -> R.string.risk_card_outdated_manual_risk_body - else -> null - }?.let { c.getString(it) } ?: "" - } else { + fun getErrorStateBody(c: Context): String { + if (isTracingOff()) { return c.getString(R.string.risk_card_body_tracing_off) } + return when (riskState) { + CALCULATION_FAILED -> c.getString(R.string.risk_card_check_failed_no_internet_body) + else -> "" + } } /** @@ -61,70 +63,66 @@ data class TracingCardState( * the persisted risk level is of importance */ fun getSavedRiskBody(c: Context): String { - return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - return if ( - riskLevelScore == RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF || - riskLevelScore == RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS || - riskLevelScore == RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - ) { - when (lastRiskLevelScoreCalculated) { - RiskLevelConstants.LOW_LEVEL_RISK, - RiskLevelConstants.INCREASED_RISK, - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> { - val arg = formatRiskLevelHeadline(c, lastRiskLevelScoreCalculated) - c.getString(R.string.risk_card_no_calculation_possible_body_saved_risk) - .format(arg) - } - else -> "" - } - } else { - "" - } - } else { - val arg = formatRiskLevelHeadline(c, lastRiskLevelScoreCalculated) - c.getString(R.string.risk_card_no_calculation_possible_body_saved_risk) - .format(arg) + // Don't display last risk when tracing is disabled + if (isTracingOff()) { + val arg = c.getString(R.string.risk_card_no_calculation_possible_headline) + return c.getString(R.string.risk_card_no_calculation_possible_body_saved_risk).format(arg) + } + + // Don't have any old risk state to display + if (lastSuccessfulRiskState == CALCULATION_FAILED) { + return "" + } + + // If we failed this time, we want to display the old risk + if (riskState == CALCULATION_FAILED) { + val arg = when (lastSuccessfulRiskState) { + INCREASED_RISK -> R.string.risk_card_increased_risk_headline + LOW_RISK -> R.string.risk_card_low_risk_headline + else -> null + }?.let { c.getString(it) } ?: "" + return c.getString(R.string.risk_card_no_calculation_possible_body_saved_risk).format(arg) } + + // We are not in an error state + return "" } /** * Formats the risk card text display of infected contacts recognized */ - fun getRiskContactBody(c: Context): String { - val resources = c.resources - val contacts = matchedKeyCount - return when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> { - if (matchedKeyCount == 0) { - c.getString(R.string.risk_card_body_contact) - } else { - resources.getQuantityString( - R.plurals.risk_card_body_contact_value_high_risk, - contacts, - contacts - ) - } - } - RiskLevelConstants.LOW_LEVEL_RISK -> { - if (matchedKeyCount == 0) { - c.getString(R.string.risk_card_body_contact) - } else { - resources.getQuantityString( - R.plurals.risk_card_body_contact_value, - contacts, - contacts - ) - } - } - else -> "" + fun getRiskContactBody(c: Context): String = when { + isTracingOff() -> { + "" + } + riskState == INCREASED_RISK && daysWithEncounters == 0 -> { + c.getString(R.string.risk_card_high_risk_no_encounters_body) + } + riskState == INCREASED_RISK -> { + c.resources.getQuantityString( + R.plurals.risk_card_high_risk_encounter_days_body, + daysWithEncounters, + daysWithEncounters + ) + } + riskState == LOW_RISK && daysWithEncounters == 0 -> { + c.getString(R.string.risk_card_low_risk_no_encounters_body) + } + riskState == LOW_RISK -> { + c.resources.getQuantityString( + R.plurals.risk_card_low_risk_encounter_days_body, + daysWithEncounters, + daysWithEncounters + ) } + else -> "" } /** * Formats the risk card icon display of infected contacts recognized */ fun getRiskContactIcon(c: Context): Drawable? = c.getDrawable( - if (riskLevelScore == RiskLevelConstants.INCREASED_RISK) { + if (riskState == INCREASED_RISK) { R.drawable.ic_risk_card_contact_increased } else { R.drawable.ic_risk_card_contact @@ -136,59 +134,41 @@ data class TracingCardState( * only in the special case of increased risk as a positive contact is a * prerequisite for increased risk */ - fun getRiskContactLast(c: Context): String { - val resources = c.resources - val days = daysSinceLastExposure - return if (riskLevelScore == RiskLevelConstants.INCREASED_RISK) { - resources.getQuantityString( - R.plurals.risk_card_increased_risk_body_contact_last, - days, - days - ) - } else { - "" + fun getRiskContactLast(c: Context): String = when { + isTracingOff() -> "" + riskState == INCREASED_RISK -> { + val formattedDate = lastEncounterAt?.toLocalDate()?.toString(DateTimeFormat.mediumDate()) + c.getString(R.string.risk_card_high_risk_most_recent_body, formattedDate) } + else -> "" } /** * Formats the risk card text display of tracing active duration in days depending on risk level * Special case for increased risk as it is then only displayed on risk detail view */ - fun getRiskActiveTracingDaysInRetentionPeriod(c: Context): String = when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> { - if (showDetails) { - if (activeTracingDaysInRetentionPeriod < TimeVariables.getDefaultRetentionPeriodInDays()) { - c.getString( - R.string.risk_card_body_saved_days - ) - .format(activeTracingDaysInRetentionPeriod) - } else { - c.getString( - R.string.risk_card_body_saved_days_full - ) - } - } else { - "" - } + fun getRiskActiveTracingDaysInRetentionPeriod(c: Context): String = when { + isTracingOff() -> "" + riskState == INCREASED_RISK && !showDetails -> "" + riskState == INCREASED_RISK && activeTracingDays < TimeVariables.getDefaultRetentionPeriodInDays() -> { + c.getString(R.string.risk_card_body_saved_days).format(activeTracingDays) + } + riskState == INCREASED_RISK && activeTracingDays >= TimeVariables.getDefaultRetentionPeriodInDays() -> { + c.getString(R.string.risk_card_body_saved_days_full) + } + riskState == LOW_RISK && activeTracingDays < TimeVariables.getDefaultRetentionPeriodInDays() -> { + c.getString(R.string.risk_card_body_saved_days).format(activeTracingDays) + } + riskState == LOW_RISK && activeTracingDays >= TimeVariables.getDefaultRetentionPeriodInDays() -> { + c.getString(R.string.risk_card_body_saved_days_full) } - RiskLevelConstants.LOW_LEVEL_RISK -> - if (activeTracingDaysInRetentionPeriod < TimeVariables.getDefaultRetentionPeriodInDays()) { - c.getString( - R.string.risk_card_body_saved_days - ) - .format(activeTracingDaysInRetentionPeriod) - } else { - c.getString( - R.string.risk_card_body_saved_days_full - ) - } else -> "" } - private fun formatRelativeDateTimeString(c: Context, date: Date): CharSequence? = + private fun formatRelativeDateTimeString(c: Context, date: Instant): CharSequence? = DateUtils.getRelativeDateTimeString( c, - date.time, + date.millis, DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS * 2, 0 @@ -201,39 +181,34 @@ data class TracingCardState( */ */ fun getTimeFetched(c: Context): String { - if (tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE) { - return if (lastTimeDiagnosisKeysFetched != null) { + if (isTracingOff()) { + return if (lastExposureDetectionTime != null) { c.getString( R.string.risk_card_body_time_fetched, - formatRelativeDateTimeString(c, lastTimeDiagnosisKeysFetched) + formatRelativeDateTimeString(c, lastExposureDetectionTime) ) } else { c.getString(R.string.risk_card_body_not_yet_fetched) } } - return when (riskLevelScore) { - RiskLevelConstants.LOW_LEVEL_RISK, - RiskLevelConstants.INCREASED_RISK -> { - if (lastTimeDiagnosisKeysFetched != null) { + return when (riskState) { + LOW_RISK, INCREASED_RISK -> { + if (lastExposureDetectionTime != null) { c.getString( R.string.risk_card_body_time_fetched, - formatRelativeDateTimeString(c, lastTimeDiagnosisKeysFetched) + formatRelativeDateTimeString(c, lastExposureDetectionTime) ) } else { c.getString(R.string.risk_card_body_not_yet_fetched) } } - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -> { - when (lastRiskLevelScoreCalculated) { - RiskLevelConstants.LOW_LEVEL_RISK, - RiskLevelConstants.INCREASED_RISK, - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> { - if (lastTimeDiagnosisKeysFetched != null) { + CALCULATION_FAILED -> { + when (lastSuccessfulRiskState) { + LOW_RISK, INCREASED_RISK -> { + if (lastExposureDetectionTime != null) { c.getString( R.string.risk_card_body_time_fetched, - formatRelativeDateTimeString(c, lastTimeDiagnosisKeysFetched) + formatRelativeDateTimeString(c, lastExposureDetectionTime) ) } else { c.getString(R.string.risk_card_body_not_yet_fetched) @@ -242,7 +217,6 @@ data class TracingCardState( else -> "" } } - else -> "" } } @@ -252,38 +226,36 @@ data class TracingCardState( * between colored / light / dark background */ fun getStableDividerColor(c: Context): Int = c.getColor( - if (!isTracingOffRiskLevel()) R.color.colorStableHairlineLight else R.color.colorStableHairlineDark + if (isTracingOff() || riskState == CALCULATION_FAILED) { + R.color.colorStableHairlineDark + } else { + R.color.colorStableHairlineLight + } ) /** * Formats the risk card button display for enable tracing depending on risk level and current view */ - fun showTracingButton(): Boolean = isTracingOffRiskLevel() && !showDetails + fun showTracingButton(): Boolean = isTracingOff() && !showDetails /** * Formats the risk card button display for manual updates depending on risk level, * background task setting and current view */ fun showUpdateButton(): Boolean = - !isTracingOffRiskLevel() && !isBackgroundJobEnabled && !showDetails + !isTracingOff() && + (isManualKeyRetrievalEnabled || riskState == CALCULATION_FAILED) && + !showDetails - fun getRiskLevelHeadline(c: Context) = formatRiskLevelHeadline(c, riskLevelScore) - - fun formatRiskLevelHeadline(c: Context, riskLevelScore: Int): String { - return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> R.string.risk_card_increased_risk_headline - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> R.string.risk_card_outdated_risk_headline - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> - R.string.risk_card_no_calculation_possible_headline - RiskLevelConstants.LOW_LEVEL_RISK -> R.string.risk_card_low_risk_headline - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> R.string.risk_card_unknown_risk_headline - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -> R.string.risk_card_unknown_risk_headline - else -> null - }?.let { c.getString(it) } ?: "" - } else { + fun getRiskLevelHeadline(c: Context): String { + if (isTracingOff()) { return c.getString(R.string.risk_card_no_calculation_possible_headline) } + return when (riskState) { + INCREASED_RISK -> R.string.risk_card_increased_risk_headline + LOW_RISK -> R.string.risk_card_low_risk_headline + CALCULATION_FAILED -> R.string.risk_card_check_failed_no_internet_headline + }.let { c.getString(it) } } fun getProgressCardHeadline(c: Context): String = when (tracingProgress) { @@ -301,16 +273,23 @@ data class TracingCardState( fun isTracingInProgress(): Boolean = tracingProgress != TracingProgress.Idle fun getRiskInfoContainerBackgroundTint(c: Context): ColorStateList { - return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> R.color.card_increased - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> R.color.card_outdated - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> R.color.card_no_calculation - RiskLevelConstants.LOW_LEVEL_RISK -> R.color.card_low - else -> R.color.card_unknown - }.let { c.getColorStateList(it) } - } else { + if (isTracingOff()) { return c.getColorStateList(R.color.card_no_calculation) } + return when (riskState) { + INCREASED_RISK -> R.color.card_increased + LOW_RISK -> R.color.card_low + CALCULATION_FAILED -> R.color.card_no_calculation + }.let { c.getColorStateList(it) } } + + fun getUpdateButtonColor(c: Context): Int = when (riskState) { + INCREASED_RISK, LOW_RISK -> R.color.colorStableLight + else -> R.color.colorAccentTintButton + }.let { c.getColor(it) } + + fun getUpdateButtonTextColor(c: Context): Int = when (riskState) { + INCREASED_RISK, LOW_RISK -> R.color.colorTextPrimary1Stable + else -> R.color.colorTextPrimary1InvertedStable + }.let { c.getColor(it) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt index 80db0f28aba95ec7aa073aa399fdaf769fdf3f4a..5fa194430033216fce6d63dc6848fe1a2bf89053 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt @@ -1,11 +1,13 @@ package de.rki.coronawarnapp.ui.tracing.card import dagger.Reusable -import de.rki.coronawarnapp.risk.ExposureResultStore -import de.rki.coronawarnapp.storage.RiskLevelRepository -import de.rki.coronawarnapp.storage.SettingsRepository +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +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.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus +import de.rki.coronawarnapp.ui.tracing.common.tryLatestResultsWithDefaults import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.flow.combine import kotlinx.coroutines.flow.Flow @@ -19,70 +21,54 @@ import javax.inject.Inject class TracingCardStateProvider @Inject constructor( tracingStatus: GeneralTracingStatus, backgroundModeStatus: BackgroundModeStatus, - settingsRepository: SettingsRepository, tracingRepository: TracingRepository, - exposureResultStore: ExposureResultStore + riskLevelStorage: RiskLevelStorage, + exposureDetectionTracker: ExposureDetectionTracker ) { - // TODO Refactor these singletons away val state: Flow<TracingCardState> = combine( tracingStatus.generalStatus.onEach { Timber.v("tracingStatus: $it") }, - RiskLevelRepository.riskLevelScore.onEach { - Timber.v("riskLevelScore: $it") - }, - RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated.onEach { - Timber.v("riskLevelScoreLastSuccessfulCalculated: $it") - }, tracingRepository.tracingProgress.onEach { Timber.v("tracingProgress: $it") }, - exposureResultStore.matchedKeyCount.onEach { - Timber.v("matchedKeyCount: $it") - }, - exposureResultStore.daysSinceLastExposure.onEach { - Timber.v("daysSinceLastExposure: $it") + riskLevelStorage.riskLevelResults.onEach { + Timber.v("riskLevelResults: $it") }, tracingRepository.activeTracingDaysInRetentionPeriod.onEach { Timber.v("activeTracingDaysInRetentionPeriod: $it") }, - tracingRepository.lastTimeDiagnosisKeysFetched.onEach { - Timber.v("lastTimeDiagnosisKeysFetched: $it") + exposureDetectionTracker.latestSubmission().onEach { + Timber.v("latestSubmission: $it") }, backgroundModeStatus.isAutoModeEnabled.onEach { Timber.v("isAutoModeEnabled: $it") - }, - settingsRepository.isManualKeyRetrievalEnabledFlow.onEach { - Timber.v("isManualKeyRetrievalEnabledFlow: $it") - }, - settingsRepository.manualKeyRetrievalTimeFlow.onEach { - Timber.v("manualKeyRetrievalTimeFlow: $it") } ) { status, - riskLevelScore, - riskLevelScoreLastSuccessfulCalculated, tracingProgress, - matchedKeyCount, - daysSinceLastExposure, + riskLevelResults, activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched, - isBackgroundJobEnabled, - isManualKeyRetrievalEnabled, - manualKeyRetrievalTime -> + latestSubmission, + isBackgroundJobEnabled -> + + val ( + latestCalc, + latestSuccessfulCalc + ) = riskLevelResults.tryLatestResultsWithDefaults() + + val isRestartButtonEnabled = !isBackgroundJobEnabled || latestCalc.riskState == RiskState.CALCULATION_FAILED TracingCardState( tracingStatus = status, - riskLevelScore = riskLevelScore, + riskState = latestCalc.riskState, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelScoreLastSuccessfulCalculated, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, - activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, - isBackgroundJobEnabled = isBackgroundJobEnabled, - isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, - manualKeyRetrievalTime = manualKeyRetrievalTime + lastSuccessfulRiskState = latestSuccessfulCalc.riskState, + lastExposureDetectionTime = latestSubmission?.startedAt, + daysWithEncounters = latestCalc.daysWithEncounters, + lastEncounterAt = latestCalc.lastRiskEncounterAt, + activeTracingDays = activeTracingDaysInRetentionPeriod, + isManualKeyRetrievalEnabled = isRestartButtonEnabled ) } .onStart { Timber.v("TracingCardState FLOW start") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt index a132f0cd0f96f0d316e2d5502ce4c3958463e900..23cf72ada6439be8d7a0d11fbf91ef045cdb5cd9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt @@ -2,67 +2,43 @@ package de.rki.coronawarnapp.ui.tracing.common import android.content.Context import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants +import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress -import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHMS -import java.util.Date abstract class BaseTracingState { abstract val tracingStatus: GeneralTracingStatus.Status - abstract val riskLevelScore: Int + abstract val riskState: RiskState abstract val tracingProgress: TracingProgress - abstract val lastRiskLevelScoreCalculated: Int - abstract val matchedKeyCount: Int - abstract val daysSinceLastExposure: Int - abstract val activeTracingDaysInRetentionPeriod: Long - abstract val lastTimeDiagnosisKeysFetched: Date? - abstract val isBackgroundJobEnabled: Boolean abstract val showDetails: Boolean // Only true for riskdetailsfragment abstract val isManualKeyRetrievalEnabled: Boolean - abstract val manualKeyRetrievalTime: Long /** * Formats the risk card colors for default and pressed states depending on risk level */ - fun getRiskColor(c: Context): Int { - return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> R.color.colorSemanticHighRisk - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> R.color.colorSemanticUnknownRisk - RiskLevelConstants.LOW_LEVEL_RISK -> R.color.colorSemanticLowRisk - else -> R.color.colorSemanticNeutralRisk - }.let { c.getColor(it) } - } else { - return c.getColor(R.color.colorSemanticUnknownRisk) - } - } + fun getRiskColor(c: Context): Int = when { + tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE -> R.color.colorSemanticUnknownRisk + riskState == RiskState.INCREASED_RISK -> R.color.colorSemanticHighRisk + riskState == RiskState.LOW_RISK -> R.color.colorSemanticLowRisk + else -> R.color.colorSemanticUnknownRisk + }.let { c.getColor(it) } - fun isTracingOffRiskLevel(): Boolean { - return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - when (riskLevelScore) { - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> true - else -> false - } - } else { - return true - } - } + fun isTracingOff(): Boolean = tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE - fun getStableTextColor(c: Context): Int = c.getColor( - if (!isTracingOffRiskLevel()) R.color.colorStableLight else R.color.colorTextPrimary1 - ) + fun getStableTextColor(c: Context): Int = when { + tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE -> R.color.colorTextPrimary1 + riskState == RiskState.INCREASED_RISK || + riskState == RiskState.LOW_RISK -> R.color.colorTextPrimary1InvertedStable + else -> R.color.colorTextPrimary1 + }.let { c.getColor(it) } /** * Change the manual update button text according to current timer */ - fun getUpdateButtonText(c: Context): String = if (manualKeyRetrievalTime <= 0) { - c.getString(R.string.risk_card_button_update) + fun getUpdateButtonText(c: Context): String = if (riskState == RiskState.CALCULATION_FAILED) { + c.getString(R.string.risk_card_check_failed_no_internet_restart_button) } else { - val hmsCooldownTime = manualKeyRetrievalTime.millisecondsToHMS() - c.getString(R.string.risk_card_button_cooldown).format(hmsCooldownTime) + c.getString(R.string.risk_card_button_update) } fun isUpdateButtonEnabled(): Boolean = isManualKeyRetrievalEnabled diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskFormatting.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskFormatting.kt deleted file mode 100644 index 30dbd4fd9c355a9b65afa78da1b73c10625dd293..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskFormatting.kt +++ /dev/null @@ -1,41 +0,0 @@ -@file:JvmName("RiskFormatting") - -package de.rki.coronawarnapp.ui.tracing.common - -import android.content.Context -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants - -/** - * Formats the risk details suggested behavior icon color depending on risk level - * This special handling is required due to light / dark mode differences and switches - * between colored / light / dark background - * - * @param riskLevelScore - * @return - */ -fun formatBehaviorIcon(context: Context, riskLevelScore: Int): Int { - val colorRes = when (riskLevelScore) { - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> R.color.colorTextSemanticNeutral - else -> R.color.colorStableLight - } - return context.getColor(colorRes) -} - -/** - * Formats the risk details suggested behavior icon background color depending on risk level - * - * @param riskLevelScore - * @return - */ -fun formatBehaviorIconBackground(context: Context, riskLevelScore: Int): Int { - val colorRes = when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> R.color.colorSemanticHighRisk - RiskLevelConstants.LOW_LEVEL_RISK -> R.color.colorSemanticLowRisk - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> R.color.colorSemanticNeutralRisk - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -> R.color.colorSemanticNeutralRisk - else -> R.color.colorSurface2 - } - return context.getColor(colorRes) -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelResultExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelResultExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..4087bedf49fde363e3046d127193f4dc28e5adb6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelResultExtensions.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.ui.tracing.common + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import org.joda.time.Instant + +fun List<RiskLevelResult>.tryLatestResultsWithDefaults(): DisplayableRiskResults { + val latestCalculation = this.maxByOrNull { it.calculatedAt } + ?: InitialLowLevelRiskLevelResult + + val lastSuccessfullyCalculated = this.filter { it.wasSuccessfullyCalculated } + .maxByOrNull { it.calculatedAt } ?: UndeterminedRiskLevelResult + + return DisplayableRiskResults( + lastCalculated = latestCalculation, + lastSuccessfullyCalculated = lastSuccessfullyCalculated + ) +} + +data class DisplayableRiskResults( + val lastCalculated: RiskLevelResult, + val lastSuccessfullyCalculated: RiskLevelResult +) + +private object InitialLowLevelRiskLevelResult : RiskLevelResult { + override val calculatedAt: Instant = Instant.now() + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: RiskLevelResult.FailureReason? = null + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} + +private object UndeterminedRiskLevelResult : RiskLevelResult { + override val calculatedAt: Instant = Instant.EPOCH + override val riskState: RiskState = RiskState.CALCULATION_FAILED + override val failureReason: RiskLevelResult.FailureReason? = null + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/DefaultRiskDetailPresenter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/DefaultRiskDetailPresenter.kt index 6ff94bb2b4bf27508702ed9dd645883557bdf72d..3369797d05cc72a0276af2cbec365985365b4e9f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/DefaultRiskDetailPresenter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/DefaultRiskDetailPresenter.kt @@ -1,15 +1,14 @@ package de.rki.coronawarnapp.ui.tracing.details import dagger.Reusable -import de.rki.coronawarnapp.risk.RiskLevelConstants +import de.rki.coronawarnapp.risk.RiskState import javax.inject.Inject @Reusable class DefaultRiskDetailPresenter @Inject constructor() { - fun isAdditionalInfoVisible(riskLevel: Int, matchedKeyCount: Int) = - riskLevel == RiskLevelConstants.LOW_LEVEL_RISK && matchedKeyCount > 0 + fun isAdditionalInfoVisible(riskState: RiskState, matchedKeyCount: Int) = + riskState == RiskState.LOW_RISK && matchedKeyCount > 0 - fun isInformationBodyNoticeVisible(riskLevel: Int) = - riskLevel != RiskLevelConstants.LOW_LEVEL_RISK + fun isInformationBodyNoticeVisible(riskState: RiskState) = riskState != RiskState.LOW_RISK } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt index 6008444b1d0cb5c7906a92b1cb25014c9dc07d5b..b7b05003b877eedfce8be7c81ce4917a1ebd2e59 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.storage.TracingRepository -import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.ui.tracing.card.TracingCardState import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel @@ -36,13 +35,11 @@ class RiskDetailsFragmentViewModel @AssistedInject constructor( fun refreshData() { tracingRepository.refreshRiskLevel() - TimerHelper.checkManualKeyRetrievalTimer() tracingRepository.refreshActiveTracingDaysInRetentionPeriod() } fun updateRiskDetails() { tracingRepository.refreshDiagnosisKeys() - settingsViewModel.updateManualKeyRetrievalEnabled(false) } @AssistedInject.Factory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt index 7db5aa19ca115756fded302a59c5a9d75eebb66c..a20d89855a8f87d8d2f4722b392fb5c93e0abc2e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt @@ -2,26 +2,24 @@ package de.rki.coronawarnapp.ui.tracing.details import android.content.Context import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.ui.tracing.common.BaseTracingState -import java.util.Date data class TracingDetailsState( override val tracingStatus: GeneralTracingStatus.Status, - override val riskLevelScore: Int, + override val riskState: RiskState, override val tracingProgress: TracingProgress, - override val lastRiskLevelScoreCalculated: Int, - override val matchedKeyCount: Int, - override val daysSinceLastExposure: Int, - override val activeTracingDaysInRetentionPeriod: Long, - override val lastTimeDiagnosisKeysFetched: Date?, - override val isBackgroundJobEnabled: Boolean, + val matchedKeyCount: Int, + val activeTracingDaysInRetentionPeriod: Long, override val isManualKeyRetrievalEnabled: Boolean, - override val manualKeyRetrievalTime: Long, val isInformationBodyNoticeVisible: Boolean, - val isAdditionalInformationVisible: Boolean + val isAdditionalInformationVisible: Boolean, + val daysSinceLastExposure: Int ) : BaseTracingState() { override val showDetails: Boolean = true @@ -31,28 +29,26 @@ data class TracingDetailsState( * in all cases when risk level is not increased */ fun isBehaviorNormalVisible(): Boolean = - riskLevelScore != RiskLevelConstants.INCREASED_RISK + riskState != INCREASED_RISK /** * Format the risk details include display for suggested behavior depending on risk level * Only applied in special case for increased risk */ fun isBehaviorIncreasedRiskVisible(): Boolean = - riskLevelScore == RiskLevelConstants.INCREASED_RISK + riskState == INCREASED_RISK /** * Format the risk details period logged card display depending on risk level * applied in case of low and high risk levels */ - fun isBehaviorPeriodLoggedVisible(): Boolean = - riskLevelScore == RiskLevelConstants.INCREASED_RISK || riskLevelScore == RiskLevelConstants.LOW_LEVEL_RISK + fun isBehaviorPeriodLoggedVisible(): Boolean = riskState == INCREASED_RISK || riskState == LOW_RISK /** * Format the risk details include display for suggested behavior depending on risk level * Only applied in special case for low level risk */ - fun isBehaviorLowLevelRiskVisible(): Boolean = - riskLevelScore == RiskLevelConstants.LOW_LEVEL_RISK && matchedKeyCount > 0 + fun isBehaviorLowLevelRiskVisible(): Boolean = riskState == LOW_RISK && matchedKeyCount > 0 /** * Formats the risk details text display for each risk level @@ -61,51 +57,41 @@ data class TracingDetailsState( val resources = c.resources val days = daysSinceLastExposure val count = matchedKeyCount - return when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> - resources.getQuantityString( - R.plurals.risk_details_information_body_increased_risk, - days, - days - ) - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> - c.getString(R.string.risk_details_information_body_outdated_risk) - RiskLevelConstants.LOW_LEVEL_RISK -> - c.getString( - if (count > 0) R.string.risk_details_information_body_low_risk_with_encounter - else R.string.risk_details_information_body_low_risk - ) - RiskLevelConstants.UNKNOWN_RISK_INITIAL -> - c.getString(R.string.risk_details_information_body_unknown_risk) - else -> "" + return when (riskState) { + INCREASED_RISK -> resources.getQuantityString( + R.plurals.risk_details_information_body_increased_risk, + days, + days + ) + CALCULATION_FAILED -> c.getString(R.string.risk_details_information_body_outdated_risk) + LOW_RISK -> c.getString( + if (count > 0) R.string.risk_details_information_body_low_risk_with_encounter + else R.string.risk_details_information_body_low_risk + ) } } /** * Formats the risk details text display for each risk level for the body notice */ - fun getRiskDetailsRiskLevelBodyNotice(c: Context): String = when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> R.string.risk_details_information_body_notice_increased + fun getRiskDetailsRiskLevelBodyNotice(c: Context): String = when (riskState) { + INCREASED_RISK -> R.string.risk_details_information_body_notice_increased else -> R.string.risk_details_information_body_notice }.let { c.getString(it) } - /** - * Formats the risk details button display for enable tracing depending on risk level - */ - fun areRiskDetailsButtonsVisible(): Boolean = - isRiskDetailsEnableTracingButtonVisible() || isRiskDetailsUpdateButtonVisible() + fun isRiskLevelButtonGroupVisible(): Boolean = isRiskDetailsEnableTracingButtonVisible() || + isRiskDetailsUpdateButtonVisible() /** * Formats the risk details button display for enable tracing depending on risk level */ - fun isRiskDetailsEnableTracingButtonVisible(): Boolean = isTracingOffRiskLevel() + fun isRiskDetailsEnableTracingButtonVisible(): Boolean = isTracingOff() /** * Formats the risk details button display for manual updates depending on risk level and * background task setting */ - fun isRiskDetailsUpdateButtonVisible(): Boolean = - !isTracingOffRiskLevel() && !isBackgroundJobEnabled + fun isRiskDetailsUpdateButtonVisible(): Boolean = !isTracingOff() && isManualKeyRetrievalEnabled /** * Formats the risk logged period card text display of tracing active duration in days depending on risk level @@ -114,4 +100,23 @@ data class TracingDetailsState( fun getRiskActiveTracingDaysInRetentionPeriodLogged(c: Context): String = c.getString( R.string.risk_details_information_body_period_logged_assessment ).format(activeTracingDaysInRetentionPeriod) + + fun getBehaviorIcon(context: Context) = when { + isTracingOff() -> R.color.colorTextSemanticNeutral + riskState == INCREASED_RISK || riskState == LOW_RISK -> R.color.colorStableLight + else -> R.color.colorTextSemanticNeutral + }.let { context.getColor(it) } + + /** + * Formats the risk details suggested behavior icon background color depending on risk level + * + * @param riskLevelScore + * @return + */ + fun getBehaviorIconBackground(context: Context) = when { + isTracingOff() -> R.color.colorSurface2 + riskState == INCREASED_RISK -> R.color.colorSemanticHighRisk + riskState == LOW_RISK -> R.color.colorSemanticLowRisk + else -> R.color.colorSurface2 + }.let { context.getColor(it) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt index 388bca23b501d24bda54b33b85110df0bceb0826..ad2a48f630657c8fd63ced416f1fcefb2a6f4e47 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.ui.tracing.details import dagger.Reusable -import de.rki.coronawarnapp.risk.ExposureResultStore -import de.rki.coronawarnapp.storage.RiskLevelRepository -import de.rki.coronawarnapp.storage.SettingsRepository +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus +import de.rki.coronawarnapp.ui.tracing.common.tryLatestResultsWithDefaults import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.flow.combine import kotlinx.coroutines.flow.Flow @@ -20,55 +20,41 @@ class TracingDetailsStateProvider @Inject constructor( private val riskDetailPresenter: DefaultRiskDetailPresenter, tracingStatus: GeneralTracingStatus, backgroundModeStatus: BackgroundModeStatus, - settingsRepository: SettingsRepository, tracingRepository: TracingRepository, - exposureResultStore: ExposureResultStore + riskLevelStorage: RiskLevelStorage ) { - // TODO Refactore these singletons away val state: Flow<TracingDetailsState> = combine( tracingStatus.generalStatus, - RiskLevelRepository.riskLevelScore, - RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated, tracingRepository.tracingProgress, - exposureResultStore.matchedKeyCount, - exposureResultStore.daysSinceLastExposure, + riskLevelStorage.riskLevelResults, tracingRepository.activeTracingDaysInRetentionPeriod, - tracingRepository.lastTimeDiagnosisKeysFetched, - backgroundModeStatus.isAutoModeEnabled, - settingsRepository.isManualKeyRetrievalEnabledFlow, - settingsRepository.manualKeyRetrievalTimeFlow + backgroundModeStatus.isAutoModeEnabled ) { status, - riskLevelScore, - riskLevelScoreLastSuccessfulCalculated, tracingProgress, - matchedKeyCount, - daysSinceLastExposure, activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched, - isBackgroundJobEnabled, - isManualKeyRetrievalEnabled, - manualKeyRetrievalTime -> + riskLevelResults, + activeTracingDaysInRetentionPeriod, + isBackgroundJobEnabled -> + + val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults() val isAdditionalInformationVisible = riskDetailPresenter.isAdditionalInfoVisible( - riskLevelScore, matchedKeyCount + latestCalc.riskState, latestCalc.matchedKeyCount + ) + val isInformationBodyNoticeVisible = riskDetailPresenter.isInformationBodyNoticeVisible( + latestCalc.riskState ) - val isInformationBodyNoticeVisible = - riskDetailPresenter.isInformationBodyNoticeVisible( - riskLevelScore - ) + + val isRestartButtonEnabled = !isBackgroundJobEnabled || latestCalc.riskState == RiskState.CALCULATION_FAILED TracingDetailsState( tracingStatus = status, - riskLevelScore = riskLevelScore, + riskState = latestCalc.riskState, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelScoreLastSuccessfulCalculated, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, + matchedKeyCount = latestCalc.matchedKeyCount, + daysSinceLastExposure = latestCalc.daysWithEncounters, activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, - isBackgroundJobEnabled = isBackgroundJobEnabled, - isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, - manualKeyRetrievalTime = manualKeyRetrievalTime, + isManualKeyRetrievalEnabled = isRestartButtonEnabled, isAdditionalInformationVisible = isAdditionalInformationVisible, isInformationBodyNoticeVisible = isInformationBodyNoticeVisible ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt index af8b61eb934fafce0b4eaaa948267883b5f2b884..6165a459dd41f949323bfa38a24c5fbfeab12a52 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SettingsViewModel.kt @@ -17,27 +17,7 @@ class SettingsViewModel @Inject constructor() : CWAViewModel() { AppInjector.component.settingsRepository } - // Will impact UI if background activity is not permitted, persistent storing is not necessary - val isBackgroundJobEnabled: LiveData<Boolean> = settingsRepository.isBackgroundJobEnabled - - val isBackgroundPriorityEnabled: LiveData<Boolean> = - settingsRepository.isBackgroundPriorityEnabled - - /** - * Is manual key retrieval enabled - * Used for "Update" button on the Risk Card and in the Risk Details - * - * @see SettingsRepository.isManualKeyRetrievalEnabled - */ - val isManualKeyRetrievalEnabled: LiveData<Boolean> = - settingsRepository.isManualKeyRetrievalEnabled - - /** - * Manual update button timer value - * - * @see SettingsRepository.manualKeyRetrievalTime - */ - val manualKeyRetrievalTime: LiveData<Long> = settingsRepository.manualKeyRetrievalTime + val isBackgroundPriorityEnabled: LiveData<Boolean> = settingsRepository.isBackgroundPriorityEnabled /** * Update connection enabled @@ -48,24 +28,6 @@ class SettingsViewModel @Inject constructor() : CWAViewModel() { settingsRepository.updateConnectionEnabled(value) } - /** - * Update background job enabled - * - * @param value - */ - fun updateBackgroundJobEnabled(value: Boolean) { - settingsRepository.updateBackgroundJobEnabled(value) - } - - /** - * Update manual key button enabled - * - * @param value - */ - fun updateManualKeyRetrievalEnabled(value: Boolean) { - settingsRepository.updateManualKeyRetrievalEnabled(value) - } - fun refreshBackgroundPriorityEnabled() { settingsRepository.refreshBackgroundPriorityEnabled() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt index 2e26c1991f2aae2889e89542223fe0116dd341ed..838637192d741986c7b2e407c2b666f590421b76 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt @@ -4,13 +4,13 @@ import android.content.Context import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.flow.shareLatest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.isActive +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -21,41 +21,41 @@ class BackgroundModeStatus @Inject constructor( @AppScope private val appScope: CoroutineScope ) { - val isBackgroundRestricted: Flow<Boolean?> = callbackFlow<Boolean> { + val isBackgroundRestricted: Flow<Boolean> = flow { while (true) { try { - send(pollIsBackgroundRestricted()) - } catch (e: Exception) { - Timber.w(e, "isBackgroundRestricted failed.") - cancel("isBackgroundRestricted failed", e) + emit(pollIsBackgroundRestricted()) + delay(POLLING_DELAY_MS) + } catch (e: CancellationException) { + Timber.d("isBackgroundRestricted was cancelled") + break } - - if (!isActive) break - - delay(POLLING_DELAY_MS) } } .distinctUntilChanged() + .onCompletion { + if (it != null) Timber.w(it, "isBackgroundRestricted failed.") + } .shareLatest( tag = "isBackgroundRestricted", scope = appScope ) - val isAutoModeEnabled: Flow<Boolean> = callbackFlow<Boolean> { + val isAutoModeEnabled: Flow<Boolean> = flow { while (true) { try { - send(pollIsAutoMode()) - } catch (e: Exception) { - Timber.w(e, "autoModeEnabled failed.") - cancel("autoModeEnabled failed", e) + emit(pollIsAutoMode()) + delay(POLLING_DELAY_MS) + } catch (e: CancellationException) { + Timber.d("isAutoModeEnabled was cancelled") + break } - - if (!isActive) break - - delay(POLLING_DELAY_MS) } } .distinctUntilChanged() + .onCompletion { + if (it != null) Timber.w(it, "autoModeEnabled failed.") + } .shareLatest( tag = "autoModeEnabled", scope = appScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt index 195bd3551aae9a2ba6a3d5306e2faeb64bcbb52c..9e35e4947e08bdbe8615982962b94fee70d786fb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt @@ -17,6 +17,9 @@ object CWADebug { if (isDeviceForTestersBuild) { fileLogger = FileLogger(application) } + + Timber.i("CWA version: %s (%s)", BuildConfig.VERSION_CODE, BuildConfig.GIT_COMMIT_SHORT_HASH) + Timber.i("CWA flavor: %s (%s)", BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE) } val isDebugBuildOrMode: Boolean diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index 6317d11ee81ca9f42ebe779abbbd69a37d5262b5..7673f4e16733553d6e9817bc3f604e55aa7206c5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -25,9 +25,9 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.diagnosiskeys.download.KeyPackageSyncSettings import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.AppDatabase import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.util.di.AppContext @@ -49,10 +49,12 @@ class DataReset @Inject constructor( private val interoperabilityRepository: InteroperabilityRepository, private val submissionRepository: SubmissionRepository, private val exposureDetectionTracker: ExposureDetectionTracker, - private val keyPackageSyncSettings: KeyPackageSyncSettings + private val keyPackageSyncSettings: KeyPackageSyncSettings, + private val riskLevelStorage: RiskLevelStorage ) { private val mutex = Mutex() + /** * Deletes all data known to the Application * @@ -66,8 +68,7 @@ class DataReset @Inject constructor( LocalData.clear() // Shared Preferences Reset SecurityHelper.resetSharedPrefs() - // Reset the current risk level stored in LiveData - RiskLevelRepository.reset() + // Reset the current states stored in LiveData submissionRepository.reset() keyCacheRepository.clear() @@ -75,6 +76,8 @@ class DataReset @Inject constructor( interoperabilityRepository.clear() exposureDetectionTracker.clear() keyPackageSyncSettings.clear() + riskLevelStorage.clear() + Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } 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 8a5729554f5166ee7d9eb192d72e899374ca2bcf..f4f6f8cdfe4c992091b005366fa18031081876ad 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 @@ -34,18 +34,18 @@ class WatchdogService @Inject constructor( fun launch() { // Only do this if the background jobs are enabled if (!ConnectivityHelper.autoModeEnabled(context)) { - Timber.d("Background jobs are not enabled, aborting.") + Timber.tag(TAG).d("Background jobs are not enabled, aborting.") return } - Timber.v("Acquiring wakelocks for watchdog routine.") + Timber.tag(TAG).v("Acquiring wakelocks for watchdog routine.") ProcessLifecycleOwner.get().lifecycleScope.launch { // A wakelock as the OS does not handle this for us like in the background job execution val wakeLock = createWakeLock() // A wifi lock to wake up the wifi connection in case the device is dozing val wifiLock = createWifiLock() - Timber.d("Automatic mode is on, check if we have downloaded keys already today") + Timber.tag(TAG).d("Automatic mode is on, check if we have downloaded keys already today") val state = taskController.submitBlocking( DefaultTaskRequest( @@ -55,7 +55,7 @@ class WatchdogService @Inject constructor( ) ) if (state.isFailed) { - Timber.e(state.error, "RetrieveDiagnosisKeysTransaction failed") + Timber.tag(TAG).e(state.error, "RetrieveDiagnosisKeysTransaction failed") // retry the key retrieval in case of an error with a scheduled work BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } @@ -78,6 +78,7 @@ class WatchdogService @Inject constructor( .apply { acquire() } companion object { + private const val TAG = "WatchdogService" private const val TEN_MINUTE_TIMEOUT_IN_MS = 10 * 60 * 1000L } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt index 84bd21ad68562197d35c5e136283ccd22ef73f7f..c3f50042c4c57701603af3c29fe10bf2782b6604 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.util.debug import android.annotation.SuppressLint import android.util.Log +import org.joda.time.Instant import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -55,7 +56,7 @@ class FileLoggerTree(private val logFile: File) : Timber.DebugTree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { logWriter?.let { try { - it.write("${System.currentTimeMillis()} ${priorityToString(priority)}/$tag: $message\n") + it.write("${Instant.now()} ${priorityToString(priority)}/$tag: $message\n") it.flush() } catch (e: IOException) { Timber.tag(TAG).e(e) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt index 6fdc8c4a0a4137d21f1efb3535cd7cab66cf6cc4..c8a9be0e7fbe0196a2c7780f9689400595a1a420 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt @@ -3,11 +3,14 @@ package de.rki.coronawarnapp.util.di import android.app.Application import android.bluetooth.BluetoothAdapter import android.content.Context +import android.content.SharedPreferences import androidx.core.app.NotificationManagerCompat import androidx.work.WorkManager import dagger.Module import dagger.Provides import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.storage.EncryptedPreferences +import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.worker.WorkManagerProvider import javax.inject.Singleton @@ -38,4 +41,9 @@ class AndroidModule { fun workManager( workManagerProvider: WorkManagerProvider ): WorkManager = workManagerProvider.workManager + + @EncryptedPreferences + @Provides + @Singleton + fun encryptedPreferences(): SharedPreferences = SecurityHelper.globalEncryptedSharedPreferencesInstance } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt index 947b6d55c29ea2894a9050b31a16069d2440c3f3..54a8c158833a3d7c82b270b6d15e1bde04bd6951 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt @@ -42,6 +42,142 @@ fun <T : Any> Flow<T>.shareLatest( ) .filterNotNull() +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + crossinline transform: suspend (T1, T2) -> R +): Flow<R> = combine( + flow, flow2 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + crossinline transform: suspend (T1, T2, T3, T4, T5) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + flow9: Flow<T9>, + flow10: Flow<T10>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10 + ) +} + @Suppress("UNCHECKED_CAST", "LongParameterList") inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R> combine( flow: Flow<T1>, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt index f014dc54c28ca112b2a49db88917add6c83fedca..04234b4c5ff3ff7d5ca3350731b2d5715ddbe140 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.util.serialization import com.google.gson.Gson import com.google.gson.TypeAdapter import com.google.gson.reflect.TypeToken +import timber.log.Timber import java.io.File import kotlin.reflect.KClass @@ -11,8 +12,28 @@ inline fun <reified T> Gson.fromJson(json: String): T = fromJson( object : TypeToken<T>() {}.type ) -inline fun <reified T> Gson.fromJson(file: File): T = file.bufferedReader().use { - fromJson(it, object : TypeToken<T>() {}.type) +/** + * Returns null if the file doesn't exist, otherwise returns the parsed object. + * Throws an exception if the object can't be parsed. + * An empty file, that was deserialized to a null value is deleted. + */ +inline fun <reified T : Any> Gson.fromJson(file: File): T? { + if (!file.exists()) { + Timber.v("fromJson(): File doesn't exist %s", file) + return null + } + + return file.bufferedReader().use { + val value: T? = fromJson(it, object : TypeToken<T>() {}.type) + if (value != null) { + Timber.v("Json read from %s", file) + value + } else { + Timber.w("Tried to parse json from file that exists, but was empty: %s", file) + if (file.delete()) Timber.w("Deleted empty json file: %s", file) + null + } + } } inline fun <reified T> Gson.toJson(data: T, file: File) = file.bufferedWriter().use { writer -> 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 9b491808f2bbdb37d9dada12a88968d1b66f1903..2088d2fbed238c49289934158195088b78a2a889 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 @@ -25,13 +25,29 @@ class CWAWorkerFactory @Inject constructor( workerClassName: String, workerParameters: WorkerParameters ): ListenableWorker? { - Timber.v("Looking up worker for %s", workerClassName) - val factory = factories.entries.find { + Timber.v("Checking in known worker factories for %s", workerClassName) + val ourWorkerFactories = factories.entries.find { Class.forName(workerClassName).isAssignableFrom(it.key) }?.value - requireNotNull(factory) { "Unknown worker: $workerClassName" } - Timber.v("Creating worker for %s with %s", workerClassName, workerParameters) - return factory.get().create(appContext, workerParameters) + return if (ourWorkerFactories != null) { + Timber.v("It's one of ours, creating worker for %s with %s", workerClassName, workerParameters) + ourWorkerFactories.get().create(appContext, workerParameters).also { + Timber.i("Our worker was created: %s", it) + } + } else { + Timber.w("Unknown worker class, trying direct instantiation on %s", workerClassName) + workerClassName.toNewWorkerInstance(appContext, workerParameters).also { + Timber.i("Unknown worker was created: %s", it) + } + } + } + + private fun String.toNewWorkerInstance(context: Context, workerParameters: WorkerParameters): ListenableWorker { + val workerClass = Class.forName(this).asSubclass(ListenableWorker::class.java) + Timber.v("Worker class created: %s", workerClass) + val workerConstructor = workerClass.getDeclaredConstructor(Context::class.java, WorkerParameters::class.java) + Timber.v("Worker constructor created: %s", workerConstructor) + return workerConstructor.newInstance(context, workerParameters) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt index dff4a55e38f96608003dd00059d3c9e64e777aa2..bd5b99c8602ddfde7036631ad4bc344c7f90d55a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkManagerProvider.kt @@ -15,17 +15,21 @@ class WorkManagerProvider @Inject constructor( ) { val workManager by lazy { - Timber.v("Setting up WorkManager.") + Timber.tag(TAG).v("Setting up WorkManager.") val configuration = Configuration.Builder().apply { setMinimumLoggingLevel(android.util.Log.DEBUG) setWorkerFactory(cwaWorkerFactory) }.build() - Timber.v("WorkManager initialize...") + Timber.tag(TAG).v("WorkManager initialize...") WorkManager.initialize(context, configuration) WorkManager.getInstance(context).also { - Timber.v("WorkManager setup done: %s", it) + Timber.tag(TAG).v("WorkManager setup done: %s", it) } } + + companion object { + private const val TAG = "WorkManagerProvider" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt index b77d7c9d79b47591ac2ae4cd42a52eca7ef227fa..522d279d6fd6455a94e7daea9c3d0a8b9d14250f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt @@ -20,13 +20,8 @@ class BackgroundNoiseOneTimeWorker @AssistedInject constructor( private val playbook: Playbook ) : CoroutineWorker(context, workerParams) { - /** - * Work execution - * - * @return Result - */ override suspend fun doWork(): Result { - Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") var result = Result.success() try { @@ -40,10 +35,14 @@ class BackgroundNoiseOneTimeWorker @AssistedInject constructor( } } - Timber.d("$id: doWork() finished with %s", result) + Timber.tag(TAG).d("$id: doWork() finished with %s", result) return result } @AssistedInject.Factory interface Factory : InjectedWorkerFactory<BackgroundNoiseOneTimeWorker> + + companion object { + private val TAG = BackgroundNoiseOneTimeWorker::class.java.simpleName + } } 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 15b175f853c7dd9ffdc7385c4130b0460d293232..d1111667fe3b73b484fb089198120c81a56593b8 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 @@ -22,19 +22,11 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( @Assisted workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) { - companion object { - private val TAG: String? = BackgroundNoisePeriodicWorker::class.simpleName - } - /** - * Work execution - * - * @return Result - * * @see BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK */ override suspend fun doWork(): Result { - Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") var result = Result.success() try { @@ -57,15 +49,19 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( Result.retry() } } - Timber.d("$id: doWork() finished with %s", result) + Timber.tag(TAG).d("$id: doWork() finished with %s", result) return result } private fun stopWorker() { BackgroundWorkScheduler.WorkType.BACKGROUND_NOISE_PERIODIC_WORK.stop() - Timber.d("$id: worker stopped") + Timber.tag(TAG).d("$id: worker stopped") } @AssistedInject.Factory interface Factory : InjectedWorkerFactory<BackgroundNoisePeriodicWorker> + + companion object { + private val TAG = BackgroundNoisePeriodicWorker::class.java.simpleName + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt index 5546e8947deebec1c3536ed4e206da4e22fbfbc3..23199d02733d645ba7688ce839cb6008126c84c0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt @@ -24,13 +24,8 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { - /** - * Work execution - * - * @return Result - */ override suspend fun doWork(): Result { - Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") var result = Result.success() taskController.submitBlocking( @@ -40,22 +35,26 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( originTag = "DiagnosisKeyRetrievalOneTimeWorker" ) ).error?.also { error: Throwable -> - Timber.w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.") + Timber.tag(TAG).w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.") if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.w(error, "$id: Retry attempts exceeded.") + Timber.tag(TAG).w(error, "$id: Retry attempts exceeded.") return Result.failure() } else { - Timber.d(error, "$id: Retrying.") + Timber.tag(TAG).d(error, "$id: Retrying.") result = Result.retry() } } - Timber.d("$id: doWork() finished with %s", result) + Timber.tag(TAG).d("$id: doWork() finished with %s", result) return result } @AssistedInject.Factory interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalOneTimeWorker> + + companion object { + private val TAG = DiagnosisKeyRetrievalOneTimeWorker::class.java.simpleName + } } 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 index 4bcd5bc8f1a684461d0a1e0d92cf290487c4741e..adff8651b5bb674bad55ecbba08bbccad7b0adab 100644 --- 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 @@ -21,38 +21,38 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor( ) : CoroutineWorker(context, workerParams) { /** - * Work execution - * - * @return Result - * * @see BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() * @see BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() */ override suspend fun doWork(): Result { - Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") var result = Result.success() try { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } catch (e: Exception) { - Timber.w( + Timber.tag(TAG).w( e, "$id: Error during BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()." ) if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.w(e, "$id: Retry attempts exceeded.") + Timber.tag(TAG).w(e, "$id: Retry attempts exceeded.") return Result.failure() } else { - Timber.d(e, "$id: Retrying.") + Timber.tag(TAG).d(e, "$id: Retrying.") result = Result.retry() } } - Timber.d("$id: doWork() finished with %s", result) + Timber.tag(TAG).d("$id: doWork() finished with %s", result) return result } @AssistedInject.Factory 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 1453a32bb33c4e5b207d49b68093c49fd667564b..1e5a281285c65a234443d037942ec99da2ab44c2 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 @@ -8,6 +8,8 @@ import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException +import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID +import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_TEST_RESULT_NOTIFICATION_ID import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData @@ -29,25 +31,20 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( ) : CoroutineWorker(context, workerParams) { /** - * Work execution - * * If background job is running for less than 21 days, testResult is checked. * If the job is running for more than 21 days, the job will be stopped * - * @return Result - * * @see LocalData.isTestResultNotificationSent * @see LocalData.initialPollingForTestResultTimeStamp */ override suspend fun doWork(): Result { - - Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") + Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount") if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.d("$id doWork() failed after $runAttemptCount attempts. Rescheduling") + Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling") BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() - Timber.d("$id Rescheduled background worker") + Timber.tag(TAG).d("$id Rescheduled background worker") return Result.failure() } @@ -58,20 +55,20 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( System.currentTimeMillis() ) < BackgroundConstants.POLLING_VALIDITY_MAX_DAYS ) { - Timber.d(" $id maximum days not exceeded") + Timber.tag(TAG).d(" $id maximum days not exceeded") val registrationToken = LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() val testResult = submissionService.asyncRequestTestResult(registrationToken) initiateNotification(testResult) - Timber.d(" $id Test Result Notification Initiated") + Timber.tag(TAG).d(" $id Test Result Notification Initiated") } else { stopWorker() - Timber.d(" $id worker stopped") + Timber.tag(TAG).d(" $id worker stopped") } } catch (e: Exception) { result = Result.retry() } - Timber.d("$id: doWork() finished with %s", result) + Timber.tag(TAG).d("$id: doWork() finished with %s", result) return result } @@ -89,22 +86,20 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( */ private fun initiateNotification(testResult: TestResult) { if (LocalData.isTestResultNotificationSent() || LocalData.submissionWasSuccessful()) { - Timber.d("$id: Notification already sent or there was a successful submission") + Timber.tag(TAG).d("$id: Notification already sent or there was a successful submission") return } - Timber.d("$id: Test Result retried is $testResult") + Timber.tag(TAG).d("$id: Test Result retried is $testResult") if (testResult == TestResult.NEGATIVE || testResult == TestResult.POSITIVE || testResult == TestResult.INVALID ) { - if (!CoronaWarnApplication.isAppInForeground) { - NotificationHelper.sendNotification( - CoronaWarnApplication.getAppContext() - .getString(R.string.notification_name), - CoronaWarnApplication.getAppContext() - .getString(R.string.notification_body) - ) - Timber.d("$id: Test Result available and notification is initiated") - } + NotificationHelper.sendNotificationIfAppIsNotInForeground( + CoronaWarnApplication.getAppContext().getString(R.string.notification_body), + NEW_MESSAGE_TEST_RESULT_NOTIFICATION_ID + ) + NotificationHelper.cancelCurrentNotification(NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID) + + Timber.tag(TAG).d("$id: Test Result available - notification issued & risk level notification canceled") LocalData.isTestResultNotificationSent(true) stopWorker() } @@ -119,9 +114,13 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private fun stopWorker() { LocalData.initialPollingForTestResultTimeStamp(0L) BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() - Timber.d("$id: Background worker stopped") + Timber.tag(TAG).d("$id: Background worker stopped") } @AssistedInject.Factory interface Factory : InjectedWorkerFactory<DiagnosisTestResultRetrievalPeriodicWorker> + + companion object { + private val TAG = DiagnosisTestResultRetrievalPeriodicWorker::class.java.simpleName + } } diff --git a/Corona-Warn-App/src/main/res/color/card_unknown.xml b/Corona-Warn-App/src/main/res/color/card_unknown.xml deleted file mode 100644 index 03c3a4f6dc017e402c3604ff157608314a2744ff..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/color/card_unknown.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@color/colorSemanticNeutralRiskPressed" android:state_pressed="true" /> <!-- pressed --> - <item android:color="@color/colorSemanticNeutralRisk" /> <!-- default --> -</selector> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml b/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml index b2248af9e91c92aa79909b8c7c3aa78d36bee227..a8c3cd2329d000e1b34429d732f4b9ded478baae 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_risk_details.xml @@ -108,6 +108,7 @@ android:background="@drawable/rectangle" android:backgroundTint="@{tracingCard.getRiskInfoContainerBackgroundTint(context)}" android:backgroundTintMode="src_over" + android:elevation="@dimen/spacing_tiny" android:padding="@dimen/spacing_normal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -309,7 +310,7 @@ android:paddingTop="@dimen/spacing_small" android:paddingEnd="@dimen/spacing_normal" android:paddingBottom="@dimen/spacing_small" - gone="@{!tracingDetails.areRiskDetailsButtonsVisible()}" + gone="@{!tracingDetails.isRiskLevelButtonGroupVisible()}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"> diff --git a/Corona-Warn-App/src/main/res/layout/include_risk_card_content.xml b/Corona-Warn-App/src/main/res/layout/include_risk_card_content.xml index 4d3f7ab8344ee894a5054d61f9707791cf14f687..5789d905d90bf2a3c5228c3f37acce081bc5dd46 100644 --- a/Corona-Warn-App/src/main/res/layout/include_risk_card_content.xml +++ b/Corona-Warn-App/src/main/res/layout/include_risk_card_content.xml @@ -35,7 +35,7 @@ android:layout_marginEnd="16dp" android:accessibilityHeading="true" android:text="@{tracingCard.getProgressCardHeadline(context)}" - android:textColor="@color/colorStableLight" + android:textColor="@{tracingCard.getStableTextColor(context)}" app:layout_constraintEnd_toStartOf="@+id/risk_card_progress_headline_icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -49,7 +49,7 @@ android:layout_height="@dimen/icon_size_risk_card" android:importantForAccessibility="no" android:src="@drawable/ic_forward" - android:tint="@color/colorStableLight" + android:tint="@{tracingCard.getStableIconColor(context)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -59,19 +59,20 @@ android:layout_width="36dp" android:layout_height="36dp" android:indeterminate="true" - app:layout_constraintBottom_toBottomOf="@+id/textView2" + android:indeterminateTint="@{tracingCard.getStableIconColor(context)}" + app:layout_constraintBottom_toBottomOf="@+id/risk_card_progress_body" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/textView2" /> + app:layout_constraintTop_toTopOf="@+id/risk_card_progress_body" /> <TextView - android:id="@+id/textView2" + android:id="@+id/risk_card_progress_body" style="@style/subtitle" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginStart="@dimen/spacing_small" android:layout_marginTop="24dp" android:text="@{tracingCard.getProgressCardBody(context)}" - android:textColor="@color/colorStableLight" + android:textColor="@{tracingCard.getStableTextColor(context)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/risk_card_progress_indicator" @@ -117,11 +118,11 @@ <TextView android:id="@+id/risk_card_body" style="@style/subtitle" - gone="@{tracingCard.getRiskBody(context).empty}" + gone="@{tracingCard.getErrorStateBody(context).empty}" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="@{tracingCard.getRiskBody(context)}" + android:text="@{tracingCard.getErrorStateBody(context)}" android:textColor="@{tracingCard.getStableTextColor(context)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -195,7 +196,7 @@ app:layout_constraintBottom_toBottomOf="@+id/risk_card_row_saved_days_body" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/risk_card_row_saved_days_body" - app:progress="@{tracingCard.activeTracingDaysInRetentionPeriod}" + app:progress="@{tracingCard.activeTracingDays}" app:progressColor="@color/colorStableLight" /> <TextView @@ -250,6 +251,8 @@ android:layout_marginTop="@dimen/spacing_normal" android:enabled="@{tracingCard.isUpdateButtonEnabled()}" android:text="@{tracingCard.getUpdateButtonText(context)}" + android:textColor="@{tracingCard.getUpdateButtonTextColor(context)}" + android:backgroundTint="@{tracingCard.getUpdateButtonColor(context)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/risk_card_button_enable_tracing" diff --git a/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior.xml b/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior.xml index cba202a8312a0bb8cc6e49cba966757682ed9ee7..23837f53f696c690000f3d5e3da4ca9bba6044a0 100644 --- a/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior.xml +++ b/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior.xml @@ -22,7 +22,7 @@ app:icon="@{@drawable/ic_risk_details_wash}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:riskLevel="@{tracingDetails.riskLevelScore}" + app:tracingDetails="@{tracingDetails}" app:layout_constraintTop_toTopOf="parent" tools:text="@string/risk_details_behavior_body_wash_hands" /> @@ -36,7 +36,7 @@ app:icon="@{@drawable/ic_risk_details_mask}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:riskLevel="@{tracingDetails.riskLevelScore}" + app:tracingDetails="@{tracingDetails}" app:layout_constraintTop_toBottomOf="@id/risk_details_behavior_wash_hands" tools:text="@string/risk_details_behavior_body_wear_mask" /> @@ -49,7 +49,7 @@ app:body="@{@string/risk_details_behavior_body_stay_away}" app:icon="@{@drawable/ic_risk_details_distance}" app:layout_constraintEnd_toEndOf="parent" - app:riskLevel="@{tracingDetails.riskLevelScore}" + app:tracingDetails="@{tracingDetails}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/risk_details_behavior_wear_mask" tools:text="@string/risk_details_behavior_body_stay_away" /> @@ -63,7 +63,7 @@ app:body="@{@string/risk_details_behavior_body_cough_sneeze}" app:icon="@{@drawable/ic_risk_details_sneeze}" app:layout_constraintEnd_toEndOf="parent" - app:riskLevel="@{tracingDetails.riskLevelScore}" + app:tracingDetails="@{tracingDetails}" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/risk_details_behavior_stay_away" tools:text="@string/risk_details_behavior_body_cough_sneeze" /> diff --git a/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_increased_risk.xml b/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_increased_risk.xml index fec6060cee5b7cd741f892888fe66b4816686237..d0f2db91ba869ddb0fcda9fa4f97f1071d3684d0 100644 --- a/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_increased_risk.xml +++ b/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_increased_risk.xml @@ -24,7 +24,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:riskLevel="@{tracingDetails.riskLevelScore}" /> + app:tracingDetails="@{tracingDetails}" /> <include android:id="@+id/risk_details_behavior_stay_away" @@ -38,7 +38,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/risk_details_behavior_stay_home" - app:riskLevel="@{tracingDetails.riskLevelScore}" /> + app:tracingDetails="@{tracingDetails}" /> <include android:id="@+id/risk_details_behavior_cough_sneeze" @@ -52,7 +52,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/risk_details_behavior_stay_away" - app:riskLevel="@{tracingDetails.riskLevelScore}" /> + app:tracingDetails="@{tracingDetails}" /> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_row.xml b/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_row.xml index c0210b7063d7e9243e73e9c8320bcecd94a84814..a7f76ee4d43424746f01a150efc3170f69cc6ee7 100644 --- a/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_row.xml +++ b/Corona-Warn-App/src/main/res/layout/include_risk_details_behavior_row.xml @@ -3,8 +3,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> <data> - - <import type="de.rki.coronawarnapp.ui.tracing.common.RiskFormatting" /> + <variable + name="tracingDetails" + type="de.rki.coronawarnapp.ui.tracing.details.TracingDetailsState" /> <variable name="body" @@ -14,9 +15,6 @@ name="icon" type="android.graphics.drawable.Drawable" /> - <variable - name="riskLevel" - type="Integer" /> </data> <androidx.constraintlayout.widget.ConstraintLayout @@ -29,7 +27,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/circle" - android:backgroundTint="@{RiskFormatting.formatBehaviorIconBackground(context,riskLevel)}" + android:backgroundTint="@{tracingDetails.getBehaviorIconBackground(context)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -42,7 +40,7 @@ android:focusable="false" android:importantForAccessibility="no" android:src="@{icon}" - android:tint="@{RiskFormatting.formatBehaviorIcon(context,riskLevel)}" + android:tint="@{tracingDetails.getBehaviorIcon(context)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_behaviour_row.xml b/Corona-Warn-App/src/main/res/layout/include_submission_behaviour_row.xml new file mode 100644 index 0000000000000000000000000000000000000000..44914dba944a826e1acdd361dd35c91b544af4c5 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_submission_behaviour_row.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <data> + <variable + name="body" + type="String" /> + + <variable + name="icon" + type="android.graphics.drawable.Drawable" /> + </data> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/risk_details_behavior_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + style="@style/icon" + android:layout_width="@dimen/icon_size_risk_details_behavior" + android:layout_height="@dimen/icon_size_risk_details_behavior" + android:layout_margin="@dimen/icon_margin_risk_details_behavior" + android:focusable="false" + android:importantForAccessibility="no" + android:src="@{icon}" + android:tint="@color/colorStableLight" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_small" + android:text="@{body}" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/risk_details_behavior_icon" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_done_content.xml b/Corona-Warn-App/src/main/res/layout/include_submission_done_content.xml index b66da1ae5a9c4565de443fa549fbb913fe04c672..dbe51a20c99baba7cf484a2041fe4681cf5d2c35 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_done_content.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_done_content.xml @@ -3,9 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> <data> - - <import type="de.rki.coronawarnapp.risk.RiskLevelConstants" /> - <variable name="illustrationDescription" type="String" /> @@ -42,7 +39,7 @@ <include android:id="@+id/submission_done_contagious" - layout="@layout/include_risk_details_behavior_row" + layout="@layout/include_submission_behaviour_row" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" @@ -50,12 +47,11 @@ app:icon="@{@drawable/ic_risk_details_contact}" app:layout_constraintEnd_toEndOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/submission_done_subtitle" - app:riskLevel="@{RiskLevelConstants.INCREASED_RISK}" /> + app:layout_constraintTop_toBottomOf="@+id/submission_done_subtitle" /> <include android:id="@+id/submission_done_isolate" - layout="@layout/include_risk_details_behavior_row" + layout="@layout/include_submission_behaviour_row" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" @@ -63,8 +59,7 @@ app:icon="@{@drawable/ic_submission_home}" app:layout_constraintEnd_toEndOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/submission_done_contagious" - app:riskLevel="@{RiskLevelConstants.INCREASED_RISK}" /> + app:layout_constraintTop_toBottomOf="@+id/submission_done_contagious" /> <include layout="@layout/include_submission_done_further_info" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_status_card_positive.xml b/Corona-Warn-App/src/main/res/layout/include_submission_status_card_positive.xml index ae7b4d88b48711a0aeacd0abf949283e238f6153..e10fd40921c3cf0fb810bedd9bc9a85aa855cb5c 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_status_card_positive.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_status_card_positive.xml @@ -2,10 +2,6 @@ <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> - <data> - <import type="de.rki.coronawarnapp.risk.RiskLevelConstants" /> - </data> - <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/submission_status_card_positive" style="@style/card" @@ -69,7 +65,7 @@ <include android:id="@+id/submission_status_card_positive_result_contact" - layout="@layout/include_risk_details_behavior_row" + layout="@layout/include_submission_behaviour_row" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" @@ -77,12 +73,11 @@ app:icon="@{@drawable/ic_risk_details_contact}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_subtitle" - app:riskLevel="@{RiskLevelConstants.INCREASED_RISK}" /> + app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_subtitle" /> <include android:id="@+id/submission_status_card_positive_result_contagious" - layout="@layout/include_risk_details_behavior_row" + layout="@layout/include_submission_behaviour_row" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" @@ -90,12 +85,11 @@ app:icon="@{@drawable/ic_submission_home}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_contact" - app:riskLevel="@{RiskLevelConstants.INCREASED_RISK}" /> + app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_contact" /> <include android:id="@+id/submission_status_card_positive_result_share" - layout="@layout/include_risk_details_behavior_row" + layout="@layout/include_submission_behaviour_row" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" @@ -103,8 +97,7 @@ app:icon="@{@drawable/ic_submission_share}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_contagious" - app:riskLevel="@{RiskLevelConstants.INCREASED_RISK}" /> + app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_contagious" /> <Button android:id="@+id/submission_status_card_positive_button" diff --git a/Corona-Warn-App/src/main/res/values-bg/strings.xml b/Corona-Warn-App/src/main/res/values-bg/strings.xml index 6a1be54319c193dc1927124d7268036aaf732ecd..972e4ef924f96fbb94835205aaab9b4905dc7c55 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -86,7 +80,7 @@ <!-- XMIT: application overview --> <string name="menu_help">"Общ преглед"</string> <!-- XMIT: application information --> - <string name="menu_information">"Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението"</string> + <string name="menu_information">"За приложението"</string> <!-- XMIT: application settings --> <string name="menu_settings">"ÐаÑтройки"</string> @@ -125,28 +119,8 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"До момента нÑма излагане на риÑк"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s излагане на ниÑък риÑк"</item> - <item quantity="other">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - <item quantity="zero">"До момента нÑма излагане на ниÑък риÑк"</item> - <item quantity="two">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - <item quantity="few">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - <item quantity="many">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s излагане на риÑк"</item> - <item quantity="other">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - <item quantity="zero">"До момента нÑма излагане на риÑк"</item> - <item quantity="two">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - <item quantity="few">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - <item quantity="many">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> - <string name="risk_card_body_saved_days">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк беше активно през %1$s от изминалите 14 дни."</string> + <string name="risk_card_body_saved_days">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е било активно през %1$s от изминалите 14 дни."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"ÐÑма прекъÑване на региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT; risk card - no update done yet --> @@ -157,8 +131,6 @@ <string name="risk_card_body_open_daily">"Бележка: МолÑ, отварÑйте приложението вÑеки ден, за да актуализирате ÑÐ²Ð¾Ñ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Ðктуализиране"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"Ðктуализиране Ñлед %1$s"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"Ðктивиране на региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -167,17 +139,6 @@ <string name="risk_card_low_risk_headline">"ÐиÑък риÑк"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Повишен риÑк"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s ден от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="other">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="zero">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="two">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="few">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="many">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"ÐеизвеÑтен риÑк"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Тъй като региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк не е било активно доÑтатъчно дълго, не можем да изчиÑлим Ð’Ð°ÑˆÐ¸Ñ Ñ€Ð¸Ñк от заразÑване."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> @@ -191,12 +152,38 @@ <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"ВашиÑÑ‚ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк не е обновÑван от повече от 48 чаÑа. МолÑ, актуализирайте го."</string> <!-- XHED: risk card - risk check failed headline, no internet connection --> - <string name="risk_card_check_failed_no_internet_headline">"Проверката за излагане на риÑк е неуÑпешна"</string> + <string name="risk_card_check_failed_no_internet_headline">"Проверката за излагане е риÑк е неуÑпешна"</string> <!-- XTXT: risk card - risk check failed, please check your internet connection --> <string name="risk_card_check_failed_no_internet_body">"СинхронизациÑта на Ñлучайни ИД ÑÑŠÑ Ñървъра е неуÑпешна. Можете да Ñ Ñ€ÐµÑтартирате ръчно."</string> <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"РеÑтартиране"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">"До момента нÑма излагане на риÑк"</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð½Ð¸Ñък риÑк - %1$d"</item> + <item quantity="other">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð½Ð¸Ñък риÑк - %1$d"</item> + <item quantity="zero">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð½Ð¸Ñък риÑк - %1$d"</item> + <item quantity="two">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð½Ð¸Ñък риÑк - %1$d"</item> + <item quantity="few">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð½Ð¸Ñък риÑк - %1$d"</item> + <item quantity="many">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð½Ð¸Ñък риÑк - %1$d"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">"До момента нÑма излагане на риÑк"</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð¿Ð¾Ð²Ð¸ÑˆÐµÐ½ риÑк - %1$d"</item> + <item quantity="other">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð¿Ð¾Ð²Ð¸ÑˆÐµÐ½ риÑк - %1$d"</item> + <item quantity="zero">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð¿Ð¾Ð²Ð¸ÑˆÐµÐ½ риÑк - %1$d"</item> + <item quantity="two">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð¿Ð¾Ð²Ð¸ÑˆÐµÐ½ риÑк - %1$d"</item> + <item quantity="few">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð¿Ð¾Ð²Ð¸ÑˆÐµÐ½ риÑк - %1$d"</item> + <item quantity="many">"Брой дни Ñ Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ñ Ð¿Ð¾Ð²Ð¸ÑˆÐµÐ½ риÑк - %1$d"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"Ðай-близка дата - %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -238,7 +225,7 @@ <!-- YMSG: Message when sharing is executed --> <string name="main_share_message">"Да Ñе преборим Ñ ÐºÐ¾Ñ€Ð¾Ð½Ð°Ð²Ð¸Ñ€ÑƒÑа заедно"<xliff:g id="line_break">"\n"</xliff:g>"Ðз ще учаÑтвам, а ти?"<xliff:g id="line_break">"\n"</xliff:g><xliff:g id="link_play_store">"https://www.corona-warn-app.de"</xliff:g>"\n"<xliff:g id="line_break">"\n"</xliff:g></string> <!-- XACT: main (share) - illustraction description, explanation image --> - <string name="main_share_illustration_description">"Човек ÑÐ¿Ð¾Ð´ÐµÐ»Ñ Corona-Warn-App Ñ Ñ‡ÐµÑ‚Ð¸Ñ€Ð¸Ð¼Ð° души."</string> + <string name="main_share_illustration_description">"Човек ÑÐ¿Ð¾Ð´ÐµÐ»Ñ Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸ÐµÑ‚Ð¾ Corona-Warn-App Ñ Ñ‡ÐµÑ‚Ð¸Ñ€Ð¸Ð¼Ð° души."</string> <!-- #################################### Main - Overview @@ -249,9 +236,9 @@ <!-- XACT: main overview page title --> <string name="main_overview_accessibility_title">"Общ преглед"</string> <!-- XHED: App overview subtitle for tracing explanation--> - <string name="main_overview_subtitle_tracing">"РегиÑтър на риÑковете"</string> + <string name="main_overview_subtitle_tracing">"РегиÑтриране на излаганиÑта на риÑк"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е една от трите оÑновни функции на приложението. Когато е активирана, вÑички оÑъщеÑтвени контакти между Ñмартфоните Ñе запиÑват, без да е необходимо да правите друго."</string> + <string name="main_overview_body_tracing">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е една от трите оÑновни функции на приложението. Когато е активирана, вÑички оÑъщеÑтвени контакти между Ñмартфони Ñе запиÑват, без да е необходимо да правите друго."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"РиÑк от заразÑване"</string> <!-- YTXT: App overview body text about risk levels --> @@ -277,11 +264,11 @@ <!-- XHED: App overview subtitle for glossary risk calculation --> <string name="main_overview_subtitle_glossary_calculation">"Проверка за излагане на риÑк"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Данните от региÑтъра на излаганиÑта на риÑк Ñе извличат и Ñинхронизират Ñ Ñ€ÐµÐ³Ð¸Ñтрираните Ñлучаи на заразÑване на други потребители. Проверката за излагане на риÑк Ñе извършва автоматично на вÑеки два чаÑа."</string> + <string name="main_overview_body_glossary_calculation">"Данните от региÑтъра на излаганиÑта на риÑк Ñе извличат и Ñинхронизират Ñ Ñ€ÐµÐ³Ð¸Ñтрираните Ñлучаи на заразÑване на други потребители. Проверката за излагане на риÑк Ñе извършва автоматично нÑколко пъти на ден."</string> <!-- XHED: App overview subtitle for glossary contact --> - <string name="main_overview_subtitle_glossary_contact">"Ð˜Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> + <string name="main_overview_subtitle_glossary_contact">"Излагане на риÑк"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"Контакти Ñ Ð¿Ð¾-голÑма продължителноÑÑ‚ и близоÑÑ‚ до лица, диагноÑтицирани Ñ COVID-19."</string> + <string name="main_overview_body_glossary_contact">"Контакт ÑÑŠÑ Ð·Ð°Ñ€Ð°Ð·ÐµÐ½Ð¾ лице, което е Ñподелило положителен резултат от теÑÑ‚ в приложението. За да бъде клаÑифицирано като излагане Ñ Ð²Ð¸Ñока Ñтепен на риÑк, то трÑбва да Ð¾Ñ‚Ð³Ð¾Ð²Ð°Ñ€Ñ Ð½Ð° определени критерии по отношение на продължителноÑÑ‚, разÑтоÑние и предполагаема заразноÑÑ‚ на другото лице."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"ИзвеÑÑ‚Ð¸Ñ Ð·Ð° излагане на риÑк"</string> <!-- YTXT: App overview body for glossary notifications --> @@ -338,13 +325,11 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged">"ВашиÑÑ‚ риÑк от заразÑване може да Ñе изчиÑли Ñамо за периодите, в които региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е било активно. Затова тази Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ñ‚Ñ€Ñбва да бъде активирана поÑтоÑнно."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> - <string name="risk_details_information_body_period_logged_assessment">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк покрива поÑледните 14 дни. За този период от време функциÑта е била активна на Вашето уÑтройÑтво в продължение на %1$s дни. Приложението изтрива автоматично по-Ñтарите региÑтри, тъй като те вече не могат да Ñлужат за предотвратÑване на заразÑването."</string> + <string name="risk_details_information_body_period_logged_assessment">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк покрива поÑледните 14 дни. За този период от време функциÑта е била активна на Ð’Ð°ÑˆÐ¸Ñ Ñмартфон в продължение на %1$s дни. Приложението изтрива автоматично по-Ñтарите региÑтри, тъй като те вече не могат да Ñлужат за предотвратÑване на заразÑването."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk_past">"Ето как е изчиÑлено Вашето ниво на риÑк"</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"Ето как Ñе изчиÑлÑва Вашето ниво на риÑк"</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Тъй като региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк не е било активно доÑтатъчно дълго, не можем да изчиÑлим Ð’Ð°ÑˆÐ¸Ñ Ñ€Ð¸Ñк от заразÑване."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"РегиÑтърът на излаганиÑта на риÑк не е обновÑван повече от 24 чаÑа."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -375,7 +360,7 @@ <string name="risk_details_explanation_dialog_title">"Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾Ñ‚Ð½Ð¾Ñно функционалноÑтта за региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- YTXT: one time risk explanation dialog - pointing to the faq page for more information--> <string name="risk_details_explanation_dialog_faq_body">"За повече Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð²Ð¸Ð¶Ñ‚Ðµ Ñтраницата „ЧЗВ“."</string> - <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> + <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> <string name="risk_details_explanation_faq_link">"https://www.coronawarn.app/en/faq/#encounter_but_green"</string> <!-- XHED: risk details - deadman notification title --> @@ -442,7 +427,7 @@ <!-- YTXT: onboarding(tracing) - explain tracing --> <string name="onboarding_tracing_body_emphasized">"Криптираните Ñлучайни идентификатори предават Ñамо Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° дата, продължителноÑÑ‚ и близоÑÑ‚ на контакта (изчиÑлена от Ñилата на Ñигнала). СамоличноÑтта Ви не може да бъде уÑтановена по Ñлучайните ИД."</string> <!-- YTXT: onboarding(tracing) - easy language explain tracing link--> - <string name="onboarding_tracing_easy_language_explanation"><a href="https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache">"Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението на опроÑтен и жеÑтомимичен език"</a></string> + <string name="onboarding_tracing_easy_language_explanation"><a href="https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache">"Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението на опроÑтен и жеÑтомимичен език."</a></string> <!-- NOTR: onboarding(tracing) - easy language explain tracing link URL--> <string name="onboarding_tracing_easy_language_explanation_url">"https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache"</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> @@ -460,13 +445,13 @@ <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"Дезактивирали Ñте фоновите актуализации за приложението Corona-Warn-App. МолÑ, активирайте ги, за да използвате автоматичното региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк. Ðко не го направите, региÑтрирането на излаганиÑта може да бъде Ñтартирано Ñамо ръчно от приложението. Може да активирате фоновите актуализации за приложението от наÑтройките на Вашето уÑтройÑтво."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> - <string name="onboarding_background_fetch_dialog_button_positive">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="onboarding_background_fetch_dialog_button_positive">"Към наÑтройките на уÑтройÑтвото"</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, continue in app --> <string name="onboarding_background_fetch_dialog_button_negative">"Ръчно Ñтартиране на региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XACT: onboarding(tracing) - dialog about energy optimized header text --> <string name="onboarding_energy_optimized_dialog_headline">"Разрешаване на приоритетната работа във фонов режим"</string> <!-- YMSI: onboarding(tracing) - dialog about energy optimized --> - <string name="onboarding_energy_optimized_dialog_body">"Ðктивирайте приоритетната работа във фонов режим, за да позволите на приложението да Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»Ñ Ñ€Ð¸Ñковете, на които Ñте изложени, по вÑÑко време като работи на заден план (препоръчително). Това ще изключи оптимизациÑта на потребление на Ð±Ð°Ñ‚ÐµÑ€Ð¸Ñ Ñамо за приложението Corona-Warn-App. Ðе очакваме това да доведе до значително по-бързо изтощаване на батериÑта на вашето уÑтройÑтво.\n\nÐко не разрешите тази наÑтройка, препоръчваме да отварÑте приложението ръчно поне веднъж на вÑеки 24 чаÑа."</string> + <string name="onboarding_energy_optimized_dialog_body">"Ðктивирайте приоритетната работа във фонов режим, за да позволите на приложението да Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»Ñ Ñ€Ð¸Ñковете, на които Ñте изложени, по вÑÑко време като работи на заден план (препоръчително). Това ще изключи оптимизациÑта на потребление на Ð±Ð°Ñ‚ÐµÑ€Ð¸Ñ Ñамо за приложението Corona-Warn-App. Ðе очакваме това да доведе до значително по-бързо изтощаване на батериÑта на Ð²Ð°ÑˆÐ¸Ñ Ñмартфон.\n\nÐко не разрешите тази наÑтройка, препоръчваме да отварÑте приложението ръчно поне веднъж на вÑеки 24 чаÑа."</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, open device settings --> <string name="onboarding_energy_optimized_dialog_button_positive">"Разрешавам"</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, continue in app --> @@ -482,17 +467,17 @@ <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Разрешаване на доÑтъп до данните за меÑтоположение"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> - <string name="onboarding_tracing_location_body">"Ðе може да бъде оÑъщеÑтвен доÑтъп до Вашето меÑтоположение. За да използвате Bluetooth, Google и/или Android изиÑкват от Ð’Ð°Ñ Ð´Ð° предоÑтавите доÑтъп до меÑтоположението на уÑтройÑтвото Ñи."</string> + <string name="onboarding_tracing_location_body">"Ðе може да бъде оÑъщеÑтвен доÑтъп до Вашето меÑтоположение. За да използвате Bluetooth, Google и/или Android изиÑкват от Ð’Ð°Ñ Ð´Ð° предоÑтавите доÑтъп до меÑтоположението на Ñмартфона Ñи."</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> - <string name="onboarding_tracing_location_button">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="onboarding_tracing_location_button">"Към наÑтройките на уÑтройÑтвото"</string> <!-- XACT: Onboarding (test) page title --> - <string name="onboarding_test_accessibility_title">"Въведение - Ñтраница 5 от 6: Ðко имате поÑтавена диагноза COVID-19..."</string> + <string name="onboarding_test_accessibility_title">"Въведение - Ñтраница 5 от 6: Ðко имате поÑтавена диагноза COVID-19"</string> <!-- XHED: onboarding(test) - about positive tests --> <string name="onboarding_test_headline">"Ðко имате поÑтавена диагноза COVID-19,..."</string> <!-- XHED: onboarding(test) - two/three line headline under an illustration --> <string name="onboarding_test_subtitle">"... Ð¼Ð¾Ð»Ñ Ñъобщете за това в приложението Corona-Warn-App. СподелÑнето на резултатите от Вашите теÑтове е доброволно и безопаÑно. Ðаправете го в името на общото здраве."</string> <!-- YTXT: onboarding(test) - explain test --> - <string name="onboarding_test_body">"Вашето извеÑтие Ñе криптира Ñ Ð²Ð¸Ñока Ñтепен на ÑигурноÑÑ‚ и Ñе обработва на защитен Ñървър. Лицата, чиито криптирани Ñлучайни ИД кодове Ñа запазени на Вашето уÑтройÑтво, ще получат предупреждение, както и Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾Ñ‚Ð½Ð¾Ñно това, което трÑбва да направÑÑ‚."</string> + <string name="onboarding_test_body">"Вашето извеÑтие Ñе криптира Ñ Ð²Ð¸Ñока Ñтепен на ÑигурноÑÑ‚ и Ñе обработва на защитен Ñървър. Лицата, чиито криптирани Ñлучайни ИД кодове Ñа запазени на Ð’Ð°ÑˆÐ¸Ñ Ñмартфон, ще получат предупреждение, както и Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾Ñ‚Ð½Ð¾Ñно това, което трÑбва да направÑÑ‚."</string> <!-- XACT: onboarding(test) - illustraction description, header image --> <string name="onboarding_test_illustration_description">"КриптираниÑÑ‚ положителен резултат от теÑÑ‚ Ñе изпраща в ÑиÑтемата, за да бъдат предупредени оÑтаналите потребители."</string> <!-- XACT: Onboarding (datashare) page title --> @@ -528,7 +513,7 @@ <!-- XTXT: settings - off, like a label next to a setting --> <string name="settings_off">"Изключено"</string> <!-- XHED: settings(tracing) - page title --> - <string name="settings_tracing_title">"РегиÑтър на риÑковете"</string> + <string name="settings_tracing_title">"РегиÑтриране на излаганиÑта на риÑк"</string> <!-- XHED: settings(tracing) - headline bellow illustration --> <string name="settings_tracing_headline">"Как работи региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT: settings(tracing) - explain text in settings overview under headline --> @@ -556,7 +541,7 @@ <!-- XTXT: settings(tracing) - explains user what to do on card if bluetooth is disabled --> <string name="settings_tracing_status_bluetooth_body">"ФункциÑта Bluetooth трÑбва да бъде включена, за да могат да Ñе региÑтрират Ñлучаите на излагане на риÑк. МолÑ, включете Ñ Ð¾Ñ‚ наÑтройките на Вашето уÑтройÑтво."</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card --> - <string name="settings_tracing_status_bluetooth_button">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="settings_tracing_status_bluetooth_button">"Към наÑтройките на уÑтройÑтвото"</string> <!--XHED : settings(tracing) - headline on card about the current status and what to do --> <string name="settings_tracing_status_location_headline">"Разрешаване на доÑтъп до данните за меÑтоположение"</string> <!-- XTXT: settings(tracing) - explains user what to do on card if location is disabled --> @@ -564,13 +549,13 @@ <!-- XTXT: settings(tracing) - explains user what to do on card if location is disabled: URL --> <string name="settings_tracing_status_location_body_url">"https://www.coronawarn.app/en/faq/#android_location"</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card - location --> - <string name="settings_tracing_status_location_button">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="settings_tracing_status_location_button">"Към наÑтройките на уÑтройÑтвото"</string> <!--XHED : settings(tracing) - headline on card about the current status and what to do --> <string name="settings_tracing_status_connection_headline">"Ðеобходима е връзка Ñ Ð¸Ð½Ñ‚ÐµÑ€Ð½ÐµÑ‚"</string> <!-- XTXT: settings(tracing) - explains user what to do on card if connection is disabled --> <string name="settings_tracing_status_connection_body">"Ðеобходима е връзка Ñ Ð¸Ð½Ñ‚ÐµÑ€Ð½ÐµÑ‚ за изчиÑлÑване на излаганиÑта на риÑк. МолÑ, Ñвържете Ñе Ñ Wi-Fi или мобилна мрежа за данни от наÑтройките на уÑтройÑтвото Ñи."</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card --> - <string name="settings_tracing_status_connection_button">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="settings_tracing_status_connection_button">"Към наÑтройките на уÑтройÑтвото"</string> <!-- XTXT: settings(tracing) - explains the circle progress indicator to the right with the current value --> <plurals name="settings_tracing_status_body_active"> <item quantity="one">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е активно от един ден. Проверката за излагане на риÑк може да бъде надеждна Ñамо ако проÑледÑването е поÑтоÑнно активирано."</item> @@ -610,9 +595,9 @@ <!-- XTXT: settings(notification) - next to a switch --> <string name="settings_notifications_subtitle_update_risk">"ПромÑна на Ð’Ð°ÑˆÐ¸Ñ Ñ€Ð¸Ñк от заразÑване"</string> <!-- XTXT: settings(notification) - next to a switch --> - <string name="settings_notifications_subtitle_update_test">"Ðаличие на резултат от Ваш теÑÑ‚ за COVID-19"</string> + <string name="settings_notifications_subtitle_update_test">"Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð½Ð° Ð’Ð°ÑˆÐ¸Ñ Ñ‚ÐµÑÑ‚ за COVID-19"</string> <!-- XBUT: settings(notification) - go to operating settings --> - <string name="settings_notifications_button_open_settings">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="settings_notifications_button_open_settings">"Към наÑтройките на уÑтройÑтвото"</string> <!-- XACT: main (overview) - illustraction description, explanation image, displays notificatin status, active --> <string name="settings_notifications_illustration_description_active">"Жена, коÑто получава извеÑтие от приложението Corona-Warn-App."</string> <!-- XACT: main (overview) - illustraction description, explanation image, displays notificatin status, inactive --> @@ -675,7 +660,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"ИнÑтитутът „Роберт Кох“ (RKI) е федералната Ñлужба за общеÑтвено здравеопазване в ГерманиÑ. Той е издател на приложението Corona-Warn-App по поръчка на федералното правителÑтво. Приложението е предназначено да бъде дигитално допълнение на вече въведените мерки за опазване на общеÑтвеното здраве: Ñоциално диÑтанциране, поддържане на виÑока хигиена и ноÑене на маÑки."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Ð’Ñеки, който използва приложението, помага за проÑледÑване и прекъÑване на веригите на заразÑване. Приложението запазва във Ð’Ð°ÑˆÐ¸Ñ Ñмартфон данните за контактите Ви Ñ Ð´Ñ€ÑƒÐ³Ð¸ хора. Получавате извеÑтие, ако Ñте били в контакт Ñ Ð»Ð¸Ñ†Ð°, които впоÑледÑтвие Ñа били диагноÑтицирани Ñ COVID-19. Вашата ÑамоличноÑÑ‚ и неприкоÑновеноÑтта на данните Ви Ñа защитени по вÑÑко време."</string> + <string name="information_about_body">"Хората, които използват приложението, помагат за проÑледÑване и прекъÑване на веригите на заразÑване. Приложението запазва във Вашето уÑтройÑтво данните за контактите Ви Ñ Ð´Ñ€ÑƒÐ³Ð¸ хора. Получавате извеÑтие, ако Ñте били в контакт Ñ Ð»Ð¸Ñ†Ð°, които впоÑледÑтвие Ñа били диагноÑтицирани Ñ COVID-19. Вашата ÑамоличноÑÑ‚ и неприкоÑновеноÑтта на данните Ви Ñа защитени по вÑÑко време."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Група лица използват Ñмартфоните Ñи, придвижвайки Ñе из града."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -745,7 +730,7 @@ <!-- YTXT: subtitle for legal information page, tax section --> <string name="information_legal_subtitle_taxid">"DE 165 893 430"</string> <!-- XACT: describes illustration --> - <string name="information_legal_illustration_description">"Ръка държи Ñмартфон, на чийто екран Ñе вижда голÑмо количеÑтво текÑÑ‚, а до Ð½ÐµÑ Ð¸Ð¼Ð° изображение на везна, коÑто Ñимволизира правната информациÑ."</string> + <string name="information_legal_illustration_description">"Ръка държи Ñмартфон, на чийто екран Ñе вижда голÑмо количеÑтво текÑÑ‚, а до Ð½ÐµÑ Ñе вижда знакът за раздел, Ñимволизиращ правната информациÑ."</string> <!-- #################################### Interoperability @@ -856,13 +841,13 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Ðктуализиране"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Изтриване на теÑта"</string> + <string name="submission_test_result_pending_remove_test_button">"Изтриване на теÑÑ‚"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Резултатът от Ð’Ð°ÑˆÐ¸Ñ Ñ‚ÐµÑÑ‚"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"ВашиÑÑ‚ лабораторен резултат не потвърждава заразÑване Ñ ÐºÐ¾Ñ€Ð¾Ð½Ð°Ð²Ð¸Ñ€ÑƒÑ SARS-CoV-2.\n\nМолÑ, изтрийте теÑта от приложението Corona-Warn-App, за да можете да запазите нов код на теÑÑ‚, ако е необходимо."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Изтриване на теÑта"</string> + <string name="submission_test_result_negative_remove_test_button">"Изтриване на теÑÑ‚"</string> <!-- XHED: Page headline for other warnings screen --> <string name="submission_test_result_positive_steps_warning_others_heading">"Предупредете другите"</string> <!-- YTXT: Body text for for other warnings screen--> @@ -925,9 +910,9 @@ <string name="submission_intro_illustration_description">"КриптираниÑÑ‚ положителен резултат от теÑÑ‚ Ñе изпраща в ÑиÑтемата, за да бъдат предупредени оÑтаналите потребители."</string> <!-- YTXT: submission introduction bullet points --> <string-array name="submission_intro_bullet_points"> - <item>"Ðко в документа за Ð’Ð°ÑˆÐ¸Ñ Ñ‚ÐµÑÑ‚ има QR код, можете да го Ñканирате и да региÑтрирате теÑта Ñи. Ð’ момента, в който резултатът бъде готов, ще можете да го видите в приложението."</item> + <item>"Ðко в документа Ñ Ñ€ÐµÐ·ÑƒÐ»Ñ‚Ð°Ñ‚Ð° от теÑта Ви има QR код, можете да го Ñканирате и да региÑтрирате теÑта Ñи. Ð’ момента, в който резултатът бъде готов, ще можете да го видите в приложението."</item> <item>"Ðко Ви е поÑтавена диагноза COVID-19, можете да предупредите оÑтаналите потребители."</item> - <item>"Ðко разполагате Ñ Ð¢ÐРкод за положителен резултат, може да го използвате, за да региÑтрирате теÑта."</item> + <item>"Ðко Ви е предоÑтавен ТÐРкод за положителен резултат, може да го използвате, за да региÑтрирате теÑта."</item> <item>"Ðко не разполагате Ñ Ð¢ÐРкод, по телефона може да заÑвите да Ви бъде предоÑтавен такъв."</item> </string-array> <!-- XACT: Submission Intro page title --> @@ -969,7 +954,7 @@ <!-- XACT: other warning - illustration description, explanation image --> <string name="submission_positive_other_illustration_description">"Смартфонът предава на ÑиÑтемата Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° положителен резултат от теÑÑ‚."</string> <!-- XHED: Title for the interop country list--> - <string name="submission_interoperability_list_title">"Ð’ момента в международното региÑтриране на излаганиÑта учаÑтват Ñледните държави:"</string> + <string name="submission_interoperability_list_title">"Ð’ момента в международното региÑтриране на излаганиÑта на риÑк от заразÑване учаÑтват Ñледните държави:"</string> <!-- Submission Country Selector --> <!-- XHED: Page title for the submission country selection page --> @@ -1048,7 +1033,7 @@ <!-- XBUT: submission contact call button --> <string name="submission_contact_button_call">"Обаждане"</string> <!-- XBUT: submission contact enter tan button --> - <string name="submission_contact_button_enter">"Въвеждане на ТÐÐ"</string> + <string name="submission_contact_button_enter">"Въведете ТÐРкод"</string> <!-- YTXT: Body text for step 1 of contact page --> <string name="submission_contact_step_1_body">"Обадете Ñе на горещата Ð»Ð¸Ð½Ð¸Ñ Ð¸ поиÑкайте ТÐРкод:"</string> <!-- XLNK: Button / hyperlink to phone call for TAN contact page --> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index 0e55aec18a8e72742e9b38c1a665df528ec6d6dc..b9b156ba817e2a87ba22428f273226805a0d29b8 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -35,12 +35,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -126,26 +120,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Bisher keine Risiko-Begegnungen"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s Begegnung mit niedrigem Risiko"</item> - <item quantity="other">"%1$s Begegnungen mit niedrigem Risiko"</item> - <item quantity="zero">"Bisher keine Begegnungen mit niedrigem Risiko"</item> - <item quantity="two">"%1$s Begegnungen mit niedrigem Risiko"</item> - <item quantity="few">"%1$s Begegnungen mit niedrigem Risiko"</item> - <item quantity="many">"%1$s Begegnungen mit niedrigem Risiko"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s Risiko-Begegnung"</item> - <item quantity="other">"%1$s Risiko-Begegnungen"</item> - <item quantity="zero">"Bisher keine Risiko-Begegnungen"</item> - <item quantity="two">"%1$s Risiko-Begegnungen"</item> - <item quantity="few">"%1$s Risiko-Begegnungen"</item> - <item quantity="many">"%1$s Risiko-Begegnungen"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Risiko-Ermittlung war für %1$s der letzten 14 Tage aktiv"</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -158,8 +132,6 @@ <string name="risk_card_body_open_daily">"Hinweis: Bitte öffnen Sie die App täglich, um den Risikostatus zu aktualisieren."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Aktualisieren"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"Aktualisierung in %1$s"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"Risiko-Ermittlung einschalten"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -168,17 +140,6 @@ <string name="risk_card_low_risk_headline">"Niedriges Risiko"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Erhöhtes Risiko"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s Tag seit der letzten Begegnung"</item> - <item quantity="other">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="zero">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="two">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="few">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="many">"%1$s Tage seit der letzten Begegnung"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"Unbekanntes Risiko"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Da Sie die Risiko-Ermittlung noch nicht lange genug aktiviert haben, konnten wir für Sie kein Infektionsrisiko berechnen."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> @@ -198,6 +159,32 @@ <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"Erneut starten"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">Keine Risiko-Begegnungen</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"Begegnungen mit niedrigem Risiko an %1$d Tag"</item> + <item quantity="other">"Begegnungen mit niedrigem Risiko an %1$d Tagen"</item> + <item quantity="zero">"Begegnungen mit niedrigem Risiko an %1$d Tagen"</item> + <item quantity="two">"Begegnungen mit niedrigem Risiko an %1$d Tagen"</item> + <item quantity="few">"Begegnungen mit niedrigem Risiko an %1$d Tagen"</item> + <item quantity="many">"Begegnungen mit niedrigem Risiko an %1$d Tagen"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">Keine Risiko-Begegnungen</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"Begegnungen an %1$d Tag mit erhöhtem Risiko"</item> + <item quantity="other">"Begegnungen an %1$d Tagen mit erhöhtem Risiko"</item> + <item quantity="zero">"Begegnungen an %1$d Tagen mit erhöhtem Risiko"</item> + <item quantity="two">"Begegnungen an %1$d Tagen mit erhöhtem Risiko"</item> + <item quantity="few">"Begegnungen an %1$d Tagen mit erhöhtem Risiko"</item> + <item quantity="many">"Begegnungen an %1$d Tagen mit erhöhtem Risiko"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"Zuletzt am %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -344,8 +331,6 @@ <string name="risk_details_subtitle_infection_risk_past">"So wurde Ihr Risiko ermittelt."</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"So wird Ihr Risiko ermittelt."</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Da Sie die Risiko-Ermittlung noch nicht lange genug aktiviert haben, konnten wir für Sie kein Infektionsrisiko berechnen."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"Ihre Risiko-Ermittlung konnte seit mehr als 24 Stunden nicht aktualisiert werden."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -376,7 +361,7 @@ <string name="risk_details_explanation_dialog_title">"Information zur Funktionsweise der Risiko-Ermittlung"</string> <!-- YTXT: one time risk explanation dialog - pointing to the faq page for more information--> <string name="risk_details_explanation_dialog_faq_body">"Weitere Informationen finden Sie in den FAQ."</string> - <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> + <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> <string name="risk_details_explanation_faq_link">"https://www.coronawarn.app/de/faq/#encounter_but_green"</string> <!-- XHED: risk details - deadman notification title --> @@ -420,9 +405,9 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"Mehr Schutz für Sie und uns alle. Mit der Corona-Warn-App durchbrechen wir Infektionsketten schneller."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"Machen Sie Ihr Smartphone zum Corona-Warn-System. Überblicken Sie Ihren Risikostatus und erfahren Sie, ob in den letzten 14 Tagen Corona-positiv getestete Personen in ihrer Nähe waren."</string> + <string name="onboarding_body">"Machen Sie Ihr Smartphone zum Corona-Warn-System. Überblicken Sie Ihren Risikostatus und erfahren Sie, ob in den letzten 14 Tagen Corona-positiv getestete Personen in Ihrer Nähe waren."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"Die App merkt sich Begegnungen zwischen Menschen, indem ihre Smartphones verschlüsselte Zufalls-IDs austauschen. Und zwar ohne dabei auf persönliche Daten zuzugreifen."</string> + <string name="onboarding_body_emphasized">"Die App merkt sich Begegnungen zwischen Menschen, indem ihre Smartphones verschlüsselte Zufalls-IDs austauschen. Persönliche Daten werden dabei nicht ausgetauscht."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"Eine vielfältige Gruppe in einer Stadt benutzt Smartphones."</string> <!-- XACT: Onboarding (privacy) page title --> diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml index 1b5a8d634f8186e447677db303334ef3e98ace06..a128b4af64dfc870cd5afcfc0b98d73d37af2ebe 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"No exposure up to now"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s exposure with low risk"</item> - <item quantity="other">"%1$s exposures with low risk"</item> - <item quantity="zero">"No exposure with low risk so far"</item> - <item quantity="two">"%1$s exposures with low risk"</item> - <item quantity="few">"%1$s exposures with low risk"</item> - <item quantity="many">"%1$s exposures with low risk"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s exposure"</item> - <item quantity="other">"%1$s exposures"</item> - <item quantity="zero">"No exposure up to now"</item> - <item quantity="two">"%1$s exposures"</item> - <item quantity="few">"%1$s exposures"</item> - <item quantity="many">"%1$s exposures"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Exposure logging was active for %1$s of the past 14 days."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -157,8 +131,6 @@ <string name="risk_card_body_open_daily">"Note: Please open the app daily to update your risk status."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Update"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"Update in %1$s"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"Activate Exposure Logging"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -167,17 +139,6 @@ <string name="risk_card_low_risk_headline">"Low Risk"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Increased Risk"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s day since the last encounter"</item> - <item quantity="other">"%1$s days since the last encounter"</item> - <item quantity="zero">"%1$s days since the last encounter"</item> - <item quantity="two">"%1$s days since the last encounter"</item> - <item quantity="few">"%1$s days since the last encounter"</item> - <item quantity="many">"%1$s days since the last encounter"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"Unknown Risk"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Since you have not activated exposure logging for long enough, we could not calculate your risk of infection."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> @@ -197,6 +158,32 @@ <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"Restart"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">"No exposure up to now"</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"Exposures with low risk on %1$d day"</item> + <item quantity="other">"Exposures with low risk on %1$d days"</item> + <item quantity="zero">"Exposures with low risk on %1$d days"</item> + <item quantity="two">"Exposures with low risk on %1$d days"</item> + <item quantity="few">"Exposures with low risk on %1$d days"</item> + <item quantity="many">"Exposures with low risk on %1$d days"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">"No exposure up to now"</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"Exposures on %1$d day with increased risk"</item> + <item quantity="other">"Exposures on %1$d days with increased risk"</item> + <item quantity="zero">"Exposures on %1$d days with increased risk"</item> + <item quantity="two">"Exposures on %1$d days with increased risk"</item> + <item quantity="few">"Exposures on %1$d days with increased risk"</item> + <item quantity="many">"Exposures on %1$d days with increased risk"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"Most recently on %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -251,7 +238,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"Exposure Logging"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"Exposure logging is one of three central features of the app. Once you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string> + <string name="main_overview_body_tracing">"Exposure logging is one of the three central features of the app. When you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Risk of Infection"</string> <!-- YTXT: App overview body text about risk levels --> @@ -277,11 +264,11 @@ <!-- XHED: App overview subtitle for glossary risk calculation --> <string name="main_overview_subtitle_glossary_calculation">"Exposure Check"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Exposure log data is retrieved and synchronized with reported infections of other users. The exposure check is performed automatically about every two hours."</string> + <string name="main_overview_body_glossary_calculation">"Exposure log data is retrieved and synchronized with reported infections of other users. Your risk is checked automatically several times per day."</string> <!-- XHED: App overview subtitle for glossary contact --> - <string name="main_overview_subtitle_glossary_contact">"Exposures"</string> + <string name="main_overview_subtitle_glossary_contact">"Exposure Risk"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"Encounters over a longer duration and close proximity to people diagnosed with COVID-19."</string> + <string name="main_overview_body_glossary_contact">"Exposure to an infected person who has shared their positive test results with others through the app. An exposure must meet certain criteria with regard to duration, distance, and suspected infectiousness of the other person to be classified as a high-risk exposure."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"Exposure Notification"</string> <!-- YTXT: App overview body for glossary notifications --> @@ -289,7 +276,7 @@ <!-- XHED: App overview subtitle for glossary keys --> <string name="main_overview_subtitle_glossary_keys">"Random ID"</string> <!-- YTXT: App overview body for glossary keys --> - <string name="main_overview_body_glossary_keys">"Random IDs are combinations of digits and letters generated randomly. They are exchanged between devices in close proximity. Random IDs cannot be traced to a specific person and are automatically deleted after 14 days. Persons diagnosed with COVID-19 can opt to share their random IDs of up to the last 14 days with other app users."</string> + <string name="main_overview_body_glossary_keys">"Random IDs are combinations of digits and letters generated randomly. They are exchanged between smartphones in close proximity. Random IDs cannot be traced to a specific person and are automatically deleted after 14 days. Persons diagnosed with COVID-19 can opt to share their random IDs of up to the last 14 days with other app users."</string> <!-- XACT: main (overview) - illustraction description, explanation image --> <string name="main_overview_illustration_description">"A smartphone displays various content, numbered 1 to 3."</string> <!-- XACT: App main page title --> @@ -338,13 +325,11 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged">"Your risk of infection can be calculated only for periods during which exposure logging was active. The logging feature should therefore remain active permanently."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> - <string name="risk_details_information_body_period_logged_assessment">"Exposure logging covers the past 14 days. During this time, the logging feature on your device was active for %1$s days. The app automatically deletes older logs, as these are no longer relevant for infection prevention."</string> + <string name="risk_details_information_body_period_logged_assessment">"Exposure logging covers the past 14 days. During this time, the logging feature on your smartphone was active for %1$s days. The app automatically deletes older logs, as these are no longer relevant for infection prevention."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk_past">"This is how your risk was calculated"</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"This is how your risk is calculated"</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Since you have not activated exposure logging for long enough, we could not calculate your risk of infection."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"Your exposure logging could not be updated for more than 24 hours."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -375,7 +360,7 @@ <string name="risk_details_explanation_dialog_title">"Information about exposure logging functionality"</string> <!-- YTXT: one time risk explanation dialog - pointing to the faq page for more information--> <string name="risk_details_explanation_dialog_faq_body">"For further information, please see our FAQ page."</string> - <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> + <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> <string name="risk_details_explanation_faq_link">"https://www.coronawarn.app/en/faq/#encounter_but_green"</string> <!-- XHED: risk details - deadman notification title --> @@ -421,7 +406,7 @@ <!-- YTXT: onboarding(together) - inform about the app --> <string name="onboarding_body">"Turn your smartphone into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their devices, whereby no personal data whatsoever is accessed."</string> + <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their smartphones, whereby no personal data whatsoever is accessed."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XACT: Onboarding (privacy) page title --> @@ -466,7 +451,7 @@ <!-- XACT: onboarding(tracing) - dialog about energy optimized header text --> <string name="onboarding_energy_optimized_dialog_headline">"Allow prioritized background activity"</string> <!-- YMSI: onboarding(tracing) - dialog about energy optimized --> - <string name="onboarding_energy_optimized_dialog_body">"Enable prioritized background activity to allow the App to determine your risk status in the background any time (recommended). This disables battery life optimization for the Corona-Warn-App only. We do not expect this to cause a significant decrease in your device\'s battery life.\n\nIf you do not allow this setting, we recommend you to open the App manually at least once every 24 hours."</string> + <string name="onboarding_energy_optimized_dialog_body">"Enable prioritized background activity to allow the App to determine your risk status in the background any time (recommended). This disables battery life optimization for the Corona-Warn-App only. We do not expect this to cause a significant decrease in your smartphone\'s battery life.\n\nIf you do not allow this setting, we recommend you to open the App manually at least once every 24 hours."</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, open device settings --> <string name="onboarding_energy_optimized_dialog_button_positive">"Allow"</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, continue in app --> @@ -482,17 +467,17 @@ <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Allow location access"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> - <string name="onboarding_tracing_location_body">"Your location cannot be accessed. Google and/or Android requires access to your device\'s location to use Bluetooth."</string> + <string name="onboarding_tracing_location_body">"Your location cannot be accessed. Google and/or Android requires access to your smartphone\'s location to use Bluetooth."</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> <string name="onboarding_tracing_location_button">"Open Device Settings"</string> <!-- XACT: Onboarding (test) page title --> - <string name="onboarding_test_accessibility_title">"Onboarding page 5 of 6: If you are diagnosed with COVID-19..."</string> + <string name="onboarding_test_accessibility_title">"Onboarding page 5 of 6: If You Are Diagnosed with COVID-19"</string> <!-- XHED: onboarding(test) - about positive tests --> <string name="onboarding_test_headline">"If you are diagnosed with COVID-19…"</string> <!-- XHED: onboarding(test) - two/three line headline under an illustration --> <string name="onboarding_test_subtitle">"… please report this in the Corona-Warn-App. Sharing your test results is voluntary and secure. Please do this for the sake of everyone\'s health."</string> <!-- YTXT: onboarding(test) - explain test --> - <string name="onboarding_test_body">"Your notification is encrypted securely and processed on a secure server. People whose encrypted random IDs your device has collected will now receive a warning along with information about what they should now do."</string> + <string name="onboarding_test_body">"Your notification is encrypted securely and processed on a secure server. People whose encrypted random IDs your smartphone has collected will now receive a warning along with information about what they should now do."</string> <!-- XACT: onboarding(test) - illustraction description, header image --> <string name="onboarding_test_illustration_description">"An encrypted positive test diagnosis is transmitted to the system, which will now warn other users."</string> <!-- XACT: Onboarding (datashare) page title --> @@ -675,7 +660,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) is Germany’s federal public health body. The RKI publishes the Corona-Warn-App on behalf of the Federal Government. The app is intended as a digital complement to public health measures already introduced: social distancing, hygiene, and face masks."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Whoever uses the app helps to trace and break chains of infection. The app saves encounters with other persons locally on your smartphone. You are notified if you have encountered persons who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> + <string name="information_about_body">"People who use the app help to trace and break chains of infection. The app saves encounters with other people locally on your device. You are notified if you have encountered people who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -856,13 +841,13 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Update"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Remove test"</string> + <string name="submission_test_result_pending_remove_test_button">"Delete Test"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Your Test Result"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"The laboratory result indicates no verification that you have coronavirus SARS-CoV-2.\n\nPlease delete the test from the Corona-Warn-App, so that you can save a new test code here if necessary."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Remove test"</string> + <string name="submission_test_result_negative_remove_test_button">"Delete Test"</string> <!-- XHED: Page headline for other warnings screen --> <string name="submission_test_result_positive_steps_warning_others_heading">"Warn Others"</string> <!-- YTXT: Body text for for other warnings screen--> @@ -1083,7 +1068,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"More than 2 weeks ago"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"No statement"</string> + <string name="submission_symptom_verify">"No answer"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1115,7 +1100,7 @@ <!-- YTXT: Body text for submission status: negative --> <string name="submission_status_card_body_negative">"You have been diagnosed negative for SARS-CoV-2."</string> <!-- YTXT: Body text for submission status fetch failed --> - <string name="submission_status_card_body_failed">"Your test is more than 21 days old and is therefore no longer relevant. Please delete the text. You can then add another."</string> + <string name="submission_status_card_body_failed">"Your test is more than 21 days old and is therefore no longer relevant. Please delete the test. You can then add another."</string> <!-- XBUT: submission status card unregistered button --> <string name="submission_status_card_button_unregistered">"Learn More and Help"</string> <!-- XBUT: submission status card show results button --> diff --git a/Corona-Warn-App/src/main/res/values-pl/strings.xml b/Corona-Warn-App/src/main/res/values-pl/strings.xml index dca95af4a27fa1f824a0cff2c376f2993b5e3bd4..ef18e2c574040ff1b6ca17f5e68193dd03d0598c 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Brak narażenia do tej pory"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s narażenie z niskim ryzykiem"</item> - <item quantity="other">"%1$s narażenia z niskim ryzykiem"</item> - <item quantity="zero">"Brak narażenia z niskim ryzykiem do tej pory"</item> - <item quantity="two">"%1$s narażeÅ„ z niskim ryzykiem"</item> - <item quantity="few">"%1$s narażenia z niskim ryzykiem"</item> - <item quantity="many">"%1$s narażeÅ„ z niskim ryzykiem"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s narażenie"</item> - <item quantity="other">"%1$s narażenia"</item> - <item quantity="zero">"Brak narażenia do tej pory"</item> - <item quantity="two">"%1$s narażenia"</item> - <item quantity="few">"%1$s narażenia"</item> - <item quantity="many">"%1$s narażeÅ„"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Rejestrowanie narażenia byÅ‚o aktywne przez %1$s z ostatnich 14 dni."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -157,8 +131,6 @@ <string name="risk_card_body_open_daily">"Uwaga: Otwieraj codziennie aplikacjÄ™, aby aktualizować swój status ryzyka."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Aktualizuj"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"Aktualizacja za %1$s"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"Aktywuj rejestrowanie narażenia"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -167,17 +139,6 @@ <string name="risk_card_low_risk_headline">"Niskie ryzyko"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Podwyższone ryzyko"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s dzieÅ„ od ostatniego kontaktu"</item> - <item quantity="other">"%1$s dnia od ostatniego kontaktu"</item> - <item quantity="zero">"%1$s dni od ostatniego kontaktu"</item> - <item quantity="two">"%1$s dni od ostatniego kontaktu"</item> - <item quantity="few">"%1$s dni od ostatniego kontaktu"</item> - <item quantity="many">"%1$s dni od ostatniego kontaktu"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"Ryzyko nieznane"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Nie możemy jeszcze podać ryzyka zakażenia, ponieważ nie mamy jeszcze wystarczajÄ…cej iloÅ›ci danych."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> @@ -197,6 +158,32 @@ <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"Uruchom ponownie"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">"Brak narażenia do tej pory"</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"Narażenia z niskim ryzykiem w ciÄ…gu %1$d dni"</item> + <item quantity="other">"Narażenia z niskim ryzykiem w ciÄ…gu %1$d dnia"</item> + <item quantity="zero">"Narażenia z niskim ryzykiem w ciÄ…gu %1$d dni"</item> + <item quantity="two">"Narażenia z niskim ryzykiem w ciÄ…gu %1$d dni"</item> + <item quantity="few">"Narażenia z niskim ryzykiem w ciÄ…gu %1$d dni"</item> + <item quantity="many">"Narażenia z niskim ryzykiem w ciÄ…gu %1$d dni"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">"Brak narażenia do tej pory"</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"Narażenia w ciÄ…gu %1$d dnia z podwyższonym ryzykiem"</item> + <item quantity="other">"Narażenia w ciÄ…gu %1$d dnia z podwyższonym ryzykiem"</item> + <item quantity="zero">"Narażenia w ciÄ…gu %1$d dni z podwyższonym ryzykiem"</item> + <item quantity="two">"Narażenia w ciÄ…gu %1$d dni z podwyższonym ryzykiem"</item> + <item quantity="few">"Narażenia w ciÄ…gu %1$d dni z podwyższonym ryzykiem"</item> + <item quantity="many">"Narażenia w ciÄ…gu %1$d dni z podwyższonym ryzykiem"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"Ostatnio dnia %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -267,29 +254,29 @@ <!-- XHED: App overview subtitle for test procedure explanation --> <string name="main_overview_headline_test">"Powiadamianie innych użytkowników"</string> <!-- YTXT: App overview body text about rest procedure --> - <string name="main_overview_body_test">"KolejnÄ… kluczowÄ… funkcjÄ… jest rejestracja testu i pobranie wyniku. W przypadku zdiagnozowania u Ciebie COVID-19 bÄ™dziesz mieć możliwość powiadomienia innych i przerwania Å‚aÅ„cucha zakażeÅ„."</string> + <string name="main_overview_body_test">"InnÄ… kluczowÄ… funkcjÄ… jest rejestracja testu i pobranie wyniku. W przypadku zdiagnozowania u Ciebie COVID-19 bÄ™dziesz mieć możliwość powiadomienia innych i przerwania Å‚aÅ„cucha zakażeÅ„."</string> <!-- XHED: App overview headline for glossary --> <string name="main_overview_headline_glossary">"Definicja terminów:"</string> <!-- XHED: App overview subtitle for glossary key storage --> <string name="main_overview_subtitle_glossary_tracing">"Dziennik narażeÅ„"</string> <!-- YTXT: App overview body for glossary key storage --> - <string name="main_overview_body_glossary_tracing">"Lista otrzymanych i tymczasowych losowych identyfikatorów zapisanych tymczasowo w pamiÄ™ci masowej systemu operacyjnego. Aplikacja korzysta z listy podczas sprawdzania narażeÅ„. Wszystkie losowe identyfikatory sÄ… automatycznie usuwane po 14 dniach."</string> + <string name="main_overview_body_glossary_tracing">"Lista otrzymanych i tymczasowych losowych identyfikatorów zapisanych tymczasowo w pamiÄ™ci masowej systemu operacyjnego. Lista ta jest odczytywana podczas sprawdzania narażeÅ„. Wszystkie losowe identyfikatory sÄ… automatycznie usuwane po 14 dniach."</string> <!-- XHED: App overview subtitle for glossary risk calculation --> - <string name="main_overview_subtitle_glossary_calculation">"Sprawdzanie narażeÅ„"</string> + <string name="main_overview_subtitle_glossary_calculation">"Sprawdzanie narażenia"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Odczytanie danych dziennika narażeÅ„ i porównanie ze zgÅ‚oszonymi zakażeniami innych użytkowników. Sprawdzanie narażeÅ„ jest wykonywane automatycznie mniej wiÄ™cej co dwie godziny."</string> + <string name="main_overview_body_glossary_calculation">"Odczytanie danych dziennika narażeÅ„ i porównanie ze zgÅ‚oszonymi zakażeniami innych użytkowników. Ryzyko jest sprawdzane automatycznie kilka razy dziennie."</string> <!-- XHED: App overview subtitle for glossary contact --> - <string name="main_overview_subtitle_glossary_contact">"Narażenia"</string> + <string name="main_overview_subtitle_glossary_contact">"Ryzyko narażenia"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"Bliskie kontakty o dość dÅ‚ugim czasie trwania z osobami, u których zdiagnozowano COVID-19."</string> + <string name="main_overview_body_glossary_contact">"Narażenie na kontakt z osobÄ… zakażonÄ…, która udostÄ™pniÅ‚a innym pozytywny wynik swojego testu za poÅ›rednictwem aplikacji. Narażenie musi speÅ‚niać okreÅ›lone kryteria dotyczÄ…ce czasu trwania, odlegÅ‚oÅ›ci i podejrzenia zakaźnoÅ›ci drugiej osoby, aby zostaÅ‚o zaklasyfikowane jako narażenie wysokiego ryzyka."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"Powiadomienie o narażeniu"</string> <!-- YTXT: App overview body for glossary notifications --> - <string name="main_overview_body_glossary_notification">"WyÅ›wietlenie narażeÅ„ w Corona-Warn-App."</string> + <string name="main_overview_body_glossary_notification">"WyÅ›wietlanie narażeÅ„ w Corona-Warn-App."</string> <!-- XHED: App overview subtitle for glossary keys --> <string name="main_overview_subtitle_glossary_keys">"Identyfikator losowy"</string> <!-- YTXT: App overview body for glossary keys --> - <string name="main_overview_body_glossary_keys">"Losowe identyfikatory sÄ… kombinacjÄ… cyfr i liter generowanych losowo. SÄ… one wymieniane pomiÄ™dzy urzÄ…dzeniami znajdujÄ…cymi siÄ™ w bliskiej odlegÅ‚oÅ›ci od siebie. Losowych identyfikatorów nie można przypisać do konkretnej osoby. SÄ… one automatycznie usuwane po 14 dniach. Osoby, u których zdiagnozowano COVID-19, mogÄ… zdecydować siÄ™ na udostÄ™pnienie swoich losowych identyfikatorów z ostatnich 14 dni innym użytkownikom aplikacji."</string> + <string name="main_overview_body_glossary_keys">"Losowe identyfikatory sÄ… kombinacjÄ… cyfr i liter generowanych losowo. SÄ… one wymieniane pomiÄ™dzy smartfonami znajdujÄ…cymi siÄ™ w bliskiej odlegÅ‚oÅ›ci od siebie. Losowych identyfikatorów nie można przypisać do konkretnej osoby. SÄ… one automatycznie usuwane po 14 dniach. Osoby, u których zdiagnozowano COVID-19, mogÄ… zdecydować siÄ™ na udostÄ™pnienie swoich losowych identyfikatorów z ostatnich 14 dni innym użytkownikom aplikacji."</string> <!-- XACT: main (overview) - illustraction description, explanation image --> <string name="main_overview_illustration_description">"Smartfon wyÅ›wietla różne treÅ›ci oznaczone numerami od 1 do 3."</string> <!-- XACT: App main page title --> @@ -306,7 +293,7 @@ <!-- XHED: risk details - subtitle for additional info in case of encounter with low risk --> <string name="risk_details_additional_info_subtitle">"Dlaczego Twoje ryzyko zakażenia jest niskie"</string> <!-- XHED: risk details - text for additional info in case of encounter with low risk --> - <string name="risk_details_additional_info_text">"ByÅ‚eÅ›(-aÅ›) narażony(-a) na kontakt z osobÄ…, u której zdiagnozowano COVID-19. Jednak na podstawie Twoich danych rejestrowania narażenia Twoje ryzyko zakażenia jest niskie. Ryzyko jest niskie, jeÅ›li Twój kontakt trwaÅ‚ krótko lub zachowany zostaÅ‚ dystans. Nie musisz siÄ™ martwić i podejmować żadnych dziaÅ‚aÅ„. Zalecamy przestrzeganie obowiÄ…zujÄ…cych reguÅ‚ dotyczÄ…cych zachowania dystansu i higieny."</string> + <string name="risk_details_additional_info_text">"ByÅ‚eÅ›(-aÅ›) narażony(-a) na kontakt z osobÄ…, u której zdiagnozowano COVID-19. Jednak na podstawie Twoich danych rejestrowania narażenia Twoje ryzyko zakażenia jest niskie. Ryzyko jest niskie, jeÅ›li Twój kontakt trwaÅ‚ krótko lub zachowany zostaÅ‚ dystans. Nie musisz siÄ™ martwić i podejmować żadnych dziaÅ‚aÅ„. Zalecamy przestrzeganie obowiÄ…zujÄ…cych reguÅ‚ dotyczÄ…cych dystansu i higieny."</string> <!-- XHED: risk details - headline, how a user should act --> <string name="risk_details_headline_behavior">"Wytyczne"</string> <!-- XHED: risk details - multiline headline, bold, how to act correct --> @@ -318,7 +305,7 @@ <!-- XMSG: risk details - wash your hands, something like a bullet point --> <string name="risk_details_behavior_body_wash_hands">"Regularnie myj rÄ™ce mydÅ‚em przez 20 sekund."</string> <!-- XMSG: risk details - wear a face mask, something like a bullet point --> - <string name="risk_details_behavior_body_wear_mask">"Załóż maseczkÄ™, jeÅ›li zamierzasz kontaktować siÄ™ fizycznie z innymi osobami."</string> + <string name="risk_details_behavior_body_wear_mask">"Załóż maseczkÄ™ na twarz, majÄ…c kontakt z innymi osobami."</string> <!-- XMSG: risk details - stay 1,5 away, something like a bullet point --> <string name="risk_details_behavior_body_stay_away">"Zachowuj odlegÅ‚ość co najmniej 1,5 metra od innych osób."</string> <!-- XMSG: risk details - cough/sneeze, something like a bullet point --> @@ -326,7 +313,7 @@ <!-- XMSG: risk details - contact your doctor, bullet point --> <string name="risk_details_behavior_increased_body_1">"Twój lekarz rodzinny"</string> <!-- XMSG: risk details - panel doctor on-call service, bullet point --> - <string name="risk_details_behavior_increased_body_2">"Lekarz dyżurny pod numerem telefonu 116117"</string> + <string name="risk_details_behavior_increased_body_2">"Pogotowie ratunkowe pod numerem telefonu 116117"</string> <!-- XMSG: risk details - public health department, bullet point --> <string name="risk_details_behavior_increased_body_3">"Organ ds. zdrowia publicznego"</string> <!-- XHED: risk details - infection risk headline, below behaviors --> @@ -338,13 +325,11 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged">"Ryzyko zakażenia można obliczyć tylko dla okresów, w których rejestrowanie narażenia byÅ‚o aktywne. Dlatego też funkcja rejestrowania powinna być stale aktywna."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> - <string name="risk_details_information_body_period_logged_assessment">"Rejestrowanie narażenia obejmuje ostatnie 14 dni. W tym czasie funkcja rejestrowania w Twoim urzÄ…dzeniu byÅ‚a aktywna przez %1$s dni. Aplikacja automatycznie usuwa starsze dzienniki, ponieważ nie sÄ… one już istotne dla zapobiegania zakażeniom."</string> + <string name="risk_details_information_body_period_logged_assessment">"Rejestrowanie narażenia obejmuje ostatnie 14 dni. W tym czasie funkcja rejestrowania w Twoim smartfonie byÅ‚a aktywna przez %1$s dni. Aplikacja automatycznie usuwa starsze dzienniki, ponieważ nie sÄ… one już istotne dla zapobiegania zakażeniom."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk_past">"Sposób, w jaki obliczono Twoje ryzyko"</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"Sposób, w jaki obliczane jest Twoje ryzyko"</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Nie możemy jeszcze podać ryzyka zakażenia, ponieważ nie mamy jeszcze wystarczajÄ…cej iloÅ›ci danych."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"Rejestrowanie narażenia nie mogÅ‚o zostać zaktualizowane przez okres dÅ‚uższy niż 24 godziny."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -375,7 +360,7 @@ <string name="risk_details_explanation_dialog_title">"Informacje o funkcjonalnoÅ›ci rejestrowania narażenia"</string> <!-- YTXT: one time risk explanation dialog - pointing to the faq page for more information--> <string name="risk_details_explanation_dialog_faq_body">"WiÄ™cej informacji znajduje siÄ™ na naszej stronie „CzÄ™sto zadawane pytaniaâ€."</string> - <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> + <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> <string name="risk_details_explanation_faq_link">"https://www.coronawarn.app/en/faq/#encounter_but_green"</string> <!-- XHED: risk details - deadman notification title --> @@ -421,7 +406,7 @@ <!-- YTXT: onboarding(together) - inform about the app --> <string name="onboarding_body">"ZmieÅ„ swój smartfon w system ostrzegania przed koronawirusem. Zapoznaj siÄ™ ze swoim statusem ryzyka i dowiedz siÄ™, czy miaÅ‚eÅ›(-aÅ›) bliski kontakt z osobÄ…, u której w ciÄ…gu ostatnich 14 dni zdiagnozowano COVID-19."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"Aplikacja rejestruje kontakty miÄ™dzy osobami poprzez wymianÄ™ zaszyfrowanych, losowych identyfikatorów miÄ™dzy ich urzÄ…dzeniami bez uzyskiwania dostÄ™pu do danych osobowych."</string> + <string name="onboarding_body_emphasized">"Aplikacja rejestruje kontakty miÄ™dzy osobami poprzez wymianÄ™ zaszyfrowanych, losowych identyfikatorów miÄ™dzy ich smartfonami bez uzyskiwania dostÄ™pu do danych osobowych."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"Grupa osób korzysta ze smartfonów na mieÅ›cie."</string> <!-- XACT: Onboarding (privacy) page title --> @@ -466,7 +451,7 @@ <!-- XACT: onboarding(tracing) - dialog about energy optimized header text --> <string name="onboarding_energy_optimized_dialog_headline">"Zezwól na priorytetowe dziaÅ‚anie w tle"</string> <!-- YMSI: onboarding(tracing) - dialog about energy optimized --> - <string name="onboarding_energy_optimized_dialog_body">"Włącz priorytetowe dziaÅ‚anie w tle, aby aplikacja mogÅ‚a w dowolnym momencie ustalić Twój status ryzyka w tle (zalecane). Powoduje to wyłączenie optymalizacji żywotnoÅ›ci baterii tylko dla aplikacji Corona-Warn-App. Nie przewidujemy w takim przypadku znacznego spadku żywotnoÅ›ci baterii urzÄ…dzenia.\n\nJeÅ›li nie chcesz zezwolić na to ustawienie, zalecamy otwieranie aplikacji rÄ™cznie co najmniej raz na dobÄ™."</string> + <string name="onboarding_energy_optimized_dialog_body">"Włącz priorytetowe dziaÅ‚anie w tle, aby aplikacja mogÅ‚a w dowolnym momencie ustalić Twój status ryzyka w tle (zalecane). Powoduje to wyłączenie optymalizacji żywotnoÅ›ci baterii tylko dla aplikacji Corona-Warn-App. Nie przewidujemy w takim przypadku znacznego spadku żywotnoÅ›ci baterii smartfona.\n\nJeÅ›li nie chcesz zezwolić na to ustawienie, zalecamy otwieranie aplikacji rÄ™cznie co najmniej raz na dobÄ™."</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, open device settings --> <string name="onboarding_energy_optimized_dialog_button_positive">"Zezwól"</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, continue in app --> @@ -482,17 +467,17 @@ <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Zezwól na dostÄ™p do lokalizacji"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> - <string name="onboarding_tracing_location_body">"Nie można uzyskać dostÄ™pu do Twojej lokalizacji. Google i/lub Android wymaga dostÄ™pu do lokalizacji Twojego urzÄ…dzenia w celu użycia Bluetooth."</string> + <string name="onboarding_tracing_location_body">"Nie można uzyskać dostÄ™pu do Twojej lokalizacji. Google i/lub Android wymaga dostÄ™pu do lokalizacji Twojego smartfona w celu użycia Bluetooth."</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> <string name="onboarding_tracing_location_button">"Otwórz ustawienia urzÄ…dzenia"</string> <!-- XACT: Onboarding (test) page title --> - <string name="onboarding_test_accessibility_title">"Strona wprowadzenia 5 z 6: JeÅ›li zdiagnozowano u Ciebie COVID-19..."</string> + <string name="onboarding_test_accessibility_title">"Strona wprowadzenia 5 z 6: JeÅ›li zdiagnozowano u Ciebie COVID-19"</string> <!-- XHED: onboarding(test) - about positive tests --> <string name="onboarding_test_headline">"JeÅ›li zdiagnozowano u Ciebie COVID-19..."</string> <!-- XHED: onboarding(test) - two/three line headline under an illustration --> <string name="onboarding_test_subtitle">"… zgÅ‚oÅ› ten fakt w Corona-Warn-App. UdostÄ™pnianie wyników testu jest dobrowolne i bezpieczne. Zrób to ze wzglÄ™du na zdrowie innych osób."</string> <!-- YTXT: onboarding(test) - explain test --> - <string name="onboarding_test_body">"Twoje powiadomienie jest szyfrowane w bezpieczny sposób i przetwarzane na bezpiecznym serwerze. Osoby, których zaszyfrowane losowe identyfikatory zostaÅ‚y zgromadzone przez Twoje urzÄ…dzenie, otrzymajÄ… wtedy ostrzeżenie wraz z informacjÄ… na temat dalszych kroków postÄ™powania."</string> + <string name="onboarding_test_body">"Twoje powiadomienie jest szyfrowane w bezpieczny sposób i przetwarzane na bezpiecznym serwerze. Osoby, których zaszyfrowane losowe identyfikatory zostaÅ‚y zgromadzone przez Twój smartfon, otrzymajÄ… wtedy ostrzeżenie wraz z informacjÄ… na temat dalszych kroków postÄ™powania."</string> <!-- XACT: onboarding(test) - illustraction description, header image --> <string name="onboarding_test_illustration_description">"Zaszyfrowana diagnoza zakażenia jest przesyÅ‚ana do systemu, który bÄ™dzie teraz ostrzegaÅ‚ innych użytkowników."</string> <!-- XACT: Onboarding (datashare) page title --> @@ -566,7 +551,7 @@ <!-- XBUT: settings(tracing) - go to operating system settings button on card - location --> <string name="settings_tracing_status_location_button">"Otwórz ustawienia urzÄ…dzenia"</string> <!--XHED : settings(tracing) - headline on card about the current status and what to do --> - <string name="settings_tracing_status_connection_headline">"Połącz z Internetem"</string> + <string name="settings_tracing_status_connection_headline">"Otwórz połączenie z Internetem"</string> <!-- XTXT: settings(tracing) - explains user what to do on card if connection is disabled --> <string name="settings_tracing_status_connection_body">"Rejestrowanie narażenia wymaga połączenia z Internetem w celu obliczenia narażeÅ„. Włącz WIFI lub dane mobilne w ustawieniach swojego urzÄ…dzenia."</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card --> @@ -675,7 +660,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Instytut Roberta Kocha (RKI) to niemiecka federalna instytucja zdrowia publicznego. RKI publikuje aplikacjÄ™ Corona-Warn-App w imieniu rzÄ…du federalnego. Aplikacja ta sÅ‚uży jako cyfrowe uzupeÅ‚nienie już wprowadzonych Å›rodków ochrony zdrowia publicznego, takich jak zachowanie dystansu spoÅ‚ecznego, dbanie o higienÄ™ oraz noszenie maseczek."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Wszystkie osoby korzystajÄ…ce z aplikacji pomagajÄ… w Å›ledzeniu i przerwaniu Å‚aÅ„cuchów zakażeÅ„. Aplikacja zapisuje kontakty z innymi osobami lokalnie na Twoim smartfonie. Otrzymasz powiadomienie, jeÅ›li okaże siÄ™, że u osób, z którymi miaÅ‚eÅ›(-aÅ›) kontakt, zdiagnozowano później COVID-19. Twoja tożsamość i prywatność sÄ… zawsze chronione."</string> + <string name="information_about_body">"Osoby korzystajÄ…ce z aplikacji pomagajÄ… w Å›ledzeniu i przerwaniu Å‚aÅ„cuchów zakażeÅ„. Aplikacja zapisuje kontakty z innymi osobami lokalnie na Twoim urzÄ…dzeniu. Otrzymasz powiadomienie, jeÅ›li okaże siÄ™, że u osób, z którymi miaÅ‚eÅ›(-aÅ›) kontakt, zdiagnozowano później COVID-19. Twoja tożsamość i prywatność sÄ… zawsze chronione."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Grupa osób korzysta ze smartfonów na mieÅ›cie."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -1001,7 +986,7 @@ <!-- YTXT: submission done further info bullet points --> <string-array name="submission_done_further_info_bullet_points"> <item>"Okres kwarantanny wynosi zazwyczaj 14 dni. Obserwuj swoje objawy i monitoruj ich rozwój."</item> - <item>"Zostaniesz poproszony(-a) przez organ ds. zdrowia publicznego o stworzenie listy osób, z którymi miaÅ‚eÅ›(-aÅ›) kontakt. Powinna ona obejmować wszystkie osoby, z którymi miaÅ‚eÅ›(-aÅ›) bliski kontakt (rozmowa twarzÄ… w twarz w odlegÅ‚oÅ›ci mniejszej niż 2 metry) przez ponad 15 minut w ciÄ…gu dwóch dni przed wystÄ…pieniem objawów."</item> + <item>"Zostaniesz poproszony(-a) przez organ ds. zdrowia publicznego o stworzenie listy osób, z którymi miaÅ‚eÅ›(-aÅ›) kontakt. Powinna ona obejmować wszystkie osoby, z którymi miaÅ‚eÅ›(-aÅ›) bliski kontakt (w odlegÅ‚oÅ›ci mniejszej niż 2 metry, rozmowa twarzÄ… w twarz) przez ponad 15 minut w ciÄ…gu dwóch dni przed wystÄ…pieniem objawów."</item> <item>"Zwróć szczególnÄ… uwagÄ™ na osoby, które nie zostanÄ… powiadomione bezpoÅ›rednio przez aplikacjÄ™, ponieważ nie posiadajÄ… smartfonów lub nie zainstalowaÅ‚y aplikacji."</item> <item>"Nawet jeÅ›li nie masz już żadnych objawów i znów czujesz siÄ™ dobrze, możesz nadal zarażać."</item> </string-array> @@ -1073,7 +1058,7 @@ <!-- XHED: Page title for calendar page in submission symptom flow --> <string name="submission_symptom_calendar_title">"PoczÄ…tek wystÄ…pienia objawów"</string> <!-- XHED: Page headline for calendar page in symptom submission flow --> - <string name="submission_symptom_calendar_headline">"Kiedy zaczÄ…Å‚eÅ›(-Å‚aÅ›) odczuwać te objawy? "</string> + <string name="submission_symptom_calendar_headline">"Kiedy zaczÄ…Å‚eÅ›(-ęłaÅ›) odczuwać te objawy? "</string> <!-- YTXT: Body text for calendar page in symptom submission flow--> <string name="submission_symptom_calendar_body">"Wybierz dokÅ‚adnÄ… datÄ™ w kalendarzu lub, jeÅ›li nie pamiÄ™tasz dokÅ‚adnej daty, wybierz jednÄ… z innych opcji."</string> <!-- XBUT: symptom calendar screen less than 7 days button --> @@ -1083,7 +1068,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"Ponad 2 tygodnie temu"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"Bez komentarza"</string> + <string name="submission_symptom_verify">"Brak odpowiedzi"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1125,7 +1110,7 @@ <!-- XHED: submission status card positive result subtitle --> <string name="submission_status_card_positive_result_subtitle">"Uwaga:"</string> <!-- YTXT: text for contagious card --> - <string name="submission_status_card_positive_result_contagious">"Możesz zarażać. Zastosuj izolacjÄ™ / unikaj kontaktu fizycznego z innymi osobami."</string> + <string name="submission_status_card_positive_result_contagious">"Możesz zarażać. Izoluj siÄ™ od innych osób."</string> <!-- YTXT: text for contact card --> <string name="submission_status_card_positive_result_contact">"Organ ds. zdrowia publicznego skontaktuje siÄ™ z TobÄ… w ciÄ…gu kilku najbliższych dni."</string> <!-- YTXT: text for share result card--> @@ -1315,14 +1300,14 @@ <string name="interoperability_title">"Rejestrowanie narażenia\nw różnych krajach"</string> <!-- XHED: Setting title of interoperability in the tracing settings view --> - <string name="settings_interoperability_title">"Rejestrowanie narażenia\nw różnych krajach"</string> + <string name="settings_interoperability_title">"Rejestrowanie narażenia w różnych krajach"</string> <!-- XTXT: Settings description of the interoperability in the tracing settings view --> <string name="settings_interoperability_subtitle">"Kraje uczestniczÄ…ce"</string> <!-- XHED: Header of interoperability information/configuration view --> - <string name="interoperability_configuration_title">"Rejestrowanie narażenia\nw różnych krajach"</string> + <string name="interoperability_configuration_title">"Rejestrowanie narażenia w różnych krajach"</string> <!-- XTXT: First section after the header of the interoperability information/configuration view --> - <string name="interoperability_configuration_first_section">"Wiele krajów współpracuje ze sobÄ… w celu aktywacji transgranicznych alertów wysyÅ‚anych poprzez wspólny serwer wymiany danych. Na przykÅ‚ad przy rejestrowaniu narażenia można wziąć pod uwagÄ™ również kontakty z użytkownikami oficjalnych aplikacji koronawirusowych z innych uczestniczÄ…cych krajów."</string> + <string name="interoperability_configuration_first_section">"Wiele krajów współpracuje ze sobÄ… w celu aktywacji transgranicznych alertów wysyÅ‚anych poprzez wspólny serwer wymiany danych. Na przykÅ‚ad przy rejestrowaniu narażenia można wziąć pod uwagÄ™ również kontakty z użytkownikami oficjalnych aplikacji koronawirusowyych z innych uczestniczÄ…cych krajów."</string> <!-- XTXT: Second section after the header of the interoperability information/configuration view --> <string name="interoperability_configuration_second_section">"W tym celu aplikacja pobiera listÄ™, która jest aktualizowana codziennie, z losowymi identyfikatorami wszystkich użytkowników, którzy udostÄ™pnili swoje losowe identyfikatory poprzez wÅ‚asnÄ… aplikacjÄ™. Lista jest nastÄ™pnie porównywana z losowymi identyfikatorami zarejestrowanymi przez Twój smartfon. Codzienne pobieranie listy z losowymi identyfikatorami jest z reguÅ‚y bezpÅ‚atne – za dane używane przez aplikacjÄ™ w tym kontekÅ›cie nie bÄ™dÄ… pobierane opÅ‚aty roamingowe w innych krajach UE."</string> <!-- XHED: Header right above the country list in the interoperability information/configuration view --> diff --git a/Corona-Warn-App/src/main/res/values-ro/strings.xml b/Corona-Warn-App/src/main/res/values-ro/strings.xml index 1ac7c15afb801f7cd3bda858dfe719f1ebcfd94b..f85f705e22acb6da1c7b576fa9c994c6367f6118 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -86,7 +80,7 @@ <!-- XMIT: application overview --> <string name="menu_help">"Sumar"</string> <!-- XMIT: application information --> - <string name="menu_information">"InformaÈ›ii despre aplicaÈ›ie"</string> + <string name="menu_information">"Info aplicaÈ›ie"</string> <!-- XMIT: application settings --> <string name="menu_settings">"Setări"</string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Nicio expunere până acum"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s expunere cu risc redus"</item> - <item quantity="other">"%1$s de expuneri cu risc redus"</item> - <item quantity="zero">"Nicio expunere cu risc redus până acum"</item> - <item quantity="two">"%1$s expuneri cu risc redus"</item> - <item quantity="few">"%1$s expuneri cu risc redus"</item> - <item quantity="many">"%1$s expuneri cu risc redus"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s expunere"</item> - <item quantity="other">"%1$s de expuneri"</item> - <item quantity="zero">"Nicio expunere până acum"</item> - <item quantity="two">"%1$s expuneri"</item> - <item quantity="few">"%1$s expuneri"</item> - <item quantity="many">"%1$s expuneri"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"ÃŽn ultimele 14 zile, înregistrarea în jurnal a expunerilor a fost activă timp de %1$s zile."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -157,8 +131,6 @@ <string name="risk_card_body_open_daily">"ReÈ›ineÈ›i: DeschideÈ›i aplicaÈ›ia zilnic pentru a actualiza starea riscului dvs."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Actualizare"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"Actualizare în %1$s"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"ActivaÈ›i înregistrarea în jurnal a expunerilor"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -167,17 +139,6 @@ <string name="risk_card_low_risk_headline">"Risc redus"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Risc crescut"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s zi de la ultima întâlnire"</item> - <item quantity="other">"%1$s de zile de la ultima întâlnire"</item> - <item quantity="zero">"%1$s zile de la ultima întâlnire"</item> - <item quantity="two">"%1$s zile de la ultima întâlnire"</item> - <item quantity="few">"%1$s zile de la ultima întâlnire"</item> - <item quantity="many">"%1$s zile de la ultima întâlnire"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"Risc necunoscut"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Deoarece nu aÈ›i activat un timp suficient de lung înregistrarea în jurnal a expunerilor, nu am putut calcula riscul dvs. de infectare."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> @@ -197,6 +158,32 @@ <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"Relansare"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">"Nicio expunere până acum"</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"Expuneri cu risc redus în %1$d zi"</item> + <item quantity="other">"Expuneri cu risc redus în %1$d de zile"</item> + <item quantity="zero">"Expuneri cu risc redus în %1$d zile"</item> + <item quantity="two">"Expuneri cu risc redus în %1$d zile"</item> + <item quantity="few">"Expuneri cu risc redus în %1$d zile"</item> + <item quantity="many">"Expuneri cu risc redus în %1$d zile"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">"Nicio expunere până acum"</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"Expuneri cu risc crescut în %1$d zi"</item> + <item quantity="other">"Expuneri cu risc crescut în %1$d de zile"</item> + <item quantity="zero">"Expuneri cu risc crescut în %1$d zile"</item> + <item quantity="two">"Expuneri cu risc crescut în %1$d zile"</item> + <item quantity="few">"Expuneri cu risc crescut în %1$d zile"</item> + <item quantity="many">"Expuneri cu risc crescut în %1$d zile"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"Cel mai recent pe %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -277,11 +264,11 @@ <!-- XHED: App overview subtitle for glossary risk calculation --> <string name="main_overview_subtitle_glossary_calculation">"Verificarea expunerii"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Datele jurnalului de expuneri sunt citite È™i sincronizate cu infectările raportate de alÈ›i utilizatori. Verificarea expunerii este efectuată automat, aproximativ la fiecare două ore."</string> + <string name="main_overview_body_glossary_calculation">"Datele jurnalului de expuneri sunt citite È™i sincronizate cu infectările raportate de alÈ›i utilizatori. Riscul dvs. este verificat automat de mai multe ori pe zi."</string> <!-- XHED: App overview subtitle for glossary contact --> - <string name="main_overview_subtitle_glossary_contact">"Expuneri"</string> + <string name="main_overview_subtitle_glossary_contact">"Risc de expunere"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"ÃŽntâlniri de o durată mai lungă È™i în strânsă proximitate cu persoane diagnosticate cu COVID-19."</string> + <string name="main_overview_body_glossary_contact">"Expunerea la o persoană infectată care È™i-a împărtășit rezultatul pozitiv al testului cu alte persoane prin intermediul aplicaÈ›iei. O expunere trebuie să îndeplinească anumite criterii cu privire la durată, distanță È™i contagiozitatea suspectată a celeilalte persoane pentru a fi clasificată ca expunere cu risc ridicat."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"Notificarea de expunere"</string> <!-- YTXT: App overview body for glossary notifications --> @@ -289,7 +276,7 @@ <!-- XHED: App overview subtitle for glossary keys --> <string name="main_overview_subtitle_glossary_keys">"ID aleatoriu"</string> <!-- YTXT: App overview body for glossary keys --> - <string name="main_overview_body_glossary_keys">"ID-urile aleatorii sunt combinaÈ›ii de cifre È™i litere generate aleatoriu. Acestea sunt schimbate între dispozitivele aflate în proximitate strânsă. ID-urile aleatorii nu pot fi asociate cu o persoană anume È™i sunt È™terse automat după 14 zile. Persoanele diagnosticate cu COVID-19 pot opta să trimită ID-urile aleatorii din ultimele 14 zile È™i altor utilizatori ai aplicaÈ›iei."</string> + <string name="main_overview_body_glossary_keys">"ID-urile aleatorii sunt combinaÈ›ii de cifre È™i litere generate aleatoriu. Acestea sunt schimbate între smartphone-urile aflate în proximitate strânsă. ID-urile aleatorii nu pot fi asociate cu o persoană anume È™i sunt È™terse automat după 14 zile. Persoanele diagnosticate cu COVID-19 pot opta să trimită ID-urile aleatorii din ultimele 14 zile È™i altor utilizatori ai aplicaÈ›iei."</string> <!-- XACT: main (overview) - illustraction description, explanation image --> <string name="main_overview_illustration_description">"Un smartphone afiÈ™ează conÈ›inut variat, numerotat de la 1 la 3."</string> <!-- XACT: App main page title --> @@ -304,7 +291,7 @@ <!-- XHED: risk details - headline for additional info in case of encounter with low risk --> <string name="risk_details_additional_info_title">"Expuneri cu risc redus"</string> <!-- XHED: risk details - subtitle for additional info in case of encounter with low risk --> - <string name="risk_details_additional_info_subtitle">"Riscul dvs. de infectare este redus"</string> + <string name="risk_details_additional_info_subtitle">"De ce riscul dvs. de infectare este redus"</string> <!-- XHED: risk details - text for additional info in case of encounter with low risk --> <string name="risk_details_additional_info_text">"AÈ›i avut o întâlnire cu o persoană diagnosticată cu COVID-19. TotuÈ™i, pe baza datelor de înregistrare în jurnal a expunerilor, riscul dvs. de infectare este redus. Riscul este redus dacă întâlnirea dvs. a fost de scurtă durată sau s-a păstrat o anumită distanță. Nu este cazul să vă îngrijoraÈ›i È™i nu este nevoie de nicio acÈ›iune specială din partea dvs. Vă recomandăm să respectaÈ›i regulile generale privind distanÈ›area È™i igiena."</string> <!-- XHED: risk details - headline, how a user should act --> @@ -328,7 +315,7 @@ <!-- XMSG: risk details - panel doctor on-call service, bullet point --> <string name="risk_details_behavior_increased_body_2">"Serviciul general de urgență la numărul de telefon 112"</string> <!-- XMSG: risk details - public health department, bullet point --> - <string name="risk_details_behavior_increased_body_3">"Autoritatea de sănătate publică din regiunea dvs."</string> + <string name="risk_details_behavior_increased_body_3">"DirecÈ›ia de sănătate publică relevantă"</string> <!-- XHED: risk details - infection risk headline, below behaviors --> <string name="risk_details_headline_infection_risk">"Risc de infectare"</string> <!-- XHED: risk details - infection period logged headling, below behaviors --> @@ -338,13 +325,11 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged">"Riscul dvs. de infectare poate fi calculat doar pentru perioadele în care a fost activă înregistrarea în jurnal a expunerilor. Prin urmare, caracteristica de înregistrare în jurnal trebuie să rămână permanent activă."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> - <string name="risk_details_information_body_period_logged_assessment">"ÃŽnregistrarea în jurnal a expunerilor acoperă ultimele 14 zile. ÃŽn această perioadă, caracteristica de înregistrare în jurnal de pe dispozitivul dvs. a fost activă timp de %1$s zile. AplicaÈ›ia È™terge automat înregistrările mai vechi din jurnal, întrucât acestea nu mai sunt relevante pentru prevenirea infectării."</string> + <string name="risk_details_information_body_period_logged_assessment">"ÃŽnregistrarea în jurnal a expunerilor acoperă ultimele 14 zile. ÃŽn această perioadă, caracteristica de înregistrare în jurnal de pe smartphone-ul dvs. a fost activă timp de %1$s (de) zile. AplicaÈ›ia È™terge automat înregistrările mai vechi din jurnal, întrucât acestea nu mai sunt relevante pentru prevenirea infectării."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk_past">"Modul în care a fost calculat riscul dvs."</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"Modul în care este calculat riscul dvs."</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Deoarece nu aÈ›i activat un timp suficient de lung înregistrarea în jurnal a expunerilor, nu am putut calcula riscul dvs. de infectare."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"ÃŽnregistrarea în jurnal a expunerilor dvs. nu a putut fi actualizată timp de peste 24 de ore."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -375,7 +360,7 @@ <string name="risk_details_explanation_dialog_title">"InformaÈ›ii despre funcÈ›ionalitatea de înregistrare în jurnal a expunerilor"</string> <!-- YTXT: one time risk explanation dialog - pointing to the faq page for more information--> <string name="risk_details_explanation_dialog_faq_body">"Pentru mai multe informaÈ›ii, consultaÈ›i pagina noastră de întrebări frecvente."</string> - <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> + <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> <string name="risk_details_explanation_faq_link">"https://www.coronawarn.app/en/faq/#encounter_but_green"</string> <!-- XHED: risk details - deadman notification title --> @@ -403,7 +388,7 @@ <!-- XBUT: onboarding - forward and deny --> <string name="onboarding_button_disable">"Nu activaÈ›i"</string> <!-- XBUT: onboarding - forward and allow --> - <string name="onboarding_button_enable">"ActivaÈ›i"</string> + <string name="onboarding_button_enable">"Activare"</string> <!-- XBUT: onboarding - back and cancel --> <string name="onboarding_button_cancel">"Anulare"</string> <!-- XBUT: onboarding - next --> @@ -421,13 +406,13 @@ <!-- YTXT: onboarding(together) - inform about the app --> <string name="onboarding_body">"TransformaÈ›i-vă smartphone-ul într-un sistem de avertizare împotriva coronavirusului. ObÈ›ineÈ›i un sumar al stării de risc È™i aflaÈ›i dacă aÈ›i intrat în contact strâns cu persoane diagnosticate cu COVID-19 în ultimele 14 zile."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"AplicaÈ›ia înregistrează în jurnal întâlnirile dintre persoane prin dispozitivele acestora, care schimbă ID-uri aleatorii criptate, fără a accesa niciun fel de date personale."</string> + <string name="onboarding_body_emphasized">"AplicaÈ›ia înregistrează în jurnal întâlnirile dintre persoane prin smartphone-urile acestora, care schimbă ID-uri aleatorii criptate, fără a accesa niciun fel de date personale."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"Un grup de persoane își utilizează smartphone-urile prin oraÈ™."</string> <!-- XACT: Onboarding (privacy) page title --> <string name="onboarding_privacy_accessibility_title">"Pagina de înregistrare 2 din 6: ConfidenÈ›ialitatea datelor. Urmează un text lung. Pentru a continua în orice moment, utilizaÈ›i butonul din partea de jos a ecranului."</string> <!-- XHED: onboarding(privacy) - title --> - <string name="onboarding_privacy_headline">"ConfidenÈ›ialitatea datelor"</string> + <string name="onboarding_privacy_headline">"ConfidenÈ›ialitate"</string> <!-- XACT: onboarding(privacy) - illustraction description, header image --> <string name="onboarding_privacy_illustration_description">"O femeie utilizează Corona-Warn-App pe smartphone. O pictogramă care arată un lacăt pe un fundal în formă de scut simbolizează datele criptate."</string> @@ -460,13 +445,13 @@ <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"AÈ›i dezactivat actualizările în fundal pentru aplicaÈ›ia Corona-Warn. ActivaÈ›i actualizările în fundal pentru a utiliza înregistrarea automată în jurnal a expunerilor. Dacă nu activaÈ›i actualizările în fundal, puteÈ›i porni doar manual din aplicaÈ›ie înregistrarea în jurnal a expunerilor. PuteÈ›i activa actualizările în fundal pentru aplicaÈ›ie din setările dispozitivului dvs."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> - <string name="onboarding_background_fetch_dialog_button_positive">"DeschideÈ›i setările dispozitivului"</string> + <string name="onboarding_background_fetch_dialog_button_positive">"DeschideÈ›i configurările dispozitivului"</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, continue in app --> <string name="onboarding_background_fetch_dialog_button_negative">"PorniÈ›i manual înregistrarea în jurnal a expunerilor"</string> <!-- XACT: onboarding(tracing) - dialog about energy optimized header text --> <string name="onboarding_energy_optimized_dialog_headline">"PermiteÈ›i activitatea în fundal cu prioritate"</string> <!-- YMSI: onboarding(tracing) - dialog about energy optimized --> - <string name="onboarding_energy_optimized_dialog_body">"ActivaÈ›i activitatea în fundal cu prioritate pentru a-i permite aplicaÈ›iei să vă determine starea de risc în fundal în orice moment (setare recomandată). Această opÈ›iune dezactivează optimizarea vieÈ›ii bateriei doar pentru aplicaÈ›ia Corona-Warn. Nu ne aÈ™teptăm ca aceasta să cauzeze o reducere semnificativă a duratei de viață a bateriei.\n\nDacă nu permiteÈ›i această setare, vă recomandăm să deschideÈ›i manual aplicaÈ›ia cel puÈ›in o dată la 24 de ore."</string> + <string name="onboarding_energy_optimized_dialog_body">"ActivaÈ›i activitatea în fundal cu prioritate pentru a-i permite aplicaÈ›iei să vă determine starea de risc în fundal în orice moment (setare recomandată). Această opÈ›iune dezactivează optimizarea vieÈ›ii bateriei doar pentru aplicaÈ›ia Corona-Warn. Nu ne aÈ™teptăm ca aceasta să cauzeze o reducere semnificativă a duratei de viață a smartphone-ului.\n\nDacă nu permiteÈ›i această setare, vă recomandăm să deschideÈ›i manual aplicaÈ›ia cel puÈ›in o dată la 24 de ore."</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, open device settings --> <string name="onboarding_energy_optimized_dialog_button_positive">"PermiteÈ›i"</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, continue in app --> @@ -482,17 +467,17 @@ <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"PermiteÈ›i accesul la locaÈ›ie"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> - <string name="onboarding_tracing_location_body">"LocaÈ›ia dvs. nu poate fi accesată. Google È™i/sau Android necesită acces la locaÈ›ia dispozitivului dvs. pentru a utiliza Bluetooth-ul."</string> + <string name="onboarding_tracing_location_body">"LocaÈ›ia dvs. nu poate fi accesată. Google È™i/sau Android necesită acces la locaÈ›ia smartphone-ului dvs. pentru a utiliza Bluetooth-ul."</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> - <string name="onboarding_tracing_location_button">"DeschideÈ›i setările dispozitivului"</string> + <string name="onboarding_tracing_location_button">"DeschideÈ›i configurările dispozitivului"</string> <!-- XACT: Onboarding (test) page title --> - <string name="onboarding_test_accessibility_title">"Pagina de înregistrare 5 din 6: Dacă sunteÈ›i diagnosticat cu COVID-19…"</string> + <string name="onboarding_test_accessibility_title">"Pagina de înregistrare 5 din 6: Dacă sunteÈ›i diagnosticat cu COVID-19"</string> <!-- XHED: onboarding(test) - about positive tests --> <string name="onboarding_test_headline">"Dacă sunteÈ›i diagnosticat cu COVID-19…"</string> <!-- XHED: onboarding(test) - two/three line headline under an illustration --> - <string name="onboarding_test_subtitle">"…vă rugăm să raportaÈ›i acest lucru în Corona-Warn-App. ÃŽmpărtășirea rezultatului testului este voluntară È™i securizată. Vă rugăm să faceÈ›i acest lucru pentru binele celorlalÈ›i."</string> + <string name="onboarding_test_subtitle">"… vă rugăm să raportaÈ›i acest lucru în Corona-Warn-App. ÃŽmpărtășirea rezultatului testului este voluntară È™i securizată. Vă rugăm să faceÈ›i acest lucru pentru binele celorlalÈ›i."</string> <!-- YTXT: onboarding(test) - explain test --> - <string name="onboarding_test_body">"Notificarea dvs. este criptată în mod securizat È™i este prelucrată pe un server sigur. Persoanele ale căror ID-uri aleatorii criptate au fost colectate de dispozitivul dvs. vor primi acum o avertizare împreună cu informaÈ›ii despre paÈ™ii de urmat."</string> + <string name="onboarding_test_body">"Notificarea dvs. este criptată în mod securizat È™i este prelucrată pe un server sigur. Persoanele ale căror ID-uri aleatorii criptate au fost colectate de smartphone-ul dvs. vor primi acum o avertizare împreună cu informaÈ›ii despre paÈ™ii de urmat."</string> <!-- XACT: onboarding(test) - illustraction description, header image --> <string name="onboarding_test_illustration_description">"Un diagnostic de test pozitiv criptat este transmis la sistem, iar acesta va avertiza apoi ceilalÈ›i utilizatori."</string> <!-- XACT: Onboarding (datashare) page title --> @@ -524,11 +509,11 @@ <!-- XHED: settings - settings overview page title --> <string name="settings_title">"Setări"</string> <!-- XTXT: settings - on, like a label next to a setting --> - <string name="settings_on">"Pornite"</string> + <string name="settings_on">"Pornită"</string> <!-- XTXT: settings - off, like a label next to a setting --> - <string name="settings_off">"Oprite"</string> + <string name="settings_off">"Oprită"</string> <!-- XHED: settings(tracing) - page title --> - <string name="settings_tracing_title">"ÃŽnregistrarea expunerilor"</string> + <string name="settings_tracing_title">"ÃŽnregistrarea în jurnal a expunerilor"</string> <!-- XHED: settings(tracing) - headline bellow illustration --> <string name="settings_tracing_headline">"Modul în care funcÈ›ionează înregistrarea în jurnal a expunerilor"</string> <!-- XTXT: settings(tracing) - explain text in settings overview under headline --> @@ -542,19 +527,19 @@ <!-- YTXT: settings(tracing) - explains tracings --> <string name="settings_tracing_body_text">"Trebuie să activaÈ›i caracteristica de înregistrare în jurnal a expunerilor, pentru ca aplicaÈ›ia să poată determina dacă aveÈ›i risc de infectare după ce aÈ›i întâlnit un utilizator al aplicaÈ›iei care este infectat. Caracteristica de înregistrare în jurnal a expunerilor funcÈ›ionează la nivel transnaÈ›ional, ceea ce înseamnă că orice expunere posibilă care îi implică pe utilizatori este detectată È™i de alte aplicaÈ›ii oficiale împotriva coronavirusului.\n\nCaracteristica de înregistrare în jurnal a expunerilor funcÈ›ionează astfel: smartphone-ul dvs. primeÈ™te prin Bluetooth ID-uri aleatorii criptate ale altor utilizatori È™i transmite propriul dvs. ID aleatoriu către smartphone-urile acestora. ÃŽn fiecare zi, aplicaÈ›ia descarcă o listă ce conÈ›ine ID-uri aleatorii – împreună cu orice alte informaÈ›ii voluntare despre debutul simptomelor – ale tuturor utilizatorilor testaÈ›i pozitiv la virus È™i care au împărtășit voluntar aceste informaÈ›ii prin aplicaÈ›ia lor. Apoi, lista este comparată cu ID-urile aleatorii ale altor utilizatori care au fost înregistrate de smartphone-ul dvs., pentru a calcula probabilitatea ca È™i dvs. să fi fost infectat È™i să vă avertizeze dacă este necesar. PuteÈ›i utiliza comutatorul pentru a dezactiva în orice moment înregistrarea în jurnal a expunerilor.\n\nAplicaÈ›ia nu colectează niciodată date personale, precum numele, adresa sau locaÈ›ia dvs., iar aceste informaÈ›ii nu sunt transmise niciodată altor utilizatori. Nu se pot utiliza ID-urile aleatorii pentru a trage concluzii despre persoane individuale."</string> <!-- XTXT: settings(tracing) - status next to switch under title --> - <string name="settings_tracing_status_active">"Activă"</string> + <string name="settings_tracing_status_active">"Activ"</string> <!-- XTXT: settings(tracing) - status next to switch under title --> - <string name="settings_tracing_status_inactive">"Oprită"</string> + <string name="settings_tracing_status_inactive">"Oprit"</string> <!-- XTXT: settings(tracing) - status next to switch under title --> <string name="settings_tracing_status_restricted">"RestricÈ›ionată"</string> <!-- XTXT: settings(tracing) - shows status under header in home, no internet --> <string name="settings_tracing_body_connection_inactive">"Fără conexiune la internet"</string> <!-- XTXT: settings(tracing) - shows status under header in home, no bluetooth --> - <string name="settings_tracing_body_bluetooth_inactive">"Bluetooth dezactivat"</string> + <string name="settings_tracing_body_bluetooth_inactive">"Bluetooth oprit"</string> <!--XHED : settings(tracing) - headline on card about the current status and what to do --> - <string name="settings_tracing_status_bluetooth_headline">"ActivaÈ›i Bluetooth-ul"</string> + <string name="settings_tracing_status_bluetooth_headline">"PorniÈ›i Bluetooth"</string> <!-- XTXT: settings(tracing) - explains user what to do on card if bluetooth is disabled --> - <string name="settings_tracing_status_bluetooth_body">"Bluetooth-ul trebuie să fie activat pentru ca înregistrarea în jurnal a expunerilor să funcÈ›ioneze. ActivaÈ›i Bluetooth-ul în setările dispozitivului."</string> + <string name="settings_tracing_status_bluetooth_body">"Bluetooth-ul trebuie să fie activat pentru ca înregistrarea în jurnal a expunerilor să funcÈ›ioneze. ActivaÈ›i Bluetooth-ul în configurările dispozitivului."</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card --> <string name="settings_tracing_status_bluetooth_button">"DeschideÈ›i configurările dispozitivului"</string> <!--XHED : settings(tracing) - headline on card about the current status and what to do --> @@ -564,11 +549,11 @@ <!-- XTXT: settings(tracing) - explains user what to do on card if location is disabled: URL --> <string name="settings_tracing_status_location_body_url">"https://www.coronawarn.app/en/faq/#android_location"</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card - location --> - <string name="settings_tracing_status_location_button">"DeschideÈ›i setările dispozitivului"</string> + <string name="settings_tracing_status_location_button">"DeschideÈ›i configurările dispozitivului"</string> <!--XHED : settings(tracing) - headline on card about the current status and what to do --> <string name="settings_tracing_status_connection_headline">"DeschideÈ›i conexiunea la internet"</string> <!-- XTXT: settings(tracing) - explains user what to do on card if connection is disabled --> - <string name="settings_tracing_status_connection_body">"ÃŽnregistrarea în jurnal a expunerilor necesită conexiunea la internet pentru a calcula expunerile. PorniÈ›i reÈ›eaua Wi-Fi sau datele mobile din setările dispozitivului dvs."</string> + <string name="settings_tracing_status_connection_body">"ÃŽnregistrarea în jurnal a expunerilor necesită conexiunea la internet pentru a calcula expunerile. PorniÈ›i reÈ›eaua WIFI sau datele celulare din setările dispozitivului dvs."</string> <!-- XBUT: settings(tracing) - go to operating system settings button on card --> <string name="settings_tracing_status_connection_button">"DeschideÈ›i configurările dispozitivului"</string> <!-- XTXT: settings(tracing) - explains the circle progress indicator to the right with the current value --> @@ -612,7 +597,7 @@ <!-- XTXT: settings(notification) - next to a switch --> <string name="settings_notifications_subtitle_update_test">"Starea testului COVID-19"</string> <!-- XBUT: settings(notification) - go to operating settings --> - <string name="settings_notifications_button_open_settings">"DeschideÈ›i setările dispozitivului"</string> + <string name="settings_notifications_button_open_settings">"DeschideÈ›i configurările dispozitivului"</string> <!-- XACT: main (overview) - illustraction description, explanation image, displays notificatin status, active --> <string name="settings_notifications_illustration_description_active">"O femeie primeÈ™te o notificare de la Corona-Warn-App."</string> <!-- XACT: main (overview) - illustraction description, explanation image, displays notificatin status, inactive --> @@ -656,7 +641,7 @@ <!-- XTXT: settings(background priority) - explains user what to do on card if background priority is enabled --> <string name="settings_background_priority_card_body">"PuteÈ›i activa È™i dezactiva activitatea în fundal cu prioritate din setările dispozitivului."</string> <!-- XBUT: settings(background priority) - go to operating system settings button on card --> - <string name="settings_background_priority_card_button">"DeschideÈ›i setările dispozitivului"</string> + <string name="settings_background_priority_card_button">"DeschideÈ›i configurările dispozitivului"</string> <!-- XHED : settings(background priority) - headline on card about the current status and what to do --> <string name="settings_background_priority_card_headline">"ModificaÈ›i activitatea în fundal cu prioritate"</string> @@ -675,11 +660,11 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) este un organism federal de sănătate publică din Germania. RKI a publicat aplicaÈ›ia Corona-Warn în numele Guvernului Federal. AplicaÈ›ia are drept scop să completeze sub formă digitală măsurile de sănătate publică deja introduse: distanÈ›area socială, igiena È™i purtarea măștii."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Persoanele care utilizează aplicaÈ›ia ajută la urmărirea È™i întreruperea lanÈ›urilor de infectare. AplicaÈ›ia salvează local, pe smartphone-ul dvs., întâlnirile cu alte persoane. SunteÈ›i notificat dacă aÈ›i întâlnit persoane care au fost diagnosticate ulterior cu COVID-19. Identitatea È™i confidenÈ›ialitatea dvs. sunt protejate întotdeauna."</string> + <string name="information_about_body">"Persoanele care utilizează aplicaÈ›ia ajută la urmărirea È™i întreruperea lanÈ›urilor de infectare. AplicaÈ›ia salvează local, pe dispozitivul dvs., întâlnirile cu alte persoane. SunteÈ›i notificat dacă aÈ›i întâlnit persoane care au fost diagnosticate ulterior cu COVID-19. Identitatea È™i confidenÈ›ialitatea dvs. sunt protejate întotdeauna."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Un grup de persoane își utilizează smartphone-urile prin oraÈ™."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> - <string name="information_privacy_title">"ConfidenÈ›ialitatea datelor"</string> + <string name="information_privacy_title">"ConfidenÈ›ialitate"</string> <!-- XACT: describes illustration --> <string name="information_privacy_illustration_description">"O femeie utilizează Corona-Warn-App pe smartphone. O pictogramă care arată un lacăt pe un fundal în formă de scut simbolizează datele criptate."</string> <!-- XTXT: Path to the full blown privacy html, to translate it exchange "_de" to "_en" and provide the corresponding html file --> @@ -811,7 +796,7 @@ <!-- XHED: Dialog headline QR Scan permission rationale --> <string name="submission_qr_code_scan_permission_rationale_dialog_headline">"Este necesară autorizaÈ›ia pentru camera foto"</string> <!-- YTXT: Dialog Body text for QR Scan permission rationale --> - <string name="submission_qr_code_scan_permission_rationale_dialog_body">"PermiteÈ›i-i aplicaÈ›iei să utilizeze camera foto pentru a scana codul QR."</string> + <string name="submission_qr_code_scan_permission_rationale_dialog_body">"PermiteÈ›i-i aplicaÈ›iei să utilizeze camera pentru a scana codul QR."</string> <!-- XBUT: Dialog(QR Scan permission rationale) - positive button (right) --> <string name="submission_qr_code_scan_permission_rationale_dialog_button_positive">"PermiteÈ›i"</string> <!-- XBUT: Dialog(QR Scan permission rationale) - negative button (left) --> @@ -827,7 +812,7 @@ <!-- QR Code Scan Invalid Dialog --> <!-- XHED: Dialog headline for invalid QR code --> - <string name="submission_qr_code_scan_invalid_dialog_headline">"Cod QR nevalabil"</string> + <string name="submission_qr_code_scan_invalid_dialog_headline">"Codul QR este nevalabil"</string> <!-- YTXT: Dialog Body text for invalid QR code --> <string name="submission_qr_code_scan_invalid_dialog_body">"Codul QR este nevalabil sau a fost deja înregistrat pe un alt smartphone. VeÈ›i primi rezultatul testului dvs. de la centrul sau laboratorul de testare, indiferent de valabilitatea codului QR. Dacă sunteÈ›i diagnosticat cu COVID-19, direcÈ›ia de sănătate publică va fi notificată prin canalul de comunicare prevăzut în mod legal È™i vă va contacta."</string> <!-- XBUT: Dialog(Invalid QR code) - positive button (right) --> @@ -856,13 +841,13 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Actualizare"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Eliminare test"</string> + <string name="submission_test_result_pending_remove_test_button">"Ștergere test"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Rezultatul testului dvs."</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"Rezultatul de laborator nu indică o confirmare a infecÈ›iei cu coronavirusul SARS-CoV-2.\n\nȘtergeÈ›i testul din Corona-Warn-App pentru a salva un nou cod de test aici dacă este necesar."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Eliminare test"</string> + <string name="submission_test_result_negative_remove_test_button">"Ștergere test"</string> <!-- XHED: Page headline for other warnings screen --> <string name="submission_test_result_positive_steps_warning_others_heading">"AvertizaÈ›i-i pe ceilalÈ›i"</string> <!-- YTXT: Body text for for other warnings screen--> @@ -916,7 +901,7 @@ <!-- XHED: Page title for menu at the start of the submission process --> <string name="submission_intro_title">"AÈ›i fost testat?"</string> <!-- XHED: Page headline for menu the at start of the submission process --> - <string name="submission_intro_headline">"Iată cum funcÈ›ionează Corona-Warn-App"</string> + <string name="submission_intro_headline">"Modul în care funcÈ›ionează Corona-Warn-App"</string> <!-- YTXT: submission introduction text --> <string name="submission_intro_text">"Pentru ca aplicaÈ›ia să funcÈ›ioneze bine, ne bazăm pe susÈ›inerea persoanelor care au fost diagnosticate cu COVID-19.\n\nDeoarece sunt schimbate doar ID-uri aleatorii criptate, dvs. veÈ›i rămâne anonim. PuteÈ›i continua astfel:"</string> <!-- XBUT: Submission introduction next button--> @@ -1001,7 +986,7 @@ <!-- YTXT: submission done further info bullet points --> <string-array name="submission_done_further_info_bullet_points"> <item>"Perioada de carantină este de obicei de 14 zile. ObservaÈ›i ce simptome aveÈ›i È™i monitorizaÈ›i dezvoltarea acestora."</item> - <item>"Autoritatea de sănătate publică din regiunea dvs. vă va solicita să întocmiÈ›i o listă a persoanelor cu care aÈ›i intrat în contact. Aceasta trebuie să cuprindă toate persoanele cu care aÈ›i avut contact strâns (mai puÈ›in de 2 metri, conversaÈ›ii față în față) timp de peste 15 minute începând cu două zile înainte de a prezenta simptome."</item> + <item>"DirecÈ›ia de sănătate publică vă va solicita să întocmiÈ›i o listă a persoanelor cu care aÈ›i intrat în contact. Aceasta trebuie să cuprindă toate persoanele cu care aÈ›i avut contact strâns (mai puÈ›in de 2 metri, conversaÈ›ii față în față) timp de peste 15 minute începând cu două zile înainte de a prezenta simptome."</item> <item>"LuaÈ›i în considerare în special persoanele care nu vor fi notificate direct de aplicaÈ›ie dacă nu au un smartphone sau dacă nu È™i-au instalat aplicaÈ›ia."</item> <item>"Chiar È™i când nu mai aveÈ›i simptome È™i vă simÈ›iÈ›i din nou bine, tot puteÈ›i să fiÈ›i contagios."</item> </string-array> @@ -1083,7 +1068,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"Cu peste 2 săptămâni în urmă"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"Nu comentez"</string> + <string name="submission_symptom_verify">"Nu răspund"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1369,6 +1354,6 @@ <!-- YMSW: Subtitle for the interoperability onboarding if country download fails --> <string name="interoperability_onboarding_list_subtitle_failrequest_no_network">"Se poate să fi pierdut conexiunea la internet. AsiguraÈ›i-vă că sunteÈ›i conectat la internet."</string> <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available --> - <string name="interoperability_onboarding_list_button_title_no_network">"DeschideÈ›i setările dispozitivului"</string> + <string name="interoperability_onboarding_list_button_title_no_network">"DeschideÈ›i configurările dispozitivului"</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values-tr/strings.xml b/Corona-Warn-App/src/main/res/values-tr/strings.xml index a21fb29d30dd9872683cc23d0b5f40d6c09b7b8d..143aabdff837caafe35e6d92a0027755de524b23 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Åžu ana dek hiçbir maruz kalma yok"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="other">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="zero">"Åžu ana dek hiçbir düşük riskli maruz kalma yok"</item> - <item quantity="two">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="few">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="many">"%1$s kez düşük riskli maruz kalma"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s maruz kalma"</item> - <item quantity="other">"%1$s maruz kalma"</item> - <item quantity="zero">"Åžu ana dek hiçbir maruz kalma yok"</item> - <item quantity="two">"%1$s maruz kalma"</item> - <item quantity="few">"%1$s maruz kalma"</item> - <item quantity="many">"%1$s maruz kalma"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Maruz kalma günlüğü son 14 günde %1$s gün etkindi."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -157,8 +131,6 @@ <string name="risk_card_body_open_daily">"Not: Risk durumunuzu güncellemek için lütfen uygulamayı her gün açın."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Güncelle"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"%1$s içinde güncelle"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"Maruz Kalma Günlüğünü EtkinleÅŸtir"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -167,23 +139,12 @@ <string name="risk_card_low_risk_headline">"Düşük Risk"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Daha Yüksek Risk"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="other">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="zero">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="two">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="few">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="many">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"Bilinmeyen Risk"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Maruz kalma günlüğünü uzun süredir etkinleÅŸtirmediÄŸiniz için enfeksiyon riskinizi hesaplayamadık."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"Maruz kalma günlüğü durduruldu"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"Son maruz kalma kontrolü:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"Son maruz kalma denetimi:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"Maruz kalma günlüğü oluÅŸturulamıyor"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> @@ -191,12 +152,38 @@ <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"Risk durumunuz 48 saatten uzun süredir güncellenmedi. Lütfen risk durumunuzu güncelleyin."</string> <!-- XHED: risk card - risk check failed headline, no internet connection --> - <string name="risk_card_check_failed_no_internet_headline">"Maruz kalma kontrolü baÅŸarısız oldu"</string> + <string name="risk_card_check_failed_no_internet_headline">"Maruz kalma denetimi baÅŸarısız oldu"</string> <!-- XTXT: risk card - risk check failed, please check your internet connection --> <string name="risk_card_check_failed_no_internet_body">"Rastgele kimliklerin sunucu ile senkronizasyonu baÅŸarısız oldu. Senkronizasyonu manüel olarak baÅŸlatabilirsiniz."</string> <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"Yeniden baÅŸlat"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">"Åžu ana dek hiçbir maruz kalma yok"</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"%1$d gün için düşük riskli maruz kalmalar"</item> + <item quantity="other">"%1$d gün için düşük riskli maruz kalmalar"</item> + <item quantity="zero">"%1$d gün için düşük riskli maruz kalmalar"</item> + <item quantity="two">"%1$d gün için düşük riskli maruz kalmalar"</item> + <item quantity="few">"%1$d gün için düşük riskli maruz kalmalar"</item> + <item quantity="many">"%1$d gün için düşük riskli maruz kalmalar"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">"Åžu ana dek hiçbir maruz kalma yok"</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"%1$d gün için artmış riskli maruz kalmalar"</item> + <item quantity="other">"%1$d gün için artmış riskli maruz kalmalar"</item> + <item quantity="zero">"%1$d gün için artmış riskli maruz kalmalar"</item> + <item quantity="two">"%1$d gün için artmış riskli maruz kalmalar"</item> + <item quantity="few">"%1$d gün için artmış riskli maruz kalmalar"</item> + <item quantity="many">"%1$d gün için artmış riskli maruz kalmalar"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"En son %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -277,11 +264,11 @@ <!-- XHED: App overview subtitle for glossary risk calculation --> <string name="main_overview_subtitle_glossary_calculation">"Maruz Kalma Denetimi"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Maruz kalma günlüğü verileri alınır ve diÄŸer kullanıcıların bildirilen enfeksiyonları ile senkronize edilir. Maruz kalma denetimi otomatik olarak yaklaşık iki saatte bir gerçekleÅŸtirilir."</string> + <string name="main_overview_body_glossary_calculation">"Maruz kalma günlüğü verileri alınır ve diÄŸer kullanıcıların bildirilen enfeksiyonları ile senkronize edilir. Risk durumunuz günde birkaç kez otomatik olarak kontrol edilir."</string> <!-- XHED: App overview subtitle for glossary contact --> - <string name="main_overview_subtitle_glossary_contact">"Maruz Kalmalar"</string> + <string name="main_overview_subtitle_glossary_contact">"Maruz Kalma Riski"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"COVID-19 tanısı konan kiÅŸilerle daha uzun süreyle ve yakın mesafede karşılaÅŸmalardır."</string> + <string name="main_overview_body_glossary_contact">"Pozitif test sonucunu uygulama üzerinden diÄŸer kullanıcılarla paylaÅŸan enfekte olan bir kiÅŸiye maruz kalma durumudur. Maruz kalmanın yüksek riskli olarak sınıflandırılması için süre, mesafe ve diÄŸer kiÅŸinin şüphelenilen enfeksiyon bulaÅŸtırma durumu ile ilgili belirli ölçütler karşılanmalıdır."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"Maruz Kalma Bildirimi"</string> <!-- YTXT: App overview body for glossary notifications --> @@ -289,7 +276,7 @@ <!-- XHED: App overview subtitle for glossary keys --> <string name="main_overview_subtitle_glossary_keys">"Rastgele Kimlik"</string> <!-- YTXT: App overview body for glossary keys --> - <string name="main_overview_body_glossary_keys">"Rastgele Kimlikler, rastgele oluÅŸturulan rakam ve harf kombinasyonlarıdır. Yakın mesafedeki cihazlar arasında deÄŸiÅŸtirilir. Rastgele Kimlikler belirli bir kiÅŸiyi izlemek üzere kullanılamaz ve 14 günün sonunda otomatik olarak silinir. COVID-19 tanısı konan kiÅŸiler son 14 güne kadar rastgele kimliklerinin uygulamanın diÄŸer kullanıcıları ile paylaşılmasını seçebilir."</string> + <string name="main_overview_body_glossary_keys">"Rastgele Kimlikler, rastgele oluÅŸturulan rakam ve harf kombinasyonlarıdır. Yakın mesafedeki akıllı telefonlar arasında deÄŸiÅŸtirilir. Rastgele Kimlikler belirli bir kiÅŸiyi izlemek üzere kullanılamaz ve 14 günün sonunda otomatik olarak silinir. COVID-19 tanısı konan kiÅŸiler son 14 güne kadar rastgele kimliklerinin uygulamanın diÄŸer kullanıcıları ile paylaşılmasını seçebilir."</string> <!-- XACT: main (overview) - illustraction description, explanation image --> <string name="main_overview_illustration_description">"Bir akıllı telefon, 1 ila 3 olarak numaralandırılmış farklı içerikleri gösterir."</string> <!-- XACT: App main page title --> @@ -338,13 +325,11 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged">"Enfeksiyon riskiniz yalnızca maruz kalma günlüğünün etkin olduÄŸu dönemler için hesaplanabilir. Bu nedenle günlüğe kaydetme özelliÄŸinin sürekli etkin kalması gerekir."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> - <string name="risk_details_information_body_period_logged_assessment">"Maruz kalma günlüğü son 14 günü kapsar. Bu süre boyunca cihazınızdaki günlüğe kaydetme özelliÄŸi %1$s gün etkindi. Uygulama, enfeksiyondan korunma için artık ilgili olmadığından daha eski kayıtları otomatik olarak siler."</string> + <string name="risk_details_information_body_period_logged_assessment">"Maruz kalma günlüğü son 14 günü kapsar. Bu süre boyunca akıllı telefonunuzdaki günlüğe kaydetme özelliÄŸi %1$s gün etkindi. Uygulama, enfeksiyondan korunma için artık ilgili olmadığından daha eski kayıtları otomatik olarak siler."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk_past">"Riskiniz bu ÅŸekilde hesaplandı"</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"Riskiniz bu ÅŸekilde hesaplanır"</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Maruz kalma günlüğünü uzun süredir etkinleÅŸtirmediÄŸiniz için enfeksiyon riskinizi hesaplayamadık."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"Maruz kalma günlüğünüz 24 saatten uzun süre için güncellenemedi."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -375,7 +360,7 @@ <string name="risk_details_explanation_dialog_title">"Maruz kalma günlüğü iÅŸlevi hakkında bilgi"</string> <!-- YTXT: one time risk explanation dialog - pointing to the faq page for more information--> <string name="risk_details_explanation_dialog_faq_body">"Daha fazla bilgi için lütfen SSS sayfamıza bakın."</string> - <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> + <!-- XLNK: FAQ URL pointing to the faq page in german. Need to use the URL for english for all other languages--> <string name="risk_details_explanation_faq_link">"https://www.coronawarn.app/en/faq/#encounter_but_green"</string> <!-- XHED: risk details - deadman notification title --> @@ -421,7 +406,7 @@ <!-- YTXT: onboarding(together) - inform about the app --> <string name="onboarding_body">"Akıllı telefonunuzu koronavirüs uyarı sistemine dönüştürün. Risk durumunuza iliÅŸkin genel bir bakış elde edin ve son 14 gün içinde COVID-19 tanısı konan herhangi biri ile yakın temasa geçip geçmediÄŸinizi öğrenin."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"Uygulama kiÅŸilerin cihazları arasında ÅŸifrelenmiÅŸ rastgele kimlikleri paylaÅŸarak karşılaÅŸma günlüğü oluÅŸturur ve bu sırada hiçbir kiÅŸisel veriye eriÅŸim saÄŸlanmaz."</string> + <string name="onboarding_body_emphasized">"Uygulama kiÅŸilerin akıllı telefonları arasında ÅŸifrelenmiÅŸ rastgele kimlikleri paylaÅŸarak karşılaÅŸma günlüğü oluÅŸturur ve bu sırada hiçbir kiÅŸisel veriye eriÅŸim saÄŸlanmaz."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"Bölgedeki bir grup insan akıllı telefonlarını kullanıyor."</string> <!-- XACT: Onboarding (privacy) page title --> @@ -466,7 +451,7 @@ <!-- XACT: onboarding(tracing) - dialog about energy optimized header text --> <string name="onboarding_energy_optimized_dialog_headline">"ÖnceliklendirilmiÅŸ arka plan aktivitesine izin ver"</string> <!-- YMSI: onboarding(tracing) - dialog about energy optimized --> - <string name="onboarding_energy_optimized_dialog_body">"Uygulamanın sürekli olarak arka planda risk durumunuzu belirlemesine izin vermek için önceliklendirilmiÅŸ arka plan aktivitesine izin verin (önerilir). Bu iÅŸlem, yalnızca Corona-Warn-App için pil ömrü optimizasyonunu devre dışı bırakır. Bu iÅŸlem sonunda cihazınızın pil ömründe kayda deÄŸer bir azalma yaÅŸanması beklenmemektedir.\n\nBu ayara izin vermek istemiyorsanız en fazla 24 saatlik aralıklarla Uygulamayı manüel olarak açmanızı öneririz."</string> + <string name="onboarding_energy_optimized_dialog_body">"Uygulamanın sürekli olarak arka planda risk durumunuzu belirlemesine izin vermek için önceliklendirilmiÅŸ arka plan aktivitesine izin verin (önerilir). Bu iÅŸlem, yalnızca Corona-Warn-App için pil ömrü optimizasyonunu devre dışı bırakır. Bu iÅŸlem sonunda akıllı telefonunuzun pil ömründe kayda deÄŸer bir azalma yaÅŸanması beklenmemektedir.\n\nBu ayara izin vermek istemiyorsanız en fazla 24 saatlik aralıklarla Uygulamayı manüel olarak açmanızı öneririz."</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, open device settings --> <string name="onboarding_energy_optimized_dialog_button_positive">"İzin Ver"</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, continue in app --> @@ -482,17 +467,17 @@ <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Konum eriÅŸimine izin ver"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> - <string name="onboarding_tracing_location_body">"Konumunuza eriÅŸilemiyor. Bluetooth\'u kullanmak için Google ve/veya Android\'in cihazınızın konumuna eriÅŸmesi gerekiyor."</string> + <string name="onboarding_tracing_location_body">"Konumunuza eriÅŸilemiyor. Bluetooth\'u kullanmak için Google ve/veya Android\'in akıllı telefonunuzun konumuna eriÅŸmesi gerekiyor."</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> <string name="onboarding_tracing_location_button">"Cihaz Ayarlarını Aç"</string> <!-- XACT: Onboarding (test) page title --> - <string name="onboarding_test_accessibility_title">"EtkinleÅŸtirme sayfası 5/6: Size COVID-19 tanısı konduysa..."</string> + <string name="onboarding_test_accessibility_title">"EtkinleÅŸtirme sayfası 5/6: COVID-19 Tanısı Aldıysanız"</string> <!-- XHED: onboarding(test) - about positive tests --> <string name="onboarding_test_headline">"COVID-19 tanısı aldıysanız..."</string> <!-- XHED: onboarding(test) - two/three line headline under an illustration --> <string name="onboarding_test_subtitle">"… lütfen bunu Corona-Warn-App\'te belirtin. Test sonuçlarınızı paylaÅŸmanız gönüllülük esasına dayalıdır ve güvenlidir. Lütfen herkesin saÄŸlığı için bunu yapın."</string> <!-- YTXT: onboarding(test) - explain test --> - <string name="onboarding_test_body">"Bildiriminiz güvenli olarak ÅŸifrelenir ve güvenli bir sunucuda iÅŸlenir. Cihazınızda ÅŸifrelenmiÅŸ rastgele kimlikleri toplanan kiÅŸilere bundan sonra atmaları gereken adımlarla birlikte bir uyarı gönderilir."</string> + <string name="onboarding_test_body">"Bildiriminiz güvenli olarak ÅŸifrelenir ve güvenli bir sunucuda iÅŸlenir. Akıllı telefonunuza ÅŸifrelenmiÅŸ rastgele kimlikleri toplanan kiÅŸilere bundan sonra atmaları gereken adımlarla birlikte bir uyarı gönderilir."</string> <!-- XACT: onboarding(test) - illustraction description, header image --> <string name="onboarding_test_illustration_description">"ÅžifrelenmiÅŸ bir pozitif test tanısı sisteme aktarılır ve diÄŸer kullanıcılar uyarılır."</string> <!-- XACT: Onboarding (datashare) page title --> @@ -675,7 +660,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI), Almanya\'nın federal kamu saÄŸlığı kurumudur. RKI, Federal Hükûmet adına Corona-Warn-App uygulamasını yayınlamaktadır. Uygulama, daha önce açıklanan kamu saÄŸlığı önlemlerine iliÅŸkin dijital bir tamamlayıcı niteliÄŸindedir: sosyal mesafe, hijyen uygulamaları ve yüz maskeleri."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Uygulamayı kullanan herkes, enfeksiyon zincirlerinin takip edilmesine ve kırılmasına yardımcı olur. Uygulama, diÄŸer kiÅŸilerle karşılaÅŸmaları akıllı telefonunuzda yerel olarak kaydeder. Daha sonra COVID-19 tanısı konan kiÅŸilerle karşılaÅŸmışsanız size bildirim gönderilir. KimliÄŸiniz ve gizliliÄŸiniz daima koruma altındadır."</string> + <string name="information_about_body">"Uygulamayı kullanan kiÅŸiler, enfeksiyon zincirlerinin takip edilmesine ve kırılmasına yardımcı olur. Uygulama, diÄŸer kiÅŸilerle karşılaÅŸmaları cihazınızda yerel olarak kaydeder. Daha sonra COVID-19 tanısı konan kiÅŸilerle karşılaÅŸmışsanız size bildirim gönderilir. KimliÄŸiniz ve gizliliÄŸiniz daima koruma altındadır."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Bölgedeki bir grup insan akıllı telefonlarını kullanıyor."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -856,13 +841,13 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Güncelle"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Testi kaldır"</string> + <string name="submission_test_result_pending_remove_test_button">"Testi Sil"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Test Sonucunuz"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"Laboratuvar sonucuna göre koronavirüs SARS-CoV-2 olduÄŸunuza dair bir doÄŸrulama yok.\n\nGerekirse yeni bir test kodu kaydedebilmeniz için lütfen testi Corona-Warn-App\'ten silin."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Testi kaldır"</string> + <string name="submission_test_result_negative_remove_test_button">"Testi Sil"</string> <!-- XHED: Page headline for other warnings screen --> <string name="submission_test_result_positive_steps_warning_others_heading">"DiÄŸer Kullanıcıları Uyarın"</string> <!-- YTXT: Body text for for other warnings screen--> @@ -949,7 +934,7 @@ <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"TAN"</string> <!-- YTXT: Body text for TAN code dispatcher option --> - <string name="submission_dispatcher_tan_code_card_text">"TAN\'yi manuel olarak girerek kaydedin."</string> + <string name="submission_dispatcher_tan_code_card_text">"TAN\'yi manüel olarak girerek kaydedin."</string> <!-- YTXT: Dispatcher text for TELE-TAN option --> <string name="submission_dispatcher_card_tan_tele">"TAN Talebi"</string> <!-- YTXT: Body text for TELE_TAN dispatcher option --> @@ -969,7 +954,7 @@ <!-- XACT: other warning - illustration description, explanation image --> <string name="submission_positive_other_illustration_description">"Bir akıllı telefon ÅŸifrelenmiÅŸ pozitif test tanısını sisteme aktarır."</string> <!-- XHED: Title for the interop country list--> - <string name="submission_interoperability_list_title">"AÅŸağıdaki ülkeler, ülkeler arası maruz kalma günlüğüne katılmaktadır:"</string> + <string name="submission_interoperability_list_title">"AÅŸağıdaki ülkeler, uluslararası maruz kalma günlüğüne katılmaktadır:"</string> <!-- Submission Country Selector --> <!-- XHED: Page title for the submission country selection page --> @@ -1083,7 +1068,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"2 haftadan uzun süre önce"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"Beyan yok"</string> + <string name="submission_symptom_verify">"Bilgi yok"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1191,7 +1176,7 @@ <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> <string name="errors_risk_detection_limit_reached_title">"Sınıra ulaşıldı"</string> <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> - <string name="errors_risk_detection_limit_reached_description">"İşletim sisteminizce tanımlanan günlük azami kontrol sayısına ulaÅŸtığınız için bugün daha fazla maruz kalma kontrolü yapılamaz. Lütfen risk durumunuzu yarın yeniden kontrol edin."</string> + <string name="errors_risk_detection_limit_reached_description">"İşletim sisteminizce tanımlanan günlük azami denetim sayısına ulaÅŸtığınız için bugün daha fazla maruz kalma denetimi yapılamaz. Lütfen risk durumunuzu yarın yeniden kontrol edin."</string> <!-- #################################### Generic Error Messages ###################################### --> @@ -1315,7 +1300,7 @@ <string name="interoperability_title">"Ülkeler Arası\nMaruz Kalma Günlüğü"</string> <!-- XHED: Setting title of interoperability in the tracing settings view --> - <string name="settings_interoperability_title">"Ülkeler Arası Maruz Kalma Günlüğü"</string> + <string name="settings_interoperability_title">"Uluslararası Maruz Kalma Günlüğü"</string> <!-- XTXT: Settings description of the interoperability in the tracing settings view --> <string name="settings_interoperability_subtitle">"Katılımcı Ülkeler"</string> @@ -1331,7 +1316,7 @@ <string name="interoperability_configuration_information">"Uygulamanın gizlilik bildirimine (uluslararası maruz kalma günlüğü özelliÄŸi için gerçekleÅŸtirilen veri iÅŸleme hakkında bilgileri) menüde “Uygulama Bilgileri†> “Veri GizliliÄŸi†bölümünde bulabilirsiniz."</string> <!-- XHED: Sub header introducing interoperability in the tracing step of onboarding --> - <string name="interoperability_onboarding_title">"Uluslararası\nMaruz Kalma Günlüğü"</string> + <string name="interoperability_onboarding_title">"Ülkeler Arası\nMaruz Kalma Günlüğü"</string> <!-- YMSG: Onboarding tracing step first section in interoperability after the title --> <string name="interoperability_onboarding_first_section">"Bazı ülkeler uluslararası uyarıları etkinleÅŸtirmek üzere iÅŸ birliÄŸi yapmaktadır. DiÄŸer bir ifadeyle, tüm katılımcı ülkelerdeki resmi korona uygulamalarının kullanıcılarına maruz kalma olasılığınız da artık hesaba katılabilir."</string> <!-- YMSG: Onboarding tracing step second section in interoperability after the title --> diff --git a/Corona-Warn-App/src/main/res/values/colors.xml b/Corona-Warn-App/src/main/res/values/colors.xml index b0246b90d5372420f43b7d242d32815a7ef2cc14..403fa720890d2880fe4d80739918623c2314fcfc 100644 --- a/Corona-Warn-App/src/main/res/values/colors.xml +++ b/Corona-Warn-App/src/main/res/values/colors.xml @@ -16,6 +16,8 @@ <!-- Text --> <color name="colorTextPrimary1">#17191A</color> + <color name="colorTextPrimary1Stable">#17191A</color> + <color name="colorTextPrimary1InvertedStable">#FFFFFF</color> <color name="colorTextPrimary2">#9917191A</color> <color name="colorTextPrimary3">#4D17191A</color> <color name="colorTextEmphasizedButton">#FFFFFF</color> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index e09a80f74071ea86da20c5707962cdda446c0ea1..77f5d6e104b9cc1499bb34f56e3d121ef8e0d273 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -35,12 +35,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -130,26 +124,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"No exposure up to now"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s exposure with low risk"</item> - <item quantity="other">"%1$s exposures with low risk"</item> - <item quantity="zero">"No exposure with low risk so far"</item> - <item quantity="two">"%1$s exposures with low risk"</item> - <item quantity="few">"%1$s exposures with low risk"</item> - <item quantity="many">"%1$s exposures with low risk"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s exposure"</item> - <item quantity="other">"%1$s exposures"</item> - <item quantity="zero">"No exposure up to now"</item> - <item quantity="two">"%1$s exposures"</item> - <item quantity="few">"%1$s exposures"</item> - <item quantity="many">"%1$s exposures"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Exposure logging was active for %1$s of the past 14 days."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -162,8 +136,6 @@ <string name="risk_card_body_open_daily">"Note: Please open the app daily to update your risk status."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Update"</string> - <!-- XBUT: risk card - update risk with time display --> - <string name="risk_card_button_cooldown">"Update in %1$s"</string> <!-- XBUT: risk card - activate tracing --> <string name="risk_card_button_enable_tracing">"Activate Exposure Logging"</string> <!-- XTXT: risk card - tracing is off, user should activate to get an updated risk level --> @@ -172,17 +144,6 @@ <string name="risk_card_low_risk_headline">"Low Risk"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Increased Risk"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s day since the last encounter"</item> - <item quantity="other">"%1$s days since the last encounter"</item> - <item quantity="zero">"%1$s days since the last encounter"</item> - <item quantity="two">"%1$s days since the last encounter"</item> - <item quantity="few">"%1$s days since the last encounter"</item> - <item quantity="many">"%1$s days since the last encounter"</item> - </plurals> - <!-- XHED: risk card - unknown risk headline --> - <string name="risk_card_unknown_risk_headline">"Unknown Risk"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> <string name="risk_card_unknown_risk_body">"Since you have not activated exposure logging for long enough, we could not calculate your risk of infection."</string> <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> @@ -202,6 +163,32 @@ <!-- XTXT: risk card - risk check failed, restart button --> <string name="risk_card_check_failed_no_internet_restart_button">"Restart"</string> + <!-- XTXT: risk card - Low risk state - No days with low risk encounters --> + <string name="risk_card_low_risk_no_encounters_body">"No exposure up to now"</string> + <!-- XTXT: risk card - Low risk state - Days with low risk encounters --> + <plurals name="risk_card_low_risk_encounter_days_body"> + <item quantity="one">"Exposures with low risk on %1$d day"</item> + <item quantity="other">"Exposures with low risk on %1$d days"</item> + <item quantity="zero">"Exposures with low risk on %1$d days"</item> + <item quantity="two">"Exposures with low risk on %1$d days"</item> + <item quantity="few">"Exposures with low risk on %1$d days"</item> + <item quantity="many">"Exposures with low risk on %1$d days"</item> + </plurals> + + <!-- XTXT: risk card - High risk state - No days with high risk encounters --> + <string name="risk_card_high_risk_no_encounters_body">"No exposure up to now"</string> + <!-- XTXT: risk card - High risk state - Days with high risk encounters --> + <plurals name="risk_card_high_risk_encounter_days_body"> + <item quantity="one">"Exposures on %1$d day with increased risk"</item> + <item quantity="other">"Exposures on %1$d days with increased risk"</item> + <item quantity="zero">"Exposures on %1$d days with increased risk"</item> + <item quantity="two">"Exposures on %1$d days with increased risk"</item> + <item quantity="few">"Exposures on %1$d days with increased risk"</item> + <item quantity="many">"Exposures on %1$d days with increased risk"</item> + </plurals> + <!-- XTXT: risk card - High risk state - Most recent date with high risk --> + <string name="risk_card_high_risk_most_recent_body">"Most recently on %1$s"</string> + <!-- #################################### Risk Card - Progress ###################################### --> @@ -256,7 +243,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"Exposure Logging"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"Exposure logging is one of three central features of the app. Once you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string> + <string name="main_overview_body_tracing">"Exposure logging is one of the three central features of the app. When you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Risk of Infection"</string> <!-- YTXT: App overview body text about risk levels --> @@ -282,11 +269,11 @@ <!-- XHED: App overview subtitle for glossary risk calculation --> <string name="main_overview_subtitle_glossary_calculation">"Exposure Check"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Exposure log data is retrieved and synchronized with reported infections of other users. The exposure check is performed automatically about every two hours."</string> + <string name="main_overview_body_glossary_calculation">"Exposure log data is retrieved and synchronized with reported infections of other users. Your risk is checked automatically several times per day."</string> <!-- XHED: App overview subtitle for glossary contact --> - <string name="main_overview_subtitle_glossary_contact">"Exposures"</string> + <string name="main_overview_subtitle_glossary_contact">"Exposure Risk"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"Encounters over an extended period and in close proximity to a person diagnosed with COVID-19."</string> + <string name="main_overview_body_glossary_contact">"Exposure to an infected person who has shared their positive test results with others through the app. An exposure must meet certain criteria with regard to duration, distance, and suspected infectiousness of the other person to be classified as a high-risk exposure."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"Exposure Notification"</string> <!-- YTXT: App overview body for glossary notifications --> @@ -294,7 +281,7 @@ <!-- XHED: App overview subtitle for glossary keys --> <string name="main_overview_subtitle_glossary_keys">"Random ID"</string> <!-- YTXT: App overview body for glossary keys --> - <string name="main_overview_body_glossary_keys">"Random IDs are combinations of digits and letters generated randomly. They are exchanged between devices in close proximity. Random IDs cannot be traced to a specific person and are automatically deleted after 14 days. Persons diagnosed with COVID-19 can opt to share their random IDs of up to the last 14 days with other app users."</string> + <string name="main_overview_body_glossary_keys">"Random IDs are combinations of digits and letters generated randomly. They are exchanged between smartphones in close proximity. Random IDs cannot be traced to a specific person and are automatically deleted after 14 days. Persons diagnosed with COVID-19 can opt to share their random IDs of up to the last 14 days with other app users."</string> <!-- XACT: main (overview) - illustraction description, explanation image --> <string name="main_overview_illustration_description">"A smartphone displays various content, numbered 1 to 3."</string> <!-- XACT: App main page title --> @@ -343,13 +330,11 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged">"Your risk of infection can be calculated only for periods during which exposure logging was active. The logging feature should therefore remain active permanently."</string> <!-- XHED: risk details - infection period logged information body, below behaviors --> - <string name="risk_details_information_body_period_logged_assessment">"Exposure logging covers the past 14 days. During this time, the logging feature on your device was active for %1$s days. The app automatically deletes older logs, as these are no longer relevant for infection prevention."</string> + <string name="risk_details_information_body_period_logged_assessment">"Exposure logging covers the past 14 days. During this time, the logging feature on your smartphone was active for %1$s days. The app automatically deletes older logs, as these are no longer relevant for infection prevention."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk_past">"This is how your risk was calculated"</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> <string name="risk_details_subtitle_infection_risk">"This is how your risk is calculated"</string> - <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> - <string name="risk_details_information_body_unknown_risk">"Since you have not activated exposure logging for long enough, we could not calculate your risk of infection."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"Your exposure logging could not be updated for more than 24 hours."</string> <!-- YTXT: risk details - low risk explanation text --> @@ -426,7 +411,7 @@ <!-- YTXT: onboarding(together) - inform about the app --> <string name="onboarding_body">"Turn your smartphone into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their devices, whereby no personal data whatsoever is accessed."</string> + <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their smartphones, whereby no personal data whatsoever is accessed."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XACT: Onboarding (privacy) page title --> @@ -471,7 +456,7 @@ <!-- XACT: onboarding(tracing) - dialog about energy optimized header text --> <string name="onboarding_energy_optimized_dialog_headline">"Allow prioritized background activity"</string> <!-- YMSI: onboarding(tracing) - dialog about energy optimized --> - <string name="onboarding_energy_optimized_dialog_body">"Enable prioritized background activity to allow the App to determine your risk status in the background any time (recommended). This disables battery life optimization for the Corona-Warn-App only. We do not expect this to cause a significant decrease in your device\'s battery life.\n\nIf you do not allow this setting, we recommend you to open the App manually at least once every 24 hours."</string> + <string name="onboarding_energy_optimized_dialog_body">"Enable prioritized background activity to allow the App to determine your risk status in the background any time (recommended). This disables battery life optimization for the Corona-Warn-App only. We do not expect this to cause a significant decrease in your smartphone\'s battery life.\n\nIf you do not allow this setting, we recommend you to open the App manually at least once every 24 hours."</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, open device settings --> <string name="onboarding_energy_optimized_dialog_button_positive">"Allow"</string> <!-- XBUT: onboarding(tracing) - dialog about energy optimized, continue in app --> @@ -487,17 +472,17 @@ <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Allow location access"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> - <string name="onboarding_tracing_location_body">"Your location cannot be accessed. Google and/or Android requires access to your device\'s location to use Bluetooth."</string> + <string name="onboarding_tracing_location_body">"Your location cannot be accessed. Google and/or Android requires access to your smartphone\'s location to use Bluetooth."</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> <string name="onboarding_tracing_location_button">"Open Device Settings"</string> <!-- XACT: Onboarding (test) page title --> - <string name="onboarding_test_accessibility_title">"Onboarding page 5 of 6: If you are diagnosed with COVID-19..."</string> + <string name="onboarding_test_accessibility_title">"Onboarding page 5 of 6: If You Are Diagnosed with COVID-19"</string> <!-- XHED: onboarding(test) - about positive tests --> <string name="onboarding_test_headline">"If you are diagnosed with COVID-19…"</string> <!-- XHED: onboarding(test) - two/three line headline under an illustration --> <string name="onboarding_test_subtitle">"… please report this in the Corona-Warn-App. Sharing your test results is voluntary and secure. Please do this for the sake of everyone\'s health."</string> <!-- YTXT: onboarding(test) - explain test --> - <string name="onboarding_test_body">"Your notification is encrypted securely and processed on a secure server. People whose encrypted random IDs your device has collected will now receive a warning along with information about what they should now do."</string> + <string name="onboarding_test_body">"Your notification is encrypted securely and processed on a secure server. People whose encrypted random IDs your smartphone has collected will now receive a warning along with information about what they should now do."</string> <!-- XACT: onboarding(test) - illustraction description, header image --> <string name="onboarding_test_illustration_description">"An encrypted positive test diagnosis is transmitted to the system, which will now warn other users."</string> <!-- XACT: Onboarding (datashare) page title --> @@ -680,7 +665,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) is Germany’s federal public health body. The RKI publishes the Corona-Warn-App on behalf of the Federal Government. The app is intended as a digital complement to public health measures already introduced: social distancing, hygiene, and face masks."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Whoever uses the app helps to trace and break chains of infection. The app saves encounters with other persons locally on your smartphone. You are notified if you have encountered persons who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> + <string name="information_about_body">"People who use the app help to trace and break chains of infection. The app saves encounters with other people locally on your device. You are notified if you have encountered people who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -889,13 +874,13 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Update"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Remove test"</string> + <string name="submission_test_result_pending_remove_test_button">"Delete Test"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Your Test Result"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"The laboratory result indicates no verification that you have coronavirus SARS-CoV-2.\n\nPlease delete the test from the Corona-Warn-App, so that you can save a new test code here if necessary."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Remove Test"</string> + <string name="submission_test_result_negative_remove_test_button">"Delete Test"</string> <!-- XHED: Page headline for other warnings screen --> <string name="submission_test_result_positive_steps_warning_others_heading">"Warn Others"</string> <!-- YTXT: Body text for for other warnings screen--> @@ -1138,7 +1123,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"More than 2 weeks ago"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"No statement"</string> + <string name="submission_symptom_verify">"No answer"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1170,7 +1155,7 @@ <!-- YTXT: Body text for submission status: negative --> <string name="submission_status_card_body_negative">"You have been diagnosed negative for SARS-CoV-2."</string> <!-- YTXT: Body text for submission status fetch failed --> - <string name="submission_status_card_body_failed">"Your test is more than 21 days old and is therefore no longer relevant. Please delete the text. You can then add another."</string> + <string name="submission_status_card_body_failed">"Your test is more than 21 days old and is therefore no longer relevant. Please delete the test. You can then add another."</string> <!-- XBUT: submission status card unregistered button --> <string name="submission_status_card_button_unregistered">"Learn More and Help"</string> <!-- XBUT: submission status card show results button --> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index 51dd9c9e93f2e565424502e1e487754f4d5dd475..9711d9301b5bf7ba424e212557896d7592e7e866 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -136,7 +136,6 @@ <style name="SixteenInclude"> <item name="android:padding">@dimen/card_padding</item> <item name="android:background">@drawable/card</item> - <item name="android:backgroundTint">@color/colorSemanticNeutralRisk</item> <item name="android:textColor">@color/colorStableLight</item> </style> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt index e06b40b3ed38a7e8d3a194f27ad2a003749bd293..d8199e64261e221a682a98b6c2e65ffecadf4055 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt @@ -1,16 +1,17 @@ package de.rki.coronawarnapp.appconfig -import de.rki.coronawarnapp.risk.RiskLevelData +import de.rki.coronawarnapp.risk.RiskLevelSettings +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController 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 io.mockk.mockkObject -import io.mockk.verify -import io.mockk.verifySequence import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestCoroutineScope import org.junit.jupiter.api.BeforeEach @@ -21,7 +22,8 @@ class ConfigChangeDetectorTest : BaseTest() { @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var taskController: TaskController - @MockK lateinit var riskLevelData: RiskLevelData + @MockK lateinit var riskLevelSettings: RiskLevelSettings + @MockK lateinit var riskLevelStorage: RiskLevelStorage private val currentConfigFake = MutableStateFlow(mockConfigId("initial")) @@ -29,11 +31,9 @@ class ConfigChangeDetectorTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - mockkObject(ConfigChangeDetector.RiskLevelRepositoryDeferrer) - every { ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() } just Runs - every { taskController.submit(any()) } just Runs every { appConfigProvider.currentConfig } returns currentConfigFake + coEvery { riskLevelStorage.clear() } just Runs } private fun mockConfigId(id: String): ConfigData { @@ -46,58 +46,59 @@ class ConfigChangeDetectorTest : BaseTest() { appConfigProvider = appConfigProvider, taskController = taskController, appScope = TestCoroutineScope(), - riskLevelData = riskLevelData + riskLevelSettings = riskLevelSettings, + riskLevelStorage = riskLevelStorage ) @Test fun `new identifier without previous one is ignored`() { - every { riskLevelData.lastUsedConfigIdentifier } returns null + every { riskLevelSettings.lastUsedConfigIdentifier } returns null createInstance().launch() - verify(exactly = 0) { + coVerify(exactly = 0) { taskController.submit(any()) - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() } } @Test fun `new identifier results in new risk level calculation`() { - every { riskLevelData.lastUsedConfigIdentifier } returns "I'm a new identifier" + every { riskLevelSettings.lastUsedConfigIdentifier } returns "I'm a new identifier" createInstance().launch() - verifySequence { - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + coVerifySequence { + riskLevelStorage.clear() taskController.submit(any()) } } @Test fun `same idetifier results in no op`() { - every { riskLevelData.lastUsedConfigIdentifier } returns "initial" + every { riskLevelSettings.lastUsedConfigIdentifier } returns "initial" createInstance().launch() - verify(exactly = 0) { + coVerify(exactly = 0) { taskController.submit(any()) - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() } } @Test fun `new emissions keep triggering the check`() { - every { riskLevelData.lastUsedConfigIdentifier } returns "initial" + every { riskLevelSettings.lastUsedConfigIdentifier } returns "initial" createInstance().launch() currentConfigFake.value = mockConfigId("Straw") currentConfigFake.value = mockConfigId("berry") - verifySequence { - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + coVerifySequence { + riskLevelStorage.clear() taskController.submit(any()) - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() taskController.submit(any()) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt index edebc20fd1c278450405d3ca202bd8ebd5721ab9..33977018ef71994627949c49d2765551a9fbf2e5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.AppFeaturesOuterClass import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -45,4 +46,27 @@ class CWAConfigMapperTest : BaseTest() { this.supportedCountries shouldBe emptyList() } } + + @Test + fun `app features are mapped`() { + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setAppFeatures( + AppFeaturesOuterClass.AppFeatures.newBuilder().apply { + addAppFeatures(AppFeaturesOuterClass.AppFeature.newBuilder().apply { }.build()) + } + ) + .build() + createInstance().map(rawConfig).apply { + appFeatures.size shouldBe 1 + } + } + + @Test + fun `app features being empty are handled`() { + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .build() + createInstance().map(rawConfig).apply { + appFeatures shouldBe emptyList() + } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt index c9459763b063371cb4d0630ac7b7ff0b483dd090..9f565b91265f00367c3ea0aeef807765c79f73e7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt @@ -47,7 +47,7 @@ class DefaultAppConfigSanityCheck : BaseTest() { fun `current default matches checksum`() { val config = context.assets.open(configName).readBytes() val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8) - sha256 shouldBe "3713298c705ee867f0b12cd2a05bc6209442baa156d8e38e19856a3a6b91a48e" + sha256 shouldBe "827fb746a1128e465d65ec77030fdf38c823dec593ae18aed55195069cf8b701" config.toSHA256() shouldBe sha256 } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt index f6dae8e55b255f6e8cb3d71bd3c8a7577cb88466..5e50eeaedd865915c26891ecd384e711160b6a7e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.appconfig.mapping.RevokedKeyPackage import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type +import de.rki.coronawarnapp.exception.http.NetworkConnectTimeoutException import io.kotest.matchers.shouldBe import io.mockk.coEvery import io.mockk.coVerify @@ -258,4 +259,18 @@ class HourPackageSyncToolTest : CommonSyncToolTest() { coVerify(exactly = 0) { keyServer.getHourIndex("EUR".loc, "2020-01-04".day) } } + + @Test + fun `network connection time out does not clear the cache and returns an unsuccessful result`() = runBlockingTest { + coEvery { keyServer.getHourIndex(any(), any()) } throws NetworkConnectTimeoutException() + + val instance = createInstance() + instance.syncMissingHourPackages(listOf("EUR".loc), false) shouldBe BaseKeyPackageSyncTool.SyncResult( + successful = false, + newPackages = emptyList() + ) + + coVerify(exactly = 1) { keyServer.getHourIndex("EUR".loc, "2020-01-04".day) } + coVerify(exactly = 0) { keyCache.delete(any()) } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt index b24f8fc8964adf921e00389250ed379eb54ea4d1..39e4639e53407bb9b4e1066c2a203045f8f5c364 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt @@ -19,6 +19,7 @@ import io.mockk.coVerifySequence import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.verifySequence import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -45,7 +46,7 @@ class ENFClientTest : BaseTest() { @BeforeEach fun setup() { MockKAnnotations.init(this) - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any()) } returns true every { exposureDetectionTracker.trackNewExposureDetection(any()) } just Runs } @@ -75,19 +76,20 @@ class ENFClientTest : BaseTest() { val client = createClient() val keyFiles = listOf(File("test")) - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any()) } returns true runBlocking { - client.provideDiagnosisKeys(keyFiles) shouldBe true + client.provideDiagnosisKeys(keyFiles, mockk()) shouldBe true } - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns false + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any()) } returns false runBlocking { - client.provideDiagnosisKeys(keyFiles) shouldBe false + client.provideDiagnosisKeys(keyFiles, mockk()) shouldBe false } coVerify(exactly = 2) { diagnosisKeyProvider.provideDiagnosisKeys( - keyFiles + keyFiles, + any() ) } } @@ -97,13 +99,13 @@ class ENFClientTest : BaseTest() { val client = createClient() val keyFiles = emptyList<File>() - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any()) } returns true runBlocking { - client.provideDiagnosisKeys(keyFiles) shouldBe true + client.provideDiagnosisKeys(keyFiles, mockk()) shouldBe true } coVerify(exactly = 0) { - diagnosisKeyProvider.provideDiagnosisKeys(any()) + diagnosisKeyProvider.provideDiagnosisKeys(any(), any()) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt index 0521bdb63cc6523f953df8dd9cf190428c27f25b..9012e23cf22f3a22d07d72703bbba373bf6788a9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt @@ -95,7 +95,8 @@ class DefaultExposureDetectionTrackerTest : BaseTest() { key shouldBe expectedIdentifier value shouldBe TrackedExposureDetection( identifier = expectedIdentifier, - startedAt = Instant.EPOCH + startedAt = Instant.EPOCH, + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..18702738c8cc0c58bde2bf7263f94895c3387d80 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt @@ -0,0 +1,94 @@ +package de.rki.coronawarnapp.nearby.modules.detectiontracker + +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import java.util.UUID + +class ExposureDetectionTrackerExtensionsTest : BaseTest() { + + @MockK lateinit var tracker: ExposureDetectionTracker + + private val fakeCalculations: MutableStateFlow<Map<String, TrackedExposureDetection>> = MutableStateFlow(emptyMap()) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { tracker.calculations } returns fakeCalculations + } + + @AfterEach + fun teardown() { + } + + private fun createFakeCalculation( + startedAt: Instant, + result: TrackedExposureDetection.Result? = TrackedExposureDetection.Result.NO_MATCHES + ) = TrackedExposureDetection( + identifier = UUID.randomUUID().toString(), + startedAt = startedAt, + finishedAt = if (result != null) startedAt.plus(100) else null, + result = result + ) + + @Test + fun `last submission`() { + val tr1 = createFakeCalculation(startedAt = Instant.EPOCH) + val tr2 = createFakeCalculation(startedAt = Instant.EPOCH.plus(1)) + val tr3 = createFakeCalculation(startedAt = Instant.EPOCH.plus(2), result = null) + fakeCalculations.value = mapOf( + tr1.identifier to tr1, + tr2.identifier to tr2, + tr3.identifier to tr3, + ) + runBlockingTest { + tracker.lastSubmission(onlyFinished = false) shouldBe tr3 + tracker.lastSubmission(onlyFinished = true) shouldBe tr2 + } + } + + @Test + fun `last submission on empty data`() { + runBlockingTest { + tracker.lastSubmission(onlyFinished = false) shouldBe null + tracker.lastSubmission(onlyFinished = true) shouldBe null + } + } + + @Test + fun `latest submission`() { + val tr1 = createFakeCalculation(startedAt = Instant.EPOCH) + val tr2 = createFakeCalculation(startedAt = Instant.EPOCH.plus(1)) + val tr3 = createFakeCalculation(startedAt = Instant.EPOCH.plus(2), result = null) + fakeCalculations.value = mapOf( + tr1.identifier to tr1, + tr2.identifier to tr2, + tr3.identifier to tr3, + ) + runBlockingTest { + tracker.latestSubmission(onlySuccessful = false).first() shouldBe tr3 + tracker.latestSubmission(onlySuccessful = true).first() shouldBe tr2 + } + } + + @Test + fun `latest submission on empty data`() = runBlockingTest { + tracker.latestSubmission(onlySuccessful = false).first() shouldBe null + tracker.latestSubmission(onlySuccessful = true).first() shouldBe null + + val tr1 = createFakeCalculation(startedAt = Instant.EPOCH) + fakeCalculations.value = mapOf(tr1.identifier to tr1) + + tracker.latestSubmission(onlySuccessful = false).first() shouldBe tr1 + tracker.latestSubmission(onlySuccessful = true).first() shouldBe tr1 + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt index 702291ceaeaacdea384fe8f930a56caec0590ede..f6d10c1e31ad6001a9444be1db0ce83e8866c67e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt @@ -34,7 +34,8 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { "identifier": "b2b98400-058d-43e6-b952-529a5255248b", "startedAt": { "iMillis": 1234 - } + }, + "enfVersion": "V2_WINDOW_MODE" }, "aeb15509-fb34-42ce-8795-7a9ae0c2f389": { "identifier": "aeb15509-fb34-42ce-8795-7a9ae0c2f389", @@ -44,7 +45,8 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { "result": "UPDATED_STATE", "finishedAt": { "iMillis": 1603473968125 - } + }, + "enfVersion": "V1_LEGACY_MODE" } } """.trimIndent() @@ -52,13 +54,15 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { private val demoData = run { val calculation1 = TrackedExposureDetection( identifier = "b2b98400-058d-43e6-b952-529a5255248b", - startedAt = Instant.ofEpochMilli(1234) + startedAt = Instant.ofEpochMilli(1234), + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE ) val calculation2 = TrackedExposureDetection( identifier = "aeb15509-fb34-42ce-8795-7a9ae0c2f389", startedAt = Instant.ofEpochMilli(5678), finishedAt = Instant.ofEpochMilli(1603473968125), - result = TrackedExposureDetection.Result.UPDATED_STATE + result = TrackedExposureDetection.Result.UPDATED_STATE, + enfVersion = TrackedExposureDetection.EnfVersion.V1_LEGACY_MODE ) mapOf( calculation1.identifier to calculation1, @@ -113,11 +117,10 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { @Test fun `saving data creates a json file`() = runBlockingTest { - createStorage().save(demoData) storageFile.exists() shouldBe true - val storedData: Map<String, TrackedExposureDetection> = gson.fromJson(storageFile) + val storedData: Map<String, TrackedExposureDetection> = gson.fromJson(storageFile)!! storedData shouldBe demoData gson.toJson(storedData) shouldBe demoJsonString diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt index da15e57215243c887afb317a87160ca618a71619..034c9d9e36c38cd3c0de667fcc78aa61fc4335c1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt @@ -1,16 +1,21 @@ package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeyFileProvider import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper.DiagnosisKeysDataMapper import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import de.rki.coronawarnapp.nearby.modules.version.OutdatedENFVersionException import io.kotest.matchers.shouldBe import io.mockk.Called import io.mockk.MockKAnnotations +import io.mockk.Runs import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify import io.mockk.coVerifySequence import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.AfterEach @@ -25,6 +30,7 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { @MockK lateinit var googleENFClient: ExposureNotificationClient @MockK lateinit var enfVersion: ENFVersion @MockK lateinit var submissionQuota: SubmissionQuota + @MockK lateinit var diagnosisKeysDataMapper: DiagnosisKeysDataMapper private val exampleKeyFiles = listOf(File("file1"), File("file2")) @@ -32,10 +38,14 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { fun setup() { MockKAnnotations.init(this) + coEvery { diagnosisKeysDataMapper.updateDiagnosisKeysDataMapping(any()) } just Runs + coEvery { submissionQuota.consumeQuota(any()) } returns true coEvery { googleENFClient.provideDiagnosisKeys(any<List<File>>()) } returns MockGMSTask.forValue(null) + coEvery { googleENFClient.provideDiagnosisKeys(any<DiagnosisKeyFileProvider>()) } returns MockGMSTask.forValue(null) + coEvery { enfVersion.requireMinimumVersion(any()) } returns Unit } @@ -47,7 +57,8 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { private fun createProvider() = DefaultDiagnosisKeyProvider( enfVersion = enfVersion, submissionQuota = submissionQuota, - enfClient = googleENFClient + enfClient = googleENFClient, + diagnosisKeysDataMapper = diagnosisKeysDataMapper ) @Test @@ -60,7 +71,7 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { val provider = createProvider() assertThrows<OutdatedENFVersionException> { - runBlockingTest { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe false + runBlockingTest { provider.provideDiagnosisKeys(exampleKeyFiles, mockk()) } shouldBe false } coVerify { @@ -70,10 +81,26 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { } @Test - fun `key provision is used on newer ENF versions`() { + fun `key provision is used with DiagnosisKeyFileProvider on ENF versions from 1_7 upwards`() { + coEvery { enfVersion.isAtLeast(any()) } returns true + + val provider = createProvider() + + runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles, mockk()) } shouldBe true + + coVerifySequence { + submissionQuota.consumeQuota(1) + googleENFClient.provideDiagnosisKeys(any<DiagnosisKeyFileProvider>()) + } + } + + @Test + fun `key provision is used with key list on ENF versions 1_6`() { + coEvery { enfVersion.isAtLeast(any()) } returns false + val provider = createProvider() - runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true + runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles, mockk()) } shouldBe true coVerifySequence { submissionQuota.consumeQuota(1) @@ -84,14 +111,15 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { @Test fun `quota is just monitored`() { coEvery { submissionQuota.consumeQuota(any()) } returns false + coEvery { enfVersion.isAtLeast(any()) } returns true val provider = createProvider() - runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true + runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles, mockk()) } shouldBe true coVerifySequence { submissionQuota.consumeQuota(1) - googleENFClient.provideDiagnosisKeys(exampleKeyFiles) + googleENFClient.provideDiagnosisKeys(any<DiagnosisKeyFileProvider>()) } } @@ -99,7 +127,7 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { fun `provide empty key list`() { val provider = createProvider() - runBlocking { provider.provideDiagnosisKeys(emptyList()) } shouldBe true + runBlocking { provider.provideDiagnosisKeys(emptyList(), mockk()) } shouldBe true coVerify { googleENFClient wasNot Called diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DefaultDiagnosisKeysDataMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DefaultDiagnosisKeysDataMapperTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..188056cc3e4bc6e74515a34cc305ee1b268e1907 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeysdatamapper/DefaultDiagnosisKeysDataMapperTest.kt @@ -0,0 +1,138 @@ +package de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper + +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.Infectiousness +import com.google.android.gms.nearby.exposurenotification.ReportType +import de.rki.coronawarnapp.nearby.modules.diagnosiskeysdatamapper.DefaultDiagnosisKeysDataMapper.Companion.hasChanged +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import io.mockk.verify +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.coroutines.runBlockingTest2 +import testhelpers.gms.MockGMSTask + +class DefaultDiagnosisKeysDataMapperTest : BaseTest() { + @MockK lateinit var googleENFClient: ExposureNotificationClient + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createMapper() = DefaultDiagnosisKeysDataMapper( + client = googleENFClient + ) + + @Test + fun `set mapping is invoked`() { + val firstMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf(0 to Infectiousness.STANDARD, 1 to Infectiousness.HIGH)) + }.build() + + val secondMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf(0 to Infectiousness.HIGH, 1 to Infectiousness.STANDARD)) + }.build() + + coEvery { googleENFClient.diagnosisKeysDataMapping } returns MockGMSTask.forValue(firstMapping) + coEvery { googleENFClient.setDiagnosisKeysDataMapping(any()) } returns MockGMSTask.forValue(null) + + val mapper = createMapper() + + runBlockingTest2 { + mapper.updateDiagnosisKeysDataMapping(secondMapping) + } + + verify { + googleENFClient.setDiagnosisKeysDataMapping(secondMapping) + } + } + + @Test + fun `set mapping is not invoked`() { + val firstMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf(0 to Infectiousness.STANDARD, 1 to Infectiousness.HIGH)) + }.build() + + val secondMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf(0 to Infectiousness.STANDARD, 1 to Infectiousness.HIGH)) + }.build() + + coEvery { googleENFClient.diagnosisKeysDataMapping } returns MockGMSTask.forValue(firstMapping) + coEvery { googleENFClient.setDiagnosisKeysDataMapping(any()) } returns MockGMSTask.forValue(null) + + val mapper = createMapper() + + runBlockingTest2 { + mapper.updateDiagnosisKeysDataMapping(secondMapping) + } + + verify(exactly = 0) { + googleENFClient.setDiagnosisKeysDataMapping(secondMapping) + } + } + + @Test + fun `mapping change detection works`() { + // Note that we cant create an empty mapping as the DiagnosisKeysDataMappingBuilder + // throws a IllegalArgumentException if one of the properties is missing + val nullMapping: DiagnosisKeysDataMapping? = null + val firstMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf(0 to Infectiousness.STANDARD, 1 to Infectiousness.HIGH)) + }.build() + val secondMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf(0 to Infectiousness.HIGH, 1 to Infectiousness.STANDARD)) + }.build() + val thirdMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf()) + }.build() + val fourthMapping = DiagnosisKeysDataMapping.DiagnosisKeysDataMappingBuilder().apply { + setReportTypeWhenMissing(ReportType.CONFIRMED_TEST) + setInfectiousnessWhenDaysSinceOnsetMissing(Infectiousness.HIGH) + setDaysSinceOnsetToInfectiousness(mapOf()) + }.build() + + firstMapping.hasChanged(nullMapping) shouldBe true + firstMapping.hasChanged(secondMapping) shouldBe true + firstMapping.hasChanged(thirdMapping) shouldBe true + + secondMapping.hasChanged(nullMapping) shouldBe true + secondMapping.hasChanged(firstMapping) shouldBe true + secondMapping.hasChanged(thirdMapping) shouldBe true + + thirdMapping.hasChanged(nullMapping) shouldBe true + thirdMapping.hasChanged(firstMapping) shouldBe true + thirdMapping.hasChanged(secondMapping) shouldBe true + + nullMapping.hasChanged(nullMapping) shouldBe true + firstMapping.hasChanged(firstMapping) shouldBe false + secondMapping.hasChanged(secondMapping) shouldBe false + thirdMapping.hasChanged(thirdMapping) shouldBe false + thirdMapping.hasChanged(fourthMapping) shouldBe false + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt index 6d9e563b93dda599d64478739717998339f72d63..48a020e449e2a295afbef3b536432b2f928ffa57 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt @@ -109,4 +109,54 @@ internal class DefaultENFVersionTest { } } } + + @Test + fun `isAtLeast is true for newer version`() { + every { client.version } returns MockGMSTask.forValue(ENFVersion.V1_7) + + runBlockingTest { + createInstance().isAtLeast(ENFVersion.V1_6) shouldBe true + } + } + + @Test + fun `isAtLeast is true for equal version`() { + every { client.version } returns MockGMSTask.forValue(ENFVersion.V1_6) + + runBlockingTest { + createInstance().isAtLeast(ENFVersion.V1_6) shouldBe true + } + } + + @Test + fun `isAtLeast is false for older version`() { + every { client.version } returns MockGMSTask.forValue(ENFVersion.V1_6) + + runBlockingTest { + createInstance().isAtLeast(ENFVersion.V1_7) shouldBe false + } + } + + @Test + fun `invalid input for isAtLeast throws IllegalArgumentException`() { + runBlockingTest { + shouldThrow<IllegalArgumentException> { + createInstance().isAtLeast(16) + } + } + } + + @Test + fun `isAtLeast returns false when client not connected`() { + every { client.version } returns MockGMSTask.forError(ApiException(Status(API_NOT_CONNECTED))) + + runBlockingTest { + createInstance().apply { + shouldNotThrowAny { + isAtLeast(ENFVersion.V1_6) shouldBe false + isAtLeast(ENFVersion.V1_7) shouldBe false + } + } + } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/NotificationConstantsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/NotificationConstantsTest.kt index 25a6f6ab2c84164611044f0f441ebc4feae8db77..25f8cc97b27b4d4947e0226ce501359a52f1d7f6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/NotificationConstantsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/NotificationConstantsTest.kt @@ -12,10 +12,5 @@ class NotificationConstantsTest { Assert.assertEquals(NotificationConstants.NOTIFICATION_SMALL_ICON, R.drawable.ic_splash_logo) Assert.assertEquals(NotificationConstants.CHANNEL_NAME, R.string.notification_name) Assert.assertEquals(NotificationConstants.CHANNEL_DESCRIPTION, R.string.notification_description) - Assert.assertEquals( - NotificationConstants.NOTIFICATION_CONTENT_TITLE_RISK_CHANGED, - R.string.notification_headline - ) - Assert.assertEquals(NotificationConstants.NOTIFICATION_CONTENT_TEXT_RISK_CHANGED, R.string.notification_body) } } 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 new file mode 100644 index 0000000000000000000000000000000000000000..f8b9208074742faf6ea029954917c843f2c99424 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -0,0 +1,200 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.ForegroundState +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockkObject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RiskLevelChangeDetectorTest : BaseTest() { + + @MockK lateinit var context: Context + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var riskLevelStorage: RiskLevelStorage + @MockK lateinit var notificationManagerCompat: NotificationManagerCompat + @MockK lateinit var foregroundState: ForegroundState + @MockK lateinit var riskLevelSettings: RiskLevelSettings + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkObject(LocalData) + + every { LocalData.isUserToBeNotifiedOfLoweredRiskLevel = any() } just Runs + every { LocalData.submissionWasSuccessful() } returns false + every { foregroundState.isInForeground } returns flowOf(true) + every { notificationManagerCompat.areNotificationsEnabled() } returns true + every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs + every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns null + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createRiskLevel( + riskState: RiskState, + calculatedAt: Instant = Instant.EPOCH + ): RiskLevelResult = object : RiskLevelResult { + override val riskState: RiskState = riskState + override val calculatedAt: Instant = calculatedAt + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val failureReason: RiskLevelResult.FailureReason? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + private fun createInstance(scope: CoroutineScope) = RiskLevelChangeDetector( + context = context, + appScope = scope, + riskLevelStorage = riskLevelStorage, + notificationManagerCompat = notificationManagerCompat, + foregroundState = foregroundState, + riskLevelSettings = riskLevelSettings + ) + + @Test + fun `nothing happens if there is only one result yet`() { + every { riskLevelStorage.riskLevelResults } returns flowOf(listOf(createRiskLevel(LOW_RISK))) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData wasNot Called + notificationManagerCompat wasNot Called + } + } + } + + @Test + fun `no risklevel change, nothing should happen`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(LOW_RISK), + createRiskLevel(LOW_RISK) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData wasNot Called + notificationManagerCompat wasNot Called + } + } + } + + @Test + fun `risklevel went from HIGH to LOW`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData.submissionWasSuccessful() + foregroundState.isInForeground + LocalData.isUserToBeNotifiedOfLoweredRiskLevel = any() + } + } + } + + @Test + fun `risklevel went from LOW to HIGH`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData.submissionWasSuccessful() + foregroundState.isInForeground + } + } + } + + @Test + fun `risklevel went from LOW to HIGH but it is has already been processed`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH) + ) + ) + every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.EPOCH.plus(1) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData wasNot Called + notificationManagerCompat wasNot Called + } + } + } + + @Test + fun `evaluate risk level change detection function`() { + RiskLevelChangeDetector.hasHighLowLevelChanged(CALCULATION_FAILED, CALCULATION_FAILED) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(LOW_RISK, LOW_RISK) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, INCREASED_RISK) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, LOW_RISK) shouldBe true + RiskLevelChangeDetector.hasHighLowLevelChanged(LOW_RISK, INCREASED_RISK) shouldBe true + RiskLevelChangeDetector.hasHighLowLevelChanged(CALCULATION_FAILED, INCREASED_RISK) shouldBe true + RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, CALCULATION_FAILED) shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelConstantsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelConstantsTest.kt deleted file mode 100644 index 696c3b8de696975b75b7183b78795d12aec7b954..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelConstantsTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package de.rki.coronawarnapp.risk - -import org.junit.Assert -import org.junit.Test - -class RiskLevelConstantsTest { - - @Test - fun allRiskLevelConstants() { - Assert.assertEquals(RiskLevelConstants.UNKNOWN_RISK_INITIAL, 0) - Assert.assertEquals(RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, 1) - Assert.assertEquals(RiskLevelConstants.LOW_LEVEL_RISK, 2) - Assert.assertEquals(RiskLevelConstants.INCREASED_RISK, 3) - Assert.assertEquals(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, 4) - Assert.assertEquals(RiskLevelConstants.UNDETERMINED, 9001) - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b401435390b6df088edcc5d583196d34be7fc066 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.joda.time.Instant +import org.junit.Test +import testhelpers.BaseTest + +class RiskLevelResultTest : BaseTest() { + + private fun createRiskLevel( + aggregatedRiskResult: AggregatedRiskResult?, + failureReason: RiskLevelResult.FailureReason? + ): RiskLevelResult = object : RiskLevelResult { + override val calculatedAt: Instant = Instant.EPOCH + override val aggregatedRiskResult: AggregatedRiskResult? = aggregatedRiskResult + override val failureReason: RiskLevelResult.FailureReason? = failureReason + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + @Test + fun testUnsuccessfulRistLevels() { + createRiskLevel( + aggregatedRiskResult = null, + failureReason = RiskLevelResult.FailureReason.UNKNOWN + ).wasSuccessfullyCalculated shouldBe false + + createRiskLevel( + aggregatedRiskResult = mockk(), + failureReason = null + ).wasSuccessfullyCalculated shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelSettingsTest.kt similarity index 91% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelSettingsTest.kt index 41a2b35177a6eab0a08331f6337a9ce6f991554d..36b3bd07bdc414de3745f27683def088c5f42cfa 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelSettingsTest.kt @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test import testhelpers.BaseTest import testhelpers.preferences.MockSharedPreferences -class RiskLevelDataTest : BaseTest() { +class RiskLevelSettingsTest : BaseTest() { @MockK lateinit var context: Context lateinit var preferences: MockSharedPreferences @@ -22,7 +22,7 @@ class RiskLevelDataTest : BaseTest() { every { context.getSharedPreferences("risklevel_localdata", Context.MODE_PRIVATE) } returns preferences } - fun createInstance() = RiskLevelData(context = context) + fun createInstance() = RiskLevelSettings(context = context) @Test fun `update last used config identifier`() { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt index 5e1cbd3839de8c388a5281a9e8084e7b3e1981a3..1e5267810c9e8314c6a8a61a3ae6aac7f2e394ca 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt @@ -1,15 +1,90 @@ package de.rki.coronawarnapp.risk +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest class RiskLevelTaskConfigTest : BaseTest() { + @MockK lateinit var exposureDetectionTracker: ExposureDetectionTracker + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + @Test fun `risk level task max execution time is not above 9 minutes`() { - val config = RiskLevelTask.Config() - config.executionTimeout.isShorterThan(Duration.standardMinutes(9)) shouldBe true + RiskLevelTask.Config(exposureDetectionTracker) + .executionTimeout + .isShorterThan(Duration.standardMinutes(9)) shouldBe true + } + + @Test + fun `risk level preconditions are met`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(mapOf("" to TrackedExposureDetection( + identifier = "", + startedAt = Instant(), + result = TrackedExposureDetection.Result.NO_MATCHES, + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE + ))) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe true + } + } + + @Test + fun `risk level preconditions are not met, because there are no detections`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(emptyMap()) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe false + } + } + + @Test + fun `risk level preconditions are not met, because there are no enf V2 detections`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(mapOf("" to TrackedExposureDetection( + identifier = "", + startedAt = Instant(), + result = TrackedExposureDetection.Result.NO_MATCHES, + enfVersion = TrackedExposureDetection.EnfVersion.V1_LEGACY_MODE + ))) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe false + } + } + + @Test + fun `risk level preconditions are not met, because detection is not finished yet`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(mapOf("" to TrackedExposureDetection( + identifier = "", + startedAt = Instant(), + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE + ))) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe false + } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt index f174600cfd181bc4936dbe935c9195c830be8c82..4d1170052ba9c2ba04ebab079f7899d823014b57 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt @@ -6,7 +6,9 @@ import android.net.Network import android.net.NetworkCapabilities import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.TimeStamper @@ -32,30 +34,19 @@ class RiskLevelTaskTest : BaseTest() { @MockK lateinit var enfClient: ENFClient @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var backgroundModeStatus: BackgroundModeStatus - @MockK lateinit var riskLevelData: RiskLevelData + @MockK lateinit var riskLevelSettings: RiskLevelSettings @MockK lateinit var configData: ConfigData @MockK lateinit var appConfigProvider: AppConfigProvider - @MockK lateinit var exposureResultStore: ExposureResultStore + @MockK lateinit var riskLevelStorage: RiskLevelStorage + @MockK lateinit var keyCacheRepository: KeyCacheRepository private val arguments: Task.Arguments = object : Task.Arguments {} - private fun createTask() = RiskLevelTask( - riskLevels = riskLevels, - context = context, - enfClient = enfClient, - timeStamper = timeStamper, - backgroundModeStatus = backgroundModeStatus, - riskLevelData = riskLevelData, - appConfigProvider = appConfigProvider, - exposureResultStore = exposureResultStore - ) - @BeforeEach fun setup() { MockKAnnotations.init(this) mockkObject(TimeVariables) - every { TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() } returns null coEvery { appConfigProvider.getAppConfig() } returns configData every { configData.identifier } returns "config-identifier" @@ -71,14 +62,28 @@ class RiskLevelTaskTest : BaseTest() { every { enfClient.isTracingEnabled } returns flowOf(true) every { timeStamper.nowUTC } returns Instant.EPOCH - every { riskLevelData.lastUsedConfigIdentifier = any() } just Runs + every { riskLevelSettings.lastUsedConfigIdentifier = any() } just Runs + + coEvery { keyCacheRepository.getAllCachedKeys() } returns emptyList() } + private fun createTask() = RiskLevelTask( + riskLevels = riskLevels, + context = context, + enfClient = enfClient, + timeStamper = timeStamper, + backgroundModeStatus = backgroundModeStatus, + riskLevelSettings = riskLevelSettings, + appConfigProvider = appConfigProvider, + riskLevelStorage = riskLevelStorage, + keyCacheRepository = keyCacheRepository + ) + @Test fun `last used config ID is set after calculation`() = runBlockingTest { // val task = createTask() // task.run(arguments) // -// verify { riskLevelData.lastUsedConfigIdentifier = "config-identifier" } +// verify { riskLevelSettings.lastUsedConfigIdentifier = "config-identifier" } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt deleted file mode 100644 index 7d4c1d7ba462d10b7c381124a96c9a39ee2bfdfd..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt +++ /dev/null @@ -1,130 +0,0 @@ -package de.rki.coronawarnapp.risk - -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertTrue -import org.junit.Test - -class RiskLevelTest { - - @Test - fun testEnum() { - assertEquals(RiskLevel.UNKNOWN_RISK_INITIAL.raw, RiskLevelConstants.UNKNOWN_RISK_INITIAL) - assertEquals( - RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF.raw, - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF - ) - assertEquals(RiskLevel.LOW_LEVEL_RISK.raw, RiskLevelConstants.LOW_LEVEL_RISK) - assertEquals(RiskLevel.INCREASED_RISK.raw, RiskLevelConstants.INCREASED_RISK) - assertEquals(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS.raw, RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS) - assertEquals(RiskLevel.UNDETERMINED.raw, RiskLevelConstants.UNDETERMINED) - } - - @Test - fun testForValue() { - assertEquals(RiskLevel.forValue(RiskLevelConstants.UNKNOWN_RISK_INITIAL), RiskLevel.UNKNOWN_RISK_INITIAL) - assertEquals( - RiskLevel.forValue(RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF), - RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF - ) - assertEquals(RiskLevel.forValue(RiskLevelConstants.LOW_LEVEL_RISK), RiskLevel.LOW_LEVEL_RISK) - assertEquals(RiskLevel.forValue(RiskLevelConstants.INCREASED_RISK), RiskLevel.INCREASED_RISK) - assertEquals( - RiskLevel.forValue(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS), - RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS - ) - - assertNotEquals(RiskLevel.forValue(RiskLevelConstants.UNKNOWN_RISK_INITIAL), RiskLevel.UNDETERMINED) - assertNotEquals( - RiskLevel.forValue(RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF), - RiskLevel.UNDETERMINED - ) - assertNotEquals(RiskLevel.forValue(RiskLevelConstants.LOW_LEVEL_RISK), RiskLevel.UNDETERMINED) - assertNotEquals(RiskLevel.forValue(RiskLevelConstants.INCREASED_RISK), RiskLevel.UNDETERMINED) - assertNotEquals(RiskLevel.forValue(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS), RiskLevel.UNDETERMINED) - } - - @Test - fun testUnsuccessfulRistLevels() { - assertTrue(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.UNDETERMINED)) - assertTrue(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF)) - assertTrue(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS)) - - assertFalse(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.UNKNOWN_RISK_INITIAL)) - assertFalse(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.LOW_LEVEL_RISK)) - assertFalse(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.INCREASED_RISK)) - } - - @Test - fun testRiskLevelChangedFromHighToHigh() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.INCREASED_RISK, - RiskLevel.INCREASED_RISK - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromLowToLow() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNKNOWN_RISK_INITIAL, - RiskLevel.LOW_LEVEL_RISK - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromLowToHigh() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNKNOWN_RISK_INITIAL, - RiskLevel.INCREASED_RISK - ) - assertTrue(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromHighToLow() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.INCREASED_RISK, - RiskLevel.UNKNOWN_RISK_INITIAL - ) - assertTrue(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromUndeterminedToLow() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNDETERMINED, - RiskLevel.UNKNOWN_RISK_INITIAL - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromUndeterminedToHigh() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNDETERMINED, - RiskLevel.INCREASED_RISK - ) - assertTrue(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromLowToUndetermined() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNKNOWN_RISK_INITIAL, - RiskLevel.UNDETERMINED - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromHighToUndetermined() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.INCREASED_RISK, - RiskLevel.UNDETERMINED - ) - assertTrue(riskLevelHasChanged) - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/TimeVariablesTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/TimeVariablesTest.kt index 4b46519654961026b9e13fea1bfc934cb4d899e2..63d994fc6f370673354be7e87266fb3cdd594ad8 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/TimeVariablesTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/TimeVariablesTest.kt @@ -34,11 +34,6 @@ class TimeVariablesTest { Assert.assertEquals(TimeVariables.getMinActivatedTracingTime(), 24) } - @Test - fun getMaxStaleExposureRiskRange() { - Assert.assertEquals(TimeVariables.getMaxStaleExposureRiskRange(), 48) - } - @Test fun getManualKeyRetrievalDelay() { mockkObject(CWADebug) 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 new file mode 100644 index 0000000000000000000000000000000000000000..8e9fd7d52bcb1d29a2e35bfe010fc7a053bd10dc --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -0,0 +1,180 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindow +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindowDaoWrapper +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +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.legacy.RiskLevelResultMigrator +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class BaseRiskLevelStorageTest : BaseTest() { + + @MockK lateinit var databaseFactory: Factory + @MockK lateinit var database: RiskResultDatabase + @MockK lateinit var riskResultTables: RiskResultsDao + @MockK lateinit var exposureWindowTables: ExposureWindowsDao + @MockK lateinit var riskLevelResultMigrator: RiskLevelResultMigrator + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { databaseFactory.create() } returns database + every { database.riskResults() } returns riskResultTables + every { database.exposureWindows() } returns exposureWindowTables + every { database.clearAllTables() } just Runs + + every { riskLevelResultMigrator.getLegacyResults() } returns emptyList() + + every { riskResultTables.allEntries() } returns emptyFlow() + coEvery { riskResultTables.insertEntry(any()) } just Runs + coEvery { riskResultTables.deleteOldest(any()) } returns 7 + + every { exposureWindowTables.allEntries() } returns emptyFlow() + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createInstance( + storedResultLimit: Int = 10, + onStoreExposureWindows: (String, RiskLevelResult) -> Unit = { id, result -> }, + onDeletedOrphanedExposureWindows: () -> Unit = {} + ) = object : BaseRiskLevelStorage( + riskResultDatabaseFactory = databaseFactory, + riskLevelResultMigrator = riskLevelResultMigrator + ) { + override val storedResultLimit: Int = storedResultLimit + + override suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) { + onStoreExposureWindows(storedResultId, result) + } + + override suspend fun deletedOrphanedExposureWindows() { + onDeletedOrphanedExposureWindows() + } + } + + @Test + fun `exposureWindows are returned from database and mapped`() { + val testDaoWrappers = flowOf(listOf(testExposureWindowDaoWrapper)) + every { exposureWindowTables.allEntries() } returns testDaoWrappers + + runBlockingTest { + val exposureWindowDAOWrappers = createInstance().exposureWindowsTables.allEntries() + exposureWindowDAOWrappers shouldBe testDaoWrappers + exposureWindowDAOWrappers.first().map { it.toExposureWindow() } shouldBe listOf(testExposureWindow) + } + } + + @Test + fun `riskLevelResults are returned from database and mapped`() { + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.allEntries() } returns flowOf(emptyList()) + + runBlockingTest { + val instance = createInstance() + instance.riskLevelResults.first() shouldBe listOf(testRisklevelResult) + + verify { riskLevelResultMigrator wasNot Called } + } + } + + @Test + fun `riskLevelResults with exposure windows are returned from database and mapped`() { + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + every { exposureWindowTables.allEntries() } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest { + val instance = createInstance() + val riskLevelResult = testRisklevelResult.copy(exposureWindows = listOf(testExposureWindow)) + instance.riskLevelResults.first() shouldBe listOf(riskLevelResult) + + verify { riskLevelResultMigrator wasNot Called } + } + } + + @Test + fun `if no risk level results are available we try to get legacy results`() { + every { riskLevelResultMigrator.getLegacyResults() } returns listOf(mockk(), mockk()) + every { riskResultTables.allEntries() } returns flowOf(emptyList()) + every { exposureWindowTables.allEntries() } returns flowOf(emptyList()) + + runBlockingTest { + val instance = createInstance() + instance.riskLevelResults.first().size shouldBe 2 + + verify { riskLevelResultMigrator.getLegacyResults() } + } + } + + @Test + fun `errors when storing risklevel result are rethrown`() = runBlockingTest { + coEvery { riskResultTables.insertEntry(any()) } throws IllegalStateException("No body expects the...") + val instance = createInstance() + shouldThrow<java.lang.IllegalStateException> { + instance.storeResult(testRisklevelResult) + } + } + + @Test + fun `errors when storing exposure window results are thrown`() = runBlockingTest { + val instance = createInstance(onStoreExposureWindows = { _, _ -> throw IllegalStateException("Surprise!") }) + shouldThrow<IllegalStateException> { + instance.storeResult(testRisklevelResult) + } + } + + @Test + fun `storeResult works`() = runBlockingTest { + val mockStoreWindows: (String, RiskLevelResult) -> Unit = spyk() + val mockDeleteOrphanedWindows: () -> Unit = spyk() + + val instance = createInstance( + onStoreExposureWindows = mockStoreWindows, + onDeletedOrphanedExposureWindows = mockDeleteOrphanedWindows + ) + instance.storeResult(testRisklevelResult) + + coVerify { + riskResultTables.insertEntry(any()) + riskResultTables.deleteOldest(instance.storedResultLimit) + mockStoreWindows.invoke(any(), testRisklevelResult) + mockDeleteOrphanedWindows.invoke() + } + } + + @Test + fun `clear works`() = runBlockingTest { + createInstance().clear() + verify { database.clearAllTables() } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..5c2582f83ebaaac2ccb9daff4d7776b02dad5227 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt @@ -0,0 +1,73 @@ +package de.rki.coronawarnapp.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import org.joda.time.Instant + +object RiskStorageTestData { + + val testRiskLevelResultDao = PersistedRiskLevelResultDao( + id = "riskresult-id", + calculatedAt = Instant.ofEpochMilli(9999L), + 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 + ) + ) + + val testRisklevelResult = RiskLevelTaskResult( + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = AggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + exposureWindows = null + ) + + val testExposureWindowDaoWrapper = PersistedExposureWindowDaoWrapper( + exposureWindowDao = PersistedExposureWindowDao( + id = 1, + riskLevelResultId = "riskresult-id", + dateMillisSinceEpoch = 123L, + calibrationConfidence = 1, + infectiousness = 2, + reportType = 3 + ), + scanInstances = listOf( + PersistedExposureWindowDao.PersistedScanInstance( + exposureWindowId = 1, + minAttenuationDb = 10, + secondsSinceLastScan = 20, + typicalAttenuationDb = 30 + ) + ) + ) + val testExposureWindow: ExposureWindow = ExposureWindow.Builder().apply { + setDateMillisSinceEpoch(123L) + setCalibrationConfidence(1) + setInfectiousness(2) + setReportType(3) + ScanInstance.Builder().apply { + setMinAttenuationDb(10) + setSecondsSinceLastScan(20) + setTypicalAttenuationDb(30) + }.build().let { setScanInstances(listOf(it)) } + }.build() +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedExposureWindowDaoTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedExposureWindowDaoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..61aee1225fd3b8dc7af92644e708f52415c6220b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedExposureWindowDaoTest.kt @@ -0,0 +1,41 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedScanInstance +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PersistedExposureWindowDaoTest : BaseTest() { + + @Test + fun `mapping is correct`() { + val window: ExposureWindow = mockk() + every { window.calibrationConfidence } returns 0 + every { window.dateMillisSinceEpoch } returns 849628347458723L + every { window.infectiousness } returns 2 + every { window.reportType } returns 2 + window.toPersistedExposureWindow("RESULT_ID").apply { + riskLevelResultId shouldBe "RESULT_ID" + dateMillisSinceEpoch shouldBe 849628347458723L + calibrationConfidence shouldBe 0 + infectiousness shouldBe 2 + reportType shouldBe 2 + } + + val scanInstance: ScanInstance = mockk() + every { scanInstance.minAttenuationDb } returns 30 + every { scanInstance.secondsSinceLastScan } returns 300 + every { scanInstance.typicalAttenuationDb } returns 25 + scanInstance.toPersistedScanInstance(5000L).apply { + exposureWindowId shouldBe 5000 + minAttenuationDb shouldBe 30 + typicalAttenuationDb shouldBe 25 + secondsSinceLastScan shouldBe 300 + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..51adcacd6ddef65210794636c93bcf0586a6648b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt @@ -0,0 +1,102 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindow +import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindowDaoWrapper +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PersistedRiskResultDaoTest : BaseTest() { + + @Test + fun `mapping successful result`() { + PersistedRiskLevelResultDao( + id = "", + calculatedAt = Instant.ofEpochMilli(931161601L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 89, + totalMinimumDistinctEncountersWithHighRisk = 59, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(852191241L), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(790335113L), + numberOfDaysWithLowRisk = 52, + numberOfDaysWithHighRisk = 81 + ) + ).toRiskResult(listOf(testExposureWindowDaoWrapper)).apply { + riskState shouldBe RiskState.LOW_RISK + calculatedAt.millis shouldBe 931161601L + exposureWindows shouldBe listOf(testExposureWindow) + failureReason shouldBe null + aggregatedRiskResult shouldNotBe null + aggregatedRiskResult?.apply { + totalRiskLevel shouldBe RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW + totalMinimumDistinctEncountersWithLowRisk shouldBe 89 + totalMinimumDistinctEncountersWithHighRisk shouldBe 59 + mostRecentDateWithLowRisk shouldNotBe null + mostRecentDateWithLowRisk?.millis shouldBe 852191241L + mostRecentDateWithHighRisk shouldNotBe null + mostRecentDateWithHighRisk?.millis shouldBe 790335113L + numberOfDaysWithLowRisk shouldBe 52 + numberOfDaysWithHighRisk shouldBe 81 + } + } + } + + @Test + fun `mapping successful result with exposure windows`() { + PersistedRiskLevelResultDao( + id = "", + calculatedAt = Instant.ofEpochMilli(931161601L), + failureReason = null, + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 89, + totalMinimumDistinctEncountersWithHighRisk = 59, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(852191241L), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(790335113L), + numberOfDaysWithLowRisk = 52, + numberOfDaysWithHighRisk = 81 + ) + ).toRiskResult().apply { + riskState shouldBe RiskState.LOW_RISK + calculatedAt.millis shouldBe 931161601L + exposureWindows shouldBe null + failureReason shouldBe null + aggregatedRiskResult shouldNotBe null + aggregatedRiskResult?.apply { + totalRiskLevel shouldBe RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW + totalMinimumDistinctEncountersWithLowRisk shouldBe 89 + totalMinimumDistinctEncountersWithHighRisk shouldBe 59 + mostRecentDateWithLowRisk shouldNotBe null + mostRecentDateWithLowRisk?.millis shouldBe 852191241L + mostRecentDateWithHighRisk shouldNotBe null + mostRecentDateWithHighRisk?.millis shouldBe 790335113L + numberOfDaysWithLowRisk shouldBe 52 + numberOfDaysWithHighRisk shouldBe 81 + } + } + } + + @Test + fun `mapping failed result`() { + PersistedRiskLevelResultDao( + id = "", + calculatedAt = Instant.ofEpochMilli(931161601L), + failureReason = RiskLevelResult.FailureReason.TRACING_OFF, + aggregatedRiskResult = null + ).toRiskResult().apply { + riskState shouldBe RiskState.CALCULATION_FAILED + calculatedAt.millis shouldBe 931161601L + exposureWindows shouldBe null + failureReason shouldBe RiskLevelResult.FailureReason.TRACING_OFF + aggregatedRiskResult shouldBe null + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..453bde1aa8e43131120df75ff2854a59eca99c08 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt @@ -0,0 +1,151 @@ +package de.rki.coronawarnapp.risk.storage.legacy + +import androidx.core.content.edit +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.MockSharedPreferences + +class RiskLevelResultMigratorTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + private val mockPreferences = MockSharedPreferences() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.EPOCH.plus(1337) + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + fun createInstance() = RiskLevelResultMigrator( + timeStamper = timeStamper, + encryptedPreferences = { mockPreferences } + ) + + @Test + fun `normal case with full values`() { + mockPreferences.edit { + putInt("preference_risk_level_score", MigrationRiskLevelConstants.INCREASED_RISK) + putInt("preference_risk_level_score_successful", MigrationRiskLevelConstants.LOW_LEVEL_RISK) + putLong("preference_timestamp_risk_level_calculation", 1234567890L) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults[0].apply { + riskState shouldBe RiskState.INCREASED_RISK + calculatedAt shouldBe Instant.ofEpochMilli(1234567890L) + } + legacyResults[1].apply { + riskState shouldBe RiskState.LOW_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + } + } + + @Test + fun `empty list if no previous data was available`() { + mockPreferences.dataMapPeek.isEmpty() shouldBe true + createInstance().getLegacyResults() shouldBe emptyList() + } + + @Test + fun `if no timestamp is available we use the current time`() { + mockPreferences.edit { + putInt("preference_risk_level_score", MigrationRiskLevelConstants.INCREASED_RISK) + putInt("preference_risk_level_score_successful", MigrationRiskLevelConstants.LOW_LEVEL_RISK) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults[0].apply { + riskState shouldBe RiskState.INCREASED_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + legacyResults[1].apply { + riskState shouldBe RiskState.LOW_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + } + } + + @Test + fun `last successful is null`() { + mockPreferences.edit { + putInt("preference_risk_level_score_successful", MigrationRiskLevelConstants.INCREASED_RISK) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults.size shouldBe 1 + legacyResults.first().apply { + riskState shouldBe RiskState.INCREASED_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + } + } + + @Test + fun `last successfully calculated is null`() { + mockPreferences.edit { + putInt("preference_risk_level_score", MigrationRiskLevelConstants.INCREASED_RISK) + putLong("preference_timestamp_risk_level_calculation", 1234567890L) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults.size shouldBe 1 + legacyResults.first().apply { + riskState shouldBe RiskState.INCREASED_RISK + calculatedAt shouldBe Instant.ofEpochMilli(1234567890L) + } + } + } + + @Test + fun `exceptions are handled gracefully`() { + mockPreferences.edit { + putInt("preference_risk_level_score", MigrationRiskLevelConstants.INCREASED_RISK) + } + every { timeStamper.nowUTC } throws Exception("Surprise!") + createInstance().getLegacyResults() shouldBe emptyList() + } + + @Test + fun `legacy risk level mapping`() { + RiskLevelResultMigrator.mapRiskLevelConstant( + MigrationRiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF + ) shouldBe RiskState.CALCULATION_FAILED + + RiskLevelResultMigrator.mapRiskLevelConstant( + MigrationRiskLevelConstants.LOW_LEVEL_RISK + ) shouldBe RiskState.LOW_RISK + + RiskLevelResultMigrator.mapRiskLevelConstant( + MigrationRiskLevelConstants.INCREASED_RISK + ) shouldBe RiskState.INCREASED_RISK + + RiskLevelResultMigrator.mapRiskLevelConstant( + MigrationRiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS + ) shouldBe RiskState.CALCULATION_FAILED + + RiskLevelResultMigrator.mapRiskLevelConstant( + MigrationRiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL + ) shouldBe RiskState.CALCULATION_FAILED + + RiskLevelResultMigrator.mapRiskLevelConstant( + MigrationRiskLevelConstants.UNDETERMINED + ) shouldBe RiskState.CALCULATION_FAILED + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt index f904964bfddde385950bac6c95efad7247fda59b..75dd982d0f4a0bc20c580e3bdc0bd08bf693e334 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.storage import android.content.Context +import com.google.gson.Gson import de.rki.coronawarnapp.util.CWADebug import io.mockk.MockKAnnotations import io.mockk.clearAllMocks @@ -16,6 +17,7 @@ class TestSettingsTest : BaseTest() { @MockK lateinit var context: Context private lateinit var mockPreferences: MockSharedPreferences + private val gson = Gson() @BeforeEach fun setup() { @@ -35,6 +37,7 @@ class TestSettingsTest : BaseTest() { } private fun buildInstance(): TestSettings = TestSettings( - context = context + context = context, + gson = gson ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt index 581eab4dc2df49394919ab0679391f98f230333f..d84078cf9b2bd447d63555b5c44d2b6ecb197b5e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt @@ -539,4 +539,44 @@ class TaskControllerTest : BaseIOTest() { instance.close() } + + @Test + fun `old tasks are pruned from history`() = runBlockingTest { + val instance = createInstance(scope = this) + + val expectedFiles = mutableListOf<File>() + + repeat(100) { + val arguments = QueueingTask.Arguments( + delay = 5, + values = listOf("TestText"), + path = File(testDir, UUID.randomUUID().toString()) + ) + expectedFiles.add(arguments.path) + + val request = DefaultTaskRequest(type = QueueingTask::class, arguments = arguments) + instance.submit(request) + delay(5) + } + + this.advanceUntilIdle() + + expectedFiles.forEach { + it.exists() shouldBe true + } + + val taskHistory = instance.tasks.first() + taskHistory.size shouldBe 50 + expectedFiles.size shouldBe 100 + + val sortedHistory = taskHistory.sortedBy { it.taskState.startedAt }.apply { + first().taskState.startedAt!!.isBefore(last().taskState.startedAt) shouldBe true + } + + expectedFiles.subList(50, 100) shouldBe sortedHistory.map { + (it.taskState.request.arguments as QueueingTask.Arguments).path + } + + instance.close() + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/GeneralTracingStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/GeneralTracingStatusTest.kt index f7f0ad930a2f0c82b942e5cdc31c1bb4b0eadf35..284789ce0bdd07669ada62f99c424bebcd3619c4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/GeneralTracingStatusTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/GeneralTracingStatusTest.kt @@ -56,10 +56,12 @@ class GeneralTracingStatusTest : BaseTest() { @Test fun `flow updates work`() = runBlockingTest { val testCollector = createInstance().generalStatus.test(startOnScope = this) + advanceUntilIdle() isBluetoothEnabled.emit(false) - isBluetoothEnabled.emit(true) + advanceUntilIdle() + isBluetoothEnabled.emit(true) advanceUntilIdle() testCollector.latestValues shouldBe listOf( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/riskdetails/DefaultRiskDetailPresenterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/riskdetails/DefaultRiskDetailPresenterTest.kt index a8363c8c376ca4ed72fb8b3704329040dd4ce21a..b8b06302d05a790816057cb5bf1e5878c27cbb32 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/riskdetails/DefaultRiskDetailPresenterTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/riskdetails/DefaultRiskDetailPresenterTest.kt @@ -1,8 +1,10 @@ package de.rki.coronawarnapp.ui.riskdetails -import de.rki.coronawarnapp.risk.RiskLevelConstants +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.ui.tracing.details.DefaultRiskDetailPresenter -import org.junit.Assert +import io.kotest.matchers.shouldBe import org.junit.Test class DefaultRiskDetailPresenterTest { @@ -10,25 +12,19 @@ class DefaultRiskDetailPresenterTest { @Test fun test_isAdditionalInfoVisible() { DefaultRiskDetailPresenter().apply { - Assert.assertFalse(isAdditionalInfoVisible(RiskLevelConstants.LOW_LEVEL_RISK, 0)) - Assert.assertTrue(isAdditionalInfoVisible(RiskLevelConstants.LOW_LEVEL_RISK, 1)) - Assert.assertFalse(isAdditionalInfoVisible(RiskLevelConstants.UNKNOWN_RISK_INITIAL, 0)) - Assert.assertFalse(isAdditionalInfoVisible(RiskLevelConstants.INCREASED_RISK, 0)) - Assert.assertFalse(isAdditionalInfoVisible(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, 0)) - Assert.assertFalse(isAdditionalInfoVisible(RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, 0)) - Assert.assertFalse(isAdditionalInfoVisible(RiskLevelConstants.UNDETERMINED, 0)) + isAdditionalInfoVisible(LOW_RISK, 0) shouldBe false + isAdditionalInfoVisible(LOW_RISK, 1) shouldBe true + isAdditionalInfoVisible(INCREASED_RISK, 0) shouldBe false + isAdditionalInfoVisible(CALCULATION_FAILED, 0) shouldBe false } } @Test fun test_isInformationBodyNoticeVisible() { DefaultRiskDetailPresenter().apply { - Assert.assertFalse(isInformationBodyNoticeVisible(RiskLevelConstants.LOW_LEVEL_RISK)) - Assert.assertTrue(isInformationBodyNoticeVisible(RiskLevelConstants.UNKNOWN_RISK_INITIAL)) - Assert.assertTrue(isInformationBodyNoticeVisible(RiskLevelConstants.INCREASED_RISK)) - Assert.assertTrue(isInformationBodyNoticeVisible(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS)) - Assert.assertTrue(isInformationBodyNoticeVisible(RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF)) - Assert.assertTrue(isInformationBodyNoticeVisible(RiskLevelConstants.UNDETERMINED)) + isInformationBodyNoticeVisible(LOW_RISK) shouldBe false + isInformationBodyNoticeVisible(INCREASED_RISK) shouldBe true + isInformationBodyNoticeVisible(CALCULATION_FAILED) shouldBe true } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt index ba00e34847588437cdfc964648d3a06e011fe2e7..dd06def65231a1bba01790341fbe48f662a6edb4 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt @@ -2,14 +2,13 @@ package de.rki.coronawarnapp.ui.tracing.card import android.content.Context import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants -import de.rki.coronawarnapp.risk.RiskLevelConstants.INCREASED_RISK -import de.rki.coronawarnapp.risk.RiskLevelConstants.LOW_LEVEL_RISK -import de.rki.coronawarnapp.risk.RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -import de.rki.coronawarnapp.risk.RiskLevelConstants.UNKNOWN_RISK_INITIAL -import de.rki.coronawarnapp.risk.RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -import de.rki.coronawarnapp.tracing.GeneralTracingStatus +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK +import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status import de.rki.coronawarnapp.tracing.TracingProgress +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.clearAllMocks @@ -17,11 +16,12 @@ import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence +import org.joda.time.Instant +import org.joda.time.format.DateTimeFormat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import java.util.Date class TracingCardStateTest : BaseTest() { @@ -38,233 +38,110 @@ class TracingCardStateTest : BaseTest() { } private fun createInstance( - tracingStatus: GeneralTracingStatus.Status = mockk(), - riskLevel: Int = 0, + tracingStatus: Status = mockk(), + riskState: RiskState = LOW_RISK, tracingProgress: TracingProgress = TracingProgress.Idle, - riskLevelLastSuccessfulCalculation: Int = 0, - matchedKeyCount: Int = 0, - daysSinceLastExposure: Int = 0, + lastSuccessfulRiskState: RiskState = LOW_RISK, + daysWithEncounters: Int = 0, + lastEncounterAt: Instant? = null, activeTracingDaysInRetentionPeriod: Long = 0, - lastTimeDiagnosisKeysFetched: Date? = mockk(), - isBackgroundJobEnabled: Boolean = false, - isManualKeyRetrievalEnabled: Boolean = false, - manualKeyRetrievalTime: Long = 0L + lastExposureDetectionTime: Instant? = mockk(), + isBackgroundJobEnabled: Boolean = false ) = TracingCardState( tracingStatus = tracingStatus, - riskLevelScore = riskLevel, + riskState = riskState, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelLastSuccessfulCalculation, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, - activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, - isBackgroundJobEnabled = isBackgroundJobEnabled, - isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, - manualKeyRetrievalTime = manualKeyRetrievalTime + lastSuccessfulRiskState = lastSuccessfulRiskState, + daysWithEncounters = daysWithEncounters, + lastEncounterAt = lastEncounterAt, + activeTracingDays = activeTracingDaysInRetentionPeriod, + lastExposureDetectionTime = lastExposureDetectionTime, + isManualKeyRetrievalEnabled = !isBackgroundJobEnabled ) @Test fun `risklevel affects icon color`() { - createInstance(riskLevel = INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getStableIconColor(context) verify { context.getColor(R.color.colorStableLight) } } - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getStableIconColor(context) - verify { context.getColor(R.color.colorTextSemanticNeutral) } - } - - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getStableIconColor(context) - verify { context.getColor(R.color.colorTextSemanticNeutral) } - } - - createInstance(riskLevel = LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { getStableIconColor(context) verify { context.getColor(R.color.colorStableLight) } } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getStableIconColor(context) - verify { context.getColor(R.color.colorStableLight) } + verify { context.getColor(R.color.colorTextSemanticNeutral) } } } @Test fun `risklevel affects riskcolors`() { - createInstance(riskLevel = INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getRiskInfoContainerBackgroundTint(context) verify { context.getColorStateList(R.color.card_increased) } } - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getRiskInfoContainerBackgroundTint(context) - verify { context.getColorStateList(R.color.card_outdated) } - } - - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getRiskInfoContainerBackgroundTint(context) - verify { context.getColorStateList(R.color.card_no_calculation) } - } - - createInstance(riskLevel = LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { getRiskInfoContainerBackgroundTint(context) verify { context.getColorStateList(R.color.card_low) } } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskInfoContainerBackgroundTint(context) - verify { context.getColorStateList(R.color.card_unknown) } + verify { context.getColorStateList(R.color.card_no_calculation) } } } @Test fun `risklevel affects risk body text`() { - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_outdated_risk_body) } - } - - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_body_tracing_off) } + createInstance(riskState = INCREASED_RISK).apply { + getErrorStateBody(context) shouldBe "" } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_unknown_risk_body) } + createInstance(riskState = LOW_RISK).apply { + getErrorStateBody(context) shouldBe "" } - createInstance(riskLevel = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_outdated_manual_risk_body) } + createInstance(riskState = CALCULATION_FAILED).apply { + getErrorStateBody(context) + verify { context.getString(R.string.risk_card_check_failed_no_internet_body) } } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { - getRiskBody(context) shouldBe "" - } - } - - @Test - fun `risklevel affected by tracing status`() { createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE + riskState = CALCULATION_FAILED, + tracingStatus = Status.TRACING_INACTIVE ).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_body_tracing_off) } - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE - ).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_body_tracing_off) } - } - - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE - ).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_body_tracing_off) } - } - - createInstance( - riskLevel = LOW_LEVEL_RISK, - tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE - ).apply { - getRiskBody(context) - verify { context.getString(R.string.risk_card_body_tracing_off) } - } - - createInstance( - riskLevel = INCREASED_RISK, - tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE - ).apply { - getRiskBody(context) + getErrorStateBody(context) verify { context.getString(R.string.risk_card_body_tracing_off) } } } @Test fun `saved risk body is affected by risklevel`() { - createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = 0 - ).apply { + createInstance(riskState = CALCULATION_FAILED, lastSuccessfulRiskState = CALCULATION_FAILED).apply { getSavedRiskBody(context) shouldBe "" } - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - riskLevelLastSuccessfulCalculation = 0 - ).apply { + createInstance(riskState = LOW_RISK, lastSuccessfulRiskState = CALCULATION_FAILED).apply { getSavedRiskBody(context) shouldBe "" } - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - riskLevelLastSuccessfulCalculation = 0 - ).apply { + createInstance(riskState = INCREASED_RISK, lastSuccessfulRiskState = INCREASED_RISK).apply { getSavedRiskBody(context) shouldBe "" } - createInstance( - riskLevel = LOW_LEVEL_RISK, - riskLevelLastSuccessfulCalculation = 0 - ).apply { + createInstance(riskState = INCREASED_RISK, lastSuccessfulRiskState = CALCULATION_FAILED).apply { getSavedRiskBody(context) shouldBe "" } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - riskLevelLastSuccessfulCalculation = 0 - ).apply { - getSavedRiskBody(context) shouldBe "" - } - - createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = INCREASED_RISK - ).apply { - getSavedRiskBody(context) shouldBe "" - } - - createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = UNKNOWN_RISK_OUTDATED_RESULTS - ).apply { - getSavedRiskBody(context) shouldBe "" - } - - createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = NO_CALCULATION_POSSIBLE_TRACING_OFF - ).apply { - getSavedRiskBody(context) shouldBe "" - } - - createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = LOW_LEVEL_RISK - ).apply { + createInstance(riskState = INCREASED_RISK, lastSuccessfulRiskState = LOW_RISK).apply { getSavedRiskBody(context) shouldBe "" } - createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = UNKNOWN_RISK_INITIAL - ).apply { - getSavedRiskBody(context) shouldBe "" - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - riskLevelLastSuccessfulCalculation = LOW_LEVEL_RISK - ).apply { + createInstance(riskState = CALCULATION_FAILED, lastSuccessfulRiskState = LOW_RISK).apply { getSavedRiskBody(context) verify { context @@ -273,10 +150,7 @@ class TracingCardStateTest : BaseTest() { } } - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - riskLevelLastSuccessfulCalculation = INCREASED_RISK - ).apply { + createInstance(riskState = CALCULATION_FAILED, lastSuccessfulRiskState = INCREASED_RISK).apply { getSavedRiskBody(context) verify { context @@ -285,22 +159,7 @@ class TracingCardStateTest : BaseTest() { } } - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - riskLevelLastSuccessfulCalculation = UNKNOWN_RISK_INITIAL - ).apply { - getSavedRiskBody(context) - verify { - context - .getString(R.string.risk_card_no_calculation_possible_body_saved_risk) - .format(context.getString(R.string.risk_card_unknown_risk_headline)) - } - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - riskLevelLastSuccessfulCalculation = LOW_LEVEL_RISK - ).apply { + createInstance(riskState = CALCULATION_FAILED, lastSuccessfulRiskState = LOW_RISK).apply { getSavedRiskBody(context) verify { context @@ -308,144 +167,32 @@ class TracingCardStateTest : BaseTest() { .format(context.getString(R.string.risk_card_low_risk_headline)) } } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - riskLevelLastSuccessfulCalculation = INCREASED_RISK - ).apply { - getSavedRiskBody(context) - verify { - context - .getString(R.string.risk_card_no_calculation_possible_body_saved_risk) - .format(context.getString(R.string.risk_card_increased_risk_headline)) - } - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - riskLevelLastSuccessfulCalculation = UNKNOWN_RISK_INITIAL - ).apply { - getSavedRiskBody(context) - verify { - context - .getString(R.string.risk_card_no_calculation_possible_body_saved_risk) - .format(context.getString(R.string.risk_card_unknown_risk_headline)) - } - } } @Test fun `risk contact body is affected by risklevel`() { - createInstance( - riskLevel = INCREASED_RISK, - matchedKeyCount = 0 - ).apply { - getRiskContactBody(context) - verify { context.getString(R.string.risk_card_body_contact) } - } - - createInstance( - riskLevel = INCREASED_RISK, - matchedKeyCount = 2 - ).apply { - getRiskContactBody(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_body_contact_value_high_risk, - 2, - 2 - ) - } - } - - createInstance( - riskLevel = LOW_LEVEL_RISK, - matchedKeyCount = 0 - ).apply { - getRiskContactBody(context) - verify { context.getString(R.string.risk_card_body_contact) } - } - - createInstance( - riskLevel = LOW_LEVEL_RISK, - matchedKeyCount = 2 - ).apply { - getRiskContactBody(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_body_contact_value, - 2, - 2 - ) - } - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - matchedKeyCount = 0 - ).apply { - getRiskContactBody(context) shouldBe "" - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - matchedKeyCount = 0 - ).apply { + createInstance(riskState = CALCULATION_FAILED, daysWithEncounters = 0).apply { getRiskContactBody(context) shouldBe "" } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - matchedKeyCount = 0 - ).apply { - getRiskContactBody(context) shouldBe "" - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - matchedKeyCount = 2 - ).apply { - getRiskContactBody(context) shouldBe "" - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - matchedKeyCount = 2 - ).apply { - getRiskContactBody(context) shouldBe "" - } - - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - matchedKeyCount = 2 - ).apply { + createInstance(riskState = CALCULATION_FAILED, daysWithEncounters = 2).apply { getRiskContactBody(context) shouldBe "" } } @Test fun `risk icon formatting`() { - createInstance(riskLevel = INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getRiskContactIcon(context) verify { context.getDrawable(R.drawable.ic_risk_card_contact_increased) } } - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getRiskContactIcon(context) - verify { context.getDrawable(R.drawable.ic_risk_card_contact) } - } - - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getRiskContactIcon(context) - verify { context.getDrawable(R.drawable.ic_risk_card_contact) } - } - - createInstance(riskLevel = LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { getRiskContactIcon(context) verify { context.getDrawable(R.drawable.ic_risk_card_contact) } } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskContactIcon(context) verify { context.getDrawable(R.drawable.ic_risk_card_contact) } } @@ -453,389 +200,216 @@ class TracingCardStateTest : BaseTest() { @Test fun `last risk contact text formatting`() { - createInstance( - riskLevel = INCREASED_RISK, - daysSinceLastExposure = 2 - ).apply { - getRiskContactLast(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_increased_risk_body_contact_last, - 2, - 2 - ) - } - } - - createInstance( - riskLevel = INCREASED_RISK, - daysSinceLastExposure = 0 - ).apply { + createInstance(riskState = INCREASED_RISK, lastEncounterAt = Instant.EPOCH).apply { getRiskContactLast(context) verify { - context.resources.getQuantityString( - R.plurals.risk_card_increased_risk_body_contact_last, - 0, - 0 + context.getString( + R.string.risk_card_high_risk_most_recent_body, + Instant.EPOCH.toLocalDate().toString(DateTimeFormat.mediumDate()) ) } } createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - daysSinceLastExposure = 2 + riskState = INCREASED_RISK, + lastEncounterAt = Instant.EPOCH, + tracingStatus = Status.TRACING_INACTIVE ).apply { getRiskContactLast(context) shouldBe "" } - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - daysSinceLastExposure = 2 - ).apply { + createInstance(riskState = LOW_RISK).apply { getRiskContactLast(context) shouldBe "" } - createInstance( - riskLevel = LOW_LEVEL_RISK, - daysSinceLastExposure = 2 - ).apply { - getRiskContactLast(context) shouldBe "" - } - - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - daysSinceLastExposure = 2 - ).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskContactLast(context) shouldBe "" } } @Test fun `text for active risktracing in retention period`() { - createInstance( - riskLevel = INCREASED_RISK, - activeTracingDaysInRetentionPeriod = 1 - ).apply { - getRiskActiveTracingDaysInRetentionPeriod(context) shouldBe "" - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - activeTracingDaysInRetentionPeriod = 1 - ).apply { + createInstance(riskState = INCREASED_RISK, activeTracingDaysInRetentionPeriod = 1).apply { getRiskActiveTracingDaysInRetentionPeriod(context) shouldBe "" } - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - activeTracingDaysInRetentionPeriod = 1 - ).apply { + createInstance(riskState = CALCULATION_FAILED, activeTracingDaysInRetentionPeriod = 1).apply { getRiskActiveTracingDaysInRetentionPeriod(context) shouldBe "" } - createInstance( - riskLevel = LOW_LEVEL_RISK, - activeTracingDaysInRetentionPeriod = 1 - ).apply { + createInstance(riskState = LOW_RISK, activeTracingDaysInRetentionPeriod = 1).apply { getRiskActiveTracingDaysInRetentionPeriod(context) verify { context.getString(R.string.risk_card_body_saved_days).format(1) } } - createInstance( - riskLevel = LOW_LEVEL_RISK, - activeTracingDaysInRetentionPeriod = 2 - ).apply { + createInstance(riskState = LOW_RISK, activeTracingDaysInRetentionPeriod = 2).apply { getRiskActiveTracingDaysInRetentionPeriod(context) verify { context.getString(R.string.risk_card_body_saved_days).format(2) } } - - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - activeTracingDaysInRetentionPeriod = 1 - ).apply { - getRiskActiveTracingDaysInRetentionPeriod(context) shouldBe "" - } } @Test fun `text for last time diagnosis keys were fetched`() { - val date = Date() + val date = Instant() createInstance( - riskLevel = INCREASED_RISK, - riskLevelLastSuccessfulCalculation = 2, - lastTimeDiagnosisKeysFetched = date + riskState = INCREASED_RISK, + lastSuccessfulRiskState = LOW_RISK, + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - riskLevelLastSuccessfulCalculation = 2, - lastTimeDiagnosisKeysFetched = date + riskState = CALCULATION_FAILED, + lastSuccessfulRiskState = LOW_RISK, + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - riskLevelLastSuccessfulCalculation = 2, - lastTimeDiagnosisKeysFetched = date + riskState = CALCULATION_FAILED, + lastSuccessfulRiskState = LOW_RISK, + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } createInstance( - riskLevel = LOW_LEVEL_RISK, - riskLevelLastSuccessfulCalculation = 2, - lastTimeDiagnosisKeysFetched = date + riskState = LOW_RISK, + lastSuccessfulRiskState = LOW_RISK, + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - riskLevelLastSuccessfulCalculation = 2, - lastTimeDiagnosisKeysFetched = date - ).apply { - getTimeFetched(context) shouldBe "" - } - - createInstance( - riskLevel = INCREASED_RISK, - lastTimeDiagnosisKeysFetched = date - ).apply { + createInstance(riskState = INCREASED_RISK, lastExposureDetectionTime = date).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - lastTimeDiagnosisKeysFetched = date - ).apply { - getTimeFetched(context) shouldBe "" - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - lastTimeDiagnosisKeysFetched = date - ).apply { + createInstance(riskState = CALCULATION_FAILED, lastExposureDetectionTime = date).apply { getTimeFetched(context) shouldBe "" } - createInstance( - riskLevel = LOW_LEVEL_RISK, - lastTimeDiagnosisKeysFetched = date - ).apply { + createInstance(riskState = LOW_RISK, lastExposureDetectionTime = date).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - lastTimeDiagnosisKeysFetched = date - ).apply { - getTimeFetched(context) shouldBe "" - } - - createInstance( - riskLevel = INCREASED_RISK, - lastTimeDiagnosisKeysFetched = null - ).apply { + createInstance(riskState = INCREASED_RISK, lastExposureDetectionTime = null).apply { getTimeFetched(context) verify { context.getString(R.string.risk_card_body_not_yet_fetched) } } - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - lastTimeDiagnosisKeysFetched = null - ).apply { + createInstance(riskState = CALCULATION_FAILED, lastExposureDetectionTime = null).apply { getTimeFetched(context) shouldBe "" } - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - lastTimeDiagnosisKeysFetched = null - ).apply { - getTimeFetched(context) shouldBe "" - } - - createInstance( - riskLevel = LOW_LEVEL_RISK, - lastTimeDiagnosisKeysFetched = null - ).apply { + createInstance(riskState = LOW_RISK, lastExposureDetectionTime = null).apply { getTimeFetched(context) verify { context.getString(R.string.risk_card_body_not_yet_fetched) } } - - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - lastTimeDiagnosisKeysFetched = null - ).apply { - getTimeFetched(context) shouldBe "" - } } @Test fun `task divider is formatted according to riskLevel`() { - createInstance(riskLevel = INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getStableDividerColor(context) verify { context.getColor(R.color.colorStableHairlineLight) } } - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { + createInstance(riskState = INCREASED_RISK, tracingStatus = Status.TRACING_INACTIVE).apply { getStableDividerColor(context) verify { context.getColor(R.color.colorStableHairlineDark) } } - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getStableDividerColor(context) - verify { context.getColor(R.color.colorStableHairlineDark) } - } - - createInstance(riskLevel = LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { getStableDividerColor(context) verify { context.getColor(R.color.colorStableHairlineLight) } } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getStableDividerColor(context) - verify { context.getColor(R.color.colorStableHairlineLight) } + verify { context.getColor(R.color.colorStableHairlineDark) } } } @Test fun `tracing button visibility depends on risklevel`() { - createInstance(riskLevel = INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { showTracingButton() shouldBe false } - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { - showTracingButton() shouldBe true - } - - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - showTracingButton() shouldBe true + createInstance(riskState = LOW_RISK).apply { + showTracingButton() shouldBe false } - createInstance(riskLevel = LOW_LEVEL_RISK).apply { + createInstance(riskState = CALCULATION_FAILED).apply { showTracingButton() shouldBe false } - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { - showTracingButton() shouldBe false + createInstance(riskState = CALCULATION_FAILED, tracingStatus = Status.TRACING_INACTIVE).apply { + showTracingButton() shouldBe true } } @Test fun `update button visibility`() { - createInstance( - riskLevel = INCREASED_RISK, - isBackgroundJobEnabled = false - ).apply { + createInstance(riskState = INCREASED_RISK, isBackgroundJobEnabled = false).apply { showUpdateButton() shouldBe true } - createInstance( - riskLevel = INCREASED_RISK, - isBackgroundJobEnabled = true - ).apply { - showUpdateButton() shouldBe false - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - isBackgroundJobEnabled = false - ).apply { - showUpdateButton() shouldBe false - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - isBackgroundJobEnabled = true - ).apply { - showUpdateButton() shouldBe false - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - isBackgroundJobEnabled = false - ).apply { - showUpdateButton() shouldBe false - } - - createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - isBackgroundJobEnabled = true - ).apply { + createInstance(riskState = INCREASED_RISK, isBackgroundJobEnabled = true).apply { showUpdateButton() shouldBe false } - createInstance( - riskLevel = LOW_LEVEL_RISK, - isBackgroundJobEnabled = false - ).apply { + createInstance(riskState = CALCULATION_FAILED, isBackgroundJobEnabled = false).apply { showUpdateButton() shouldBe true } - createInstance( - riskLevel = LOW_LEVEL_RISK, - isBackgroundJobEnabled = true - ).apply { - showUpdateButton() shouldBe false + createInstance(riskState = CALCULATION_FAILED, isBackgroundJobEnabled = true).apply { + showUpdateButton() shouldBe true } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - isBackgroundJobEnabled = false - ).apply { + createInstance(riskState = LOW_RISK, isBackgroundJobEnabled = false).apply { showUpdateButton() shouldBe true } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - isBackgroundJobEnabled = true - ).apply { + createInstance(riskState = LOW_RISK, isBackgroundJobEnabled = true).apply { showUpdateButton() shouldBe false } } @Test fun `risklevel headline is affected by score`() { - createInstance(riskLevel = INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getRiskLevelHeadline(context) verify { context.getString(R.string.risk_card_increased_risk_headline) } } - createInstance(riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskLevelHeadline(context) - verify { context.getString(R.string.risk_card_outdated_risk_headline) } + verify { context.getString(R.string.risk_card_check_failed_no_internet_headline) } } - createInstance(riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { + createInstance(riskState = CALCULATION_FAILED, tracingStatus = Status.TRACING_INACTIVE).apply { getRiskLevelHeadline(context) verify { context.getString(R.string.risk_card_no_calculation_possible_headline) } } - createInstance(riskLevel = LOW_LEVEL_RISK).apply { - getRiskLevelHeadline(context) - verify { context.getString(R.string.risk_card_low_risk_headline) } - } - - createInstance(riskLevel = UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = INCREASED_RISK, tracingStatus = Status.TRACING_INACTIVE).apply { getRiskLevelHeadline(context) - verify { context.getString(R.string.risk_card_unknown_risk_headline) } + verify { context.getString(R.string.risk_card_no_calculation_possible_headline) } } - createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - tracingProgress = TracingProgress.Downloading - ).apply { + createInstance(riskState = LOW_RISK).apply { getRiskLevelHeadline(context) - verify { context.getString(R.string.risk_card_unknown_risk_headline) } + verify { context.getString(R.string.risk_card_low_risk_headline) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt index 75687a7d8950be735359262e26f93162fbbd1a87..2f280ca94b56a7a1a4f7a637630ce0f6a111df09 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt @@ -2,8 +2,11 @@ package de.rki.coronawarnapp.ui.tracing.common import android.content.Context import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants -import de.rki.coronawarnapp.tracing.GeneralTracingStatus +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK +import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status import de.rki.coronawarnapp.tracing.TracingProgress import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -11,27 +14,15 @@ import io.mockk.clearAllMocks import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify -import io.mockk.verifySequence import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import java.util.Date class BaseTracingStateTest : BaseTest() { @MockK(relaxed = true) lateinit var context: Context - val constants = listOf( - RiskLevelConstants.UNKNOWN_RISK_INITIAL, - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - RiskLevelConstants.LOW_LEVEL_RISK, - RiskLevelConstants.INCREASED_RISK, - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL, - RiskLevelConstants.UNDETERMINED - ) - @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -43,126 +34,82 @@ class BaseTracingStateTest : BaseTest() { } private fun createInstance( - tracingStatus: GeneralTracingStatus.Status = mockk(), - riskLevelScore: Int = 0, + tracingStatus: Status = mockk(), + riskState: RiskState = LOW_RISK, tracingProgress: TracingProgress = TracingProgress.Idle, - riskLevelLastSuccessfulCalculation: Int = 0, - matchedKeyCount: Int = 0, - daysSinceLastExposure: Int = 0, - activeTracingDaysInRetentionPeriod: Long = 0, - lastTimeDiagnosisKeysFetched: Date? = mockk(), - isBackgroundJobEnabled: Boolean = false, isManualKeyRetrievalEnabled: Boolean = false, - manualKeyRetrievalTime: Long = 0L, showDetails: Boolean = false ) = object : BaseTracingState() { - override val tracingStatus: GeneralTracingStatus.Status = tracingStatus - override val riskLevelScore: Int = riskLevelScore + override val tracingStatus: Status = tracingStatus + override val riskState: RiskState = riskState override val tracingProgress: TracingProgress = tracingProgress - override val lastRiskLevelScoreCalculated: Int = riskLevelLastSuccessfulCalculation - override val matchedKeyCount: Int = matchedKeyCount - override val daysSinceLastExposure: Int = daysSinceLastExposure - override val activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod - override val lastTimeDiagnosisKeysFetched: Date? = lastTimeDiagnosisKeysFetched - override val isBackgroundJobEnabled: Boolean = isBackgroundJobEnabled override val showDetails: Boolean = showDetails override val isManualKeyRetrievalEnabled: Boolean = isManualKeyRetrievalEnabled - override val manualKeyRetrievalTime: Long = manualKeyRetrievalTime } @Test fun `risk color`() { - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getRiskColor(context) verify { context.getColor(R.color.colorSemanticHighRisk) } } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getRiskColor(context) - verify { context.getColor(R.color.colorSemanticUnknownRisk) } - } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getRiskColor(context) - verify { context.getColor(R.color.colorSemanticUnknownRisk) } - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { getRiskColor(context) verify { context.getColor(R.color.colorSemanticLowRisk) } } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskColor(context) - verify { context.getColor(R.color.colorSemanticNeutralRisk) } + verify { context.getColor(R.color.colorSemanticUnknownRisk) } } } @Test fun `risk tracing off level`() { - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - isTracingOffRiskLevel() shouldBe true - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - isTracingOffRiskLevel() shouldBe true + createInstance(riskState = CALCULATION_FAILED, tracingStatus = Status.TRACING_INACTIVE).apply { + isTracingOff() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - isTracingOffRiskLevel() shouldBe false + createInstance(riskState = CALCULATION_FAILED).apply { + isTracingOff() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { - isTracingOffRiskLevel() shouldBe false + createInstance(riskState = LOW_RISK).apply { + isTracingOff() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { - isTracingOffRiskLevel() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - isTracingOffRiskLevel() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { - isTracingOffRiskLevel() shouldBe false + createInstance(riskState = INCREASED_RISK).apply { + isTracingOff() shouldBe false } } @Test fun `text color`() { - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getStableTextColor(context) - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getStableTextColor(context) - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - getStableTextColor(context) - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { - getStableTextColor(context) - } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + + createInstance(riskState = CALCULATION_FAILED).apply { getStableTextColor(context) + verify { context.getColor(R.color.colorTextPrimary1) } } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { + createInstance(riskState = LOW_RISK).apply { getStableTextColor(context) + verify { context.getColor(R.color.colorTextPrimary1InvertedStable) } } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = INCREASED_RISK).apply { getStableTextColor(context) + verify { context.getColor(R.color.colorTextPrimary1InvertedStable) } } - verifySequence { - context.getColor(R.color.colorTextPrimary1) - context.getColor(R.color.colorTextPrimary1) - context.getColor(R.color.colorStableLight) - context.getColor(R.color.colorStableLight) - context.getColor(R.color.colorStableLight) - context.getColor(R.color.colorStableLight) - context.getColor(R.color.colorStableLight) + createInstance(riskState = INCREASED_RISK, tracingStatus = Status.TRACING_INACTIVE).apply { + getStableTextColor(context) + verify { context.getColor(R.color.colorTextPrimary1) } } } @Test fun `update button text`() { - createInstance(manualKeyRetrievalTime = 0).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getUpdateButtonText(context) - verify { context.getString(R.string.risk_card_button_update) } + verify { context.getString(R.string.risk_card_check_failed_no_internet_restart_button) } } - createInstance(manualKeyRetrievalTime = 1).apply { + createInstance().apply { getUpdateButtonText(context) - verify { context.getString(R.string.risk_card_button_cooldown) } + verify { context.getString(R.string.risk_card_button_update) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskFormattingTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskFormattingTest.kt deleted file mode 100644 index c96d629ea4c2f7a93db2e1daf9bcfe0521e1c475..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskFormattingTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -package de.rki.coronawarnapp.ui.tracing.common - -import android.content.Context -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants -import io.mockk.MockKAnnotations -import io.mockk.clearAllMocks -import io.mockk.impl.annotations.MockK -import io.mockk.verifySequence -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import testhelpers.BaseTest - -class RiskFormattingTest : BaseTest() { - - @MockK(relaxed = true) lateinit var context: Context - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - } - - @AfterEach - fun teardown() { - clearAllMocks() - } - - @Test - fun `risklevel affects icon`() { - formatBehaviorIcon(context, RiskLevelConstants.INCREASED_RISK) - formatBehaviorIcon(context, RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS) - formatBehaviorIcon(context, RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF) - formatBehaviorIcon(context, RiskLevelConstants.LOW_LEVEL_RISK) - formatBehaviorIcon(context, RiskLevelConstants.UNKNOWN_RISK_INITIAL) - - verifySequence { - context.getColor(R.color.colorStableLight) - context.getColor(R.color.colorTextSemanticNeutral) - context.getColor(R.color.colorTextSemanticNeutral) - context.getColor(R.color.colorStableLight) - context.getColor(R.color.colorStableLight) - } - } - - @Test - fun `risklevel affects icon background`() { - formatBehaviorIconBackground(context, RiskLevelConstants.INCREASED_RISK) - formatBehaviorIconBackground(context, RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS) - formatBehaviorIconBackground( - context, - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF - ) - formatBehaviorIconBackground(context, RiskLevelConstants.LOW_LEVEL_RISK) - formatBehaviorIconBackground(context, RiskLevelConstants.UNKNOWN_RISK_INITIAL) - - verifySequence { - context.getColor(R.color.colorSemanticHighRisk) - context.getColor(R.color.colorSurface2) - context.getColor(R.color.colorSurface2) - context.getColor(R.color.colorSemanticLowRisk) - context.getColor(R.color.colorSemanticNeutralRisk) - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelResultExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelResultExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ae361d55af51e0786196de156ab0f83c3a91c42 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelResultExtensionsTest.kt @@ -0,0 +1,86 @@ +package de.rki.coronawarnapp.ui.tracing.common + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import io.kotest.matchers.longs.shouldBeInRange +import io.kotest.matchers.shouldBe +import io.mockk.mockk +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RiskLevelResultExtensionsTest : BaseTest() { + + private fun createRiskLevelResult( + hasResult: Boolean, + calculatedAt: Instant + ): RiskLevelResult = object : RiskLevelResult { + override val calculatedAt: Instant = calculatedAt + override val aggregatedRiskResult: AggregatedRiskResult? = if (hasResult) mockk() else null + override val failureReason: RiskLevelResult.FailureReason? + get() = if (!hasResult) RiskLevelResult.FailureReason.UNKNOWN else null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + @Test + fun `getLastestAndLastSuccessful on empty results`() { + val emptyResults = emptyList<RiskLevelResult>() + + emptyResults.tryLatestResultsWithDefaults().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 + } + } + } + + @Test + fun `getLastestAndLastSuccessful last calculation was successful`() { + val results = listOf( + createRiskLevelResult(hasResult = true, calculatedAt = Instant.EPOCH), + createRiskLevelResult(hasResult = true, calculatedAt = Instant.EPOCH.plus(1)) + ) + + results.tryLatestResultsWithDefaults().apply { + lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1) + lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1) + } + } + + @Test + fun `getLastestAndLastSuccessful last calculation was not successful`() { + val results = listOf( + createRiskLevelResult(hasResult = true, calculatedAt = Instant.EPOCH), + createRiskLevelResult(hasResult = true, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevelResult(hasResult = false, calculatedAt = Instant.EPOCH.plus(2)) + ) + + results.tryLatestResultsWithDefaults().apply { + lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(2) + lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1) + } + } + + @Test + fun `getLastestAndLastSuccessful no successful calculations yet`() { + val results = listOf( + createRiskLevelResult(hasResult = false, calculatedAt = Instant.EPOCH.plus(10)), + createRiskLevelResult(hasResult = false, calculatedAt = Instant.EPOCH.plus(11)), + createRiskLevelResult(hasResult = false, calculatedAt = Instant.EPOCH.plus(12)), + createRiskLevelResult(hasResult = false, calculatedAt = Instant.EPOCH.plus(13)) + ) + + results.tryLatestResultsWithDefaults().apply { + lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(13) + lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt index 93154d75926eaf91c65b80950faeac0735a74072..432070fbfe7f712f3a1447e944673d01ae42ecc0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt @@ -3,7 +3,10 @@ package de.rki.coronawarnapp.ui.tracing.details import android.content.Context import android.content.res.Resources import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevelConstants +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress import io.kotest.matchers.shouldBe @@ -17,7 +20,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import java.util.Date class TracingDetailsStateTest : BaseTest() { @@ -38,161 +40,92 @@ class TracingDetailsStateTest : BaseTest() { private fun createInstance( tracingStatus: GeneralTracingStatus.Status = mockk(), - riskLevelScore: Int = 0, + riskState: RiskState, tracingProgress: TracingProgress = TracingProgress.Idle, - riskLevelLastSuccessfulCalculation: Int = 0, matchedKeyCount: Int = 3, daysSinceLastExposure: Int = 2, activeTracingDaysInRetentionPeriod: Long = 0, - lastTimeDiagnosisKeysFetched: Date? = mockk(), isBackgroundJobEnabled: Boolean = false, - isManualKeyRetrievalEnabled: Boolean = false, - manualKeyRetrievalTime: Long = 0L, isInformationBodyNoticeVisible: Boolean = false, isAdditionalInformationVisible: Boolean = false ) = TracingDetailsState( tracingStatus = tracingStatus, - riskLevelScore = riskLevelScore, + riskState = riskState, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelLastSuccessfulCalculation, matchedKeyCount = matchedKeyCount, daysSinceLastExposure = daysSinceLastExposure, activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, - isBackgroundJobEnabled = isBackgroundJobEnabled, - isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, - manualKeyRetrievalTime = manualKeyRetrievalTime, + isManualKeyRetrievalEnabled = !isBackgroundJobEnabled, isInformationBodyNoticeVisible = isInformationBodyNoticeVisible, isAdditionalInformationVisible = isAdditionalInformationVisible ) @Test fun `normal behavior visibility`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = LOW_RISK).apply { isBehaviorNormalVisible() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - isBehaviorNormalVisible() shouldBe true - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { - isBehaviorNormalVisible() shouldBe true - } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { isBehaviorNormalVisible() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - isBehaviorNormalVisible() shouldBe true - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - isBehaviorNormalVisible() shouldBe true - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = CALCULATION_FAILED).apply { isBehaviorNormalVisible() shouldBe true } } @Test fun `increased risk visibility`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { + createInstance(riskState = LOW_RISK).apply { isBehaviorIncreasedRiskVisible() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - isBehaviorIncreasedRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { - isBehaviorIncreasedRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { isBehaviorIncreasedRiskVisible() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - isBehaviorIncreasedRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - isBehaviorIncreasedRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = CALCULATION_FAILED).apply { isBehaviorIncreasedRiskVisible() shouldBe false } } @Test fun `logged period card visibility`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - isBehaviorPeriodLoggedVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - isBehaviorPeriodLoggedVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { isBehaviorPeriodLoggedVisible() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { isBehaviorPeriodLoggedVisible() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - isBehaviorPeriodLoggedVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - isBehaviorPeriodLoggedVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = CALCULATION_FAILED).apply { isBehaviorPeriodLoggedVisible() shouldBe false } } @Test fun `low level risk visibility`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - isBehaviorLowLevelRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - isBehaviorLowLevelRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, matchedKeyCount = 1).apply { + createInstance(riskState = LOW_RISK, matchedKeyCount = 1).apply { isBehaviorLowLevelRiskVisible() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, matchedKeyCount = 0).apply { + createInstance(riskState = LOW_RISK, matchedKeyCount = 0).apply { isBehaviorLowLevelRiskVisible() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { isBehaviorLowLevelRiskVisible() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - isBehaviorLowLevelRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - isBehaviorLowLevelRiskVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = CALCULATION_FAILED).apply { isBehaviorLowLevelRiskVisible() shouldBe false } } @Test fun `risk details body text`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - getRiskDetailsRiskLevelBody(context) - verify { context.getString(R.string.risk_details_information_body_unknown_risk) } - } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getRiskDetailsRiskLevelBody(context) shouldBe "" - } - createInstance( - riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, - matchedKeyCount = 1 - ).apply { + createInstance(riskState = LOW_RISK, matchedKeyCount = 1).apply { getRiskDetailsRiskLevelBody(context) verify { context.getString(R.string.risk_details_information_body_low_risk_with_encounter) } } - createInstance( - riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, - matchedKeyCount = 0 - ).apply { + createInstance(riskState = LOW_RISK, matchedKeyCount = 0).apply { getRiskDetailsRiskLevelBody(context) verify { context.getString(R.string.risk_details_information_body_low_risk) } } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getRiskDetailsRiskLevelBody(context) verify { resources.getQuantityString( @@ -201,207 +134,78 @@ class TracingDetailsStateTest : BaseTest() { ) } } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskDetailsRiskLevelBody(context) verify { context.getString(R.string.risk_details_information_body_outdated_risk) } } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - getRiskDetailsRiskLevelBody(context) shouldBe "" - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { - getRiskDetailsRiskLevelBody(context) shouldBe "" - } } @Test fun `riskdetails body notice`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - getRiskDetailsRiskLevelBodyNotice(context) - verify { context.getString(R.string.risk_details_information_body_notice) } - } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { - getRiskDetailsRiskLevelBodyNotice(context) - verify { context.getString(R.string.risk_details_information_body_notice) } - } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { getRiskDetailsRiskLevelBodyNotice(context) verify { context.getString(R.string.risk_details_information_body_notice) } } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { + createInstance(riskState = INCREASED_RISK).apply { getRiskDetailsRiskLevelBodyNotice(context) verify { context.getString(R.string.risk_details_information_body_notice_increased) } } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - getRiskDetailsRiskLevelBodyNotice(context) - verify { context.getString(R.string.risk_details_information_body_notice) } - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { - getRiskDetailsRiskLevelBodyNotice(context) - verify { context.getString(R.string.risk_details_information_body_notice) } - } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = CALCULATION_FAILED).apply { getRiskDetailsRiskLevelBodyNotice(context) verify { context.getString(R.string.risk_details_information_body_notice) } } } @Test - fun `risk details buttons visibility`() { - createInstance( - riskLevelScore = RiskLevelConstants.INCREASED_RISK, - isBackgroundJobEnabled = true - ).apply { - areRiskDetailsButtonsVisible() shouldBe false - } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - isBackgroundJobEnabled = true - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - isBackgroundJobEnabled = true - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, - isBackgroundJobEnabled = true - ).apply { - areRiskDetailsButtonsVisible() shouldBe false - } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL, - isBackgroundJobEnabled = true - ).apply { - areRiskDetailsButtonsVisible() shouldBe false - } - createInstance( - riskLevelScore = RiskLevelConstants.INCREASED_RISK, - isBackgroundJobEnabled = false - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - isBackgroundJobEnabled = false - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - isBackgroundJobEnabled = false - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, - isBackgroundJobEnabled = false - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL, - isBackgroundJobEnabled = false - ).apply { - areRiskDetailsButtonsVisible() shouldBe true - } - } - - @Test - fun `enable tracing button visibility`() { - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL).apply { - isRiskDetailsEnableTracingButtonVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF).apply { + fun `is tracing enable tracing button visible`() { + createInstance(riskState = LOW_RISK, tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE).apply { isRiskDetailsEnableTracingButtonVisible() shouldBe true } - createInstance(riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK).apply { + createInstance(riskState = LOW_RISK).apply { isRiskDetailsEnableTracingButtonVisible() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.INCREASED_RISK).apply { - isRiskDetailsEnableTracingButtonVisible() shouldBe false - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS).apply { - isRiskDetailsEnableTracingButtonVisible() shouldBe true - } - createInstance(riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL).apply { + createInstance(riskState = INCREASED_RISK).apply { isRiskDetailsEnableTracingButtonVisible() shouldBe false } - createInstance(riskLevelScore = RiskLevelConstants.UNDETERMINED).apply { + createInstance(riskState = CALCULATION_FAILED).apply { isRiskDetailsEnableTracingButtonVisible() shouldBe false } } @Test - fun `risk details update button visibility`() { - createInstance( - riskLevelScore = RiskLevelConstants.INCREASED_RISK, - isBackgroundJobEnabled = true - ).apply { - isRiskDetailsUpdateButtonVisible() shouldBe false - } + fun `manual update button visible`() { createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - isBackgroundJobEnabled = true + riskState = INCREASED_RISK, + isBackgroundJobEnabled = false, + tracingStatus = GeneralTracingStatus.Status.TRACING_INACTIVE ).apply { isRiskDetailsUpdateButtonVisible() shouldBe false } - createInstance( - riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - isBackgroundJobEnabled = true - ).apply { + createInstance(riskState = INCREASED_RISK, isBackgroundJobEnabled = true).apply { isRiskDetailsUpdateButtonVisible() shouldBe false } - createInstance( - riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, - isBackgroundJobEnabled = true - ).apply { - isRiskDetailsUpdateButtonVisible() shouldBe false + createInstance(riskState = INCREASED_RISK, isBackgroundJobEnabled = false).apply { + isRiskDetailsUpdateButtonVisible() shouldBe true } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL, - isBackgroundJobEnabled = true - ).apply { + + createInstance(riskState = LOW_RISK, isBackgroundJobEnabled = true).apply { isRiskDetailsUpdateButtonVisible() shouldBe false } - - createInstance( - riskLevelScore = RiskLevelConstants.INCREASED_RISK, - isBackgroundJobEnabled = false - ).apply { + createInstance(riskState = LOW_RISK, isBackgroundJobEnabled = false).apply { isRiskDetailsUpdateButtonVisible() shouldBe true } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS, - isBackgroundJobEnabled = false - ).apply { - isRiskDetailsUpdateButtonVisible() shouldBe false - } - createInstance( - riskLevelScore = RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF, - isBackgroundJobEnabled = false - ).apply { + + createInstance(riskState = CALCULATION_FAILED, isBackgroundJobEnabled = true).apply { isRiskDetailsUpdateButtonVisible() shouldBe false } - createInstance( - riskLevelScore = RiskLevelConstants.LOW_LEVEL_RISK, - isBackgroundJobEnabled = false - ).apply { - isRiskDetailsUpdateButtonVisible() shouldBe true - } - createInstance( - riskLevelScore = RiskLevelConstants.UNKNOWN_RISK_INITIAL, - isBackgroundJobEnabled = false - ).apply { + createInstance(riskState = CALCULATION_FAILED, isBackgroundJobEnabled = false).apply { isRiskDetailsUpdateButtonVisible() shouldBe true } } @Test fun `format active tracing days in retention`() { - createInstance().apply { + createInstance(riskState = LOW_RISK).apply { getRiskActiveTracingDaysInRetentionPeriodLogged(context) verify { context.getString(R.string.risk_details_information_body_period_logged_assessment) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/serialization/GsonExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/serialization/GsonExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..72b6ed30afc63566e18ffef7f03839246faaeafe --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/serialization/GsonExtensionsTest.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.util.serialization + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File +import java.util.UUID + +class GsonExtensionsTest : BaseIOTest() { + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + private val testFile = File(testDir, "testfile") + private val gson = Gson() + + @BeforeEach + fun setup() { + testDir.mkdirs() + } + + @AfterEach + fun teardown() { + testDir.deleteRecursively() + } + + data class TestData( + val value: String + ) + + @Test + fun `serialize and deserialize`() { + val testData = TestData(value = UUID.randomUUID().toString()) + gson.toJson(testData, testFile) + + gson.fromJson<TestData>(testFile) shouldBe testData + } + + @Test + fun `deserialize an empty file`() { + testFile.createNewFile() + testFile.exists() shouldBe true + + val testData: TestData? = gson.fromJson(testFile) + + testData shouldBe null + + testFile.exists() shouldBe false + } + + @Test + fun `deserialize a malformed file`() { + testFile.writeText("{") + + shouldThrow<JsonSyntaxException> { + gson.fromJson(testFile) + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..f62ca872c48a4d513795d1ee5561e063100a6e83 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt @@ -0,0 +1,63 @@ +package de.rki.coronawarnapp.util.worker + +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 io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import javax.inject.Provider + +class CWAWorkerFactoryTest : BaseTest() { + + private val workerFactories = + mutableMapOf<Class<out ListenableWorker>, Provider<InjectedWorkerFactory<out ListenableWorker>>>() + + @MockK lateinit var context: Context + @MockK lateinit var workerParameters: WorkerParameters + @MockK lateinit var ourWorker: DiagnosisKeyRetrievalOneTimeWorker + @MockK lateinit var ourFactory: DiagnosisKeyRetrievalOneTimeWorker.Factory + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { ourFactory.create(context, workerParameters) } returns ourWorker + workerFactories[DiagnosisKeyRetrievalOneTimeWorker::class.java] = Provider { ourFactory } + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + fun createInstance() = CWAWorkerFactory( + workerFactories + ) + + @Test + fun `instantiate one of our workers`() { + val instance = createInstance() + instance.createWorker( + context, DiagnosisKeyRetrievalOneTimeWorker::class.qualifiedName!!, workerParameters + ) shouldBe ourWorker + } + + @Test + fun `instantiate an unknown worker`() { + val instance = createInstance() + val worker1 = instance.createWorker(context, DiagnosticsWorker::class.qualifiedName!!, workerParameters) + worker1 shouldNotBe null + val worker2 = instance.createWorker(context, DiagnosticsWorker::class.qualifiedName!!, workerParameters) + worker1 shouldNotBe worker2 + } +} 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 88976e539bab83ed2fb7aa482ee3f35e26557652..385d00586af931d1260de2b514a4cd0c2801ac29 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 @@ -8,7 +8,7 @@ import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationSender import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.playbook.Playbook -import de.rki.coronawarnapp.risk.ExposureResultStore +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.di.AssistedInjectModule import io.github.classgraph.ClassGraph @@ -95,5 +95,5 @@ class MockProvider { fun enfClient(): ENFClient = mockk() @Provides - fun exposureSummaryRepository(): ExposureResultStore = mockk() + fun exposureSummaryRepository(): RiskLevelStorage = mockk() } diff --git a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json index 405aa42953a5f7a37a9ae6e98748fb95bafc9ee0..2b11428431878d27278183ac72545a1c0ebdb851 100644 --- a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json +++ b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json @@ -76,12 +76,12 @@ } ], "trlEncoding": { - "infectiousnessOffsetStandard": 0, - "infectiousnessOffsetHigh": 4, - "reportTypeOffsetRecursive": 4, - "reportTypeOffsetSelfReport": 3, - "reportTypeOffsetConfirmedClinicalDiagnosis": 2, - "reportTypeOffsetConfirmedTest": 1 + "infectiousnessOffsetStandard": 1, + "infectiousnessOffsetHigh": 2, + "reportTypeOffsetRecursive": 0, + "reportTypeOffsetSelfReport": 2, + "reportTypeOffsetConfirmedClinicalDiagnosis": 4, + "reportTypeOffsetConfirmedTest": 6 }, "transmissionRiskLevelMultiplier": 0.2 }, @@ -211,8 +211,8 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 2, - "infectiousness": 1, + "reportType": 4, + "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ { @@ -271,8 +271,8 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 1, - "infectiousness": 2, + "reportType": 2, + "infectiousness": 1, "calibrationConfidence": 0, "scanInstances": [ { @@ -306,8 +306,8 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 1, - "infectiousness": 2, + "reportType": 2, + "infectiousness": 1, "calibrationConfidence": 0, "scanInstances": [ { @@ -521,8 +521,8 @@ }, { "ageInDays": 1, - "reportType": 4, - "infectiousness": 1, + "reportType": 3, + "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ { @@ -665,7 +665,7 @@ "exposureWindows": [ { "ageInDays": 3, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -683,7 +683,7 @@ }, { "ageInDays": 2, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -701,7 +701,7 @@ }, { "ageInDays": 4, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -731,7 +731,7 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -749,7 +749,7 @@ }, { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -779,7 +779,7 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -797,7 +797,7 @@ }, { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 1, "scanInstances": [ @@ -827,7 +827,7 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -845,8 +845,8 @@ }, { "ageInDays": 1, - "reportType": 3, - "infectiousness": 2, + "reportType": 1, + "infectiousness": 1, "calibrationConfidence": 0, "scanInstances": [ { @@ -875,7 +875,7 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -893,7 +893,7 @@ }, { "ageInDays": 2, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -941,7 +941,7 @@ }, { "ageInDays": 1, - "reportType": 4, + "reportType": 1, "infectiousness": 2, "calibrationConfidence": 0, "scanInstances": [ @@ -1001,8 +1001,8 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 3, - "infectiousness": 2, + "reportType": 1, + "infectiousness": 1, "calibrationConfidence": 0, "scanInstances": [ { @@ -1031,8 +1031,8 @@ "exposureWindows": [ { "ageInDays": 1, - "reportType": 3, - "infectiousness": 2, + "reportType": 1, + "infectiousness": 1, "calibrationConfidence": 0, "scanInstances": [ { @@ -1060,6 +1060,46 @@ "expAgeOfMostRecentDateWithHighRisk": null, "expNumberOfDaysWithLowRisk": 1, "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "ignores negative secondsSinceLastScan (can happen when time-travelling, not officially supported)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 1, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 25, + "typicalAttenuation": 25, + "secondsSinceLastScan": -86160 + }, + { + "minAttenuation": 25, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 25, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 25, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expTotalMinimumDistinctEncountersWithHighRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 1 } ] } \ No newline at end of file diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9d495c8c22e32f7ad4fcaa7548c38ac13ebda676 --- /dev/null +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.util.CWADebug +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.gms.MockGMSTask +import java.io.File + +class ExposureWindowProviderTest : BaseTest() { + @MockK lateinit var googleENFClient: ExposureNotificationClient + + private val exampleKeyFiles = listOf(File("file1"), File("file2")) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { googleENFClient.exposureWindows } returns MockGMSTask.forValue(emptyList()) + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createProvider() = DefaultExposureWindowProvider( + client = googleENFClient + ) + + @Test + fun `fake exposure windows only in tester builds`() { + val instance = createProvider() + CWADebug.isDeviceForTestersBuild shouldBe false + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..09b586353b1c3d5f3e1b28887814710876fc388e --- /dev/null +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -0,0 +1,121 @@ +package de.rki.coronawarnapp.test.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DefaultRiskLevelStorageTest : BaseTest() { + + @MockK lateinit var databaseFactory: RiskResultDatabase.Factory + @MockK lateinit var database: RiskResultDatabase + @MockK lateinit var riskResultTables: RiskResultDatabase.RiskResultsDao + @MockK lateinit var exposureWindowTables: RiskResultDatabase.ExposureWindowsDao + @MockK lateinit var riskLevelResultMigrator: RiskLevelResultMigrator + + private val testRiskLevelResultDao = PersistedRiskLevelResultDao( + id = "riskresult-id", + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + failureReason = null + ) + private val testRisklevelResult = RiskLevelTaskResult( + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = AggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + exposureWindows = listOf( + ExposureWindow.Builder().build(), + ExposureWindow.Builder().build() + ), + failureReason = null + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { databaseFactory.create() } returns database + every { database.riskResults() } returns riskResultTables + every { database.exposureWindows() } returns exposureWindowTables + every { database.clearAllTables() } just Runs + + every { riskLevelResultMigrator.getLegacyResults() } returns emptyList() + + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + coEvery { riskResultTables.insertEntry(any()) } just Runs + coEvery { riskResultTables.deleteOldest(any()) } returns 7 + + every { exposureWindowTables.allEntries() } returns emptyFlow() + coEvery { exposureWindowTables.insertWindows(any()) } returns listOf(111L, 222L) + coEvery { exposureWindowTables.insertScanInstances(any()) } just Runs + coEvery { exposureWindowTables.deleteByRiskResultId(any()) } returns 1 + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createInstance() = DefaultRiskLevelStorage( + riskResultDatabaseFactory = databaseFactory, + riskLevelResultMigrator = riskLevelResultMigrator + ) + + @Test + fun `stored item limit for deviceForTesters`() { + createInstance().storedResultLimit shouldBe 2 * 6 + } + + @Test + fun `we are NOT storing or cleaning up exposure windows`() = runBlockingTest { + val instance = createInstance() + instance.storeResult(testRisklevelResult) + + coVerify { + riskResultTables.insertEntry(any()) + riskResultTables.deleteOldest(instance.storedResultLimit) + } + + coVerify(exactly = 0) { + exposureWindowTables.insertWindows(any()) + exposureWindowTables.insertScanInstances(any()) + exposureWindowTables.deleteByRiskResultId(listOf("riskresult-id")) + } + } +} diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9b77f2560de2a6f1a7d0e0f84642cd3a9085468 --- /dev/null +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt @@ -0,0 +1,48 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.storage.TestSettings +import de.rki.coronawarnapp.util.CWADebug +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.gms.MockGMSTask +import java.io.File + +class ExposureWindowProviderTest : BaseTest() { + @MockK lateinit var googleENFClient: ExposureNotificationClient + @MockK lateinit var testSettings: TestSettings + @MockK lateinit var fakeExposureWindowProvider: FakeExposureWindowProvider + + private val exampleKeyFiles = listOf(File("file1"), File("file2")) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { googleENFClient.exposureWindows } returns MockGMSTask.forValue(emptyList()) + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createProvider() = DefaultExposureWindowProvider( + client = googleENFClient, + testSettings = testSettings, + fakeExposureWindowProvider = fakeExposureWindowProvider + ) + + @Test + fun `fake exposure windows only in tester builds`() { + val instance = createProvider() + CWADebug.isDeviceForTestersBuild shouldBe true + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..396eee55921cc00a270a2aacbaba4b20fbe18372 --- /dev/null +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -0,0 +1,119 @@ +package de.rki.coronawarnapp.test.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DefaultRiskLevelStorageTest : BaseTest() { + + @MockK lateinit var databaseFactory: RiskResultDatabase.Factory + @MockK lateinit var database: RiskResultDatabase + @MockK lateinit var riskResultTables: RiskResultDatabase.RiskResultsDao + @MockK lateinit var exposureWindowTables: RiskResultDatabase.ExposureWindowsDao + @MockK lateinit var riskLevelResultMigrator: RiskLevelResultMigrator + + private val testRiskLevelResultDao = PersistedRiskLevelResultDao( + id = "riskresult-id", + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + failureReason = null + ) + private val testRisklevelResult = RiskLevelTaskResult( + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = AggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + failureReason = null, + exposureWindows = listOf( + ExposureWindow.Builder().build(), + ExposureWindow.Builder().build() + ) + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { databaseFactory.create() } returns database + every { database.riskResults() } returns riskResultTables + every { database.exposureWindows() } returns exposureWindowTables + every { database.clearAllTables() } just Runs + + every { riskLevelResultMigrator.getLegacyResults() } returns emptyList() + + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + coEvery { riskResultTables.insertEntry(any()) } just Runs + coEvery { riskResultTables.deleteOldest(any()) } returns 7 + + every { exposureWindowTables.allEntries() } returns emptyFlow() + coEvery { exposureWindowTables.insertWindows(any()) } returns listOf(111L, 222L) + coEvery { exposureWindowTables.insertScanInstances(any()) } just Runs + coEvery { exposureWindowTables.deleteByRiskResultId(any()) } returns 1 + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createInstance() = DefaultRiskLevelStorage( + riskResultDatabaseFactory = databaseFactory, + riskLevelResultMigrator = riskLevelResultMigrator + ) + + @Test + fun `stored item limit for deviceForTesters`() { + createInstance().storedResultLimit shouldBe 14 * 6 + } + + @Test + fun `we are storing and cleaning up exposure windows`() = runBlockingTest { + val instance = createInstance() + instance.storeResult(testRisklevelResult) + + coVerify { + riskResultTables.insertEntry(any()) + riskResultTables.deleteOldest(instance.storedResultLimit) + + exposureWindowTables.insertWindows(any()) + exposureWindowTables.insertScanInstances(any()) + exposureWindowTables.deleteByRiskResultId(listOf("riskresult-id")) + } + } +} diff --git a/build.gradle b/build.gradle index 2851fded9a7f361f2cdaa600d2cc590acca676dc..67466e3fc7bcc8c214aad718c373d5fd5b18dbcb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.10' + ext.kotlin_version = '1.4.20' ext.protobufVersion = '0.8.12' ext.navVersion = "2.2.2"