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/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..15968158f9b4d89f681d127bba1cdfc97559bd9e 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
@@ -34,8 +34,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 +65,7 @@ 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
+    @Inject lateinit var riskLevelStorage: RiskLevelStorage
     private val vm: TestForApiFragmentViewModel by cwaViewModels { viewModelFactory }
 
     companion object {
@@ -159,7 +159,7 @@ 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.exposureWindows.first().toString()
 
                     withContext(Dispatchers.Main) {
                         showToast(summary)
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 dd93a0e154d8e7ece32be12a0984a78c197c3560..8cbf5d39e7817597eede7b9abf20a9c51a7808fd 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
@@ -11,21 +11,20 @@ import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
 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.risk.RiskLevelTask
 import de.rki.coronawarnapp.risk.TimeVariables
 import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+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.TestSettings
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.task.submitBlocking
 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.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.di.AppContext
@@ -52,7 +51,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
     private val keyCacheRepository: KeyCacheRepository,
     private val appConfigProvider: AppConfigProvider,
     tracingCardStateProvider: TracingCardStateProvider,
-    private val exposureResultStore: ExposureResultStore,
+    private val riskLevelStorage: RiskLevelStorage,
     private val testSettings: TestSettings
 ) : CWAViewModel(
     dispatcherProvider = dispatcherProvider
@@ -76,19 +75,26 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
         .sample(150L)
         .asLiveData(dispatcherProvider.Default)
 
-    val exposureWindowCountString = exposureResultStore
-        .entities
-        .map { "Retrieved ${it.exposureWindows.size} Exposure Windows" }
+    val exposureWindowCountString = riskLevelStorage
+        .exposureWindows
+        .map { "Retrieved ${it.size} Exposure Windows" }
         .asLiveData()
 
-    val exposureWindows = exposureResultStore
-        .entities
-        .map { if (it.exposureWindows.isEmpty()) "Exposure windows list is empty" else it.exposureWindows.toString() }
+    val exposureWindows = riskLevelStorage
+        .exposureWindows
+        .map { if (it.isEmpty()) "Exposure windows list is empty" else it.toString() }
         .asLiveData()
 
-    val aggregatedRiskResult = exposureResultStore
-        .entities
-        .map { if (it.aggregatedRiskResult != null) it.aggregatedRiskResult.toReadableString() else "Aggregated risk result is not available" }
+    val aggregatedRiskResult = riskLevelStorage
+        .riskLevelResults
+        .map {
+            val latest = it.maxByOrNull { it.calculatedAt }
+            if (latest?.aggregatedRiskResult != null) {
+                latest.aggregatedRiskResult?.toReadableString()
+            } else {
+                "Aggregated risk result is not available"
+            }
+        }
         .asLiveData()
 
     private fun AggregatedRiskResult.toReadableString(): String = StringBuilder()
@@ -129,26 +135,24 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
         .toString()
 
     val additionalRiskCalcInfo = combine(
-        RiskLevelRepository.riskLevelScore,
-        RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated,
-        exposureResultStore.matchedKeyCount,
-        exposureResultStore.daysSinceLastExposure,
+        riskLevelStorage.riskLevelResults,
         LocalData.lastTimeDiagnosisKeysFromServerFetchFlow()
-    ) { riskLevelScore,
-        riskLevelScoreLastSuccessfulCalculated,
-        matchedKeyCount,
-        daysSinceLastExposure,
-        lastTimeDiagnosisKeysFromServerFetch ->
+    ) { riskLevelResults, lastTimeDiagnosisKeysFromServerFetch ->
+
+        val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults()
+
         createAdditionalRiskCalcInfo(
-            riskLevelScore = riskLevelScore,
-            riskLevelScoreLastSuccessfulCalculated = riskLevelScoreLastSuccessfulCalculated,
-            matchedKeyCount = matchedKeyCount,
-            daysSinceLastExposure = daysSinceLastExposure,
+            latestCalc.calculatedAt,
+            riskLevelScore = latestCalc.riskLevel.raw,
+            riskLevelScoreLastSuccessfulCalculated = latestSuccessfulCalc.riskLevel.raw,
+            matchedKeyCount = latestCalc.matchedKeyCount,
+            daysSinceLastExposure = latestCalc.daysWithEncounters,
             lastTimeDiagnosisKeysFromServerFetch = lastTimeDiagnosisKeysFromServerFetch
         )
     }.asLiveData()
 
     private suspend fun createAdditionalRiskCalcInfo(
+        lastTimeRiskLevelCalculation: Instant,
         riskLevelScore: Int,
         riskLevelScoreLastSuccessfulCalculated: Int,
         matchedKeyCount: Int,
@@ -162,11 +166,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
         .appendLine("Last Time Server Fetch: ${lastTimeDiagnosisKeysFromServerFetch?.time?.let { Instant.ofEpochMilli(it) }}")
         .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() {
@@ -204,10 +204,8 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
                     // Export File Reset
                     keyCacheRepository.clear()
 
-                    exposureResultStore.entities.value = ExposureResult(emptyList(), null)
+                    riskLevelStorage.clear()
 
-                    LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw)
-                    LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw)
                     LocalData.lastTimeDiagnosisKeysFromServerFetch(null)
                 } catch (e: Exception) {
                     e.report(ExceptionCategory.INTERNAL)
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/ConfigChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt
index 588451c9dec8b5393a6c55e1aaf9fbb00bc2317b..da3440abb8741903b56bf3c87c5f51b674e61a0f 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,7 +19,8 @@ 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() {
@@ -36,28 +36,20 @@ class ConfigChangeDetector @Inject constructor(
     }
 
     @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.")
             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()
+            riskLevelStorage.clear()
             taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector"))
         } else {
             Timber.v("Config identifier ($oldConfigId) didn't change, NOOP.")
         }
     }
-
-    @VisibleForTesting
-    internal object RiskLevelRepositoryDeferrer {
-
-        fun resetRiskLevel() {
-            RiskLevelRepository.setRiskLevelScore(RiskLevel.UNDETERMINED)
-        }
-    }
 }
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/ExposureStateUpdateWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt
index f0f0ed5c5ff56d8758407875de879465d2e79560..e383844c36934a71d25d72f588af93f36b11dae9 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,18 +17,12 @@ 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")
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/notification/NotificationHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt
index c642a8a4b7788ec131309bd8704549889abffebc..2da051ce6df105ace8c956cff183d8f82f7cb1b6 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
@@ -189,12 +189,10 @@ object NotificationHelper {
      *
      * @param title: String
      * @param content: String
-     * @param visibility: Int
      * @param expandableLongText: Boolean
      * @param notificationId: NotificationId
      * @param pendingIntent: PendingIntent
      */
-
     fun sendNotification(
         title: String,
         content: String,
@@ -209,19 +207,6 @@ object NotificationHelper {
         }
     }
 
-    /**
-     * Send notification
-     * Build and send notification with content and visibility.
-     * Notification is only sent if app is not in foreground.
-     *
-     * @param content: String
-     */
-    fun sendNotification(content: String) {
-        if (!CoronaWarnApplication.isAppInForeground) {
-            sendNotification("", content, true)
-        }
-    }
-
     /**
      * Log notification build
      * Log success or failure of creating new notification
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
index 05267394d6cabd8aa691caf1ae5c8a8deeba455e..89e6d8a21b0d52d1bc9595da1771db09cabf5f13 100644
--- 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
@@ -47,38 +47,5 @@ enum class RiskLevel(val raw: Int) {
                 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..78c75891b5d926b3f3c58a28f503d02b18eaa94d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
@@ -0,0 +1,92 @@
+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.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 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 oldRiskLevel = oldResult.riskLevel
+        val newRiskLevel = newResult.riskLevel
+
+        Timber.d("last CalculatedS core is ${oldRiskLevel.raw} and Current Risk Level is ${newRiskLevel.raw}")
+
+        if (hasHighLowLevelChanged(oldRiskLevel, newRiskLevel) && !LocalData.submissionWasSuccessful()) {
+            Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}")
+
+            if (!foregroundState.isInForeground.first()) {
+                NotificationHelper.sendNotification("", context.getString(R.string.notification_body), true)
+            } else {
+                Timber.d("App is in foreground, not sending notifications")
+            }
+
+            Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskLevel")
+        }
+
+        if (
+            oldRiskLevel.raw == RiskLevelConstants.INCREASED_RISK &&
+            newRiskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK
+        ) {
+            LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true
+
+            Timber.d("Risk level changed LocalData is updated. Current Risk level is $newRiskLevel")
+        }
+    }
+
+    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: RiskLevel, current: RiskLevel) =
+            previous.isIncreasedRisk != current.isIncreasedRisk
+
+        private val RiskLevel.isIncreasedRisk: Boolean
+            get() = this == RiskLevel.INCREASED_RISK
+    }
+}
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..b8baae9d8a01fc587eee2872935987eb6c29ec8f
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt
@@ -0,0 +1,53 @@
+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 riskLevel: RiskLevel
+    val calculatedAt: Instant
+
+    val aggregatedRiskResult: AggregatedRiskResult?
+
+    /**
+     * This will only be filled in deviceForTester builds
+     */
+    val exposureWindows: List<ExposureWindow>?
+
+    val wasSuccessfullyCalculated: Boolean
+        get() = !UNSUCCESSFUL_RISK_LEVELS.contains(riskLevel)
+
+    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
+        }
+
+    companion object {
+        private val UNSUCCESSFUL_RISK_LEVELS = arrayOf(
+            RiskLevel.UNDETERMINED,
+            RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF,
+            RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS,
+            RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS_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 95%
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..4af4d0c0517a0ae81111d0e403907d08d1b0fb8c 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
@@ -7,7 +7,7 @@ import javax.inject.Inject
 import javax.inject.Singleton
 
 @Singleton
-class RiskLevelData @Inject constructor(
+class RiskLevelSettings @Inject constructor(
     @AppContext private val context: Context
 ) {
 
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..8f2cba566887a75b1b1918481ef165c514584722 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,10 +1,6 @@
 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
@@ -12,7 +8,6 @@ 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
@@ -20,8 +15,7 @@ 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.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.task.Task
 import de.rki.coronawarnapp.task.TaskCancellationException
 import de.rki.coronawarnapp.task.TaskFactory
@@ -40,66 +34,86 @@ 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
+) : 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 {
+    @Suppress("LongMethod")
+    override suspend fun run(arguments: Task.Arguments): RiskLevelTaskResult {
         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)
+                return RiskLevelTaskResult(
+                    riskLevel = UNDETERMINED,
+                    calculatedAt = timeStamper.nowUTC
+                )
             }
 
             if (!enfClient.isTracingEnabled.first()) {
-                return Result(NO_CALCULATION_POSSIBLE_TRACING_OFF)
+                return RiskLevelTaskResult(
+                    riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF,
+                    calculatedAt = timeStamper.nowUTC
+                )
             }
 
             val configData: ConfigData = appConfigProvider.getAppConfig()
 
-            return Result(
-                when {
-                    calculationNotPossibleBecauseOfNoKeys().also {
-                        checkCancel()
-                    } -> UNKNOWN_RISK_INITIAL
-
-                    calculationNotPossibleBecauseOfOutdatedResults().also {
-                        checkCancel()
-                    } -> if (backgroundJobsEnabled()) {
-                        UNKNOWN_RISK_OUTDATED_RESULTS
-                    } else {
-                        UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL
-                    }
+            return kotlin.run evaluation@{
+                if (calculationNotPossibleBecauseOfNoKeys()) {
+                    return@evaluation RiskLevelTaskResult(
+                        riskLevel = UNKNOWN_RISK_INITIAL,
+                        calculatedAt = timeStamper.nowUTC
+                    )
+                }
 
-                    isIncreasedRisk(configData).also {
-                        checkCancel()
-                    } -> INCREASED_RISK
+                if (calculationNotPossibleBecauseOfOutdatedResults()) {
+                    return@evaluation RiskLevelTaskResult(
+                        riskLevel = when (backgroundJobsEnabled()) {
+                            true -> UNKNOWN_RISK_OUTDATED_RESULTS
+                            false -> UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL
+                        },
+                        calculatedAt = timeStamper.nowUTC
+                    )
+                }
+                checkCancel()
 
-                    !isActiveTracingTimeAboveThreshold().also {
-                        checkCancel()
-                    } -> UNKNOWN_RISK_INITIAL
+                val risklevelResult = calculateRiskLevel(configData)
+                if (risklevelResult.isIncreasedRisk) {
+                    return@evaluation risklevelResult
+                }
+                checkCancel()
 
-                    else -> LOW_LEVEL_RISK
-                }.also {
-                    checkCancel()
-                    updateRepository(it, timeStamper.nowUTC.millis)
-                    riskLevelData.lastUsedConfigIdentifier = configData.identifier
+                if (!isActiveTracingTimeAboveThreshold()) {
+                    return@evaluation RiskLevelTaskResult(
+                        riskLevel = UNKNOWN_RISK_INITIAL,
+                        calculatedAt = timeStamper.nowUTC
+                    )
                 }
-            )
+                checkCancel()
+
+                return@evaluation risklevelResult
+            }.also {
+                checkCancel()
+                Timber.i("Evaluation finished with %s", it)
+
+                Timber.tag(TAG).d("storeTaskResult(...)")
+                riskLevelStorage.storeResult(it)
+
+                riskLevelSettings.lastUsedConfigIdentifier = configData.identifier
+            }
         } catch (error: Exception) {
             Timber.tag(TAG).e(error)
             error.report(ExceptionCategory.EXPOSURENOTIFICATION)
@@ -111,6 +125,7 @@ class RiskLevelTask @Inject constructor(
     }
 
     private fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean {
+        Timber.tag(TAG).d("Evaluating calculationNotPossibleBecauseOfOutdatedResults()")
         // if the last calculation is longer in the past as the defined threshold we return the stale state
         val timeSinceLastDiagnosisKeyFetchFromServer =
             TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer()
@@ -119,19 +134,30 @@ class RiskLevelTask @Inject constructor(
                 )
         /** 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()
+        return (timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() >
+            TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold()).also {
+            if (it) {
+                Timber.tag(TAG).i("Calculation was not possible because reults are outdated.")
+            } else {
+                Timber.tag(TAG).d("Results are not out dated, continuing evaluation.")
+            }
+        }
     }
 
-    private fun calculationNotPossibleBecauseOfNoKeys() =
-        (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also {
+    private fun calculationNotPossibleBecauseOfNoKeys(): Boolean {
+        Timber.tag(TAG).d("Evaluating calculationNotPossibleBecauseOfNoKeys()")
+        return (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also {
             if (it) {
-                Timber.tag(TAG)
-                    .v("No last time diagnosis keys from server fetch timestamp was found")
+                Timber.tag(TAG).v("No last time diagnosis keys from server fetch timestamp was found")
+            } else {
+                Timber.tag(TAG).d("Diagnosis keys from server are available, continuing evaluation.")
             }
         }
+    }
 
     private fun isActiveTracingTimeAboveThreshold(): Boolean {
+        Timber.tag(TAG).d("Evaluating isActiveTracingTimeAboveThreshold()")
+
         val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration()
         val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours()
         val durationTracingIsActiveThreshold = TimeVariables.getMinActivatedTracingTime().toLong()
@@ -144,85 +170,25 @@ class RiskLevelTask @Inject constructor(
         }
     }
 
-    private suspend fun isIncreasedRisk(configData: ExposureWindowRiskCalculationConfig): Boolean {
+    private suspend fun calculateRiskLevel(configData: ExposureWindowRiskCalculationConfig): RiskLevelTaskResult {
+        Timber.tag(TAG).d("Evaluating isIncreasedRisk(...)")
         val exposureWindows = enfClient.exposureWindows()
 
-        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
+        return riskLevels.determineRisk(configData, exposureWindows).let {
+            Timber.tag(TAG).d("Evaluated increased risk: %s", it)
+            if (it.isIncreasedRisk()) {
+                Timber.tag(TAG).i("Risk is increased!")
             } else {
-                exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithLowRisk
-                exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithLowRisk
-            }
-            exposureResultStore.entities.value = ExposureResult(exposureWindows, this)
-        }.isIncreasedRisk()
-    }
-
-    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)
-            }
-
-            // 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.")
-
-            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.")
+                Timber.tag(TAG).d("Risk is not increased, continuing evaluating.")
             }
 
-            throw error
-        }
-    }
-
-    /**
-     * 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}")
-
-        if (RiskLevel.riskLevelChangedBetweenLowAndHigh(lastCalculatedScore, riskLevel) &&
-            !LocalData.submissionWasSuccessful()
-        ) {
-            Timber.d(
-                "Notification Permission = ${
-                    NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled()
-                }"
+            RiskLevelTaskResult(
+                riskLevel = if (it.isIncreasedRisk()) INCREASED_RISK else LOW_LEVEL_RISK,
+                aggregatedRiskResult = it,
+                exposureWindows = exposureWindows,
+                calculatedAt = timeStamper.nowUTC
             )
-
-            NotificationHelper.sendNotification(
-                CoronaWarnApplication.getAppContext().getString(R.string.notification_body)
-            )
-
-            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() {
@@ -245,12 +211,6 @@ class RiskLevelTask @Inject constructor(
         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
         override val executionTimeout: Duration = Duration.standardMinutes(8),
@@ -262,10 +222,10 @@ class RiskLevelTask @Inject constructor(
 
     class Factory @Inject constructor(
         private val taskByDagger: Provider<RiskLevelTask>
-    ) : TaskFactory<DefaultProgress, Result> {
+    ) : TaskFactory<DefaultProgress, RiskLevelTaskResult> {
 
         override suspend fun createConfig(): TaskFactory.Config = Config()
-        override val taskProvider: () -> Task<DefaultProgress, Result> = {
+        override val taskProvider: () -> Task<DefaultProgress, RiskLevelTaskResult> = {
             taskByDagger.get()
         }
     }
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..f930cf13468683bd8334080e3a3b3c69d747c377
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt
@@ -0,0 +1,13 @@
+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 riskLevel: RiskLevel,
+    override val calculatedAt: Instant,
+    override val aggregatedRiskResult: AggregatedRiskResult? = null,
+    override val exposureWindows: List<ExposureWindow>? = null
+) : Task.Result, RiskLevelResult
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/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..4c5c45727b0ed71ed0ffca4db488d85ae60dc637
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt
@@ -0,0 +1,100 @@
+package de.rki.coronawarnapp.risk.storage
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.risk.RiskLevel
+import de.rki.coronawarnapp.risk.RiskLevelResult
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+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 kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import org.joda.time.Instant
+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
+
+    override val exposureWindows: Flow<List<ExposureWindow>> = exposureWindowsTables.allEntries().map { windows ->
+        windows.map { it.toExposureWindow() }
+    }
+
+    final override val riskLevelResults: Flow<List<RiskLevelResult>> = riskResultsTables.allEntries()
+        .map { latestResults ->
+            latestResults.map { it.toRiskResult() }
+        }
+        .map { results ->
+            if (results.isEmpty()) {
+                riskLevelResultMigrator.getLegacyResults()
+            } else {
+                results
+            }
+        }
+
+    override val lastRiskLevelResult: Flow<RiskLevelResult> = riskLevelResults.map { results ->
+        results.maxByOrNull { it.calculatedAt } ?: INITIAL_RESULT
+    }
+
+    override suspend fun storeResult(result: RiskLevelResult) {
+        Timber.d("Storing result (exposureWindows.size=%s)", result.exposureWindows?.size)
+
+        val storedResultId = try {
+            val startTime = System.currentTimeMillis()
+
+            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()
+    }
+
+    companion object {
+        private val INITIAL_RESULT = object : RiskLevelResult {
+            override val riskLevel: RiskLevel = RiskLevel.LOW_LEVEL_RISK
+            override val calculatedAt: Instant = Instant.EPOCH
+            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/risk/storage/RiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6a261f4480e4b020daaa8ec34542df56d7a969e1
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.risk.storage
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.risk.RiskLevelResult
+import kotlinx.coroutines.flow.Flow
+
+interface RiskLevelStorage {
+
+    val exposureWindows: Flow<List<ExposureWindow>>
+
+    val riskLevelResults: Flow<List<RiskLevelResult>>
+
+    val lastRiskLevelResult: Flow<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..e9f15fcb579bd514218e3e541f1f6531b040655d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt
@@ -0,0 +1,73 @@
+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.RiskLevel
+import de.rki.coronawarnapp.risk.RiskLevelTaskResult
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping
+import org.joda.time.Instant
+
+@Entity(tableName = "riskresults")
+data class PersistedRiskLevelResultDao(
+    @PrimaryKey @ColumnInfo(name = "id") val id: String,
+    @ColumnInfo(name = "riskLevel") val riskLevel: RiskLevel,
+    @ColumnInfo(name = "calculatedAt") val calculatedAt: Instant,
+    @Embedded val aggregatedRiskResult: PersistedAggregatedRiskResult?
+) {
+
+    fun toRiskResult() = RiskLevelTaskResult(
+        riskLevel = riskLevel,
+        calculatedAt = calculatedAt,
+        aggregatedRiskResult = aggregatedRiskResult?.toAggregatedRiskResult()
+    )
+
+    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: Int?): RiskLevel? =
+            value?.let { RiskLevel.values().single { it.raw == value } }
+
+        @TypeConverter
+        fun fromType(type: RiskLevel?): Int? = type?.raw
+    }
+}
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..4fbf56ecaabaf13f9741b48448b21adb1c86b30b
--- /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,
+    riskLevel = riskLevel,
+    calculatedAt = calculatedAt,
+    aggregatedRiskResult = aggregatedRiskResult?.toPersistedAggregatedRiskResult()
+)
+
+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..0e39b13cb57d73e36bb764f7f6b2b2691ac66a3a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt
@@ -0,0 +1,75 @@
+package de.rki.coronawarnapp.risk.storage.legacy
+
+import android.content.SharedPreferences
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import dagger.Lazy
+import de.rki.coronawarnapp.risk.RiskLevel
+import de.rki.coronawarnapp.risk.RiskLevelResult
+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(): RiskLevel? {
+        val rawRiskLevel = prefs.getInt("preference_risk_level_score", -1)
+        return if (rawRiskLevel != -1) RiskLevel.forValue(rawRiskLevel) else null
+    }
+
+    private fun lastSuccessfullyCalculatedRiskLevel(): RiskLevel? {
+        val rawRiskLevel = prefs.getInt("preference_risk_level_score_successful", -1)
+        return if (rawRiskLevel != -1) RiskLevel.forValue(rawRiskLevel) else null
+    }
+
+    fun getLegacyResults(): List<RiskLevelResult> = try {
+        val legacyResults = mutableListOf<RiskLevelResult>()
+        lastCalculatedRiskLevel()?.let {
+            legacyResults.add(
+                LegacyResult(
+                    riskLevel = it,
+                    calculatedAt = lastTimeRiskLevelCalculation() ?: timeStamper.nowUTC
+                )
+            )
+        }
+
+        lastSuccessfullyCalculatedRiskLevel()?.let {
+            legacyResults.add(LegacyResult(riskLevel = it, calculatedAt = timeStamper.nowUTC))
+        }
+
+        legacyResults
+    } catch (e: Exception) {
+        Timber.e(e, "Failed to parse legacy risklevel data.")
+        emptyList()
+    }
+
+    data class LegacyResult(
+        override val riskLevel: RiskLevel,
+        override val calculatedAt: Instant
+    ) : RiskLevelResult {
+        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/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..326f0579081966e92e4e4aeaf4a0394e81fe2974 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,7 +4,6 @@ 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
@@ -261,78 +260,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
@@ -401,37 +328,6 @@ object LocalData {
     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
      ****************************************************/
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 62db1f8e70df71d7e2a1866c33ff8e6cfdb85d4d..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt
+++ /dev/null
@@ -1,106 +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]
-     *
-     * @see de.rki.coronawarnapp.util.DataReset
-     *
-     */
-    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/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt
index 75be4b8cf6503bb73bdf1a9dbda5c92e1fad2206..34c4f5fc3ef7226cac825e8d0b3d4f45d9b6169f 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
@@ -172,10 +172,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/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt
index 99d389d7d608dc1a9491aaa1e5c444c344da490a..60fe10547b02b591e589071454249ac763a71f46 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
@@ -106,7 +106,6 @@ class HomeFragmentViewModel @AssistedInject constructor(
         tracingRepository.refreshRiskLevel()
         tracingRepository.refreshActiveTracingDaysInRetentionPeriod()
         TimerHelper.checkManualKeyRetrievalTimer()
-        tracingRepository.refreshLastSuccessfullyCalculatedScore()
     }
 
     fun tracingExplanationWasShown() {
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..69e50181fe5307301d8eae1526bea80d61772583 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
@@ -10,18 +10,21 @@ 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 de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate
+import org.joda.time.Instant
+import org.joda.time.format.DateTimeFormat
 import java.util.Date
 
 data class TracingCardState(
     override val tracingStatus: GeneralTracingStatus.Status,
     override val riskLevelScore: Int,
     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 lastRiskLevelScoreCalculated: Int,
+    val daysWithEncounters: Int,
+    val lastEncounterAt: Instant?,
+    val activeTracingDaysInRetentionPeriod: Long,
+    val lastTimeDiagnosisKeysFetched: Date?,
+    val isBackgroundJobEnabled: Boolean,
     override val isManualKeyRetrievalEnabled: Boolean,
     override val manualKeyRetrievalTime: Long,
     override val showDetails: Boolean = false
@@ -92,27 +95,26 @@ data class TracingCardState(
      */
     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)
+                if (daysWithEncounters == 0) {
+                    c.getString(R.string.risk_card_high_risk_no_encounters_body)
                 } else {
                     resources.getQuantityString(
-                        R.plurals.risk_card_body_contact_value_high_risk,
-                        contacts,
-                        contacts
+                        R.plurals.risk_card_high_risk_encounter_days_body,
+                        daysWithEncounters,
+                        daysWithEncounters
                     )
                 }
             }
             RiskLevelConstants.LOW_LEVEL_RISK -> {
-                if (matchedKeyCount == 0) {
-                    c.getString(R.string.risk_card_body_contact)
+                if (daysWithEncounters == 0) {
+                    c.getString(R.string.risk_card_low_risk_no_encounters_body)
                 } else {
                     resources.getQuantityString(
-                        R.plurals.risk_card_body_contact_value,
-                        contacts,
-                        contacts
+                        R.plurals.risk_card_low_risk_encounter_days_body,
+                        daysWithEncounters,
+                        daysWithEncounters
                     )
                 }
             }
@@ -136,18 +138,11 @@ 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 = if (riskLevelScore == RiskLevelConstants.INCREASED_RISK) {
+        val formattedDate = lastEncounterAt?.toLocalDate()?.toString(DateTimeFormat.mediumDate())
+        c.getString(R.string.risk_card_high_risk_most_recent_body, formattedDate)
+    } else {
+        ""
     }
 
     /**
@@ -211,7 +206,7 @@ data class TracingCardState(
                 c.getString(R.string.risk_card_body_not_yet_fetched)
             }
         }
-            return when (riskLevelScore) {
+        return when (riskLevelScore) {
             RiskLevelConstants.LOW_LEVEL_RISK,
             RiskLevelConstants.INCREASED_RISK -> {
                 if (lastTimeDiagnosisKeysFetched != null) {
@@ -302,14 +297,14 @@ data class TracingCardState(
 
     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 {
+            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 {
             return c.getColorStateList(R.color.card_no_calculation)
         }
     }
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..93c77e2289c91f7b097421624289832053ad05ab 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,11 @@
 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.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.storage.SettingsRepository
 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
@@ -21,28 +21,18 @@ class TracingCardStateProvider @Inject constructor(
     backgroundModeStatus: BackgroundModeStatus,
     settingsRepository: SettingsRepository,
     tracingRepository: TracingRepository,
-    exposureResultStore: ExposureResultStore
+    riskLevelStorage: RiskLevelStorage
 ) {
 
-    // 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")
@@ -60,25 +50,24 @@ class TracingCardStateProvider @Inject constructor(
             Timber.v("manualKeyRetrievalTimeFlow: $it")
         }
     ) { status,
-        riskLevelScore,
-        riskLevelScoreLastSuccessfulCalculated,
         tracingProgress,
-        matchedKeyCount,
-        daysSinceLastExposure,
+        riskLevelResults,
         activeTracingDaysInRetentionPeriod,
         lastTimeDiagnosisKeysFetched,
         isBackgroundJobEnabled,
         isManualKeyRetrievalEnabled,
         manualKeyRetrievalTime ->
 
+        val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults()
+
         TracingCardState(
             tracingStatus = status,
-            riskLevelScore = riskLevelScore,
+            riskLevelScore = latestCalc.riskLevel.raw,
             tracingProgress = tracingProgress,
-            lastRiskLevelScoreCalculated = riskLevelScoreLastSuccessfulCalculated,
+            lastRiskLevelScoreCalculated = latestSuccessfulCalc.riskLevel.raw,
             lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched,
-            matchedKeyCount = matchedKeyCount,
-            daysSinceLastExposure = daysSinceLastExposure,
+            daysWithEncounters = latestCalc.daysWithEncounters,
+            lastEncounterAt = latestCalc.lastRiskEncounterAt,
             activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod,
             isBackgroundJobEnabled = isBackgroundJobEnabled,
             isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled,
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..f562b2a250efbfec9e06786417c450a95a36ae75 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
@@ -6,18 +6,11 @@ import de.rki.coronawarnapp.risk.RiskLevelConstants
 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 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
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d190ff869e2e50f709ca3d1021f9c9471a01a504
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensions.kt
@@ -0,0 +1,43 @@
+package de.rki.coronawarnapp.ui.tracing.common
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.risk.RiskLevel
+import de.rki.coronawarnapp.risk.RiskLevelResult
+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 riskLevel: RiskLevel = RiskLevel.LOW_LEVEL_RISK
+    override val calculatedAt: Instant = Instant.now()
+    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 riskLevel: RiskLevel = RiskLevel.UNDETERMINED
+    override val calculatedAt: Instant = Instant.EPOCH
+    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/TracingDetailsState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt
index 7db5aa19ca115756fded302a59c5a9d75eebb66c..c167733575c82a39e46f2f9188b53f1b4472d0fc 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
@@ -6,22 +6,19 @@ import de.rki.coronawarnapp.risk.RiskLevelConstants
 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 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,
+    val isBackgroundJobEnabled: Boolean,
     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
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..c24de33521b3711f1c10cf2e6e8e4afde713b101 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.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.storage.SettingsRepository
 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
@@ -22,50 +22,41 @@ class TracingDetailsStateProvider @Inject constructor(
     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
     ) { status,
-        riskLevelScore,
-        riskLevelScoreLastSuccessfulCalculated,
         tracingProgress,
-        matchedKeyCount,
-        daysSinceLastExposure, activeTracingDaysInRetentionPeriod,
-        lastTimeDiagnosisKeysFetched,
+        riskLevelResults,
+        activeTracingDaysInRetentionPeriod,
         isBackgroundJobEnabled,
         isManualKeyRetrievalEnabled,
         manualKeyRetrievalTime ->
 
+        val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults()
+
         val isAdditionalInformationVisible = riskDetailPresenter.isAdditionalInfoVisible(
-            riskLevelScore, matchedKeyCount
+            latestCalc.riskLevel.raw, latestCalc.matchedKeyCount
+        )
+        val isInformationBodyNoticeVisible = riskDetailPresenter.isInformationBodyNoticeVisible(
+            latestCalc.riskLevel.raw
         )
-        val isInformationBodyNoticeVisible =
-            riskDetailPresenter.isInformationBodyNoticeVisible(
-                riskLevelScore
-            )
 
         TracingDetailsState(
             tracingStatus = status,
-            riskLevelScore = riskLevelScore,
+            riskLevelScore = latestCalc.riskLevel.raw,
             tracingProgress = tracingProgress,
-            lastRiskLevelScoreCalculated = riskLevelScoreLastSuccessfulCalculated,
-            matchedKeyCount = matchedKeyCount,
-            daysSinceLastExposure = daysSinceLastExposure,
+            matchedKeyCount = latestCalc.matchedKeyCount,
+            daysSinceLastExposure = latestCalc.daysWithEncounters,
             activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod,
-            lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched,
             isBackgroundJobEnabled = isBackgroundJobEnabled,
             isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled,
             manualKeyRetrievalTime = manualKeyRetrievalTime,
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 fd1064bbfef209742a3e763e9ecd386543311e80..cfd87b55762e01eae7698ced66028aa8f1a838e3 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
@@ -27,7 +27,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 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
@@ -65,8 +64,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()
@@ -74,6 +72,7 @@ class DataReset @Inject constructor(
         interoperabilityRepository.clear()
         exposureDetectionTracker.clear()
         keyPackageSyncSettings.clear()
+
         Timber.w("CWA LOCAL DATA DELETION COMPLETED.")
     }
 }
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..d5dcfbc2a0e34b162d0c26c98ce38d24efa479ad 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,86 @@ fun <T : Any> Flow<T>.shareLatest(
     )
     .filterNotNull()
 
+@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/res/values-bg/strings.xml b/Corona-Warn-App/src/main/res/values-bg/strings.xml
index 469ac6b4b49c3c138f8703f8e2d5db12bedf9726..5d8c2cdd3f2a8dd8817fba4c35c0072fb244b634 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>
@@ -125,26 +119,6 @@
                   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>
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
@@ -167,15 +141,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 -->
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 d024cca935d97319b2a2836ee1d2c7a278b7cd78..266c4eea2ab2589f4e135eb24424b6161cd2c9a1 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 -->
@@ -168,15 +142,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 -->
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 a5e01cdeff99cac3ee50b5509b42354aebefa547..334936e23b415c5c293b7a60a70c3484701be007 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 -->
@@ -167,15 +141,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 -->
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 eded35c4be1a262535dce655302b0fba532e50e5..045ea94fbec30cea390a9df274344d47c14639cc 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 -->
@@ -167,15 +141,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 -->
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 0c5a9e4f4e0e8f0252b4a82e4df7ddcaa07dac65..c9195cae2f84eed65972d422311ac86927bbeb32 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>
@@ -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 -->
@@ -167,15 +141,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 -->
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 78dc5a455f852fb5ba62c532be47921025386790..99da3bf37b913883e1be1ddd8d0d9f838e3ac9b9 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 -->
@@ -167,15 +141,6 @@
     <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 -->
diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml
index f40fe15af2be5b2b4de248d79c2275640f69ad12..24720e30883db8e325bda5a21b3adf3f1eec6e08 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 -->
@@ -172,15 +146,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 -->
@@ -226,7 +191,7 @@
         <item quantity="many" />
     </plurals>
     <!-- XTXT: risk card - High risk state - Most recent date with high risk -->
-    <string name="risk_card_high_risk_most_recent_body" />
+    <string name="risk_card_high_risk_most_recent_body">"Last contact at %1$s"</string>
 
     <!-- ####################################
               Risk Card - Progress
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/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/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt
index d484333fe3dfab9d173bb2e716572ff21f19a9e6..3092fb5d3457283c591e17720f3efba28f1f880e 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
@@ -100,7 +100,6 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() {
         }
     }
 
-
     @Test
     fun `quota is just monitored`() {
         coEvery { submissionQuota.consumeQuota(any()) } returns false
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..2f8b7d350b15ce23ac86587e6364698a01d96976
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
@@ -0,0 +1,174 @@
+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.RiskLevel.INCREASED_RISK
+import de.rki.coronawarnapp.risk.RiskLevel.LOW_LEVEL_RISK
+import de.rki.coronawarnapp.risk.RiskLevel.UNDETERMINED
+import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL
+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
+
+    @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
+    }
+
+    @AfterEach
+    fun tearDown() {
+        clearAllMocks()
+    }
+
+    private fun createRiskLevel(
+        riskLevel: RiskLevel,
+        calculatedAt: Instant = Instant.EPOCH
+    ): RiskLevelResult = object : RiskLevelResult {
+        override val riskLevel: RiskLevel = riskLevel
+        override val calculatedAt: Instant = calculatedAt
+        override val aggregatedRiskResult: AggregatedRiskResult? = 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
+    )
+
+    @Test
+    fun `nothing happens if there is only one result yet`() {
+        every { riskLevelStorage.riskLevelResults } returns flowOf(listOf(createRiskLevel(LOW_LEVEL_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_LEVEL_RISK),
+                createRiskLevel(LOW_LEVEL_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_LEVEL_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_LEVEL_RISK, calculatedAt = Instant.EPOCH)
+            )
+        )
+
+        runBlockingTest {
+            val instance = createInstance(scope = this)
+            instance.launch()
+
+            advanceUntilIdle()
+
+            coVerifySequence {
+                LocalData.submissionWasSuccessful()
+                foregroundState.isInForeground
+            }
+        }
+    }
+
+    @Test
+    fun `evaluate risk level change detection function`() {
+        RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, INCREASED_RISK) shouldBe false
+        RiskLevelChangeDetector.hasHighLowLevelChanged(UNKNOWN_RISK_INITIAL, LOW_LEVEL_RISK) shouldBe false
+        RiskLevelChangeDetector.hasHighLowLevelChanged(UNKNOWN_RISK_INITIAL, INCREASED_RISK) shouldBe true
+        RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, UNKNOWN_RISK_INITIAL) shouldBe true
+        RiskLevelChangeDetector.hasHighLowLevelChanged(UNDETERMINED, UNKNOWN_RISK_INITIAL) shouldBe false
+        RiskLevelChangeDetector.hasHighLowLevelChanged(UNDETERMINED, INCREASED_RISK) shouldBe true
+        RiskLevelChangeDetector.hasHighLowLevelChanged(UNKNOWN_RISK_INITIAL, UNDETERMINED) shouldBe false
+        RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, UNDETERMINED) shouldBe true
+    }
+}
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..a5871486aa59588946febedac5d0ef71e47083cc
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt
@@ -0,0 +1,31 @@
+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 org.joda.time.Instant
+import org.junit.Test
+import testhelpers.BaseTest
+
+class RiskLevelResultTest : BaseTest() {
+
+    private fun createRiskLevel(riskLevel: RiskLevel): RiskLevelResult = object : RiskLevelResult {
+        override val riskLevel: RiskLevel = riskLevel
+        override val calculatedAt: Instant = Instant.EPOCH
+        override val aggregatedRiskResult: AggregatedRiskResult? = null
+        override val exposureWindows: List<ExposureWindow>? = null
+        override val matchedKeyCount: Int = 0
+        override val daysWithEncounters: Int = 0
+    }
+
+    @Test
+    fun testUnsuccessfulRistLevels() {
+        createRiskLevel(RiskLevel.UNDETERMINED).wasSuccessfullyCalculated shouldBe false
+        createRiskLevel(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF).wasSuccessfullyCalculated shouldBe false
+        createRiskLevel(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS).wasSuccessfullyCalculated shouldBe false
+
+        createRiskLevel(RiskLevel.UNKNOWN_RISK_INITIAL).wasSuccessfullyCalculated shouldBe true
+        createRiskLevel(RiskLevel.LOW_LEVEL_RISK).wasSuccessfullyCalculated shouldBe true
+        createRiskLevel(RiskLevel.INCREASED_RISK).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/RiskLevelTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt
index f174600cfd181bc4936dbe935c9195c830be8c82..df3dbcab8f7dfa56e2efe7aeee5c50c814680479 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
@@ -7,6 +7,7 @@ import android.net.NetworkCapabilities
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.appconfig.ConfigData
 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,10 +33,10 @@ 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
 
     private val arguments: Task.Arguments = object : Task.Arguments {}
 
@@ -45,9 +46,9 @@ class RiskLevelTaskTest : BaseTest() {
         enfClient = enfClient,
         timeStamper = timeStamper,
         backgroundModeStatus = backgroundModeStatus,
-        riskLevelData = riskLevelData,
+        riskLevelSettings = riskLevelSettings,
         appConfigProvider = appConfigProvider,
-        exposureResultStore = exposureResultStore
+        riskLevelStorage = riskLevelStorage
     )
 
     @BeforeEach
@@ -71,7 +72,7 @@ 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
     }
 
     @Test
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
index 7d4c1d7ba462d10b7c381124a96c9a39ee2bfdfd..74290d95b7338ebfb6ad2cc584ed9a9a9d667994 100644
--- 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
@@ -1,9 +1,7 @@
 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 {
@@ -44,87 +42,4 @@ class RiskLevelTest {
         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/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..033cae2147a1e19fc25e3c78bd41a240a25ce216
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt
@@ -0,0 +1,229 @@
+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.RiskLevel
+import de.rki.coronawarnapp.risk.RiskLevelResult
+import de.rki.coronawarnapp.risk.RiskLevelTaskResult
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+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.internal.riskresults.PersistedRiskLevelResultDao
+import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao.PersistedAggregatedRiskResult
+import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao
+import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper
+import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+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.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 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
+
+    private val testRiskLevelResultDao = PersistedRiskLevelResultDao(
+        id = "riskresult-id",
+        riskLevel = RiskLevel.INCREASED_RISK,
+        calculatedAt = Instant.ofEpochMilli(9999L),
+        aggregatedRiskResult = PersistedAggregatedRiskResult(
+            totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
+            totalMinimumDistinctEncountersWithLowRisk = 1,
+            totalMinimumDistinctEncountersWithHighRisk = 2,
+            mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+            mostRecentDateWithHighRisk = Instant.ofEpochMilli(4),
+            numberOfDaysWithLowRisk = 5,
+            numberOfDaysWithHighRisk = 6
+        )
+    )
+
+    private val testRisklevelResult = RiskLevelTaskResult(
+        riskLevel = RiskLevel.INCREASED_RISK,
+        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
+    )
+
+    private 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
+            )
+        )
+    )
+    private val testExposureWindow = 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()
+
+    @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`() {
+        every { exposureWindowTables.allEntries() } returns flowOf(listOf(testExposureWindowDaoWrapper))
+
+        runBlockingTest {
+            val instance = createInstance()
+            instance.exposureWindows.first() shouldBe listOf(testExposureWindow)
+        }
+    }
+
+    @Test
+    fun `riskLevelResults are returned from database and mapped`() {
+        every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao))
+
+        runBlockingTest {
+            val instance = createInstance()
+            instance.riskLevelResults.first() shouldBe listOf(testRisklevelResult)
+
+            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())
+
+        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/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..155c9e81ba5a4db2683f8585bf9d3cd01a2e13ee
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt
@@ -0,0 +1,46 @@
+package de.rki.coronawarnapp.risk.storage.internal
+
+import de.rki.coronawarnapp.risk.RiskLevel
+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 is correct`() {
+        PersistedRiskLevelResultDao(
+            id = "",
+            riskLevel = RiskLevel.LOW_LEVEL_RISK,
+            calculatedAt = Instant.ofEpochMilli(931161601L),
+            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 {
+            riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK
+            calculatedAt.millis shouldBe 931161601L
+            exposureWindows 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
+            }
+        }
+    }
+}
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..832d0e8c332837c7d93883c529fed885ce9995c0
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt
@@ -0,0 +1,124 @@
+package de.rki.coronawarnapp.risk.storage.legacy
+
+import androidx.core.content.edit
+import de.rki.coronawarnapp.risk.RiskLevel
+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", RiskLevel.INCREASED_RISK.raw)
+            putInt("preference_risk_level_score_successful", RiskLevel.LOW_LEVEL_RISK.raw)
+            putLong("preference_timestamp_risk_level_calculation", 1234567890L)
+        }
+        createInstance().apply {
+            val legacyResults = getLegacyResults()
+            legacyResults[0].apply {
+                riskLevel shouldBe RiskLevel.INCREASED_RISK
+                calculatedAt shouldBe Instant.ofEpochMilli(1234567890L)
+            }
+            legacyResults[1].apply {
+                riskLevel shouldBe RiskLevel.LOW_LEVEL_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", RiskLevel.INCREASED_RISK.raw)
+            putInt("preference_risk_level_score_successful", RiskLevel.LOW_LEVEL_RISK.raw)
+        }
+        createInstance().apply {
+            val legacyResults = getLegacyResults()
+            legacyResults[0].apply {
+                riskLevel shouldBe RiskLevel.INCREASED_RISK
+                calculatedAt shouldBe Instant.EPOCH.plus(1337)
+            }
+            legacyResults[1].apply {
+                riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK
+                calculatedAt shouldBe Instant.EPOCH.plus(1337)
+            }
+        }
+    }
+
+    @Test
+    fun `last successful is null`() {
+        mockPreferences.edit {
+            putInt("preference_risk_level_score_successful", RiskLevel.INCREASED_RISK.raw)
+        }
+        createInstance().apply {
+            val legacyResults = getLegacyResults()
+            legacyResults.size shouldBe 1
+            legacyResults.first().apply {
+                riskLevel shouldBe RiskLevel.INCREASED_RISK
+                calculatedAt shouldBe Instant.EPOCH.plus(1337)
+            }
+        }
+    }
+
+    @Test
+    fun `last successfully calculated is null`() {
+        mockPreferences.edit {
+            putInt("preference_risk_level_score", RiskLevel.INCREASED_RISK.raw)
+            putLong("preference_timestamp_risk_level_calculation", 1234567890L)
+        }
+        createInstance().apply {
+            val legacyResults = getLegacyResults()
+            legacyResults.size shouldBe 1
+            legacyResults.first().apply {
+                riskLevel shouldBe RiskLevel.INCREASED_RISK
+                calculatedAt shouldBe Instant.ofEpochMilli(1234567890L)
+            }
+        }
+    }
+
+    @Test
+    fun `exceptions are handled gracefully`() {
+        mockPreferences.edit {
+            putInt("preference_risk_level_score", RiskLevel.INCREASED_RISK.raw)
+        }
+        every { timeStamper.nowUTC } throws Exception("Surprise!")
+        createInstance().getLegacyResults() shouldBe emptyList()
+    }
+}
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..1d6bd137da9f06e25732fbe70724562c0646d7b6 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
@@ -17,6 +17,7 @@ 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.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
@@ -42,8 +43,8 @@ class TracingCardStateTest : BaseTest() {
         riskLevel: Int = 0,
         tracingProgress: TracingProgress = TracingProgress.Idle,
         riskLevelLastSuccessfulCalculation: Int = 0,
-        matchedKeyCount: Int = 0,
-        daysSinceLastExposure: Int = 0,
+        daysWithEncounters: Int = 0,
+        lastEncounterAt: Instant? = null,
         activeTracingDaysInRetentionPeriod: Long = 0,
         lastTimeDiagnosisKeysFetched: Date? = mockk(),
         isBackgroundJobEnabled: Boolean = false,
@@ -54,8 +55,8 @@ class TracingCardStateTest : BaseTest() {
         riskLevelScore = riskLevel,
         tracingProgress = tracingProgress,
         lastRiskLevelScoreCalculated = riskLevelLastSuccessfulCalculation,
-        matchedKeyCount = matchedKeyCount,
-        daysSinceLastExposure = daysSinceLastExposure,
+        daysWithEncounters = daysWithEncounters,
+        lastEncounterAt = lastEncounterAt,
         activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod,
         lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched,
         isBackgroundJobEnabled = isBackgroundJobEnabled,
@@ -336,88 +337,44 @@ class TracingCardStateTest : BaseTest() {
 
     @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
+            daysWithEncounters = 0
         ).apply {
             getRiskContactBody(context) shouldBe ""
         }
 
         createInstance(
             riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF,
-            matchedKeyCount = 0
+            daysWithEncounters = 0
         ).apply {
             getRiskContactBody(context) shouldBe ""
         }
 
         createInstance(
             riskLevel = UNKNOWN_RISK_INITIAL,
-            matchedKeyCount = 0
+            daysWithEncounters = 0
         ).apply {
             getRiskContactBody(context) shouldBe ""
         }
 
         createInstance(
             riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS,
-            matchedKeyCount = 2
+            daysWithEncounters = 2
         ).apply {
             getRiskContactBody(context) shouldBe ""
         }
 
         createInstance(
             riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF,
-            matchedKeyCount = 2
+            daysWithEncounters = 2
         ).apply {
             getRiskContactBody(context) shouldBe ""
         }
 
         createInstance(
             riskLevel = UNKNOWN_RISK_INITIAL,
-            matchedKeyCount = 2
+            daysWithEncounters = 2
         ).apply {
             getRiskContactBody(context) shouldBe ""
         }
@@ -454,57 +411,25 @@ 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 {
-            getRiskContactLast(context)
-            verify {
-                context.resources.getQuantityString(
-                    R.plurals.risk_card_increased_risk_body_contact_last,
-                    0,
-                    0
-                )
-            }
-        }
-
-        createInstance(
-            riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS,
-            daysSinceLastExposure = 2
+            riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS
         ).apply {
             getRiskContactLast(context) shouldBe ""
         }
 
         createInstance(
-            riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF,
-            daysSinceLastExposure = 2
+            riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF
         ).apply {
             getRiskContactLast(context) shouldBe ""
         }
 
         createInstance(
-            riskLevel = LOW_LEVEL_RISK,
-            daysSinceLastExposure = 2
+            riskLevel = LOW_LEVEL_RISK
         ).apply {
             getRiskContactLast(context) shouldBe ""
         }
 
         createInstance(
-            riskLevel = UNKNOWN_RISK_INITIAL,
-            daysSinceLastExposure = 2
+            riskLevel = UNKNOWN_RISK_INITIAL
         ).apply {
             getRiskContactLast(context) shouldBe ""
         }
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..d5e6090b8b42814db11bf29edf1bbc5f899e1b77 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
@@ -59,12 +59,6 @@ class BaseTracingStateTest : BaseTest() {
         override val tracingStatus: GeneralTracingStatus.Status = tracingStatus
         override val riskLevelScore: Int = riskLevelScore
         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
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensionsTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..77208efddd71c37f580c89709c0bb2dd043e0417
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensionsTest.kt
@@ -0,0 +1,93 @@
+package de.rki.coronawarnapp.ui.tracing.common
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.risk.RiskLevel
+import de.rki.coronawarnapp.risk.RiskLevelResult
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+import io.kotest.matchers.longs.shouldBeInRange
+import io.kotest.matchers.shouldBe
+import org.joda.time.Instant
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class RiskLevelExtensionsTest : BaseTest() {
+
+    private fun createRiskLevel(
+        riskLevel: RiskLevel,
+        calculatedAt: Instant
+    ): RiskLevelResult = object : RiskLevelResult {
+        override val riskLevel: RiskLevel = riskLevel
+        override val calculatedAt: Instant = calculatedAt
+        override val aggregatedRiskResult: AggregatedRiskResult? = 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 {
+                riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK
+                val now = Instant.now().millis
+                calculatedAt.millis shouldBeInRange ((now - 60 * 1000L)..now + 60 * 1000L)
+            }
+            lastSuccessfullyCalculated.apply {
+                riskLevel shouldBe RiskLevel.UNDETERMINED
+            }
+        }
+    }
+
+    @Test
+    fun `getLastestAndLastSuccessful last calculation was successful`() {
+        val results = listOf(
+            createRiskLevel(RiskLevel.INCREASED_RISK, calculatedAt = Instant.EPOCH),
+            createRiskLevel(RiskLevel.LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH.plus(1))
+        )
+
+        results.tryLatestResultsWithDefaults().apply {
+            lastCalculated.riskLevel shouldBe lastSuccessfullyCalculated.riskLevel
+            lastCalculated.calculatedAt shouldBe lastSuccessfullyCalculated.calculatedAt
+
+            lastCalculated.riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK
+            lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1)
+        }
+    }
+
+    @Test
+    fun `getLastestAndLastSuccessful last calculation was not successful`() {
+        val results = listOf(
+            createRiskLevel(RiskLevel.INCREASED_RISK, calculatedAt = Instant.EPOCH),
+            createRiskLevel(RiskLevel.LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+            createRiskLevel(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF, calculatedAt = Instant.EPOCH.plus(2))
+        )
+
+        results.tryLatestResultsWithDefaults().apply {
+            lastCalculated.riskLevel shouldBe RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF
+            lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(2)
+
+            lastSuccessfullyCalculated.riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK
+            lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1)
+        }
+    }
+
+    @Test
+    fun `getLastestAndLastSuccessful no successful calculations yet`() {
+        val results = listOf(
+            createRiskLevel(RiskLevel.UNDETERMINED, calculatedAt = Instant.EPOCH.plus(10)),
+            createRiskLevel(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL, calculatedAt = Instant.EPOCH.plus(11)),
+            createRiskLevel(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS, calculatedAt = Instant.EPOCH.plus(12)),
+            createRiskLevel(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF, calculatedAt = Instant.EPOCH.plus(13))
+        )
+
+        results.tryLatestResultsWithDefaults().apply {
+            lastCalculated.riskLevel shouldBe RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF
+            lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(13)
+
+            lastSuccessfullyCalculated.riskLevel shouldBe RiskLevel.UNDETERMINED
+            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..46f3bf4433ae4684a960c3d1e56c751120f96df2 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
@@ -17,7 +17,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() {
 
@@ -40,11 +39,9 @@ class TracingDetailsStateTest : BaseTest() {
         tracingStatus: GeneralTracingStatus.Status = mockk(),
         riskLevelScore: Int = 0,
         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,
@@ -54,11 +51,9 @@ class TracingDetailsStateTest : BaseTest() {
         tracingStatus = tracingStatus,
         riskLevelScore = riskLevelScore,
         tracingProgress = tracingProgress,
-        lastRiskLevelScoreCalculated = riskLevelLastSuccessfulCalculation,
         matchedKeyCount = matchedKeyCount,
         daysSinceLastExposure = daysSinceLastExposure,
         activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod,
-        lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched,
         isBackgroundJobEnabled = isBackgroundJobEnabled,
         isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled,
         manualKeyRetrievalTime = manualKeyRetrievalTime,
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/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..a739176bea2011d5e042eb2e17916cf241da7021
--- /dev/null
+++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
@@ -0,0 +1,122 @@
+package de.rki.coronawarnapp.test.risk.storage
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.risk.RiskLevel
+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",
+        riskLevel = RiskLevel.INCREASED_RISK,
+        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
+        )
+    )
+    private val testRisklevelResult = RiskLevelTaskResult(
+        riskLevel = RiskLevel.INCREASED_RISK,
+        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()
+        )
+    )
+
+    @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/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..f2036adfd34846ddb9e2b22eaca6ec3d0f86ba30
--- /dev/null
+++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
@@ -0,0 +1,120 @@
+package de.rki.coronawarnapp.test.risk.storage
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.risk.RiskLevel
+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",
+        riskLevel = RiskLevel.INCREASED_RISK,
+        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
+        )
+    )
+    private val testRisklevelResult = RiskLevelTaskResult(
+        riskLevel = RiskLevel.INCREASED_RISK,
+        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()
+        )
+    )
+
+    @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"))
+        }
+    }
+}