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 1597035d148b03bf712480763452f931b8b968a7..3df815452531e193db77cd23fa939caceaec3e62 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” &gt; “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 96b185dbee57e79b0ab43e9100e8a9ad43e9b305..5080e0ad08571819f0077898676ba7acd9fc12d4 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 -->
@@ -881,13 +866,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-->
@@ -1109,7 +1094,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 -->
@@ -1141,7 +1126,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"
 
diff --git a/gradle.properties b/gradle.properties
index 74d188b604d15701249933c321a8a5819370f05b..5cdf3a8743880773cde8183127a00717c836c067 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,4 @@ org.gradle.dependency.verification.console=verbose
 VERSION_MAJOR=1
 VERSION_MINOR=9
 VERSION_PATCH=0
-VERSION_BUILD=1
+VERSION_BUILD=4