diff --git a/.circleci/config.yml b/.circleci/config.yml
index 1f005f286ecff1198d18270136e180513e6f47c4..3e7d9b10cbbc2210e2db04ff498f4665902f7e5b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -223,9 +223,6 @@ jobs:
       - run-gradle-cmd:
           desc: JaCoCo report
           cmd: ":Corona-Warn-App:jacocoTestReportDeviceRelease -i"
-      - run:
-          name: Skip SonarCloud for external Pull Requests
-          command: '[[ -v CIRCLE_PR_REPONAME ]] && circleci-agent step halt || true'
       - scan-sonar
   quick_build_device_for_testers_signed:
     executor: android/android
@@ -275,9 +272,10 @@ jobs:
       - run:
           name: Send to T-System
           command: |
+            fileName=$(find Corona-Warn-App/build/outputs/apk/deviceForTesters/release -name '*Corona-Warn-App*.apk')
             curl --location --request POST $tsystems_upload_url \
             --header "Authorization: Bearer $tsystems_upload_bearer" \
-            --form 'file=@Corona-Warn-App/build/outputs/apk/deviceForTesters/release/Corona-Warn-App-deviceForTesters-release.apk' \
+            --form "file=@${fileName}" \
 workflows:
   version: 2
   quick_build:
diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml
index 13dca3db33d0f66c8b8af3bd6b1eb06db666aa83..8cbe448f6ffb21cd44d01a5f534581dff4675ce2 100644
--- a/.github/workflows/gradle-wrapper-validation.yml
+++ b/.github/workflows/gradle-wrapper-validation.yml
@@ -3,10 +3,10 @@ name: "Validate Gradle Wrapper"
 on:
   push:
     branches:
-      - master
+      - main
   pull_request:
     branches:
-      - master
+      - main
 
 jobs:
   validation:
diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 83f49f75d73e527a3c7e21e3fb8fa5694c66c4da..255c61a8b5e1c2bfb4ad40b161fb60b148ff5e3b 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -163,11 +163,12 @@ android {
             }
             println("deviceForTesters adjusted versionName: $adjustedVersionName")
         }
-
-        variant.outputs.each { output ->
-            def apkName = "Corona-Warn-App-${output.versionNameOverride}-${flavor.name}-${variant.buildType.name}.apk"
-            println("APK Name: $apkName")
-            output.outputFileName = apkName
+        if (flavor.name != "device") {
+            variant.outputs.each { output ->
+                def apkName = "Corona-Warn-App-${output.versionNameOverride}-${flavor.name}-${variant.buildType.name}.apk"
+                println("Override APK Name: $apkName")
+                output.outputFileName = apkName
+            }
         }
     }
 
diff --git a/Corona-Warn-App/proguard-rules.pro b/Corona-Warn-App/proguard-rules.pro
index 13dfa13cd055802fd1285f1182d31af5f22bf11a..a494dc86a67e75431934ce0fd0adeeab58b9ddee 100644
--- a/Corona-Warn-App/proguard-rules.pro
+++ b/Corona-Warn-App/proguard-rules.pro
@@ -72,9 +72,6 @@
 -dontwarn sun.misc.**
 #-keep class com.google.gson.stream.** { *; }
 
-# Application classes that will be serialized/deserialized over Gson
--keep class com.google.gson.examples.android.model.** { <fields>; }
-
 # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
 # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
 -keep class * extends com.google.gson.TypeAdapter
@@ -87,4 +84,6 @@
   @com.google.gson.annotations.SerializedName <fields>;
 }
 
-##---------------End: proguard configuration for Gson  ----------
\ No newline at end of file
+##---------------End: proguard configuration for Gson  ----------
+
+-keep class de.rki.coronawarnapp.server.protocols.internal.** { *; }
\ No newline at end of file
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt
deleted file mode 100644
index 32df9c31bd567455f39b6541c680ada03eb64c44..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package de.rki.coronawarnapp.storage
-
-import android.content.Context
-import androidx.room.Room
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-/**
- * ExposureSummaryDao test.
- */
-@RunWith(AndroidJUnit4::class)
-class ExposureSummaryDaoTest {
-    private lateinit var dao: ExposureSummaryDao
-    private lateinit var db: AppDatabase
-
-    @Before
-    fun setUp() {
-        val context = ApplicationProvider.getApplicationContext<Context>()
-        db = Room.inMemoryDatabaseBuilder(
-            context, AppDatabase::class.java
-        ).build()
-        dao = db.exposureSummaryDao()
-    }
-
-    /**
-     * Test Create / Read DB operations.
-     */
-    @Test
-    fun testCROperations() {
-        runBlocking {
-            val testEntity1 = ExposureSummaryEntity().apply {
-                this.daysSinceLastExposure = 1
-                this.matchedKeyCount = 1
-                this.maximumRiskScore = 1
-                this.summationRiskScore = 1
-            }
-
-            val testEntity2 = ExposureSummaryEntity().apply {
-                this.daysSinceLastExposure = 2
-                this.matchedKeyCount = 2
-                this.maximumRiskScore = 2
-                this.summationRiskScore = 2
-            }
-
-            assertThat(dao.getExposureSummaryEntities().isEmpty()).isTrue()
-
-            val id1 = dao.insertExposureSummaryEntity(testEntity1)
-            var selectAll = dao.getExposureSummaryEntities()
-            var selectLast = dao.getLatestExposureSummary()
-            assertThat(dao.getExposureSummaryEntities().isEmpty()).isFalse()
-            assertThat(selectAll.size).isEqualTo(1)
-            assertThat(selectAll[0].id).isEqualTo(id1)
-            assertThat(selectLast).isNotNull()
-            assertThat(selectLast?.id).isEqualTo(id1)
-
-            val id2 = dao.insertExposureSummaryEntity(testEntity2)
-            selectAll = dao.getExposureSummaryEntities()
-            selectLast = dao.getLatestExposureSummary()
-            assertThat(selectAll.isEmpty()).isFalse()
-            assertThat(selectAll.size).isEqualTo(2)
-            assertThat(selectLast).isNotNull()
-            assertThat(selectLast?.id).isEqualTo(id2)
-        }
-    }
-
-    @After
-    fun closeDb() {
-        db.close()
-    }
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt
deleted file mode 100644
index 6771827fa9cab7f2cc67ea55cdb6a0c537a6f3ec..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt
+++ /dev/null
@@ -1,163 +0,0 @@
-package de.rki.coronawarnapp.test
-
-import android.content.Context
-import android.text.format.Formatter
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionFinished
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionStarted
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadFinished
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadStarted
-import de.rki.coronawarnapp.risk.RiskLevelTask
-import de.rki.coronawarnapp.task.Task
-import de.rki.coronawarnapp.task.common.DefaultTaskRequest
-import de.rki.coronawarnapp.task.submitAndListen
-import de.rki.coronawarnapp.util.di.AppInjector
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.map
-import timber.log.Timber
-import java.util.UUID
-
-class RiskLevelAndKeyRetrievalBenchmark(
-    private val context: Context,
-    private val countries: List<String>
-) {
-
-    /**
-     * the key cache instance used to store queried dates and hours
-     */
-    private val keyCache = AppInjector.component.keyCacheRepository
-
-    /**
-     * Calls the RetrieveDiagnosisKeysTransaction and RiskLevelTransaction and measures them.
-     * Results are displayed using a label
-     * @param callCount defines how often the transactions should be called (each call will be
-     * measured separately)
-     */
-    suspend fun start(
-        callCount: Int,
-        onBenchmarkCompletedListener: OnBenchmarkCompletedListener
-    ) {
-
-        var resultInfo = StringBuilder()
-            .append(
-                "MEASUREMENT Running for Countries:\n " +
-                    "${countries.joinToString(", ")}\n\n"
-            )
-            .append("Result: \n\n")
-            .append("#\t Combined \t Download \t Sub \t Risk \t File # \t  F. size\n")
-
-        onBenchmarkCompletedListener(resultInfo.toString())
-
-        repeat(callCount) { index ->
-
-            keyCache.clear()
-
-            var keyRetrievalError = ""
-            var keyFileCount: Int = -1
-            var keyFileDownloadDuration: Long = -1
-            var keyFilesSize: Long = -1
-            var apiSubmissionDuration: Long = -1
-
-            measureDiagnosticKeyRetrieval(
-                label = "#$index",
-                countries = countries,
-                downloadFinished = { duration, keyCount, totalFileSize ->
-                    keyFileCount = keyCount
-                    keyFileDownloadDuration = duration
-                    keyFilesSize = totalFileSize
-                }, apiSubmissionFinished = { duration ->
-                    apiSubmissionDuration = duration
-                })
-
-            var calculationDuration: Long = -1
-            var calculationError = ""
-
-            measureKeyCalculation("#$index") {
-                if (it != null) calculationDuration = it
-
-                // build result entry for current iteration with all gathered data
-                resultInfo.append(
-                    "${index + 1}. \t ${calculationDuration + keyFileDownloadDuration + apiSubmissionDuration} ms \t " +
-                        "$keyFileDownloadDuration ms " + "\t $apiSubmissionDuration ms" +
-                        "\t $calculationDuration ms \t $keyFileCount \t " +
-                        "${Formatter.formatFileSize(context, keyFilesSize)}\n"
-                )
-
-                if (keyRetrievalError.isNotEmpty()) {
-                    resultInfo.append("Key Retrieval Error: $keyRetrievalError\n")
-                }
-
-                if (calculationError.isNotEmpty()) {
-                    resultInfo.append("Calculation Error: $calculationError\n")
-                }
-
-                onBenchmarkCompletedListener(resultInfo.toString())
-            }
-        }
-    }
-
-    private suspend fun measureKeyCalculation(label: String, callback: (Long?) -> Unit) {
-        val uuid = UUID.randomUUID()
-        val t0 = System.currentTimeMillis()
-        AppInjector.component.taskController.tasks
-            .map {
-                it
-                    .map { taskInfo -> taskInfo.taskState }
-                    .filter { taskState -> taskState.request.id == uuid && taskState.isFinished }
-            }
-            .collect {
-                it.firstOrNull()?.also { state ->
-                    Timber.v("MEASURE [Risk Level Calculation] $label finished")
-                    callback.invoke(
-                        if (state.error != null)
-                            null
-                        else
-                            System.currentTimeMillis() - t0
-                    )
-                }
-            }
-        Timber.v("MEASURE [Risk Level Calculation] $label started")
-        AppInjector.component.taskController.submit(
-            DefaultTaskRequest(
-                RiskLevelTask::class,
-                object : Task.Arguments {},
-                uuid
-            )
-        )
-    }
-
-    private suspend fun measureDiagnosticKeyRetrieval(
-        label: String,
-        countries: List<String>,
-        downloadFinished: (duration: Long, keyCount: Int, fileSize: Long) -> Unit,
-        apiSubmissionFinished: (duration: Long) -> Unit
-    ) {
-        var keyFileDownloadStart: Long = -1
-        var apiSubmissionStarted: Long = -1
-
-        AppInjector.component.taskController.submitAndListen(
-            DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(countries))
-        ).collect { progress: Task.Progress ->
-            when (progress) {
-                is KeyFilesDownloadStarted -> {
-                    Timber.v("MEASURE [Diagnostic Key Files] $label started")
-                    keyFileDownloadStart = System.currentTimeMillis()
-                }
-                is KeyFilesDownloadFinished -> {
-                    Timber.v("MEASURE [Diagnostic Key Files] $label finished")
-                    val duration = System.currentTimeMillis() - keyFileDownloadStart
-                    downloadFinished(duration, progress.keyCount, progress.fileSize)
-                }
-                is ApiSubmissionStarted -> {
-                    apiSubmissionStarted = System.currentTimeMillis()
-                }
-                is ApiSubmissionFinished -> {
-                    val duration = System.currentTimeMillis() - apiSubmissionStarted
-                    apiSubmissionFinished(duration)
-                }
-            }
-        }
-    }
-}
-
-typealias OnBenchmarkCompletedListener = (resultInfo: String) -> Unit
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt
index 3244c5f34b18712f59e379f811d8d198268bca99..2466cb7d6598603c6d97ef9a27ce9805094073f0 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt
@@ -3,11 +3,13 @@ package de.rki.coronawarnapp.test.api.ui
 import de.rki.coronawarnapp.util.CWADebug
 
 data class LoggerState(
-    val isLogging: Boolean
+    val isLogging: Boolean,
+    val logsize: Long
 ) {
     companion object {
         internal fun CWADebug.toLoggerState() = LoggerState(
-            isLogging = fileLogger?.isLogging ?: false
+            isLogging = fileLogger?.isLogging ?: false,
+            logsize = fileLogger?.logFile?.length() ?: 0L
         )
     }
 }
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 694615a9ae234ae6f626b12a7ff9f0f179402ff1..c03b84f39023d1999f7d0e38b93fa25b1e6d0d10 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
@@ -16,7 +16,6 @@ import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.RecyclerView
 import androidx.viewpager2.widget.ViewPager2
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
 import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
 import com.google.android.material.snackbar.Snackbar
 import com.google.gson.Gson
@@ -32,25 +31,24 @@ import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL
 import de.rki.coronawarnapp.exception.TransactionException
 import de.rki.coronawarnapp.exception.reporting.report
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
+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.server.protocols.AppleLegacyKeyExchange
 import de.rki.coronawarnapp.sharing.ExposureSharingService
 import de.rki.coronawarnapp.storage.AppDatabase
-import de.rki.coronawarnapp.storage.ExposureSummaryRepository
-import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository
 import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
 import de.rki.coronawarnapp.util.KeyFileHelper
-import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.joda.time.DateTime
@@ -66,6 +64,8 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
     InternalExposureNotificationPermissionHelper.Callback, AutoInject {
 
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    @Inject lateinit var enfClient: ENFClient
+    @Inject lateinit var exposureResultStore: ExposureResultStore
     private val vm: TestForApiFragmentViewModel by cwaViewModels { viewModelFactory }
 
     companion object {
@@ -85,10 +85,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
         }
     }
 
-    private val enfClient by lazy {
-        AppInjector.component.enfClient
-    }
-
     private var myExposureKeysJSON: String? = null
     private var myExposureKeys: List<TemporaryExposureKey>? = mutableListOf()
     private var otherExposureKey: AppleLegacyKeyExchange.Key? = null
@@ -96,8 +92,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
 
     private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper
 
-    private var token: String? = null
-
     private lateinit var qrPager: ViewPager2
     private lateinit var qrPagerAdapter: RecyclerView.Adapter<QRPagerAdapter.QRViewHolder>
 
@@ -108,8 +102,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        token = UUID.randomUUID().toString()
-
         internalExposureNotificationPermissionHelper =
             InternalExposureNotificationPermissionHelper(this, this)
 
@@ -127,7 +119,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
         binding.apply {
             buttonApiTestStart.setOnClickListener { start() }
             buttonApiGetExposureKeys.setOnClickListener { getExposureKeys() }
-            buttonApiGetCheckExposure.setOnClickListener { checkExposure() }
 
             buttonApiScanQrCode.setOnClickListener {
                 IntentIntegrator.forSupportFragment(this@TestForAPIFragment)
@@ -168,8 +159,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
 
             buttonRetrieveExposureSummary.setOnClickListener {
                 vm.launch {
-                    val summary = ExposureSummaryRepository.getExposureSummaryRepository()
-                        .getExposureSummaryEntities().toString()
+                    val summary = exposureResultStore.entities.first().exposureWindows.toString()
 
                     withContext(Dispatchers.Main) {
                         showToast(summary)
@@ -206,12 +196,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
         }
     }
 
-    override fun onResume() {
-        super.onResume()
-
-        updateExposureSummaryDisplay(null)
-    }
-
     private val prettyKey = { key: AppleLegacyKeyExchange.Key ->
         StringBuilder()
             .append("\nKey data: ${key.keyData}")
@@ -274,9 +258,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
         if (null == otherExposureKey) {
             showToast("No other keys provided. Please fill the EditText with the JSON containing keys")
         } else {
-            token = UUID.randomUUID().toString()
-            LocalData.googleApiToken(token)
-
             val appleKeyList = mutableListOf<AppleLegacyKeyExchange.Key>()
 
             for (key in otherExposureKeyList) {
@@ -298,7 +279,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
 
             val dir = File(
                 File(requireContext().getExternalFilesDir(null), "key-export"),
-                token ?: ""
+                UUID.randomUUID().toString()
             )
             dir.mkdirs()
 
@@ -306,15 +287,13 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
             lifecycleScope.launch {
                 googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir)
 
-                Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token")
+                Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys")
                 try {
                     // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API
                     enfClient.provideDiagnosisKeys(
-                        googleFileList,
-                        AppInjector.component.appConfigProvider.getAppConfig().exposureDetectionConfiguration,
-                        token!!
+                        googleFileList
                     )
-                    showToast("Provided ${appleKeyList.size} keys to Google API with token $token")
+                    showToast("Provided ${appleKeyList.size} keys to Google API")
                 } catch (e: Exception) {
                     e.report(ExceptionCategory.EXPOSURENOTIFICATION)
                 }
@@ -322,51 +301,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i),
         }
     }
 
-    private fun checkExposure() {
-        Timber.d("Check Exposure with token $token")
-
-        lifecycleScope.launch {
-            try {
-                val exposureSummary =
-                    InternalExposureNotificationClient.asyncGetExposureSummary(token!!)
-                updateExposureSummaryDisplay(exposureSummary)
-                showToast("Updated Exposure Summary with token $token")
-                Timber.d("Received exposure with token $token from QR Code")
-                Timber.i(exposureSummary.toString())
-            } catch (e: Exception) {
-                e.report(ExceptionCategory.EXPOSURENOTIFICATION)
-            }
-        }
-    }
-
-    private fun updateExposureSummaryDisplay(exposureSummary: ExposureSummary?) {
-
-        binding.labelExposureSummaryMatchedKeyCount.text = getString(
-            R.string.test_api_body_matchedKeyCount,
-            (exposureSummary?.matchedKeyCount ?: "-").toString()
-        )
-
-        binding.labelExposureSummaryDaysSinceLastExposure.text = getString(
-            R.string.test_api_body_daysSinceLastExposure,
-            (exposureSummary?.daysSinceLastExposure ?: "-").toString()
-        )
-
-        binding.labelExposureSummaryMaximumRiskScore.text = getString(
-            R.string.test_api_body_maximumRiskScore,
-            (exposureSummary?.maximumRiskScore ?: "-").toString()
-        )
-
-        binding.labelExposureSummarySummationRiskScore.text = getString(
-            R.string.test_api_body_summation_risk,
-            (exposureSummary?.summationRiskScore ?: "-").toString()
-        )
-
-        binding.labelExposureSummaryAttenuation.text = getString(
-            R.string.test_api_body_attenuation,
-            (exposureSummary?.attenuationDurationsInMinutes?.joinToString() ?: "-").toString()
-        )
-    }
-
     private fun updateKeysDisplay() {
 
         val myKeys =
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt
index f2ebfd26e4c413d2b3c80cb8cce605669310ee03..d277c9b961d19aa3ab2eba3c8eb262c0fe71fe51 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt
@@ -18,7 +18,7 @@ class TestForApiFragmentViewModel @AssistedInject constructor(
 ) : CWAViewModel() {
 
     fun calculateRiskLevelClicked() {
-        taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
+        taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "TestForApiFragmentViewModel"))
     }
 
     val gmsState by smartLiveData {
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt
index 742411f39edaa87b0a911267cb3b4d527326c194..09567384fd47b06010a3379f788b0f8b5f2a6f78 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt
@@ -32,12 +32,9 @@ class AppConfigTestFragment : Fragment(R.layout.fragment_test_appconfig), AutoIn
         super.onViewCreated(view, savedInstanceState)
 
         vm.currentConfig.observe2(this) { data ->
-            binding.currentConfiguration.text =
-                data?.rawConfig?.toString() ?: "No config available."
-            binding.lastUpdate.text = data?.updatedAt?.let { timeFormatter.print(it) } ?: "n/a"
-            binding.timeOffset.text = data?.let {
-                "${it.localOffset.millis}ms (configType=${it.configType})"
-            } ?: "n/a"
+            binding.currentConfiguration.text = data.rawConfig.toString()
+            binding.lastUpdate.text = timeFormatter.print(data.updatedAt)
+            binding.timeOffset.text = "${data.localOffset.millis}ms (configType=${data.configType})"
         }
 
         vm.errorEvent.observe2(this) {
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt
index 234022907e2c603253e6ac70c8abffa1dd72143d..44d3e0cd08a2688511a8c20589bfe3b428659d36 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt
@@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.asLiveData
 import androidx.lifecycle.map
-import androidx.lifecycle.viewModelScope
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.bugreporting.event.BugEvent
 import de.rki.coronawarnapp.bugreporting.reportProblem
@@ -12,9 +11,7 @@ import de.rki.coronawarnapp.bugreporting.storage.repository.BugRepository
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import timber.log.Timber
-import java.lang.Exception
 
 class SettingsCrashReportViewModel @AssistedInject constructor(
     private val crashReportRepository: BugRepository
@@ -28,7 +25,7 @@ class SettingsCrashReportViewModel @AssistedInject constructor(
         createBugEventFormattedText(it)
     }
 
-    fun deleteAllCrashReports() = viewModelScope.launch(Dispatchers.IO) {
+    fun deleteAllCrashReports() = launch(Dispatchers.IO) {
         crashReportRepository.clear()
     }
 
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt
index 91b1206da2ff70f7df02367cf75d340c70809c70..47f43dae0edf5cf59621975f0923cd8b83f15cee 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.test.debugoptions.ui
 
 import android.annotation.SuppressLint
 import android.os.Bundle
+import android.text.format.Formatter
 import android.view.View
 import android.widget.RadioButton
 import android.widget.RadioGroup
@@ -31,24 +32,14 @@ class DebugOptionsFragment : Fragment(R.layout.fragment_test_debugoptions), Auto
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        // Debug card
-        binding.backgroundNotificationsToggle.apply {
-            setOnClickListener { vm.setBackgroundNotifications(isChecked) }
-        }
-        vm.backgroundNotificationsToggleEvent.observe2(this@DebugOptionsFragment) {
-            showSnackBar("Background Notifications are activated: $it")
-        }
-        vm.debugOptionsState.observe2(this) { state ->
-            binding.apply {
-                backgroundNotificationsToggle.isChecked = state.areNotificationsEnabled
-            }
-        }
         binding.testLogfileToggle.apply {
             setOnClickListener { vm.setLoggerEnabled(isChecked) }
         }
         vm.loggerState.observe2(this) { state ->
             binding.apply {
                 testLogfileToggle.isChecked = state.isLogging
+                val logSize = Formatter.formatShortFileSize(requireContext(), state.logsize)
+                testLogfileToggle.text = "Logfile enabled ($logSize)"
                 testLogfileShare.setGone(!state.isLogging)
             }
         }
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt
index c58e64556e2a35b1f6604de20ecaee41ba75638e..0c654b99f52600578c531ccdbe98c181a332ee81 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt
@@ -1,15 +1,9 @@
 package de.rki.coronawarnapp.test.debugoptions.ui
 
 import android.content.Context
-import androidx.lifecycle.viewModelScope
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.environment.EnvironmentSetup
 import de.rki.coronawarnapp.environment.EnvironmentSetup.Type.Companion.toEnvironmentType
-import de.rki.coronawarnapp.risk.RiskLevelTask
-import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.storage.TestSettings
-import de.rki.coronawarnapp.task.TaskController
-import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.test.api.ui.EnvironmentState.Companion.toEnvironmentState
 import de.rki.coronawarnapp.test.api.ui.LoggerState.Companion.toLoggerState
 import de.rki.coronawarnapp.util.CWADebug
@@ -19,24 +13,14 @@ import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.ui.smartLiveData
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
 import java.io.File
 
 class DebugOptionsFragmentViewModel @AssistedInject constructor(
     @AppContext private val context: Context,
     private val envSetup: EnvironmentSetup,
-    private val testSettings: TestSettings,
-    private val taskController: TaskController,
     dispatcherProvider: DispatcherProvider
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
-    val debugOptionsState by smartLiveData {
-        DebugOptionsState(
-            areNotificationsEnabled = LocalData.backgroundNotification()
-        )
-    }
-
     val environmentState by smartLiveData {
         envSetup.toEnvironmentState()
     }
@@ -50,16 +34,6 @@ class DebugOptionsFragmentViewModel @AssistedInject constructor(
         }
     }
 
-    val backgroundNotificationsToggleEvent = SingleLiveEvent<Boolean>()
-
-    fun setBackgroundNotifications(enabled: Boolean) {
-        debugOptionsState.update {
-            LocalData.backgroundNotification(enabled)
-            it.copy(areNotificationsEnabled = enabled)
-        }
-        backgroundNotificationsToggleEvent.postValue(enabled)
-    }
-
     val loggerState by smartLiveData {
         CWADebug.toLoggerState()
     }
@@ -71,15 +45,11 @@ class DebugOptionsFragmentViewModel @AssistedInject constructor(
         loggerState.update { CWADebug.toLoggerState() }
     }
 
-    fun calculateRiskLevelClicked() {
-        taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
-    }
-
     val logShareEvent = SingleLiveEvent<File?>()
 
     fun shareLogFile() {
         CWADebug.fileLogger?.let {
-            viewModelScope.launch(context = Dispatchers.Default) {
+            launch {
                 if (!it.logFile.exists()) return@launch
 
                 val externalPath = File(
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsState.kt
deleted file mode 100644
index da57e399eaf9c4caf03c07a9e2fa719d3375fa17..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsState.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package de.rki.coronawarnapp.test.debugoptions.ui
-
-data class DebugOptionsState(
-    val areNotificationsEnabled: Boolean
-)
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
index 36d71e9cb89f0fde006cbb5c9f2df53e228d6c33..ef3eff6e88c7362e04b9766d57312bedf0c536ae 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
@@ -8,6 +8,7 @@ import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment
 import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment
 import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment
 import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment
+import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment
 import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -23,6 +24,7 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() {
             TestRiskLevelCalculationFragment.MENU_ITEM,
             KeyDownloadTestFragment.MENU_ITEM,
             TestTaskControllerFragment.MENU_ITEM,
+            SubmissionTestFragment.MENU_ITEM,
             SettingsCrashReportFragment.MENU_ITEM
         ).let { MutableLiveData(it) }
     }
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt
index 0867b6e6bfcbce4261cde511273134389288fe83..b0e40407ce9b0895cc283865456e43f7303882d0 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt
@@ -1,27 +1,20 @@
 package de.rki.coronawarnapp.test.risklevel.ui
 
-import android.content.Intent
 import android.os.Bundle
 import android.view.View
 import android.widget.Toast
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.navigation.fragment.navArgs
-import com.google.zxing.integration.android.IntentIntegrator
-import com.google.zxing.integration.android.IntentResult
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding
-import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange
-import de.rki.coronawarnapp.sharing.ExposureSharingService
 import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
 import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel
-import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
-import timber.log.Timber
 import javax.inject.Inject
 
 @Suppress("LongMethod")
@@ -39,7 +32,6 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le
     )
 
     private val settingsViewModel: SettingsViewModel by activityViewModels()
-    private val submissionViewModel: SubmissionViewModel by activityViewModels()
 
     private val binding: FragmentTestRiskLevelCalculationBinding by viewBindingLazy()
 
@@ -51,10 +43,12 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le
         }
 
         binding.settingsViewModel = settingsViewModel
-        binding.submissionViewModel = submissionViewModel
+
+        vm.showRiskStatusCard.observe2(this) {
+            binding.showRiskStatusCard = it
+        }
 
         binding.buttonRetrieveDiagnosisKeys.setOnClickListener { vm.retrieveDiagnosisKeys() }
-        binding.buttonProvideKeyViaQr.setOnClickListener { vm.scanLocalQRCodeAndProvide() }
         binding.buttonCalculateRiskLevel.setOnClickListener { vm.calculateRiskLevel() }
         binding.buttonClearDiagnosisKeyCache.setOnClickListener { vm.clearKeyCache() }
 
@@ -66,66 +60,32 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le
             ).show()
         }
 
-        vm.riskScoreState.observe2(this) { state ->
-            binding.labelRiskScore.text = state.riskScoreMsg
-            binding.labelBackendParameters.text = state.backendParameters
-            binding.labelExposureSummary.text = state.exposureSummary
-            binding.labelFormula.text = state.formula
-            binding.labelExposureInfo.text = state.exposureInfo
+        vm.additionalRiskCalcInfo.observe2(this) {
+            binding.labelRiskAdditionalInfo.text = it
         }
-        vm.startENFObserver()
 
-        vm.apiKeysProvidedEvent.observe2(this) { event ->
-            Toast.makeText(
-                requireContext(),
-                "Provided ${event.keyCount} keys to Google API with token ${event.token}",
-                Toast.LENGTH_SHORT
-            ).show()
+        vm.aggregatedRiskResult.observe2(this) {
+            binding.labelAggregatedRiskResult.text = it
         }
 
-        vm.startLocalQRCodeScanEvent.observe2(this) {
-            IntentIntegrator.forSupportFragment(this)
-                .setOrientationLocked(false)
-                .setBeepEnabled(false)
-                .initiateScan()
+        vm.exposureWindowCountString.observe2(this) {
+            binding.labelExposureWindowCount.text = it
         }
-    }
-
-    override fun onResume() {
-        super.onResume()
-        vm.calculateRiskLevel()
-    }
-
-    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        val result: IntentResult =
-            IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
-                ?: return super.onActivityResult(requestCode, resultCode, data)
 
-        if (result.contents == null) {
-            Toast.makeText(requireContext(), "Cancelled", Toast.LENGTH_LONG).show()
-            return
+        vm.exposureWindows.observe2(this) {
+            binding.labelExposureWindows.text = it
         }
 
-        ExposureSharingService.getOthersKeys(result.contents) { key: AppleLegacyKeyExchange.Key? ->
-            Timber.i("Keys scanned: %s", key)
-            if (key == null) {
-                Toast.makeText(
-                    requireContext(), "No Key data found in QR code", Toast.LENGTH_SHORT
-                ).show()
-                return@getOthersKeys Unit
-            }
-
-            val text = binding.transmissionNumber.text.toString()
-            val number = if (!text.isBlank()) Integer.valueOf(text) else 5
-            vm.provideDiagnosisKey(number, key)
+        vm.backendParameters.observe2(this) {
+            binding.labelBackendParameters.text = it
         }
     }
 
     companion object {
         val TAG: String = TestRiskLevelCalculationFragment::class.simpleName!!
         val MENU_ITEM = TestMenuItem(
-            title = "Risklevel Calculation",
-            description = "Risklevel calculation related test options.",
+            title = "ENF v2 Calculation",
+            description = "Window Mode related overview.",
             targetId = R.id.test_risklevel_calculation_fragment
         )
     }
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 d0a85e7f0f7f54678a5258bb921b8bca8595726b..35c5e63f9b8c3f63e4c06d605c7b1ec59e867b08 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
@@ -1,96 +1,196 @@
 package de.rki.coronawarnapp.test.risklevel.ui
 
 import android.content.Context
-import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.asLiveData
-import androidx.lifecycle.viewModelScope
-import com.google.android.gms.nearby.exposurenotification.ExposureInformation
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
-import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
 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.nearby.ENFClient
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
+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.RiskLevels
 import de.rki.coronawarnapp.risk.TimeVariables
-import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
 import de.rki.coronawarnapp.storage.AppDatabase
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.RiskLevelRepository
+import de.rki.coronawarnapp.storage.SubmissionRepository
 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.util.KeyFileHelper
+import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.di.AppContext
-import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.security.SecurityHelper
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.sample
-import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import org.joda.time.Instant
 import timber.log.Timber
-import java.io.File
-import java.util.UUID
+import java.util.Date
 import java.util.concurrent.TimeUnit
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
 
 class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
     @Assisted private val handle: SavedStateHandle,
     @Assisted private val exampleArg: String?,
     @AppContext private val context: Context, // App context
     dispatcherProvider: DispatcherProvider,
-    private val enfClient: ENFClient,
-    private val riskLevels: RiskLevels,
     private val taskController: TaskController,
     private val keyCacheRepository: KeyCacheRepository,
-    tracingCardStateProvider: TracingCardStateProvider
+    private val appConfigProvider: AppConfigProvider,
+    tracingCardStateProvider: TracingCardStateProvider,
+    private val exposureResultStore: ExposureResultStore
 ) : CWAViewModel(
     dispatcherProvider = dispatcherProvider
 ) {
 
-    val startLocalQRCodeScanEvent = SingleLiveEvent<Unit>()
+    init {
+        Timber.d("CWAViewModel: %s", this)
+        Timber.d("SavedStateHandle: %s", handle)
+        Timber.d("Example arg: %s", exampleArg)
+    }
+
     val riskLevelResetEvent = SingleLiveEvent<Unit>()
-    val apiKeysProvidedEvent = SingleLiveEvent<DiagnosisKeyProvidedEvent>()
-    val riskScoreState = MutableLiveData<RiskScoreState>(RiskScoreState())
+
+    val showRiskStatusCard = SubmissionRepository.deviceUIStateFlow.map {
+        it.withSuccess(false) { true }
+    }.asLiveData(dispatcherProvider.Default)
 
     val tracingCardState = tracingCardStateProvider.state
         .sample(150L)
         .asLiveData(dispatcherProvider.Default)
 
-    init {
-        Timber.d("CWAViewModel: %s", this)
-        Timber.d("SavedStateHandle: %s", handle)
-        Timber.d("Example arg: %s", exampleArg)
-    }
+    val exposureWindowCountString = exposureResultStore
+        .entities
+        .map { "Retrieved ${it.exposureWindows.size} Exposure Windows" }
+        .asLiveData()
+
+    val exposureWindows = exposureResultStore
+        .entities
+        .map { if (it.exposureWindows.isEmpty()) "Exposure windows list is empty" else it.exposureWindows.toString() }
+        .asLiveData()
+
+    val aggregatedRiskResult = exposureResultStore
+        .entities
+        .map { if (it.aggregatedRiskResult != null) it.aggregatedRiskResult.toReadableString() else "Aggregated risk result is not available" }
+        .asLiveData()
+
+    private fun AggregatedRiskResult.toReadableString(): String = StringBuilder()
+        .appendLine("Total RiskLevel: $totalRiskLevel")
+        .appendLine("Total Minimum Distinct Encounters With High Risk: $totalMinimumDistinctEncountersWithHighRisk")
+        .appendLine("Total Minimum Distinct Encounters With Low Risk: $totalMinimumDistinctEncountersWithLowRisk")
+        .appendLine("Most Recent Date With High Risk: $mostRecentDateWithHighRisk")
+        .appendLine("Most Recent Date With Low Risk: $mostRecentDateWithLowRisk")
+        .appendLine("Number of Days With High Risk: $numberOfDaysWithHighRisk")
+        .appendLine("Number of Days With Low Risk: $numberOfDaysWithLowRisk")
+        .toString()
+
+    val backendParameters = appConfigProvider
+        .currentConfig
+        .map { it.toReadableString() }
+        .asLiveData()
+
+    private fun ConfigData.toReadableString(): String = StringBuilder()
+        .appendLine("Transmission RiskLevel Multiplier: $transmissionRiskLevelMultiplier")
+        .appendLine()
+        .appendLine("Minutes At Attenuation Filters:")
+        .appendLine(minutesAtAttenuationFilters)
+        .appendLine()
+        .appendLine("Minutes At Attenuation Weights:")
+        .appendLine(minutesAtAttenuationWeights)
+        .appendLine()
+        .appendLine("Transmission RiskLevel Encoding:")
+        .appendLine(transmissionRiskLevelEncoding)
+        .appendLine()
+        .appendLine("Transmission RiskLevel Filters:")
+        .appendLine(transmissionRiskLevelFilters)
+        .appendLine()
+        .appendLine("Normalized Time Per Exposure Window To RiskLevel Mapping:")
+        .appendLine(normalizedTimePerExposureWindowToRiskLevelMapping)
+        .appendLine()
+        .appendLine("Normalized Time Per Day To RiskLevel Mapping List:")
+        .appendLine(normalizedTimePerDayToRiskLevelMappingList)
+        .toString()
+
+    val additionalRiskCalcInfo = combine(
+        RiskLevelRepository.riskLevelScore,
+        RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated,
+        exposureResultStore.matchedKeyCount,
+        exposureResultStore.daysSinceLastExposure,
+        LocalData.lastTimeDiagnosisKeysFromServerFetchFlow()
+    ) { riskLevelScore,
+        riskLevelScoreLastSuccessfulCalculated,
+        matchedKeyCount,
+        daysSinceLastExposure,
+        lastTimeDiagnosisKeysFromServerFetch ->
+        createAdditionalRiskCalcInfo(
+            riskLevelScore = riskLevelScore,
+            riskLevelScoreLastSuccessfulCalculated = riskLevelScoreLastSuccessfulCalculated,
+            matchedKeyCount = matchedKeyCount,
+            daysSinceLastExposure = daysSinceLastExposure,
+            lastTimeDiagnosisKeysFromServerFetch = lastTimeDiagnosisKeysFromServerFetch
+        )
+    }.asLiveData()
+
+    private suspend fun createAdditionalRiskCalcInfo(
+        riskLevelScore: Int,
+        riskLevelScoreLastSuccessfulCalculated: Int,
+        matchedKeyCount: Int,
+        daysSinceLastExposure: Int,
+        lastTimeDiagnosisKeysFromServerFetch: Date?
+    ): String = StringBuilder()
+        .appendLine("Risk Level: ${RiskLevel.forValue(riskLevelScore)}")
+        .appendLine("Last successful Risk Level: ${RiskLevel.forValue(riskLevelScoreLastSuccessfulCalculated)}")
+        .appendLine("Matched key count: $matchedKeyCount")
+        .appendLine("Days since last Exposure: $daysSinceLastExposure days")
+        .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) }
+            }"
+        )
+        .toString()
 
     fun retrieveDiagnosisKeys() {
+        Timber.d("Starting download diagnosis keys task")
         launch {
             taskController.submitBlocking(
-                DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments())
+                DefaultTaskRequest(
+                    DownloadDiagnosisKeysTask::class,
+                    DownloadDiagnosisKeysTask.Arguments(),
+                    originTag = "TestRiskLevelCalculationFragmentCWAViewModel.retrieveDiagnosisKeys()"
+                )
             )
-            calculateRiskLevel()
         }
     }
 
     fun calculateRiskLevel() {
-        taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
+        Timber.d("Starting calculate risk task")
+        taskController.submit(
+            DefaultTaskRequest(
+                RiskLevelTask::class,
+                originTag = "TestRiskLevelCalculationFragmentCWAViewModel.calculateRiskLevel()"
+            )
+        )
     }
 
     fun resetRiskLevel() {
-        viewModelScope.launch {
+        Timber.d("Resetting risk level")
+        launch {
             withContext(Dispatchers.IO) {
                 try {
                     // Preference reset
@@ -100,10 +200,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
                     // Export File Reset
                     keyCacheRepository.clear()
 
+                    exposureResultStore.entities.value = ExposureResult(emptyList(), null)
+
                     LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw)
                     LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw)
                     LocalData.lastTimeDiagnosisKeysFromServerFetch(null)
-                    LocalData.googleApiToken(null)
                 } catch (e: Exception) {
                     e.report(ExceptionCategory.INTERNAL)
                 }
@@ -113,185 +214,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
         }
     }
 
-    data class RiskScoreState(
-        val riskScoreMsg: String = "",
-        val backendParameters: String = "",
-        val exposureSummary: String = "",
-        val formula: String = "",
-        val exposureInfo: String = ""
-    )
-
-    fun startENFObserver() {
-        viewModelScope.launch {
-            try {
-                var workState = riskScoreState.value!!
-
-                val googleToken = LocalData.googleApiToken() ?: UUID.randomUUID().toString()
-                val exposureSummary =
-                    InternalExposureNotificationClient.asyncGetExposureSummary(googleToken)
-
-                val expDetectConfig: RiskCalculationConfig =
-                    AppInjector.component.appConfigProvider.getAppConfig()
-
-                val riskLevelScore = riskLevels.calculateRiskScore(
-                    expDetectConfig.attenuationDuration,
-                    exposureSummary
-                )
-
-                val riskAsString = "Level: ${RiskLevelRepository.getLastCalculatedScore()}\n" +
-                    "Last successful Level: " +
-                    "${LocalData.lastSuccessfullyCalculatedRiskLevel()}\n" +
-                    "Calculated Score: ${riskLevelScore}\n" +
-                    "Last Time Server Fetch: ${LocalData.lastTimeDiagnosisKeysFromServerFetch()}\n" +
-                    "Tracing Duration: " +
-                    "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" +
-                    "Tracing Duration in last 14 days: " +
-                    "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days \n" +
-                    "Last time risk level calculation ${LocalData.lastTimeRiskLevelCalculation()}"
-
-                workState = workState.copy(riskScoreMsg = riskAsString)
-
-                val lowClass =
-                    expDetectConfig.riskScoreClasses.riskClassesList?.find { low -> low.label == "LOW" }
-                val highClass =
-                    expDetectConfig.riskScoreClasses.riskClassesList?.find { high -> high.label == "HIGH" }
-
-                val configAsString =
-                    "Attenuation Weight Low: ${expDetectConfig.attenuationDuration.weights?.low}\n" +
-                        "Attenuation Weight Mid: ${expDetectConfig.attenuationDuration.weights?.mid}\n" +
-                        "Attenuation Weight High: ${expDetectConfig.attenuationDuration.weights?.high}\n\n" +
-                        "Attenuation Offset: ${expDetectConfig.attenuationDuration.defaultBucketOffset}\n" +
-                        "Attenuation Normalization: " +
-                        "${expDetectConfig.attenuationDuration.riskScoreNormalizationDivisor}\n\n" +
-                        "Risk Score Low Class: ${lowClass?.min ?: 0} - ${lowClass?.max ?: 0}\n" +
-                        "Risk Score High Class: ${highClass?.min ?: 0} - ${highClass?.max ?: 0}"
-
-                workState = workState.copy(backendParameters = configAsString)
-
-                val summaryAsString =
-                    "Days Since Last Exposure: ${exposureSummary.daysSinceLastExposure}\n" +
-                        "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" +
-                        "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" +
-                        "Attenuation Durations: [${
-                            exposureSummary.attenuationDurationsInMinutes?.get(
-                                0
-                            )
-                        }," +
-                        "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," +
-                        "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" +
-                        "Summation Risk Score: ${exposureSummary.summationRiskScore}"
-
-                workState = workState.copy(exposureSummary = summaryAsString)
-
-                val maxRisk = exposureSummary.maximumRiskScore
-                val atWeights = expDetectConfig.attenuationDuration.weights
-                val attenuationDurationInMin =
-                    exposureSummary.attenuationDurationsInMinutes
-                val attenuationConfig = expDetectConfig.attenuationDuration
-                val formulaString =
-                    "($maxRisk / ${attenuationConfig.riskScoreNormalizationDivisor}) * " +
-                        "(${attenuationDurationInMin?.get(0)} * ${atWeights?.low} " +
-                        "+ ${attenuationDurationInMin?.get(1)} * ${atWeights?.mid} " +
-                        "+ ${attenuationDurationInMin?.get(2)} * ${atWeights?.high} " +
-                        "+ ${attenuationConfig.defaultBucketOffset})"
-
-                workState = workState.copy(formula = formulaString)
-
-                val token = LocalData.googleApiToken()
-                if (token != null) {
-                    val exposureInformation = asyncGetExposureInformation(token)
-
-                    var infoString = ""
-                    exposureInformation.forEach {
-                        infoString += "Attenuation duration in min.: " +
-                            "[${it.attenuationDurationsInMinutes?.get(0)}, " +
-                            "${it.attenuationDurationsInMinutes?.get(1)}," +
-                            "${it.attenuationDurationsInMinutes?.get(2)}]\n" +
-                            "Attenuation value: ${it.attenuationValue}\n" +
-                            "Duration in min.: ${it.durationMinutes}\n" +
-                            "Risk Score: ${it.totalRiskScore}\n" +
-                            "Transmission Risk Level: ${it.transmissionRiskLevel}\n" +
-                            "Date Millis Since Epoch: ${it.dateMillisSinceEpoch}\n\n"
-                    }
-
-                    workState = workState.copy(exposureInfo = infoString)
-                }
-
-                riskScoreState.postValue(workState)
-            } catch (e: Exception) {
-                e.report(ExceptionCategory.EXPOSURENOTIFICATION)
-            }
-        }
-    }
-
-    private suspend fun asyncGetExposureInformation(token: String): List<ExposureInformation> =
-        suspendCoroutine { cont ->
-            enfClient.internalClient.getExposureInformation(token)
-                .addOnSuccessListener {
-                    cont.resume(it)
-                }.addOnFailureListener {
-                    cont.resumeWithException(it)
-                }
-        }
-
-    data class DiagnosisKeyProvidedEvent(
-        val keyCount: Int,
-        val token: String
-    )
-
-    fun provideDiagnosisKey(transmissionNumber: Int, key: AppleLegacyKeyExchange.Key) {
-        val token = UUID.randomUUID().toString()
-        LocalData.googleApiToken(token)
-
-        val appleKeyList = mutableListOf<AppleLegacyKeyExchange.Key>()
-
-        AppleLegacyKeyExchange.Key.newBuilder()
-            .setKeyData(key.keyData)
-            .setRollingPeriod(144)
-            .setRollingStartNumber(key.rollingStartNumber)
-            .setTransmissionRiskLevel(transmissionNumber)
-            .build()
-            .also { appleKeyList.add(it) }
-
-        val appleFiles = listOf(
-            AppleLegacyKeyExchange.File.newBuilder()
-                .addAllKeys(appleKeyList)
-                .build()
-        )
-
-        val dir = File(File(context.getExternalFilesDir(null), "key-export"), token)
-        dir.mkdirs()
-
-        var googleFileList: List<File>
-        viewModelScope.launch {
-            googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir)
-
-            Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token")
-            try {
-                // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API
-                enfClient.provideDiagnosisKeys(
-                    googleFileList,
-                    AppInjector.component.appConfigProvider.getAppConfig().exposureDetectionConfiguration,
-                    token
-                )
-                apiKeysProvidedEvent.postValue(
-                    DiagnosisKeyProvidedEvent(
-                        keyCount = appleFiles.size,
-                        token = token
-                    )
-                )
-            } catch (e: Exception) {
-                e.report(ExceptionCategory.EXPOSURENOTIFICATION)
-            }
-        }
-    }
-
-    fun scanLocalQRCodeAndProvide() {
-        startLocalQRCodeScanEvent.postValue(Unit)
-    }
-
     fun clearKeyCache() {
-        viewModelScope.launch { keyCacheRepository.clear() }
+        Timber.d("Clearing key cache")
+        launch { keyCacheRepository.clear() }
     }
 
     @AssistedInject.Factory
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e758be589d545f339e9700bdabdea8d6934a3749
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragment.kt
@@ -0,0 +1,45 @@
+package de.rki.coronawarnapp.test.submission.ui
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.FragmentTestSubmissionBinding
+import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.ui.observe2
+import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
+import javax.inject.Inject
+
+@SuppressLint("SetTextI18n")
+class SubmissionTestFragment : Fragment(R.layout.fragment_test_submission), AutoInject {
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val vm: SubmissionTestFragmentViewModel by cwaViewModels { viewModelFactory }
+
+    private val binding: FragmentTestSubmissionBinding by viewBindingLazy()
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        vm.currentTestId.observe2(this) {
+            binding.registrationTokenCurrent.text = "Current: '$it'"
+        }
+
+        binding.apply {
+            deleteTokenAction.setOnClickListener { vm.deleteRegistrationToken() }
+            scrambleTokenAction.setOnClickListener { vm.scrambleRegistrationToken() }
+        }
+    }
+
+    companion object {
+        val TAG: String = SubmissionTestFragment::class.simpleName!!
+        val MENU_ITEM = TestMenuItem(
+            title = "Submission Test Options",
+            description = "Submission related test options..",
+            targetId = R.id.test_submission_fragment
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f123767b6fb40c678849cb0723ff722422cb95bb
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.test.submission.ui
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
+
+@Module
+abstract class SubmissionTestFragmentModule {
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(SubmissionTestFragmentViewModel::class)
+    abstract fun testKeyDownloadFragment(
+        factory: SubmissionTestFragmentViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c5d678dedff0cc54c1c97663f696f15303b27288
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt
@@ -0,0 +1,31 @@
+package de.rki.coronawarnapp.test.submission.ui
+
+import androidx.lifecycle.asLiveData
+import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.storage.LocalData
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import kotlinx.coroutines.flow.MutableStateFlow
+import java.util.UUID
+
+class SubmissionTestFragmentViewModel @AssistedInject constructor(
+    dispatcherProvider: DispatcherProvider
+) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
+
+    private val internalToken = MutableStateFlow(LocalData.registrationToken())
+    val currentTestId = internalToken.asLiveData()
+
+    fun scrambleRegistrationToken() {
+        LocalData.registrationToken(UUID.randomUUID().toString())
+        internalToken.value = LocalData.registrationToken()
+    }
+
+    fun deleteRegistrationToken() {
+        LocalData.registrationToken(null)
+        internalToken.value = LocalData.registrationToken()
+    }
+
+    @AssistedInject.Factory
+    interface Factory : SimpleCWAViewModelFactory<SubmissionTestFragmentViewModel>
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
index 41cca0d6071aab1aca83653e9283893028312c39..e406155bac41e9b12c5287b985790baad7617a8f 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
@@ -14,6 +14,8 @@ import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment
 import de.rki.coronawarnapp.test.menu.ui.TestMenuFragmentModule
 import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment
 import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragmentModule
+import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment
+import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragmentModule
 import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment
 import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragmentModule
 
@@ -40,4 +42,7 @@ abstract class MainActivityTestModule {
 
     @ContributesAndroidInjector(modules = [KeyDownloadTestFragmentModule::class])
     abstract fun keyDownload(): KeyDownloadTestFragment
+
+    @ContributesAndroidInjector(modules = [SubmissionTestFragmentModule::class])
+    abstract fun submissionTest(): SubmissionTestFragment
 }
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml
index 867b86569315d89fa9c080b0944a56deee7970cb..bd1eeb247b6ff2ef4c04476c71df6cdda8331671 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml
@@ -32,18 +32,6 @@
                     app:layout_constraintStart_toStartOf="parent"
                     app:layout_constraintTop_toTopOf="parent" />
 
-                <Switch
-                    android:id="@+id/background_notifications_toggle"
-                    style="@style/body1"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    android:text="@string/test_api_switch_background_notifications"
-                    android:theme="@style/switchBase"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/debug_container_title" />
-
                 <Switch
                     android:id="@+id/test_logfile_toggle"
                     style="@style/body1"
@@ -55,7 +43,7 @@
                     android:theme="@style/switchBase"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/background_notifications_toggle" />
+                    app:layout_constraintTop_toBottomOf="@+id/debug_container_title" />
 
                 <Button
                     android:id="@+id/test_logfile_share"
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml
index a4eb49227c15f443d2dde6b13eb76d3affca190c..30b10c80b5acd13b4e1e7963521ba2fe44500099 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml
@@ -61,36 +61,6 @@
                     android:layout_marginBottom="@dimen/spacing_tiny"
                     android:text="@string/test_api_exposure_summary_headline" />
 
-                <TextView
-                    android:id="@+id/label_exposure_summary_matchedKeyCount"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/test_api_body_matchedKeyCount" />
-
-                <TextView
-                    android:id="@+id/label_exposure_summary_daysSinceLastExposure"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/test_api_body_daysSinceLastExposure" />
-
-                <TextView
-                    android:id="@+id/label_exposure_summary_maximumRiskScore"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/test_api_body_maximumRiskScore" />
-
-                <TextView
-                    android:id="@+id/label_exposure_summary_summationRiskScore"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/test_api_body_summation_risk" />
-
-                <TextView
-                    android:id="@+id/label_exposure_summary_attenuation"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:text="@string/test_api_body_attenuation" />
-
                 <Button
                     android:id="@+id/button_api_scan_qr_code"
                     style="@style/buttonPrimary"
@@ -106,14 +76,6 @@
                     android:layout_height="wrap_content"
                     android:layout_marginTop="@dimen/spacing_tiny"
                     android:text="@string/test_api_button_enter_other_keys" />
-
-                <Button
-                    android:id="@+id/button_api_get_check_exposure"
-                    style="@style/buttonPrimary"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    android:text="@string/test_api_button_check_exposure" />
             </LinearLayout>
 
             <LinearLayout
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml
index 9e29d88d479a233ef358cbe19976dfd67bd4798f..7d94e7c3dca3a8e044acb4aac523109b7266aa7c 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml
@@ -11,8 +11,8 @@
         <import type="de.rki.coronawarnapp.util.formatter.FormatterSubmissionHelper" />
 
         <variable
-            name="submissionViewModel"
-            type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" />
+            name="showRiskStatusCard"
+            type="Boolean" />
 
         <variable
             name="settingsViewModel"
@@ -32,7 +32,6 @@
         <LinearLayout
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_margin="@dimen/spacing_normal"
             android:orientation="vertical">
 
             <TextView
@@ -48,7 +47,7 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/spacing_normal"
-                android:visibility="@{FormatterSubmissionHelper.formatShowRiskStatusCard(submissionViewModel.deviceUiState)}"
+                gone="@{showRiskStatusCard == null || !showRiskStatusCard}"
                 android:focusable="true"
                 android:backgroundTint="@{tracingCard.getRiskInfoContainerBackgroundTint(context)}"
                 android:backgroundTintMode="src_over">
@@ -60,34 +59,6 @@
 
             </FrameLayout>
 
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/spacing_normal"
-                android:orientation="horizontal">
-
-                <TextView
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:text="Transmission Risk Level for scan: " />
-
-                <EditText
-                    android:id="@+id/transmission_number"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:ems="10"
-                    android:inputType="number"
-                    android:text="5" />
-            </LinearLayout>
-
-            <Button
-                android:id="@+id/button_provide_key_via_qr"
-                style="@style/buttonPrimary"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginBottom="@dimen/spacing_normal"
-                android:text="Scan Local QR Code" />
-
             <Button
                 android:id="@+id/button_retrieve_diagnosis_keys"
                 style="@style/buttonPrimary"
@@ -121,30 +92,30 @@
                 android:text="Clear Diagnosis-Key cache" />
 
             <TextView
-                android:id="@+id/label_exposure_summary_title"
+                android:id="@+id/label_aggregated_risk_result_title"
                 style="@style/headline6"
                 android:accessibilityHeading="true"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/spacing_normal"
-                android:text="Exposure Summary" />
+                android:text="Aggregated Risk Result" />
 
             <TextView
-                android:id="@+id/label_exposure_summary"
+                android:id="@+id/label_aggregated_risk_result"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:text="-" />
 
             <TextView
-                android:id="@+id/label_risk_score_title"
+                android:id="@+id/label_risk_additional_info_title"
                 style="@style/headline6"
                 android:accessibilityHeading="true"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:text="Risk Score" />
+                android:text="Risk Calculation Additional Information" />
 
             <TextView
-                android:id="@+id/label_risk_score"
+                android:id="@+id/label_risk_additional_info"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:text="-" />
@@ -164,31 +135,24 @@
                 android:text="-" />
 
             <TextView
-                android:id="@+id/label_formula_title"
+                android:id="@+id/label_exposure_window_title"
                 style="@style/headline6"
                 android:accessibilityHeading="true"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:text="Used Formula" />
+                android:text="Exposure Windows" />
 
             <TextView
-                android:id="@+id/label_formula"
+                android:id="@+id/label_exposure_window_count"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:text="-" />
 
             <TextView
-                android:id="@+id/label_exposure_info_title"
-                style="@style/headline6"
-                android:accessibilityHeading="true"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:text="Exposure Information" />
-
-            <TextView
-                android:id="@+id/label_exposure_info"
-                android:layout_width="match_parent"
+                android:id="@+id/label_exposure_windows"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
+                android:paddingTop="5dp"
                 android:text="-" />
 
         </LinearLayout>
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml
new file mode 100644
index 0000000000000000000000000000000000000000..ac69680e903cf5df93ecff2f503a2a65dcee8fe5
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:ignore="HardcodedText">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="8dp"
+        android:orientation="vertical"
+        android:paddingBottom="32dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            style="@style/card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_tiny"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="8dp"
+            android:orientation="vertical">
+            <TextView
+                android:id="@+id/registration_token_title"
+                style="@style/body1"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Submission registration token "
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+            <TextView
+                android:id="@+id/registration_token_current"
+                style="@style/body2"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/registration_token_title"
+                tools:text="Current test ID: 1234567890" />
+            <Button
+                android:id="@+id/delete_token_action"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Delete token"
+                app:layout_constraintEnd_toStartOf="@+id/scramble_token_action"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/registration_token_current" />
+            <Button
+                android:id="@+id/scramble_token_action"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Random token"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/delete_token_action"
+                app:layout_constraintTop_toBottomOf="@+id/registration_token_current" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </LinearLayout>
+</androidx.core.widget.NestedScrollView>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
index 226a618a77e11982b3a1ead5d9fbf879429c7188..14d89e5a3de75e9d667feafc4a67a991d2e0759d 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
@@ -31,6 +31,9 @@
         <action
             android:id="@+id/action_test_menu_fragment_to_keyDownloadTestFragment"
             app:destination="@id/test_keydownload_fragment" />
+        <action
+            android:id="@+id/action_test_menu_fragment_to_submissionTestFragment"
+            app:destination="@id/test_submission_fragment" />
     </fragment>
 
     <fragment
@@ -86,5 +89,10 @@
         android:name="de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment"
         android:label="KeyDownloadTestFragment"
         tools:layout="@layout/fragment_test_keydownload" />
+    <fragment
+        android:id="@+id/test_submission_fragment"
+        android:name="de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment"
+        android:label="SubmissionTestFragment"
+        tools:layout="@layout/fragment_test_submission" />
 
 </navigation>
diff --git a/Corona-Warn-App/src/main/assets/privacy_de.html b/Corona-Warn-App/src/main/assets/privacy_de.html
index dd168314234a071eee96b203b3a0d4c60167b306..eb3a024ed1a780760397c4a5df80ed208e0e8b42 100644
--- a/Corona-Warn-App/src/main/assets/privacy_de.html
+++ b/Corona-Warn-App/src/main/assets/privacy_de.html
@@ -80,7 +80,7 @@
 <p>
     Die Nutzung der App ist freiwillig. Es ist allein Ihre Entscheidung, ob Sie
     die App installieren, welche App-Funktionen Sie nutzen und ob Sie Daten mit
-    anderen teilen. Alle App-Funktionen, die eine Datenweitergabe erfordern,
+    anderen teilen. Alle App-Funktionen, die eine Datenweitergabe Ihrer Begegnungs- und Gesundheitsdaten erfordern,
     holen vorher Ihre ausdrückliche Einwilligung ein. Falls Sie eine
     Einwilligung nicht erteilen oder nachträglich widerrufen, entstehen Ihnen
     keine Nachteile.
@@ -89,11 +89,16 @@
     3. Auf welcher Rechtsgrundlage werden Ihre Daten verarbeitet?
 </h1>
 <p>
-    Ihre Daten werden grundsätzlich nur auf Grundlage einer von Ihnen erteilten
+    Ihre Daten werden grundsätzlich auf Grundlage einer von Ihnen erteilten
     ausdrücklichen Einwilligung verarbeitet. Die Rechtsgrundlage ist Art. 6
     Abs. 1 S. 1 lit. a DSGVO sowie im Falle von Gesundheitsdaten Art. 9 Abs. 2
     lit. a DSGVO. Sie können eine erteilte Einwilligung jederzeit widerrufen.
-    Weitere Informationen zu Ihrem Widerrufsrecht finden Sie unter Punkt 12.
+    Weitere Informationen zu Ihrem Widerrufsrecht finden Sie unter Punkt 12. 
+    Die Verarbeitung von Zugriffsdaten für den Abruf der täglichen Statistiken 
+    (siehe hierzu Punkt 6 d.) erfolgt im Rahmen der Information der Öffentlichkeit
+    durch das RKI gem. § 4 Abs. 4 BGA-NachfG auf Basis von Art. 6 Abs. 1 S. 1 lit e. 
+    DSGVO i.V.m § 3 BDSG.
+
 </p>
 <h1>
     4. An wen richtet sich die App?
@@ -270,7 +275,7 @@
     Gesundheitshinweise zu geben.
 </p>
 <p>
-    Hierzu ruft die App im Hintergrundbetrieb vom Serversystem täglich eine
+    Hierzu ruft die App im Hintergrundbetrieb vom Serversystem mehrmals täglich eine
     aktuelle Liste mit den Zufalls-IDs und eventuellen Angaben zum
     Symptombeginn von Nutzern ab, die Corona-positiv getestet wurden und
     freiwillig über die offizielle Corona-App eines am länderübergreifenden
@@ -454,14 +459,11 @@
     d. Informatorische Nutzung der App
 </h2>
 <p>
-    Soweit Sie die App nur informatorisch nutzen, also keine der oben genannten
-    Funktionen verwenden, findet die Verarbeitung ausschließlich lokal auf
-    Ihrem Smartphone statt und es werden keine personenbezogenen Daten durch
-    das RKI verarbeitet. In der App verlinkte Webseiten, z. B.: www.bundesregierung.de, werden im
-    Standard-Browser
-    (Android-Smartphones) oder in der App (iPhones) geöffnet und angezeigt.
-    Welche Daten dabei verarbeitet werden, wird von den jeweiligen Anbietern
-    der aufgerufenen Webseite festgelegt.
+    Die täglichen Statistiken, die in der App erscheinen, erhält die App 
+    automatisch über das Serversystem. Dabei fallen Zugriffsdaten an.
+    In der App verlinkte Webseiten, z. B.: www.bundesregierung.de, werden im
+    Standard-Browser (Android-Smartphones) oder in der App (iPhones) geöffnet und angezeigt.
+    Welche Daten dabei verarbeitet werden, wird von den jeweiligen Anbietern der aufgerufenen Webseite festgelegt.
 </p>
 <h1>
     7. Wie funktioniert das länderübergreifende Warnsystem?
@@ -790,5 +792,5 @@
     datenschutz@rki.de.
 </p>
 <p>
-    Stand: 15.10.2020
+    Stand: 15.11.2020
 </p>
diff --git a/Corona-Warn-App/src/main/assets/privacy_en.html b/Corona-Warn-App/src/main/assets/privacy_en.html
index ffce0a25d17049236cf4e60689b8a29d14b1e00d..411f8c5accb2a0ed0ad356308fcdea5d8ac0d5ec 100644
--- a/Corona-Warn-App/src/main/assets/privacy_en.html
+++ b/Corona-Warn-App/src/main/assets/privacy_en.html
@@ -1,9 +1,7 @@
 <p>
     Privacy notice
 </p>
-<p>
-    Last amended: 17 October 2020.
-</p>
+
 <p>
     This privacy notice explains how your data is processed and what data
     protection rights you have when using the German Federal Government’s
@@ -95,7 +93,11 @@
     legal basis is Art. 6(1) Sentence 1(a) GDPR and, in the case of health
     data, Art. 9(2)(a) GDPR. After granting your consent, you can withdraw it
     at any time. Please refer to Section 12 for further information about your
-    right of withdrawal.
+    right of withdrawal. On the basis of Art. 6(1) Sentence 1(e) GDPR in conjunction 
+    with Sect. 3 of the German Federal Data Protection Act (BDSG), the processing of 
+    access data for the retrieval of daily statistics (see Section 6 d.) is performed 
+    as part of the RKI’s duty to inform the public pursuant to Sect. 4(4) of the Act 
+    on Successor Agencies to the Federal Health Agency (BGA-NachfG).
 </p>
 <h1>
     4. Who is the app aimed at?
@@ -161,7 +163,7 @@
 </h2>
 <p>
     As soon as you enable your iPhone’s or your Android smartphone’s COVID-19
-    Exposure Notification System (which is called “Exposure Notification” or
+    Exposure Notification System (which is called “Exposure Notifications” or
     “COVID-19 Exposure Notifications” respectively), your smartphone transmits
     so-called exposure data via Bluetooth, which other smartphones in your
     vicinity can record. Your smartphone, in turn, also receives the exposure
@@ -268,16 +270,14 @@
     recommendations for what to do next.
 </p>
 <p>
-    For this purpose, the app runs in the background and retrieves an
-    up-to-date list every day of random IDs, and possible information about the
-    onset of symptoms, from the server system. This list contains the random
-    IDs and possible symptom information of users who have tested positive for
-    coronavirus and voluntarily used the warning feature in their app, which is
-    the official coronavirus app in any country participating in the
-    transnational warning system (see Section 7) (hereinafter referred to as a <strong>positive
-    list</strong>). The random IDs in the positive list also
-    contain a transmission risk value and an indication of the type of
-    diagnosis (see Section 6 c.).
+    For this purpose, the app retrieves an up-to-date list from the server system 
+    several times a day. This list contains the random IDs, along with any voluntary 
+    symptom information, of users who have tested positive for coronavirus and used 
+    the warning feature in their app, which is the official coronavirus app in any 
+    country participating in the transnational warning system (see Section 7) 
+    (hereinafter referred to as a <strong>positive list</strong>). The random IDs in 
+    the positive list also contain a transmission risk value and an indication of the type 
+    of diagnosis (see Section 6 c.).
 </p>
 <p>
     The app passes the random IDs to the COVID-19 Exposure Notification System,
@@ -443,13 +443,11 @@
     d. Using the app for information purposes only
 </h2>
 <p>
-    As long as you use the app for information purposes only, i.e. do not use
-    any of the features mentioned above, then processing only takes place
-    locally on your smartphone and the RKI will not process any personal data.
-    Websites linked in the app, such as www.bundesregierung.de, are
-    opened and displayed in your smartphone’s standard browser (Android
-    smartphones) or within the app (iPhones). The data processed here is
-    determined by the respective providers of the websites accessed.
+    The app automatically receives the daily statistics that appear in the app 
+    via the server system. This generates access data. Websites linked in the app, 
+    such as www.bundesregierung.de, are opened and displayed in your smartphone’s 
+    standard browser (Android smartphones) or within the app (iPhones). Which data 
+    is processed in this context depends on the respective providers of the websites accessed.
 </p>
 <h1>7. How does the transnational warning system work?
 
@@ -767,5 +765,5 @@
     13353 Berlin, or by emailing datenschutz@rki.de.
 </p>
 <p>
-    ***
+    Last amended: 15 November 2020 
 </p>
diff --git a/Corona-Warn-App/src/main/assets/privacy_tr.html b/Corona-Warn-App/src/main/assets/privacy_tr.html
index 55243e394d5031feef9b974a735dce3ed2b231d2..cebbc84a12a3faf296ba8a5961597b2705c86aa7 100644
--- a/Corona-Warn-App/src/main/assets/privacy_tr.html
+++ b/Corona-Warn-App/src/main/assets/privacy_tr.html
@@ -79,22 +79,25 @@
 <p>
     Uygulamanın kullanımı isteğe bağlıdır; Uygulamayı yüklemeniz, Uygulamanın
     hangi işlevlerini kullanmanız ve verileri diğer kişilerle paylaşmanız
-    noktasında yalnızca siz karar verirsiniz. Uygulamanın veri aktarımını
-    gerektiren tüm işlevleri, öncesinde sizin açık bir şekilde onay vermenizi
-    ister. Onay vermezseniz veya sonradan bu onayı geri alırsanız, bu durum
-    sizin için bir sakınca doğurmaz.
+    noktasında yalnızca siz karar verirsiniz. Maruz kalma veya sağlık verilerinizin 
+    aktarılmasını gerektiren Uygulamanın tüm işlevleri, sizden önceden açıkça rızanızı 
+    vermenizi gerektirir. Rızanızı vermezseniz veya sonradan bu rızayı geri alırsanız, 
+    bu durum sizin için bir sakınca doğurmaz.
 </p>
 <h1>
     3. Verileriniz işlenmesinde hangi yasal dayanaklar söz konusudur?
     
 </h1>
 <p>
-    Verileriniz esas itibariyle yalnızca açık bir şekilde verdiğiniz onay
+    Verileriniz esas itibariyle açık bir şekilde verdiğiniz onay
     temelinde iÅŸlenir. Bu baÄŸlamdaki yasal dayanak, GVKT (Genel Veri Koruma
     Tüzüğü) madde 6, fıkra 1, cümle 1, bent a ve sağlık verileri durumundaki
     yasal dayanak ise GVKT madde 9, fıkra 2, bent a’dır. Verdiğiniz onayı,
     istediğiniz zaman geri alabilirsiniz. Onayınız geri alma hakkı ile ilgili
-    ayrıntılı bilgileri madde 12’de bulabilirsiniz.
+    ayrıntılı bilgileri madde 12’de bulabilirsiniz. Günlük istatistiklerin alınması 
+    için erişim verilerinin işlenmesi (bkz. Madde 6 d.), GVKT madde 6, fıkra 1, cümle 1, bent 
+    e ile bağlantılı olarak BGA-NachfG (Federal Sağlık Kurumu Halef Kuruluşları 
+    hakkında Kanun) madde 4, fıkra 4 uyarınca RKI tarafından toplumun bilgilendirilmesi kapsamında gerçekleşir.
 </p>
 <h1>
     4. Uygulama kimleri hedefler?
@@ -271,14 +274,13 @@
     bilgileri temin etmektir.
 </p>
 <p>
-    Bu amaç için Uygulama, arka planda çalışarak sunucu sisteminden, Korona
-    testi pozitif çıkan ve sınır ötesi uyarı sistemine katılan ülkelerin resmi
-    Korona uygulamaları aracılığıyla gönüllü olarak bir uyarı tetikleyen
-    kullanıcılardan rastgele kimlik numaraları ve varsa semptomların
-    başlangıcına ilişkin bilgiyi içeren günlük bir liste çağırır (bundan böyle: <strong>pozitif
-    liste</strong>). Pozitif listedeki rastgele kimlik
-    numaraları, ek olarak ayrıca bir taşıma riski değeri ve tanı tipi hakkında
-    bilgi de içerir (bkz. Madde 6 c.).
+    Bu amaç doğrultusunda Uygulama, arka planda çalışarak sunucu sisteminden, 
+    Korona testi pozitif çıkan ve sınır ötesi uyarı sistemine katılan ülkelerin 
+    resmi Korona uygulamaları aracılığıyla bir uyarı tetikleyen kullanıcılardan 
+    rastgele kimlik numaraları ve varsa semptomların başlangıcına ilişkin bilgiyi 
+    içeren listeleri günde birçok kez çağırır (bundan böyle: <strong>pozitif liste</strong>).
+    Pozitif listedeki rastgele kimlik numaraları, ek olarak ayrıca bir taşıma riski 
+    değeri ve tanı tipi hakkında bilgi de içerir (bkz. Madde 6 c.).
 </p>
 <p>
     Uygulama bu rastgele kimlik numaralarını, COVID-19 bildirim sistemine
@@ -446,13 +448,11 @@
     d. Uygulamanın bilgilenme amaçlı kullanımı
 </h2>
 <p>
-    Uygulamayı yalnızca bilgi edinme amaçlı kullanıyorsanız, yani yukarıda
-    belirtilen işlevlerden hiçbirini kullanmıyorsanız, veri işleme yalnızca
-    kendi akıllı telefonunuzda gerçekleşir ve RKI tarafından hiçbir kişisel
-    veri işlenmez. Uygulamada, örneğin www.bundesregierung.de gibi
-    bağlantılı web siteleri açılır ve standart tarayıcıda (Android akıllı
-    telefonlar) veya Uygulamada (iPhone’lar) görüntülenir. Hangi verilerin
-    işleneceği, erişilen web sitesinin ilgili sağlayıcısı tarafından
+    Uygulama otomatik olarak sunucu sistemi üzerinden günlük istatistikleri alır 
+    ve bunlar Uygulamada görüntülenir. Bu sırada erişim verileri oluşur. Uygulamada, 
+    örneğin www.bundesregierung.de gibi bağlantılı web siteleri açılır ve standart 
+    tarayıcıda (Android akıllı telefonlar) veya Uygulamada (iPhone’lar) görüntülenir. 
+    Hangi verilerin işleneceği, erişilen web sitesinin ilgili sağlayıcısı tarafından
     belirlenmektedir.
 </p>
 <h1>
@@ -772,5 +772,4 @@
     veya e-posta yoluyla: datenschutz@rki.de.
 </p>
 <p>
-    Yayım tarihi: 17.10.2020
-</p>
+    Baskı: 15.11.2020
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 ebe7c473fc3d6037becb4bcab9981d8fe5fccf6f..941a2b4e1890e8c0f69d4804f7c9f454313a7242 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
@@ -11,6 +11,7 @@ import androidx.work.WorkManager
 import dagger.android.AndroidInjector
 import dagger.android.DispatchingAndroidInjector
 import dagger.android.HasAndroidInjector
+import de.rki.coronawarnapp.appconfig.ConfigChangeDetector
 import de.rki.coronawarnapp.bugreporting.loghistory.LogHistoryTree
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
 import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver
@@ -23,7 +24,6 @@ import de.rki.coronawarnapp.util.ForegroundState
 import de.rki.coronawarnapp.util.WatchdogService
 import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.di.ApplicationComponent
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
@@ -44,6 +44,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
     @Inject lateinit var taskController: TaskController
     @Inject lateinit var foregroundState: ForegroundState
     @Inject lateinit var workManager: WorkManager
+    @Inject lateinit var configChangeDetector: ConfigChangeDetector
     @Inject lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler
     @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree
 
@@ -66,10 +67,6 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
 
         registerActivityLifecycleCallbacks(activityLifecycleCallback)
 
-        // notification to test the WakeUpService from Google when the app was force stopped
-        BackgroundWorkHelper.sendDebugNotification(
-            "Application onCreate", "App was woken up"
-        )
         watchdogService.launch()
 
         foregroundState.isInForeground
@@ -79,6 +76,8 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
         if (LocalData.onboardingCompletedTimestamp() != null) {
             deadmanNotificationScheduler.schedulePeriodic()
         }
+
+        configChangeDetector.launch()
     }
 
     private val activityLifecycleCallback = object : ActivityLifecycleCallbacks {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
index 19ae812f371b271dcd77c11f8037b61d8d393f09..26283253c14f7814cd03a08ec043b7b62cbb9ea9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt
@@ -3,12 +3,12 @@ package de.rki.coronawarnapp.appconfig
 import android.content.Context
 import dagger.Module
 import dagger.Provides
-import de.rki.coronawarnapp.appconfig.download.AppConfigApiV1
-import de.rki.coronawarnapp.appconfig.download.AppConfigHttpCache
+import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2
 import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper
+import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper
 import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper
-import de.rki.coronawarnapp.appconfig.mapping.RiskCalculationConfigMapper
+import de.rki.coronawarnapp.appconfig.sources.remote.AppConfigHttpCache
 import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
 import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
 import de.rki.coronawarnapp.util.di.AppContext
@@ -42,7 +42,7 @@ class AppConfigModule {
         @DownloadCDNServerUrl url: String,
         gsonConverterFactory: GsonConverterFactory,
         @AppConfigHttpCache cache: Cache
-    ): AppConfigApiV1 {
+    ): AppConfigApiV2 {
 
         val cachingClient = client.newBuilder().apply {
             cache(cache)
@@ -57,21 +57,23 @@ class AppConfigModule {
             .baseUrl(url)
             .addConverterFactory(gsonConverterFactory)
             .build()
-            .create(AppConfigApiV1::class.java)
+            .create(AppConfigApiV2::class.java)
     }
 
     @Provides
-    fun cwaMapper(mapper: CWAConfigMapper): CWAConfig.Mapper = mapper
+    fun cwaMapper(mapper: CWAConfigMapper):
+        CWAConfig.Mapper = mapper
 
     @Provides
     fun downloadMapper(mapper: KeyDownloadParametersMapper): KeyDownloadConfig.Mapper = mapper
 
     @Provides
-    fun exposurMapper(mapper: ExposureDetectionConfigMapper): ExposureDetectionConfig.Mapper =
-        mapper
+    fun exposureMapper(mapper: ExposureDetectionConfigMapper):
+        ExposureDetectionConfig.Mapper = mapper
 
     @Provides
-    fun riskMapper(mapper: RiskCalculationConfigMapper): RiskCalculationConfig.Mapper = mapper
+    fun windowRiskMapper(mapper: ExposureWindowRiskCalculationConfigMapper):
+        ExposureWindowRiskCalculationConfig.Mapper = mapper
 
     companion object {
         private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt
index 71906e83169d8e0ef4a68e34aa1208ec958defc5..ab47bc6b2443d8173084797a0fcf293d9d17ced4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.appconfig
 
+import de.rki.coronawarnapp.appconfig.internal.AppConfigSource
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.flow.HotDataFlow
@@ -13,7 +14,7 @@ import javax.inject.Singleton
 
 @Singleton
 class AppConfigProvider @Inject constructor(
-    private val source: AppConfigSource,
+    private val appConfigSource: AppConfigSource,
     private val dispatcherProvider: DispatcherProvider,
     @AppScope private val scope: CoroutineScope
 ) {
@@ -22,9 +23,9 @@ class AppConfigProvider @Inject constructor(
         loggingTag = "AppConfigProvider",
         scope = scope,
         coroutineContext = dispatcherProvider.IO,
-        sharingBehavior = SharingStarted.WhileSubscribed(replayExpirationMillis = 0)
+        sharingBehavior = SharingStarted.Lazily
     ) {
-        source.retrieveConfig()
+        appConfigSource.getConfigData()
     }
 
     val currentConfig: Flow<ConfigData> = configHolder.data
@@ -34,7 +35,7 @@ class AppConfigProvider @Inject constructor(
         // we'd still like to have that new config in any case.
         val deferred = scope.async(context = dispatcherProvider.IO) {
             configHolder.updateBlocking {
-                source.retrieveConfig()
+                appConfigSource.getConfigData()
             }
         }
         return deferred.await()
@@ -42,7 +43,7 @@ class AppConfigProvider @Inject constructor(
 
     suspend fun clear() {
         Timber.tag(TAG).v("clear()")
-        source.clear()
+        appConfigSource.clear()
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigSource.kt
deleted file mode 100644
index fa981d6dc18048fd2a70a74e5bc42672db1f5bff..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigSource.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-package de.rki.coronawarnapp.appconfig
-
-import de.rki.coronawarnapp.appconfig.download.AppConfigServer
-import de.rki.coronawarnapp.appconfig.download.AppConfigStorage
-import de.rki.coronawarnapp.appconfig.download.DefaultAppConfigSource
-import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
-import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
-import kotlinx.coroutines.withContext
-import org.joda.time.Duration
-import org.joda.time.Instant
-import timber.log.Timber
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class AppConfigSource @Inject constructor(
-    private val server: AppConfigServer,
-    private val storage: AppConfigStorage,
-    private val parser: ConfigParser,
-    private val defaultAppConfig: DefaultAppConfigSource,
-    private val dispatcherProvider: DispatcherProvider
-) {
-
-    suspend fun retrieveConfig(): ConfigData = withContext(dispatcherProvider.IO) {
-        Timber.v("retrieveConfig()")
-        val (serverBytes, serverError) = try {
-            server.downloadAppConfig() to null
-        } catch (e: Exception) {
-            Timber.tag(TAG).w(e, "Failed to download AppConfig from server .")
-            null to e
-        }
-
-        var parsedConfig: ConfigData? = serverBytes?.let { configDownload ->
-            try {
-                parser.parse(configDownload.rawData).let {
-                    Timber.tag(TAG).d("Got a valid AppConfig from server, saving.")
-                    storage.setStoredConfig(configDownload)
-                    DefaultConfigData(
-                        mappedConfig = it,
-                        serverTime = configDownload.serverTime,
-                        localOffset = configDownload.localOffset,
-                        identifier = configDownload.etag,
-                        configType = ConfigData.Type.FROM_SERVER
-                    )
-                }
-            } catch (e: Exception) {
-                Timber.tag(TAG).e(e, "Failed to parse AppConfig from server, trying fallback.")
-                null
-            }
-        }
-
-        if (parsedConfig == null) {
-            parsedConfig = storage.getStoredConfig()?.let { storedDownloadConfig ->
-                try {
-                    storedDownloadConfig.let {
-                        DefaultConfigData(
-                            mappedConfig = parser.parse(it.rawData),
-                            serverTime = it.serverTime,
-                            localOffset = it.localOffset,
-                            identifier = it.etag,
-                            configType = ConfigData.Type.LAST_RETRIEVED
-                        )
-                    }
-                } catch (e: Exception) {
-                    Timber.tag(TAG).e(e, "Fallback config exists but could not be parsed!")
-                    throw e
-                }
-            }
-        }
-
-        if (parsedConfig == null) {
-            Timber.tag(TAG).w("Current or fallback config was unavailable, using default.")
-            parsedConfig = DefaultConfigData(
-                mappedConfig = parser.parse(defaultAppConfig.getRawDefaultConfig()),
-                serverTime = Instant.EPOCH,
-                localOffset = Duration.standardHours(12),
-                identifier = "fallback.local",
-                configType = ConfigData.Type.LOCAL_DEFAULT
-            )
-        }
-
-        return@withContext parsedConfig
-    }
-
-    suspend fun clear() {
-        storage.setStoredConfig(null)
-
-        server.clearCache()
-    }
-
-    companion object {
-        private const val TAG = "AppConfigRetriever"
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt
index 0a11bf822114ca77b02a3bdd283c0a83b6dad69a..89532fb14ebdc44427de454e33ce6f4de4994de2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt
@@ -1,16 +1,17 @@
 package de.rki.coronawarnapp.appconfig
 
 import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
-import de.rki.coronawarnapp.server.protocols.internal.AppFeaturesOuterClass
-import de.rki.coronawarnapp.server.protocols.internal.AppVersionConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppFeaturesOuterClass
 
 interface CWAConfig {
 
-    val appVersion: AppVersionConfig.ApplicationVersionConfiguration
+    val latestVersionCode: Long
+
+    val minVersionCode: Long
 
     val supportedCountries: List<String>
 
-    val appFeatureus: AppFeaturesOuterClass.AppFeatures
+    val appFeatures: AppFeaturesOuterClass.AppFeatures
 
     interface Mapper : ConfigMapper<CWAConfig>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt
new file mode 100644
index 0000000000000000000000000000000000000000..588451c9dec8b5393a6c55e1aaf9fbb00bc2317b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt
@@ -0,0 +1,63 @@
+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.RiskLevelTask
+import de.rki.coronawarnapp.storage.RiskLevelRepository
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.distinctUntilChangedBy
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import timber.log.Timber
+import javax.inject.Inject
+
+class ConfigChangeDetector @Inject constructor(
+    private val appConfigProvider: AppConfigProvider,
+    private val taskController: TaskController,
+    @AppScope private val appScope: CoroutineScope,
+    private val riskLevelData: RiskLevelData
+) {
+
+    fun launch() {
+        Timber.v("Monitoring config changes.")
+        appConfigProvider.currentConfig
+            .distinctUntilChangedBy { it.identifier }
+            .onEach {
+                Timber.v("Running app config change checks.")
+                check(it.identifier)
+            }
+            .catch { Timber.e(it, "App config change checks failed.") }
+            .launchIn(appScope)
+    }
+
+    @VisibleForTesting
+    internal fun check(newIdentifier: String) {
+        if (riskLevelData.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
+        if (newIdentifier != oldConfigId) {
+            Timber.i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.")
+            RiskLevelRepositoryDeferrer.resetRiskLevel()
+            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/appconfig/ConfigData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt
index e903926da969d2243dc02bc4332f41d6aff51f02..ffe3d8c379724cb321d1eea2dc42f757553c97f9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt
@@ -44,4 +44,10 @@ interface ConfigData : ConfigMapping {
          */
         LOCAL_DEFAULT
     }
+
+    /**
+     * Has the config validity expired?
+     * Is this configs update date, past the maximum cache age?
+     */
+    fun isValid(nowUTC: Instant): Boolean
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt
index be47c2f17b4d1834f1de0d0370eb2e4918138de5..a27962042d4328cf8164f1d2b11a6969ecd4c6db 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt
@@ -1,8 +1,7 @@
 package de.rki.coronawarnapp.appconfig
 
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
-import de.rki.coronawarnapp.server.protocols.internal.ExposureDetectionParameters
+import de.rki.coronawarnapp.server.protocols.internal.v2.ExposureDetectionParameters
 import org.joda.time.Duration
 
 interface ExposureDetectionConfig {
@@ -11,8 +10,7 @@ interface ExposureDetectionConfig {
     val minTimeBetweenDetections: Duration
     val overallDetectionTimeout: Duration
 
-    val exposureDetectionConfiguration: ExposureConfiguration
-    val exposureDetectionParameters: ExposureDetectionParameters.ExposureDetectionParametersAndroid
+    val exposureDetectionParameters: ExposureDetectionParameters.ExposureDetectionParametersAndroid?
 
     interface Mapper : ConfigMapper<ExposureDetectionConfig>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..80eadd589d6bf68fc19648513195ab5e36841036
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.appconfig
+
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+
+interface ExposureWindowRiskCalculationConfig {
+    val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>
+    val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>
+    val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding
+    val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>
+    val transmissionRiskLevelMultiplier: Double
+    val normalizedTimePerExposureWindowToRiskLevelMapping:
+        List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
+    val normalizedTimePerDayToRiskLevelMappingList:
+        List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
+
+    interface Mapper {
+        fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/RiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/RiskCalculationConfig.kt
deleted file mode 100644
index 2c19c0be637e843d0137d3db4983f1ccce8de97c..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/RiskCalculationConfig.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.rki.coronawarnapp.appconfig
-
-import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
-import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
-import de.rki.coronawarnapp.server.protocols.internal.RiskScoreClassificationOuterClass
-
-interface RiskCalculationConfig {
-
-    val minRiskScore: Int
-
-    val attenuationDuration: AttenuationDurationOuterClass.AttenuationDuration
-
-    val riskScoreClasses: RiskScoreClassificationOuterClass.RiskScoreClassification
-
-    interface Mapper : ConfigMapper<RiskCalculationConfig>
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV1.kt
deleted file mode 100644
index 0c3f61077cc884adf45aa8a02a78e2ed5fe7154d..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV1.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.rki.coronawarnapp.appconfig.download
-
-import okhttp3.ResponseBody
-import retrofit2.Response
-import retrofit2.http.GET
-import retrofit2.http.Path
-
-interface AppConfigApiV1 {
-
-    @GET("/version/v1/configuration/country/{country}/app_config")
-    suspend fun getApplicationConfiguration(
-        @Path("country") country: String
-    ): Response<ResponseBody>
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5d1c0f2c3d0e9654b9af57aa6121bb3cd9d53950
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt
@@ -0,0 +1,11 @@
+package de.rki.coronawarnapp.appconfig.download
+
+import okhttp3.ResponseBody
+import retrofit2.Response
+import retrofit2.http.GET
+
+interface AppConfigApiV2 {
+
+    @GET("/version/v1/app_config_android")
+    suspend fun getApplicationConfiguration(): Response<ResponseBody>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSource.kt
deleted file mode 100644
index ddb4e4528de00d44c08b7ba6e0910371c2ab9916..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSource.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.rki.coronawarnapp.appconfig.download
-
-import android.content.Context
-import dagger.Reusable
-import de.rki.coronawarnapp.util.di.AppContext
-import javax.inject.Inject
-
-@Reusable
-class DefaultAppConfigSource @Inject constructor(
-    @AppContext private val context: Context
-) {
-
-    fun getRawDefaultConfig(): ByteArray {
-        return context.assets.open("default_app_config.bin").readBytes()
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1e5cc4959e860fec7548e6e1a1954451487261f8
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSource.kt
@@ -0,0 +1,58 @@
+package de.rki.coronawarnapp.appconfig.internal
+
+import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.sources.fallback.DefaultAppConfigSource
+import de.rki.coronawarnapp.appconfig.sources.local.LocalAppConfigSource
+import de.rki.coronawarnapp.appconfig.sources.remote.RemoteAppConfigSource
+import de.rki.coronawarnapp.util.TimeStamper
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class AppConfigSource @Inject constructor(
+    private val remoteAppConfigSource: RemoteAppConfigSource,
+    private val localAppConfigSource: LocalAppConfigSource,
+    private val defaultAppConfigSource: DefaultAppConfigSource,
+    private val timeStamper: TimeStamper
+) {
+
+    suspend fun getConfigData(): ConfigData {
+        Timber.tag(TAG).d("getConfigData()")
+
+        val localConfig = localAppConfigSource.getConfigData()
+        if (localConfig != null && localConfig.isValid(timeStamper.nowUTC)) {
+            Timber.tag(TAG).d("Returning local config, still valid.")
+            return localConfig
+        } else {
+            Timber.tag(TAG).d("Local app config was unavailable(${localConfig == null} or invalid.")
+        }
+
+        val remoteConfig = remoteAppConfigSource.getConfigData()
+
+        return when {
+            remoteConfig != null -> {
+                Timber.tag(TAG).d("Returning remote config.")
+                remoteConfig
+            }
+            localConfig != null -> {
+                Timber.tag(TAG).d("Remote config was unavailable, returning local config, even if expired.")
+                localConfig
+            }
+            else -> {
+                Timber.tag(TAG).w("Remote & Local config available! Returning DEFAULT!")
+                defaultAppConfigSource.getConfigData()
+            }
+        }
+    }
+
+    suspend fun clear() {
+        Timber.tag(TAG).d("clear()")
+        remoteAppConfigSource.clear()
+        localAppConfigSource.clear()
+    }
+
+    companion object {
+        private const val TAG = "AppConfigSource"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationCorruptException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationCorruptException.kt
similarity index 86%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationCorruptException.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationCorruptException.kt
index bd6940034f8af15de4d110ab65ece6eb9a34cd94..65876d4f255a200d6f44235fcdd3a66ff1457144 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationCorruptException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationCorruptException.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.internal
 
 import de.rki.coronawarnapp.exception.reporting.ErrorCodes
 import de.rki.coronawarnapp.exception.reporting.ReportedException
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationInvalidException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationInvalidException.kt
similarity index 88%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationInvalidException.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationInvalidException.kt
index 63cb1069e92febfd84f5a894a8d9dbf3c26ea618..15ec3692bca6ba188403a32a2e17de7a8dac0fea 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationInvalidException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationInvalidException.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.internal
 
 import de.rki.coronawarnapp.exception.reporting.ErrorCodes
 import de.rki.coronawarnapp.exception.reporting.ReportedException
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/DefaultConfigData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt
similarity index 57%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/DefaultConfigData.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt
index d2ab41944ce8533f43ab7a82be7421899f823790..f7bde81b1a52cfd96f878fa0dc43d310df3f7afd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/DefaultConfigData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt
@@ -1,15 +1,22 @@
-package de.rki.coronawarnapp.appconfig
+package de.rki.coronawarnapp.appconfig.internal
 
+import de.rki.coronawarnapp.appconfig.ConfigData
 import de.rki.coronawarnapp.appconfig.mapping.ConfigMapping
 import org.joda.time.Duration
 import org.joda.time.Instant
 
-data class DefaultConfigData(
+data class ConfigDataContainer(
     val serverTime: Instant,
+    val cacheValidity: Duration,
     val mappedConfig: ConfigMapping,
     override val identifier: String,
     override val localOffset: Duration,
     override val configType: ConfigData.Type
 ) : ConfigData, ConfigMapping by mappedConfig {
     override val updatedAt: Instant = serverTime.plus(localOffset)
+
+    override fun isValid(nowUTC: Instant): Boolean {
+        val expiresAt = updatedAt.plus(cacheValidity)
+        return nowUTC.isBefore(expiresAt)
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ConfigDownload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/InternalConfigData.kt
similarity index 77%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ConfigDownload.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/InternalConfigData.kt
index d6114e5a89fa050b3773d9263efbfa5a1801d552..6eb41191dd06a5f5b81e030232193cf2221f2c10 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ConfigDownload.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/InternalConfigData.kt
@@ -1,20 +1,21 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.internal
 
 import com.google.gson.annotations.SerializedName
 import org.joda.time.Duration
 import org.joda.time.Instant
 
-data class ConfigDownload(
+data class InternalConfigData(
     @SerializedName("rawData") val rawData: ByteArray,
     @SerializedName("etag") val etag: String,
     @SerializedName("serverTime") val serverTime: Instant,
-    @SerializedName("localOffset") val localOffset: Duration
+    @SerializedName("localOffset") val localOffset: Duration,
+    @SerializedName("cacheValidity") val cacheValidity: Duration
 ) {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (javaClass != other?.javaClass) return false
 
-        other as ConfigDownload
+        other as InternalConfigData
 
         if (!rawData.contentEquals(other.rawData)) return false
         if (serverTime != other.serverTime) return false
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt
index 8d78dddcdb0e87acb6e06cb32e7f3405e426a2b1..8c2e9502ffe4d60f99dfab4d11b24ea62e67a4b1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt
@@ -3,24 +3,24 @@ package de.rki.coronawarnapp.appconfig.mapping
 import androidx.annotation.VisibleForTesting
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.CWAConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppFeaturesOuterClass
-import de.rki.coronawarnapp.server.protocols.internal.AppVersionConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppFeaturesOuterClass
 import timber.log.Timber
 import javax.inject.Inject
 
 @Reusable
 class CWAConfigMapper @Inject constructor() : CWAConfig.Mapper {
-    override fun map(rawConfig: AppConfig.ApplicationConfiguration): CWAConfig {
+    override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): CWAConfig {
         return CWAConfigContainer(
-            appVersion = rawConfig.appVersion,
+            latestVersionCode = rawConfig.latestVersionCode,
+            minVersionCode = rawConfig.minVersionCode,
             supportedCountries = rawConfig.getMappedSupportedCountries(),
-            appFeatureus = rawConfig.appFeatures
+            appFeatures = rawConfig.appFeatures
         )
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    internal fun AppConfig.ApplicationConfiguration.getMappedSupportedCountries(): List<String> =
+    internal fun AppConfigAndroid.ApplicationConfigurationAndroid.getMappedSupportedCountries(): List<String> =
         when {
             supportedCountriesList == null -> emptyList()
             supportedCountriesList.size == 1 && !VALID_CC.matches(supportedCountriesList.single()) -> {
@@ -31,9 +31,10 @@ class CWAConfigMapper @Inject constructor() : CWAConfig.Mapper {
         }
 
     data class CWAConfigContainer(
-        override val appVersion: AppVersionConfig.ApplicationVersionConfiguration,
+        override val latestVersionCode: Long,
+        override val minVersionCode: Long,
         override val supportedCountries: List<String>,
-        override val appFeatureus: AppFeaturesOuterClass.AppFeatures
+        override val appFeatures: AppFeaturesOuterClass.AppFeatures
     ) : CWAConfig
 
     companion object {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt
index 58c4b88b2f7beaff9765715e7c1ed54a4916f199..1c822d0f2a7b8b5080b0de703f59362489fb83a0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.appconfig.mapping
 
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 
 interface ConfigMapper<T> {
-    fun map(rawConfig: AppConfig.ApplicationConfiguration): T
+    fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): T
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
index 9858ec812bfc0b74c37330b2d44704decd085a5d..08a358c7a40c3734859822addcf878a5abf31b79 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt
@@ -2,16 +2,16 @@ package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
-import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 
 interface ConfigMapping :
     CWAConfig,
     KeyDownloadConfig,
     ExposureDetectionConfig,
-    RiskCalculationConfig {
+    ExposureWindowRiskCalculationConfig {
 
     @Deprecated("Try to access a more specific config type, avoid the RAW variant.")
-    val rawConfig: AppConfig.ApplicationConfiguration
+    val rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
index 8449b81b7cf3c5e996dc8121f97de585bc0ef693..54ccf2419a424b1fcbd86acd3ed50d2c6c1e74d4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt
@@ -3,9 +3,9 @@ package de.rki.coronawarnapp.appconfig.mapping
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
-import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -14,7 +14,7 @@ class ConfigParser @Inject constructor(
     private val cwaConfigMapper: CWAConfig.Mapper,
     private val keyDownloadConfigMapper: KeyDownloadConfig.Mapper,
     private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper,
-    private val riskCalculationConfigMapper: RiskCalculationConfig.Mapper
+    private val exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper
 ) {
 
     fun parse(configBytes: ByteArray): ConfigMapping = try {
@@ -24,7 +24,7 @@ class ConfigParser @Inject constructor(
                 cwaConfig = cwaConfigMapper.map(it),
                 keyDownloadConfig = keyDownloadConfigMapper.map(it),
                 exposureDetectionConfig = exposureDetectionConfigMapper.map(it),
-                riskCalculationConfig = riskCalculationConfigMapper.map(it)
+                exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(it)
             )
         }
     } catch (e: Exception) {
@@ -32,8 +32,8 @@ class ConfigParser @Inject constructor(
         throw e
     }
 
-    private fun parseRawArray(configBytes: ByteArray): AppConfig.ApplicationConfiguration {
+    private fun parseRawArray(configBytes: ByteArray): AppConfigAndroid.ApplicationConfigurationAndroid {
         Timber.v("Parsing config (size=%dB)", configBytes.size)
-        return AppConfig.ApplicationConfiguration.parseFrom(configBytes)
+        return AppConfigAndroid.ApplicationConfigurationAndroid.parseFrom(configBytes)
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
index 783385ddfdd3e7c06198eaff8c0ef5ba5a2961ff..81643e178e87768eea0208249bf3268ade972c25 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt
@@ -2,18 +2,18 @@ package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
-import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 
 data class DefaultConfigMapping(
-    override val rawConfig: AppConfig.ApplicationConfiguration,
+    override val rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid,
     val cwaConfig: CWAConfig,
     val keyDownloadConfig: KeyDownloadConfig,
     val exposureDetectionConfig: ExposureDetectionConfig,
-    val riskCalculationConfig: RiskCalculationConfig
+    val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig
 ) : ConfigMapping,
     CWAConfig by cwaConfig,
     KeyDownloadConfig by keyDownloadConfig,
     ExposureDetectionConfig by exposureDetectionConfig,
-    RiskCalculationConfig by riskCalculationConfig
+    ExposureWindowRiskCalculationConfig by exposureWindowRiskCalculationConfig
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt
index 92473af001e20e37facf9758760eb9724195c0b3..5110334a3cbb27f5b89cf0671428fc95b3de6b32 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt
@@ -1,20 +1,22 @@
 package de.rki.coronawarnapp.appconfig.mapping
 
 import androidx.annotation.VisibleForTesting
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import de.rki.coronawarnapp.server.protocols.internal.ExposureDetectionParameters.ExposureDetectionParametersAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.ExposureDetectionParameters.ExposureDetectionParametersAndroid
 import org.joda.time.Duration
 import javax.inject.Inject
 
 @Reusable
 class ExposureDetectionConfigMapper @Inject constructor() : ExposureDetectionConfig.Mapper {
-    override fun map(rawConfig: AppConfig.ApplicationConfiguration): ExposureDetectionConfig {
-        val exposureParams = rawConfig.androidExposureDetectionParameters
+    override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureDetectionConfig {
+        val exposureParams = if (rawConfig.hasExposureDetectionParameters()) {
+            rawConfig.exposureDetectionParameters
+        } else {
+            null
+        }
         return ExposureDetectionConfigContainer(
-            exposureDetectionConfiguration = rawConfig.mapRiskScoreToExposureConfiguration(),
             exposureDetectionParameters = exposureParams,
             maxExposureDetectionsPerUTCDay = exposureParams.maxExposureDetectionsPerDay(),
             minTimeBetweenDetections = exposureParams.minTimeBetweenExposureDetections(),
@@ -23,8 +25,7 @@ class ExposureDetectionConfigMapper @Inject constructor() : ExposureDetectionCon
     }
 
     data class ExposureDetectionConfigContainer(
-        override val exposureDetectionConfiguration: ExposureConfiguration,
-        override val exposureDetectionParameters: ExposureDetectionParametersAndroid,
+        override val exposureDetectionParameters: ExposureDetectionParametersAndroid?,
         override val maxExposureDetectionsPerUTCDay: Int,
         override val minTimeBetweenDetections: Duration,
         override val overallDetectionTimeout: Duration
@@ -33,77 +34,28 @@ class ExposureDetectionConfigMapper @Inject constructor() : ExposureDetectionCon
 
 // If we are outside the valid data range, fallback to default value.
 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-fun ExposureDetectionParametersAndroid.overAllDetectionTimeout(): Duration = when {
-    overallTimeoutInSeconds > 3600 -> Duration.standardMinutes(15)
-    overallTimeoutInSeconds <= 0 -> Duration.standardMinutes(15)
-    else -> Duration.standardSeconds(overallTimeoutInSeconds.toLong())
-}
+fun ExposureDetectionParametersAndroid?.overAllDetectionTimeout(): Duration =
+    if (this == null || overallTimeoutInSeconds > 3600 || overallTimeoutInSeconds <= 0) {
+        Duration.standardMinutes(15)
+    } else {
+        Duration.standardSeconds(overallTimeoutInSeconds.toLong())
+    }
 
 // If we are outside the valid data range, fallback to default value.
 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-fun ExposureDetectionParametersAndroid.maxExposureDetectionsPerDay(): Int = when {
-    maxExposureDetectionsPerInterval > 6 -> 6
-    maxExposureDetectionsPerInterval < 0 -> 6
-    else -> maxExposureDetectionsPerInterval
-}
+fun ExposureDetectionParametersAndroid?.maxExposureDetectionsPerDay(): Int =
+    if (this == null || maxExposureDetectionsPerInterval > 6 || maxExposureDetectionsPerInterval < 0) {
+        6
+    } else {
+        maxExposureDetectionsPerInterval
+    }
 
 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-fun ExposureDetectionParametersAndroid.minTimeBetweenExposureDetections(): Duration {
-    val detectionsPerDay = maxExposureDetectionsPerDay()
+fun ExposureDetectionParametersAndroid?.minTimeBetweenExposureDetections(): Duration {
+    val detectionsPerDay = this.maxExposureDetectionsPerDay()
     return if (detectionsPerDay == 0) {
         Duration.standardDays(99)
     } else {
         (24 / detectionsPerDay).let { Duration.standardHours(it.toLong()) }
     }
 }
-
-@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-fun AppConfig.ApplicationConfiguration.mapRiskScoreToExposureConfiguration(): ExposureConfiguration =
-    ExposureConfiguration
-        .ExposureConfigurationBuilder()
-        .setTransmissionRiskScores(
-            this.exposureConfig.transmission.appDefined1Value,
-            this.exposureConfig.transmission.appDefined2Value,
-            this.exposureConfig.transmission.appDefined3Value,
-            this.exposureConfig.transmission.appDefined4Value,
-            this.exposureConfig.transmission.appDefined5Value,
-            this.exposureConfig.transmission.appDefined6Value,
-            this.exposureConfig.transmission.appDefined7Value,
-            this.exposureConfig.transmission.appDefined8Value
-        )
-        .setDurationScores(
-            this.exposureConfig.duration.eq0MinValue,
-            this.exposureConfig.duration.gt0Le5MinValue,
-            this.exposureConfig.duration.gt5Le10MinValue,
-            this.exposureConfig.duration.gt10Le15MinValue,
-            this.exposureConfig.duration.gt15Le20MinValue,
-            this.exposureConfig.duration.gt20Le25MinValue,
-            this.exposureConfig.duration.gt25Le30MinValue,
-            this.exposureConfig.duration.gt30MinValue
-        )
-        .setDaysSinceLastExposureScores(
-            this.exposureConfig.daysSinceLastExposure.ge14DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge12Lt14DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge10Lt12DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge8Lt10DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge6Lt8DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge4Lt6DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge2Lt4DaysValue,
-            this.exposureConfig.daysSinceLastExposure.ge0Lt2DaysValue
-        )
-        .setAttenuationScores(
-            this.exposureConfig.attenuation.gt73DbmValue,
-            this.exposureConfig.attenuation.gt63Le73DbmValue,
-            this.exposureConfig.attenuation.gt51Le63DbmValue,
-            this.exposureConfig.attenuation.gt33Le51DbmValue,
-            this.exposureConfig.attenuation.gt27Le33DbmValue,
-            this.exposureConfig.attenuation.gt15Le27DbmValue,
-            this.exposureConfig.attenuation.gt10Le15DbmValue,
-            this.exposureConfig.attenuation.le10DbmValue
-        )
-        .setMinimumRiskScore(this.minRiskScore)
-        .setDurationAtAttenuationThresholds(
-            this.attenuationDuration.thresholds.lower,
-            this.attenuationDuration.thresholds.upper
-        )
-        .build()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ab55b97ee827ef27a091e1575c0eae048e50ee7a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt
@@ -0,0 +1,52 @@
+package de.rki.coronawarnapp.appconfig.mapping
+
+import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
+import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import javax.inject.Inject
+
+@Reusable
+class ExposureWindowRiskCalculationConfigMapper @Inject constructor() :
+    ExposureWindowRiskCalculationConfig.Mapper {
+
+    override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig {
+        if (!rawConfig.hasRiskCalculationParameters()) {
+            throw ApplicationConfigurationInvalidException(
+                message = "Risk Calculation Parameters are missing"
+            )
+        }
+
+        val riskCalculationParameters = rawConfig.riskCalculationParameters
+
+        return ExposureWindowRiskCalculationContainer(
+            minutesAtAttenuationFilters = riskCalculationParameters
+                .minutesAtAttenuationFiltersList,
+            minutesAtAttenuationWeights = riskCalculationParameters
+                .minutesAtAttenuationWeightsList,
+            transmissionRiskLevelEncoding = riskCalculationParameters
+                .trlEncoding,
+            transmissionRiskLevelFilters = riskCalculationParameters
+                .trlFiltersList,
+            transmissionRiskLevelMultiplier = riskCalculationParameters
+                .transmissionRiskLevelMultiplier,
+            normalizedTimePerExposureWindowToRiskLevelMapping = riskCalculationParameters
+                .normalizedTimePerEWToRiskLevelMappingList,
+            normalizedTimePerDayToRiskLevelMappingList = riskCalculationParameters
+                .normalizedTimePerDayToRiskLevelMappingList
+        )
+    }
+
+    data class ExposureWindowRiskCalculationContainer(
+        override val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>,
+        override val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>,
+        override val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding,
+        override val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>,
+        override val transmissionRiskLevelMultiplier: Double,
+        override val normalizedTimePerExposureWindowToRiskLevelMapping:
+        List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>,
+        override val normalizedTimePerDayToRiskLevelMappingList:
+        List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
+    ) : ExposureWindowRiskCalculationConfig
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt
index 4c55393ec77de81c5d97d75e2804218d419fbcf6..f1a5a2671e661788c9f5086b18aefd016af84bb0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt
@@ -4,8 +4,8 @@ import androidx.annotation.VisibleForTesting
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
 import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import de.rki.coronawarnapp.server.protocols.internal.KeyDownloadParameters.KeyDownloadParametersAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.KeyDownloadParameters.KeyDownloadParametersAndroid
 import org.joda.time.Duration
 import org.joda.time.LocalDate
 import org.joda.time.LocalTime
@@ -15,8 +15,12 @@ import javax.inject.Inject
 
 @Reusable
 class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapper {
-    override fun map(rawConfig: AppConfig.ApplicationConfiguration): KeyDownloadConfig {
-        val rawParameters = rawConfig.androidKeyDownloadParameters
+    override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): KeyDownloadConfig {
+        val rawParameters = if (rawConfig.hasKeyDownloadParameters()) {
+            rawConfig.keyDownloadParameters
+        } else {
+            null
+        }
 
         return KeyDownloadConfigContainer(
             individualDownloadTimeout = rawParameters.individualTimeout(),
@@ -27,21 +31,25 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp
     }
 
     // If we are outside the valid data range, fallback to default value.
-    private fun KeyDownloadParametersAndroid.individualTimeout(): Duration = when {
-        downloadTimeoutInSeconds > 1800 -> Duration.standardSeconds(60)
-        downloadTimeoutInSeconds <= 0 -> Duration.standardSeconds(60)
-        else -> Duration.standardSeconds(downloadTimeoutInSeconds.toLong())
-    }
+    private fun KeyDownloadParametersAndroid?.individualTimeout(): Duration =
+        if (this == null || downloadTimeoutInSeconds > 1800 || downloadTimeoutInSeconds <= 0) {
+            Duration.standardSeconds(60)
+        } else {
+            Duration.standardSeconds(downloadTimeoutInSeconds.toLong())
+        }
 
     // If we are outside the valid data range, fallback to default value.
-    private fun KeyDownloadParametersAndroid.overAllTimeout(): Duration = when {
-        overallTimeoutInSeconds > 1800 -> Duration.standardMinutes(8)
-        overallTimeoutInSeconds <= 0 -> Duration.standardMinutes(8)
-        else -> Duration.standardSeconds(overallTimeoutInSeconds.toLong())
-    }
+    private fun KeyDownloadParametersAndroid?.overAllTimeout(): Duration =
+        if (this == null || overallTimeoutInSeconds > 1800 || overallTimeoutInSeconds <= 0) {
+            Duration.standardMinutes(8)
+        } else {
+            Duration.standardSeconds(overallTimeoutInSeconds.toLong())
+        }
 
-    private fun KeyDownloadParametersAndroid.mapDayEtags(): List<RevokedKeyPackage.Day> =
-        this.revokedDayPackagesList.mapNotNull {
+    private fun KeyDownloadParametersAndroid?.mapDayEtags(): List<RevokedKeyPackage.Day> {
+        if (this == null) return emptyList()
+
+        return this.revokedDayPackagesList.mapNotNull {
             try {
                 RevokedKeyPackage.Day(
                     etag = it.etag,
@@ -53,9 +61,12 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp
                 null
             }
         }
+    }
 
-    private fun KeyDownloadParametersAndroid.mapHourEtags(): List<RevokedKeyPackage.Hour> =
-        this.revokedHourPackagesList.mapNotNull {
+    private fun KeyDownloadParametersAndroid?.mapHourEtags(): List<RevokedKeyPackage.Hour> {
+        if (this == null) return emptyList()
+
+        return this.revokedHourPackagesList.mapNotNull {
             try {
                 RevokedKeyPackage.Hour(
                     etag = it.etag,
@@ -68,6 +79,7 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp
                 null
             }
         }
+    }
 
     data class KeyDownloadConfigContainer(
         override val individualDownloadTimeout: Duration,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapper.kt
deleted file mode 100644
index dd36d4ea99f8f56af32e83fee682d90cc164460f..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapper.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package de.rki.coronawarnapp.appconfig.mapping
-
-import dagger.Reusable
-import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
-import de.rki.coronawarnapp.server.protocols.internal.RiskScoreClassificationOuterClass
-import javax.inject.Inject
-
-@Reusable
-class RiskCalculationConfigMapper @Inject constructor() : RiskCalculationConfig.Mapper {
-
-    override fun map(rawConfig: AppConfig.ApplicationConfiguration): RiskCalculationConfig {
-        return RiskCalculationContainer(
-            minRiskScore = rawConfig.minRiskScore,
-            riskScoreClasses = rawConfig.riskScoreClasses,
-            attenuationDuration = rawConfig.attenuationDuration
-        )
-    }
-
-    data class RiskCalculationContainer(
-        override val minRiskScore: Int,
-        override val attenuationDuration: AttenuationDurationOuterClass.AttenuationDuration,
-        override val riskScoreClasses: RiskScoreClassificationOuterClass.RiskScoreClassification
-    ) : RiskCalculationConfig
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7000a6c71cb59f6be2550516a1f17376648fcdc7
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSource.kt
@@ -0,0 +1,31 @@
+package de.rki.coronawarnapp.appconfig.sources.fallback
+
+import android.content.Context
+import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
+import de.rki.coronawarnapp.util.di.AppContext
+import org.joda.time.Duration
+import org.joda.time.Instant
+import javax.inject.Inject
+
+@Reusable
+class DefaultAppConfigSource @Inject constructor(
+    @AppContext private val context: Context,
+    private val configParser: ConfigParser
+) {
+
+    fun getRawDefaultConfig(): ByteArray {
+        return context.assets.open("default_app_config_android.bin").readBytes()
+    }
+
+    fun getConfigData(): ConfigData = ConfigDataContainer(
+        mappedConfig = configParser.parse(getRawDefaultConfig()),
+        serverTime = Instant.EPOCH,
+        localOffset = Duration.ZERO,
+        identifier = "fallback.local",
+        configType = ConfigData.Type.LOCAL_DEFAULT,
+        cacheValidity = Duration.ZERO
+    )
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt
similarity index 68%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorage.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt
index 1d939896ce9e0b2f7eadf831cb40586ac73ce50b..dda995f9b17ddd06d498c0292e125490eed35eeb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorage.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt
@@ -1,7 +1,10 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.local
 
 import android.content.Context
 import com.google.gson.Gson
+import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.serialization.BaseGson
@@ -38,17 +41,18 @@ class AppConfigStorage @Inject constructor(
     private val configFile = File(configDir, "appconfig.json")
     private val mutex = Mutex()
 
-    suspend fun getStoredConfig(): ConfigDownload? = mutex.withLock {
+    suspend fun getStoredConfig(): InternalConfigData? = mutex.withLock {
         Timber.v("get() AppConfig")
 
         if (!configFile.exists() && legacyConfigFile.exists()) {
             Timber.i("Returning legacy config.")
             return@withLock try {
-                ConfigDownload(
+                InternalConfigData(
                     rawData = legacyConfigFile.readBytes(),
                     serverTime = timeStamper.nowUTC,
                     localOffset = Duration.ZERO,
-                    etag = "legacy.migration"
+                    etag = "legacy.migration",
+                    cacheValidity = Duration.standardMinutes(5)
                 )
             } catch (e: Exception) {
                 Timber.e(e, "Legacy config exits but couldn't be read.")
@@ -57,14 +61,17 @@ class AppConfigStorage @Inject constructor(
         }
 
         return@withLock try {
-            gson.fromJson<ConfigDownload>(configFile)
+            gson.fromJson<InternalConfigData>(configFile).also {
+                requireNotNull(it.rawData)
+            }
         } catch (e: Exception) {
             Timber.e(e, "Couldn't load config.")
+            if (configFile.delete()) Timber.w("Config file was deleted.")
             null
         }
     }
 
-    suspend fun setStoredConfig(value: ConfigDownload?): Unit = mutex.withLock {
+    suspend fun setStoredConfig(value: InternalConfigData?): Unit = mutex.withLock {
         Timber.v("set(...) AppConfig: %s", value)
 
         if (configDir.mkdirs()) Timber.v("Parent folder created.")
@@ -73,7 +80,12 @@ class AppConfigStorage @Inject constructor(
             Timber.v("Overwriting %d from %s", configFile.length(), configFile.lastModified())
         }
 
-        if (value != null) {
+        if (value == null) {
+            if (configFile.delete()) Timber.d("Config file was deleted (value=null).")
+            return
+        }
+
+        try {
             gson.toJson(value, configFile)
 
             if (legacyConfigFile.exists()) {
@@ -81,8 +93,11 @@ class AppConfigStorage @Inject constructor(
                     Timber.i("Legacy config file deleted, superseeded.")
                 }
             }
-        } else {
-            configFile.delete()
+        } catch (e: Exception) {
+            // We'll not rethrow as we could still keep working just with the remote config,
+            // but we will notify the user.
+            Timber.e(e, "Failed to config data to local storage.")
+            e.report(ExceptionCategory.INTERNAL)
         }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c6a015c9abec1077a8e01c3a17dc3dfd722b868e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSource.kt
@@ -0,0 +1,52 @@
+package de.rki.coronawarnapp.appconfig.sources.local
+
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class LocalAppConfigSource @Inject constructor(
+    private val storage: AppConfigStorage,
+    private val parser: ConfigParser,
+    private val dispatcherProvider: DispatcherProvider
+) {
+
+    suspend fun getConfigData(): ConfigData? = withContext(dispatcherProvider.IO) {
+        Timber.tag(TAG).v("retrieveConfig()")
+
+        val configDownload = storage.getStoredConfig()
+        if (configDownload == null) {
+            Timber.tag(TAG).d("No stored config available.")
+            return@withContext null
+        }
+
+        return@withContext try {
+            configDownload.let {
+                ConfigDataContainer(
+                    mappedConfig = parser.parse(it.rawData),
+                    serverTime = it.serverTime,
+                    localOffset = it.localOffset,
+                    identifier = it.etag,
+                    configType = ConfigData.Type.LAST_RETRIEVED,
+                    cacheValidity = it.cacheValidity
+                )
+            }
+        } catch (e: Exception) {
+            Timber.tag(TAG).e(e, "Fallback config exists but could not be parsed!")
+            null
+        }
+    }
+
+    suspend fun clear() {
+        storage.setStoredConfig(null)
+    }
+
+    companion object {
+        private const val TAG = "LocalAppConfigSource"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigHttpCache.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigHttpCache.kt
similarity index 71%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigHttpCache.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigHttpCache.kt
index 253ac97d3fbedc4a7c0c5429cad471f77cac0837..0d4e68160cfa2f0b4f99686b73748dba1828bda7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigHttpCache.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigHttpCache.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.remote
 
 import javax.inject.Qualifier
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt
similarity index 76%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigServer.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt
index 7d174ac15251e8ca5f928e8b135f1985f32050bd..b9ae35a337a8921ec33729aea891fc2b66c97b18 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt
@@ -1,15 +1,18 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.remote
 
 import dagger.Lazy
 import dagger.Reusable
-import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
-import de.rki.coronawarnapp.environment.download.DownloadCDNHomeCountry
+import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2
+import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException
+import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException
+import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.ZipHelper.readIntoMap
 import de.rki.coronawarnapp.util.ZipHelper.unzip
 import de.rki.coronawarnapp.util.retrofit.etag
 import de.rki.coronawarnapp.util.security.VerificationKeys
 import okhttp3.Cache
+import okhttp3.CacheControl
 import org.joda.time.Duration
 import org.joda.time.Instant
 import org.joda.time.format.DateTimeFormat
@@ -21,17 +24,16 @@ import javax.inject.Inject
 
 @Reusable
 class AppConfigServer @Inject constructor(
-    private val api: Lazy<AppConfigApiV1>,
+    private val api: Lazy<AppConfigApiV2>,
     private val verificationKeys: VerificationKeys,
     private val timeStamper: TimeStamper,
-    @DownloadCDNHomeCountry private val homeCountry: LocationCode,
     @AppConfigHttpCache private val cache: Cache
 ) {
 
-    internal suspend fun downloadAppConfig(): ConfigDownload {
+    internal suspend fun downloadAppConfig(): InternalConfigData {
         Timber.tag(TAG).d("Fetching app config.")
 
-        val response = api.get().getApplicationConfiguration(homeCountry.identifier)
+        val response = api.get().getApplicationConfiguration()
         if (!response.isSuccessful) throw HttpException(response)
 
         val rawConfig = with(
@@ -56,19 +58,26 @@ class AppConfigServer @Inject constructor(
         // If this is a cached response, we need the original timestamp to calculate the time offset
         val localTime = response.getCacheTimestamp() ?: timeStamper.nowUTC
 
+        val headers = response.headers()
+
         // Shouldn't happen, but hey ¯\_(ツ)_/¯
-        val etag =
-            response.headers().etag() ?: throw ApplicationConfigurationInvalidException(message = "Server has no ETAG.")
+        val etag = headers.etag()
+            ?: throw ApplicationConfigurationInvalidException(message = "Server has no ETAG.")
 
         val serverTime = response.getServerDate() ?: localTime
         val offset = Duration(serverTime, localTime)
         Timber.tag(TAG).v("Time offset was %dms", offset.millis)
 
-        return ConfigDownload(
+        val cacheControl = CacheControl.parse(headers)
+
+        val maxCacheAge = Duration.standardSeconds(cacheControl.maxAgeSeconds.toLong())
+
+        return InternalConfigData(
             rawData = rawConfig,
             etag = etag,
             serverTime = serverTime,
-            localOffset = offset
+            localOffset = offset,
+            cacheValidity = maxCacheAge
         )
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e5006f0279a83676f128d16bb1b1a5f68d7caa92
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt
@@ -0,0 +1,57 @@
+package de.rki.coronawarnapp.appconfig.sources.remote
+
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
+import de.rki.coronawarnapp.appconfig.sources.local.AppConfigStorage
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RemoteAppConfigSource @Inject constructor(
+    private val server: AppConfigServer,
+    private val storage: AppConfigStorage,
+    private val parser: ConfigParser,
+    private val dispatcherProvider: DispatcherProvider
+) {
+
+    suspend fun getConfigData(): ConfigData? = withContext(dispatcherProvider.IO) {
+        Timber.tag(TAG).v("retrieveConfig()")
+
+        val configDownload = try {
+            server.downloadAppConfig()
+        } catch (e: Exception) {
+            Timber.tag(TAG).w(e, "Failed to download AppConfig from server .")
+            return@withContext null
+        }
+
+        return@withContext try {
+            parser.parse(configDownload.rawData).let {
+                Timber.tag(TAG).d("Got a valid AppConfig from server, saving.")
+                storage.setStoredConfig(configDownload)
+                ConfigDataContainer(
+                    mappedConfig = it,
+                    serverTime = configDownload.serverTime,
+                    localOffset = configDownload.localOffset,
+                    identifier = configDownload.etag,
+                    configType = ConfigData.Type.FROM_SERVER,
+                    cacheValidity = configDownload.cacheValidity
+                )
+            }
+        } catch (e: Exception) {
+            Timber.tag(TAG).e(e, "Failed to parse AppConfig from server, trying fallback.")
+            null
+        }
+    }
+
+    fun clear() {
+        server.clearCache()
+    }
+
+    companion object {
+        private const val TAG = "AppConfigRetriever"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
index 870c80617d260aafb04eedc58c461b86ae003372..d04dc5137d1dc33c3eb31577ef33fade451f4594 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
@@ -15,18 +15,14 @@ import de.rki.coronawarnapp.task.TaskFactory
 import de.rki.coronawarnapp.task.TaskFactory.Config.CollisionBehavior
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.ui.toLazyString
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import kotlinx.coroutines.channels.ConflatedBroadcastChannel
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.asFlow
 import kotlinx.coroutines.flow.first
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
 import org.joda.time.Duration
 import org.joda.time.Instant
 import timber.log.Timber
 import java.util.Date
-import java.util.UUID
 import javax.inject.Inject
 import javax.inject.Provider
 
@@ -50,10 +46,6 @@ class DownloadDiagnosisKeysTask @Inject constructor(
             Timber.d("Running with arguments=%s", arguments)
             arguments as Arguments
 
-            if (arguments.withConstraints) {
-                if (!noKeysFetchedToday()) return object : Task.Result {}
-            }
-
             /**
              * Handles the case when the ENClient got disabled but the Task is still scheduled
              * in a background job. Also it acts as a failure catch in case the orchestration code did
@@ -68,10 +60,6 @@ class DownloadDiagnosisKeysTask @Inject constructor(
             val currentDate = Date(timeStamper.nowUTC.millis)
             Timber.tag(TAG).d("Using $currentDate as current date in task.")
 
-            /****************************************************
-             * RETRIEVE TOKEN
-             ****************************************************/
-            val token = retrieveToken(rollbackItems)
             throwIfCancelled()
 
             // RETRIEVE RISK SCORE PARAMETERS
@@ -94,11 +82,12 @@ class DownloadDiagnosisKeysTask @Inject constructor(
 
             if (wasLastDetectionPerformedRecently(now, exposureConfig, trackedExposureDetections)) {
                 // At most one detection every 6h
+                Timber.tag(TAG).i("task aborted, because detection was performed recently")
                 return object : Task.Result {}
             }
 
             if (hasRecentDetectionAndNoNewFiles(now, keySyncResult, trackedExposureDetections)) {
-                //  Last check was within 24h, and there are no new files.
+                Timber.tag(TAG).i("task aborted, last check was within 24h, and there are no new files")
                 return object : Task.Result {}
             }
 
@@ -113,20 +102,17 @@ class DownloadDiagnosisKeysTask @Inject constructor(
             )
 
             Timber.tag(TAG).d("Attempting submission to ENF")
-            val isSubmissionSuccessful = enfClient.provideDiagnosisKeys(
-                keyFiles = availableKeyFiles,
-                configuration = exposureConfig.exposureDetectionConfiguration,
-                token = token
-            )
-            Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", isSubmissionSuccessful, token)
-
-            internalProgress.send(Progress.ApiSubmissionFinished)
-            throwIfCancelled()
+            val isSubmissionSuccessful = enfClient.provideDiagnosisKeys(availableKeyFiles)
+            Timber.tag(TAG).d("Diagnosis Keys provided (success=%s)", isSubmissionSuccessful)
 
+            // EXPOSUREAPP-3878 write timestamp immediately after submission,
+            // so that progress observers can rely on a clean app state
             if (isSubmissionSuccessful) {
                 saveTimestamp(currentDate, rollbackItems)
             }
 
+            internalProgress.send(Progress.ApiSubmissionFinished)
+
             return object : Task.Result {}
         } catch (error: Exception) {
             Timber.tag(TAG).e(error)
@@ -181,35 +167,6 @@ class DownloadDiagnosisKeysTask @Inject constructor(
         LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate)
     }
 
-    private fun retrieveToken(rollbackItems: MutableList<RollbackItem>): String {
-        val googleAPITokenForRollback = LocalData.googleApiToken()
-        rollbackItems.add {
-            LocalData.googleApiToken(googleAPITokenForRollback)
-        }
-        return UUID.randomUUID().toString().also {
-            LocalData.googleApiToken(it)
-        }
-    }
-
-    private fun noKeysFetchedToday(): Boolean {
-        val currentDate = DateTime(timeStamper.nowUTC, DateTimeZone.UTC)
-        val lastFetch = DateTime(
-            LocalData.lastTimeDiagnosisKeysFromServerFetch(),
-            DateTimeZone.UTC
-        )
-        return (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
-            currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()).also {
-            if (it) {
-                Timber.tag(TAG)
-                    .d("No keys fetched today yet (last=%s, now=%s)", lastFetch, currentDate)
-                BackgroundWorkHelper.sendDebugNotification(
-                    "Start Task",
-                    "No keys fetched today yet \n${DateTime.now()}\nUTC: $currentDate"
-                )
-            }
-        }
-    }
-
     private fun rollback(rollbackItems: MutableList<RollbackItem>) {
         try {
             Timber.tag(TAG).d("Initiate Rollback")
@@ -249,8 +206,7 @@ class DownloadDiagnosisKeysTask @Inject constructor(
     }
 
     class Arguments(
-        val requestedCountries: List<String>? = null,
-        val withConstraints: Boolean = false
+        val requestedCountries: List<String>? = null
     ) : Task.Arguments
 
     data class Config(
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 ad63bbb134ffc3583c9fea34bc4c31e1ee2cb724..a15a2fa6969f165ac04109a4a55e3022a8bf878a 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
@@ -11,7 +11,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
 import de.rki.coronawarnapp.storage.DeviceStorage
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate
-import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalTime
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import kotlinx.coroutines.CoroutineScope
@@ -19,6 +18,7 @@ import kotlinx.coroutines.Deferred
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.withContext
+import org.joda.time.DateTimeZone
 import org.joda.time.Instant
 import org.joda.time.LocalDate
 import org.joda.time.LocalTime
@@ -117,10 +117,10 @@ class HourPackageSyncTool @Inject constructor(
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal fun expectNewHourPackages(cachedHours: List<CachedKey>, now: Instant): Boolean {
-        val previousHour = now.toLocalTime().minusHours(1)
-        val newestHour = cachedHours.map { it.info.toDateTime() }.maxOrNull()?.toLocalTime()
+        val today = now.toDateTime(DateTimeZone.UTC)
+        val newestHour = cachedHours.map { it.info.toDateTime() }.maxOrNull()
 
-        return previousHour.hourOfDay != newestHour?.hourOfDay
+        return today.minusHours(1).hourOfDay != newestHour?.hourOfDay || today.toLocalDate() != newestHour.toLocalDate()
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt
index 46499c31230bf37e06c79f5c124a5e02f9f61215..767788b052571ee50d306089dec02aba0a7513d6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt
@@ -1,9 +1,11 @@
 package de.rki.coronawarnapp.diagnosiskeys.download
 
+import android.annotation.SuppressLint
 import android.content.Context
 import com.google.gson.Gson
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.preferences.FlowPreference
+import de.rki.coronawarnapp.util.preferences.clearAndNotify
 import de.rki.coronawarnapp.util.serialization.BaseGson
 import org.joda.time.Instant
 import javax.inject.Inject
@@ -32,6 +34,11 @@ class KeyPackageSyncSettings @Inject constructor(
         writer = FlowPreference.gsonWriter(gson)
     )
 
+    @SuppressLint("ApplySharedPref")
+    fun clear() {
+        prefs.clearAndNotify()
+    }
+
     data class LastDownload(
         val startedAt: Instant,
         val finishedAt: Instant? = null,
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 4fa8b6effbc3f77ca7a780ac791cd07eb93f8ffc..fc89eff4627de3bd0fb3069f8937876563fb0341 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
@@ -27,7 +27,6 @@ open class CwaSuccessResponseWithCodeMismatchNotSupportedError(statusCode: Int)
 open class CwaInformationalNotSupportedError(statusCode: Int) : CwaWebException(statusCode)
 open class CwaRedirectNotSupportedError(statusCode: Int) : CwaWebException(statusCode)
 
-class CwaUnknownHostException : CwaWebException(901)
 class BadRequestException : CwaClientError(400)
 class UnauthorizedException : CwaClientError(401)
 class ForbiddenException : CwaClientError(403)
@@ -44,5 +43,6 @@ 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)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt
index bdaf800b81943a52a073d86be4c6f94551347c1c..5f177f2920d7e8bc8bf9738df7e19189f9aa6f0b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt
@@ -15,9 +15,11 @@ import de.rki.coronawarnapp.util.HasHumanReadableError
 import de.rki.coronawarnapp.util.tryHumanReadableError
 import java.io.PrintWriter
 import java.io.StringWriter
+import java.util.concurrent.CancellationException
 
-fun Throwable.report(exceptionCategory: ExceptionCategory) =
+fun Throwable.report(exceptionCategory: ExceptionCategory) {
     this.report(exceptionCategory, null, null)
+}
 
 fun Throwable.report(
     exceptionCategory: ExceptionCategory,
@@ -26,7 +28,14 @@ fun Throwable.report(
 ) {
     if (CWADebug.isAUnitTest) return
 
+    // CancellationException is a part of normal operation. It is used to cancel a running
+    // asynchronous operation. It is not a failure and should not be reported as such.
+    if (this is CancellationException) return
+
     reportProblem(tag = prefix, info = suffix)
+
+    if (CWADebug.isAUnitTest) return
+
     val context = CoronaWarnApplication.getAppContext()
 
     val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL)
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 bc347abb919fbbeff9ada2c2d768b3ef679334ff..1061abc7e164af4ff5ac41ec2e63776004ebcce3 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 java.net.SocketTimeoutException
 import java.net.UnknownHostException
 import javax.net.ssl.HttpsURLConnection
 
@@ -66,6 +67,8 @@ class HttpErrorParser : Interceptor {
                     throw CwaWebException(code)
                 }
             }
+        } catch (err: SocketTimeoutException) {
+            throw NetworkConnectTimeoutException()
         } catch (err: UnknownHostException) {
             throw CwaUnknownHostException()
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt
index c3de7abd0615b66f64d5eb6255c6d1c255e571e8..587603da757c029bf13e0a553538130c9737fa96 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt
@@ -2,18 +2,21 @@
 
 package de.rki.coronawarnapp.nearby
 
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider
 import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport
 import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus
+import de.rki.coronawarnapp.nearby.modules.version.ENFVersion
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 import org.joda.time.Instant
 import timber.log.Timber
 import java.io.File
+import java.util.UUID
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -23,31 +26,26 @@ class ENFClient @Inject constructor(
     private val diagnosisKeyProvider: DiagnosisKeyProvider,
     private val tracingStatus: TracingStatus,
     private val scanningSupport: ScanningSupport,
-    private val exposureDetectionTracker: ExposureDetectionTracker
-) : DiagnosisKeyProvider, TracingStatus, ScanningSupport {
+    private val exposureWindowProvider: ExposureWindowProvider,
+    private val exposureDetectionTracker: ExposureDetectionTracker,
+    private val enfVersion: ENFVersion
+) : DiagnosisKeyProvider, TracingStatus, ScanningSupport, ExposureWindowProvider, ENFVersion by enfVersion {
 
     // TODO Remove this once we no longer need direct access to the ENF Client,
     // i.e. in **[InternalExposureNotificationClient]**
     internal val internalClient: ExposureNotificationClient
         get() = googleENFClient
 
-    override suspend fun provideDiagnosisKeys(
-        keyFiles: Collection<File>,
-        configuration: ExposureConfiguration?,
-        token: String
-    ): Boolean {
-        Timber.d(
-            "asyncProvideDiagnosisKeys(keyFiles=%s, configuration=%s, token=%s)",
-            keyFiles, configuration, token
-        )
+    override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean {
+        Timber.d("asyncProvideDiagnosisKeys(keyFiles=$keyFiles)")
 
         return if (keyFiles.isEmpty()) {
             Timber.d("No key files submitted, returning early.")
             true
         } else {
             Timber.d("Forwarding %d key files to our DiagnosisKeyProvider.", keyFiles.size)
-            exposureDetectionTracker.trackNewExposureDetection(token)
-            diagnosisKeyProvider.provideDiagnosisKeys(keyFiles, configuration, token)
+            exposureDetectionTracker.trackNewExposureDetection(UUID.randomUUID().toString())
+            diagnosisKeyProvider.provideDiagnosisKeys(keyFiles)
         }
     }
 
@@ -72,4 +70,6 @@ class ENFClient @Inject constructor(
                 .filter { !it.isCalculating && it.isSuccessful }
                 .maxByOrNull { it.finishedAt ?: Instant.EPOCH }
         }
+
+    override suspend fun exposureWindows(): List<ExposureWindow> = exposureWindowProvider.exposureWindows()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt
index 9d98d5b33bf809dbc19995310857d2cda775fcb6..1d4220ee0df1f0640c4d5587ddd56bfdbd669e77 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt
@@ -9,10 +9,14 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.DefaultExposureDetec
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DefaultDiagnosisKeyProvider
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.DefaultExposureWindowProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider
 import de.rki.coronawarnapp.nearby.modules.locationless.DefaultScanningSupport
 import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport
 import de.rki.coronawarnapp.nearby.modules.tracing.DefaultTracingStatus
 import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus
+import de.rki.coronawarnapp.nearby.modules.version.DefaultENFVersion
+import de.rki.coronawarnapp.nearby.modules.version.ENFVersion
 import de.rki.coronawarnapp.util.di.AppContext
 import javax.inject.Singleton
 
@@ -39,8 +43,17 @@ class ENFModule {
     fun scanningSupport(scanningSupport: DefaultScanningSupport): ScanningSupport =
         scanningSupport
 
+    @Singleton
+    @Provides
+    fun exposureWindowProvider(exposureWindowProvider: DefaultExposureWindowProvider): ExposureWindowProvider =
+        exposureWindowProvider
+
     @Singleton
     @Provides
     fun calculationTracker(exposureDetectionTracker: DefaultExposureDetectionTracker): ExposureDetectionTracker =
         exposureDetectionTracker
+
+    @Singleton
+    @Provides
+    fun enfClientVersion(enfVersion: DefaultENFVersion): ENFVersion = enfVersion
 }
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 b0c92e94c97c65e27ed314b736c0a94589c994c8..f0f0ed5c5ff56d8758407875de879465d2e79560 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
@@ -4,14 +4,13 @@ import android.content.Context
 import androidx.work.CoroutineWorker
 import androidx.work.WorkerParameters
 import com.google.android.gms.common.api.ApiException
-import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.NoTokenException
 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.storage.ExposureSummaryRepository
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
@@ -20,23 +19,22 @@ 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")
-            val token = inputData.getString(ExposureNotificationClient.EXTRA_TOKEN)
-                ?: throw NoTokenException(IllegalArgumentException("no token was found in the intent"))
-            Timber.v("valid token $token retrieved")
-            InternalExposureNotificationClient
-                .asyncGetExposureSummary(token).also {
-                    ExposureSummaryRepository.getExposureSummaryRepository()
-                        .insertExposureSummaryEntity(it)
-                    Timber.v("exposure summary state updated: $it")
-                }
+            enfClient.exposureWindows().let {
+                exposureResultStore.entities.value = ExposureResult(it, null)
+                Timber.v("exposure summary state updated: $it")
+            }
 
-            taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
+            taskController.submit(
+                DefaultTaskRequest(RiskLevelTask::class, originTag = "ExposureStateUpdateWorker")
+            )
             Timber.v("risk level calculation triggered")
         } catch (e: ApiException) {
             e.report(ExceptionCategory.EXPOSURENOTIFICATION)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt
index 53c0aa60066a0dc91d8212d7d03d93bfda8faeb3..3ded662b655f7aa9dc7b87cdada9cbd6425031a6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.nearby
 
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
 import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.risk.TimeVariables
@@ -89,15 +88,6 @@ object InternalExposureNotificationClient {
             }
     }
 
-    suspend fun getVersion(): Long = suspendCoroutine { cont ->
-        exposureNotificationClient.version
-            .addOnSuccessListener {
-                cont.resume(it)
-            }.addOnFailureListener {
-                cont.resumeWithException(it)
-            }
-    }
-
     /**
      * Retrieves key history from the data store on the device for uploading to your
      * internet-accessible server. Calling this method prompts Google Play services to display
@@ -118,22 +108,4 @@ object InternalExposureNotificationClient {
                     cont.resumeWithException(it)
                 }
         }
-
-    /**
-     * Retrieves the ExposureSummary object that matches the token from
-     * provideDiagnosisKeys() that you provide to the method. The ExposureSummary
-     * object provides a high-level overview of the exposure that a user has experienced.
-     *
-     * @param token
-     * @return
-     */
-    suspend fun asyncGetExposureSummary(token: String): ExposureSummary =
-        suspendCoroutine { cont ->
-            exposureNotificationClient.getExposureSummary(token)
-                .addOnSuccessListener {
-                    cont.resume(it)
-                }.addOnFailureListener {
-                    cont.resumeWithException(it)
-                }
-        }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt
index c5e4b8073deb172dcb4856afb9d2406af38ef94d..909a66247fd7405a3239631942052de2892add21 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt
@@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.plus
 import org.joda.time.Duration
 import timber.log.Timber
+import java.util.UUID
 import javax.inject.Inject
 import javax.inject.Singleton
 import kotlin.math.min
@@ -63,7 +64,7 @@ class DefaultExposureDetectionTracker @Inject constructor(
                         }
                     }
 
-                    delay(TIMEOUT_CHECK_INTERVALL.millis)
+                    delay(TIMEOUT_CHECK_INTERVAL.millis)
                 }
             }.launchIn(scope + dispatcherProvider.Default)
         }
@@ -93,34 +94,17 @@ class DefaultExposureDetectionTracker @Inject constructor(
         }
     }
 
-    override fun finishExposureDetection(identifier: String, result: Result) {
+    override fun finishExposureDetection(identifier: String?, result: Result) {
         Timber.i("finishExposureDetection(token=%s, result=%s)", identifier, result)
         detectionStates.updateSafely {
             mutate {
-                val existing = this[identifier]
-                if (existing != null) {
-                    if (existing.result == Result.TIMEOUT) {
-                        Timber.w("Detection is late, already hit timeout, still updating.")
-                    } else if (existing.result != null) {
-                        Timber.e("Duplicate callback. Result is already set for detection!")
-                    }
-                    this[identifier] = existing.copy(
-                        result = result,
-                        finishedAt = timeStamper.nowUTC
-                    )
+                if (identifier == null) {
+                    val id = this.findUnfinishedOrCreateIdentifier()
+                    finishDetection(id, result)
                 } else {
-                    Timber.e(
-                        "Unknown detection finished (token=%s, result=%s)",
-                        identifier,
-                        result
-                    )
-                    this[identifier] = TrackedExposureDetection(
-                        identifier = identifier,
-                        result = result,
-                        startedAt = timeStamper.nowUTC,
-                        finishedAt = timeStamper.nowUTC
-                    )
+                    finishDetection(identifier, result)
                 }
+
                 val toKeep = entries
                     .sortedByDescending { it.value.startedAt } // Keep newest
                     .subList(0, min(entries.size, MAX_ENTRY_SIZE))
@@ -134,9 +118,59 @@ class DefaultExposureDetectionTracker @Inject constructor(
         }
     }
 
+    private fun Map<String, TrackedExposureDetection>.findUnfinishedOrCreateIdentifier(): String {
+        val newestUnfinishedDetection = this
+            .map { it.value }
+            .filter { it.finishedAt == null }
+            .maxByOrNull { it.startedAt.millis }
+
+        return if (newestUnfinishedDetection != null) {
+            Timber.d("findUnfinishedOrCreateIdentifier(): Found unfinished detection, return identifier")
+            newestUnfinishedDetection.identifier
+        } else {
+            Timber.d("findUnfinishedOrCreateIdentifier(): No unfinished detection found, create identifier")
+            UUID.randomUUID().toString()
+        }
+    }
+
+    private fun MutableMap<String, TrackedExposureDetection>.finishDetection(identifier: String, result: Result) {
+        Timber.i("finishDetection(token=%s, result=%s)", identifier, result)
+        val existing = this[identifier]
+        if (existing != null) {
+            if (existing.result == Result.TIMEOUT) {
+                Timber.w("Detection is late, already hit timeout, still updating.")
+            } else if (existing.result != null) {
+                Timber.e("Duplicate callback. Result is already set for detection!")
+            }
+            this[identifier] = existing.copy(
+                result = result,
+                finishedAt = timeStamper.nowUTC
+            )
+        } else {
+            Timber.e(
+                "Unknown detection finished (token=%s, result=%s)",
+                identifier,
+                result
+            )
+            this[identifier] = TrackedExposureDetection(
+                identifier = identifier,
+                result = result,
+                startedAt = timeStamper.nowUTC,
+                finishedAt = timeStamper.nowUTC
+            )
+        }
+    }
+
+    override fun clear() {
+        Timber.i("clear()")
+        detectionStates.updateSafely {
+            emptyMap()
+        }
+    }
+
     companion object {
         private const val TAG = "DefaultExposureDetectionTracker"
         private const val MAX_ENTRY_SIZE = 5
-        private val TIMEOUT_CHECK_INTERVALL = Duration.standardMinutes(3)
+        private val TIMEOUT_CHECK_INTERVAL = Duration.standardMinutes(3)
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt
index 4d9bcf0c6e318c96a314d71bdd0050f0ecf2bd88..0d6a6e86b50ea87bc3d58913e6773f339b9f410a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt
@@ -7,5 +7,7 @@ interface ExposureDetectionTracker {
 
     fun trackNewExposureDetection(identifier: String)
 
-    fun finishExposureDetection(identifier: String, result: TrackedExposureDetection.Result)
+    fun finishExposureDetection(identifier: String? = null, result: TrackedExposureDetection.Result)
+
+    fun clear()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt
index 01e93c4b9ab724a44dc953b0466c2b6a7b366c31..c4299cafe56f85562c86d276a10d910bb5e39287 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt
@@ -2,6 +2,8 @@ package de.rki.coronawarnapp.nearby.modules.detectiontracker
 
 import android.content.Context
 import com.google.gson.Gson
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.serialization.BaseGson
 import de.rki.coronawarnapp.util.serialization.fromJson
@@ -44,11 +46,13 @@ class ExposureDetectionTrackerStorage @Inject constructor(
             if (!storageFile.exists()) return@withLock emptyMap()
 
             gson.fromJson<Map<String, TrackedExposureDetection>>(storageFile).also {
+                require(it.size >= 0)
                 Timber.v("Loaded detection data: %s", it)
                 lastCalcuationData = it
             }
         } catch (e: Exception) {
             Timber.e(e, "Failed to load tracked detections.")
+            if (storageFile.delete()) Timber.w("Storage file was deleted.")
             emptyMap()
         }
     }
@@ -63,6 +67,7 @@ class ExposureDetectionTrackerStorage @Inject constructor(
             gson.toJson(data, storageFile)
         } catch (e: Exception) {
             Timber.e(e, "Failed to save tracked detections.")
+            e.report(ExceptionCategory.INTERNAL)
         }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt
index 59114d58d4c0bab4709d0568ffb39c052923913d..64f2e75d7addfca50ab691aa7d42bc7d8cd3f4cd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt
@@ -1,12 +1,9 @@
-@file:Suppress("DEPRECATION")
-
 package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider
 
 import com.google.android.gms.common.api.ApiException
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
 import de.rki.coronawarnapp.exception.reporting.ReportingConstants
-import de.rki.coronawarnapp.util.GoogleAPIVersion
+import de.rki.coronawarnapp.nearby.modules.version.ENFVersion
 import timber.log.Timber
 import java.io.File
 import javax.inject.Inject
@@ -17,100 +14,41 @@ import kotlin.coroutines.suspendCoroutine
 
 @Singleton
 class DefaultDiagnosisKeyProvider @Inject constructor(
-    private val googleAPIVersion: GoogleAPIVersion,
+    private val enfVersion: ENFVersion,
     private val submissionQuota: SubmissionQuota,
     private val enfClient: ExposureNotificationClient
 ) : DiagnosisKeyProvider {
 
-    override suspend fun provideDiagnosisKeys(
-        keyFiles: Collection<File>,
-        configuration: ExposureConfiguration?,
-        token: String
-    ): Boolean {
-        return try {
-            if (keyFiles.isEmpty()) {
-                Timber.d("No key files submitted, returning early.")
-                return true
-            }
-
-            val usedConfiguration = if (configuration == null) {
-                Timber.w("Passed configuration was NULL, creating fallback.")
-                ExposureConfiguration.ExposureConfigurationBuilder().build()
-            } else {
-                configuration
-            }
-
-            if (googleAPIVersion.isAtLeast(GoogleAPIVersion.V16)) {
-                provideKeys(keyFiles, usedConfiguration, token)
-            } else {
-                provideKeysLegacy(keyFiles, usedConfiguration, token)
-            }
-        } catch (e: Exception) {
-            Timber.e(
-                e, "Error during provideDiagnosisKeys(keyFiles=%s, configuration=%s, token=%s)",
-                keyFiles, configuration, token
-            )
-            throw e
+    override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean {
+        if (keyFiles.isEmpty()) {
+            Timber.d("No key files submitted, returning early.")
+            return true
         }
-    }
 
-    private suspend fun provideKeys(
-        files: Collection<File>,
-        configuration: ExposureConfiguration,
-        token: String
-    ): Boolean {
-        Timber.d("Using non-legacy key provision.")
+        // Check version of ENF, WindowMode since v1.5, but version check since v1.6
+        // Will throw if requirement is not satisfied
+        enfVersion.requireMinimumVersion(ENFVersion.V1_6)
 
         if (!submissionQuota.consumeQuota(1)) {
-            Timber.w("Not enough quota available.")
-            // TODO Currently only logging, we'll be more strict in a future release
-            // return false
-        }
-
-        performSubmission(files, configuration, token)
-        return true
-    }
-
-    /**
-     * We use Batch Size 1 and thus submit multiple times to the API.
-     * This means that instead of directly submitting all files at once, we have to split up
-     * our file list as this equals a different batch for Google every time.
-     */
-    private suspend fun provideKeysLegacy(
-        keyFiles: Collection<File>,
-        configuration: ExposureConfiguration,
-        token: String
-    ): Boolean {
-        Timber.d("Using LEGACY key provision.")
-
-        if (!submissionQuota.consumeQuota(keyFiles.size)) {
-            Timber.w("Not enough quota available.")
-            // TODO What about proceeding with partial submission?
-            // TODO Currently only logging, we'll be more strict in a future release
-            // return false
+            Timber.e("No key files submitted because not enough quota available.")
+            // Needs discussion until armed, concerns: Hiding other underlying issues.
+//            return false
         }
 
-        keyFiles.forEach { performSubmission(listOf(it), configuration, token) }
-        return true
-    }
-
-    private suspend fun performSubmission(
-        keyFiles: Collection<File>,
-        configuration: ExposureConfiguration,
-        token: String
-    ): Void = suspendCoroutine { cont ->
-        Timber.d("Performing key submission.")
-        enfClient
-            .provideDiagnosisKeys(keyFiles.toList(), configuration, token)
-            .addOnSuccessListener { cont.resume(it) }
-            .addOnFailureListener {
-                val wrappedException = when {
-                    it is ApiException && it.statusCode == ReportingConstants.STATUS_CODE_REACHED_REQUEST_LIMIT -> {
-                        QuotaExceededException(cause = it)
-                    }
-                    else -> it
+        return suspendCoroutine { cont ->
+            Timber.d("Performing key submission.")
+            enfClient
+                .provideDiagnosisKeys(keyFiles.toList())
+                .addOnSuccessListener { cont.resume(true) }
+                .addOnFailureListener {
+                    val wrappedException =
+                        when (it is ApiException &&
+                            it.statusCode == ReportingConstants.STATUS_CODE_REACHED_REQUEST_LIMIT) {
+                            true -> QuotaExceededException(cause = it)
+                            false -> it
+                        }
+                    cont.resumeWithException(wrappedException)
                 }
-                cont.resumeWithException(wrappedException)
-            }
+        }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt
index accedeed05ba0a7be5e2259326da4960db3ab3b7..b3339619f5b4e96635d1326a43cfb8d1cc09951e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt
@@ -1,25 +1,19 @@
 package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider
 
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import java.io.File
 
 interface DiagnosisKeyProvider {
 
     /**
-     * Takes an ExposureConfiguration object. Inserts a list of files that contain key
-     * information into the on-device database. Provide the keys of confirmed cases retrieved
-     * from your internet-accessible server to the Google Play service once requested from the
-     * API. Information about the file format is in the Exposure Key Export File Format and
-     * Verification document that is linked from google.com/covid19/exposurenotifications.
+     * Inserts a list of files that contain key information into the on-device database.
+     * Provide the keys of confirmed cases retrieved from your internet-accessible server to
+     * the Google Play service once requested from the API. Information about the file format
+     * is in the Exposure Key Export File Format and Verification document that is linked
+     * from google.com/covid19/exposurenotifications.
      *
      * @param keyFiles
-     * @param configuration
-     * @param token
      * @return
      */
-    suspend fun provideDiagnosisKeys(
-        keyFiles: Collection<File>,
-        configuration: ExposureConfiguration?,
-        token: String
-    ): Boolean
+
+    suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt
index d9bd53506983a3d65e700bd694c21e6ca8d2a618..b671c3502ad5bf0fc014461b1e182895a6c25875 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt
@@ -86,6 +86,10 @@ class SubmissionQuota @Inject constructor(
 
     companion object {
         @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-        internal const val DEFAULT_QUOTA = 20
+        /**
+         * This quota should be 6 when using ExposureWindow
+         * See: https://developers.google.com/android/exposure-notifications/release-notes
+         */
+        internal const val DEFAULT_QUOTA = 6
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5f83c9bba28645b8e7e2e6b1cbb298dbdc71dd00
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.nearby.modules.exposurewindow
+
+import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+@Singleton
+class DefaultExposureWindowProvider @Inject constructor(
+    private val client: ExposureNotificationClient
+) : ExposureWindowProvider {
+    override suspend fun exposureWindows(): List<ExposureWindow> = suspendCoroutine { cont ->
+        client.exposureWindows
+            .addOnSuccessListener { cont.resume(it) }
+            .addOnFailureListener { cont.resumeWithException(it) }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..713715c879f0f75746ff3503d4d1116875d7fca7
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt
@@ -0,0 +1,7 @@
+package de.rki.coronawarnapp.nearby.modules.exposurewindow
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+
+interface ExposureWindowProvider {
+    suspend fun exposureWindows(): List<ExposureWindow>
+}
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 1c2581e299982b099739d281873f7e971bab9c3b..c2319109f5914784469d885563b9d82fb7511809 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
@@ -3,9 +3,10 @@ package de.rki.coronawarnapp.nearby.modules.tracing
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
 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.CoroutineScope
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.channels.sendBlocking
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
@@ -23,28 +24,32 @@ import kotlin.coroutines.suspendCoroutine
 
 @Singleton
 class DefaultTracingStatus @Inject constructor(
-    private val client: ExposureNotificationClient
+    private val client: ExposureNotificationClient,
+    @AppScope val scope: CoroutineScope
 ) : TracingStatus {
 
     override val isTracingEnabled: Flow<Boolean> = callbackFlow<Boolean> {
-        var isRunning = true
-        while (isRunning && isActive) {
+        while (true) {
             try {
-                sendBlocking(pollIsEnabled())
+                send(pollIsEnabled())
             } catch (e: Exception) {
                 Timber.w(e, "ENF isEnabled failed.")
-                sendBlocking(false)
+                send(false)
                 e.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null)
                 cancel("ENF isEnabled failed", e)
             }
+            if (!isActive) break
             delay(POLLING_DELAY_MS)
         }
-        awaitClose { isRunning = false }
     }
         .distinctUntilChanged()
         .onStart { Timber.v("isTracingEnabled FLOW start") }
         .onEach { Timber.v("isTracingEnabled FLOW emission: %b", it) }
         .onCompletion { Timber.v("isTracingEnabled FLOW completed.") }
+        .shareLatest(
+            tag = TAG,
+            scope = scope
+        )
 
     private suspend fun pollIsEnabled(): Boolean = suspendCoroutine { cont ->
         client.isEnabled
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7652fb055bdb64e4cbd76d4528a7047ffbfeef91
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt
@@ -0,0 +1,47 @@
+package de.rki.coronawarnapp.nearby.modules.version
+
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes
+import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+@Singleton
+class DefaultENFVersion @Inject constructor(
+    private val client: ExposureNotificationClient
+) : ENFVersion {
+
+    override suspend fun getENFClientVersion(): Long? = try {
+        internalGetENFClientVersion()
+    } catch (e: Exception) {
+        Timber.w(e, "Failed to get ENFClient version.")
+        null
+    }
+
+    override suspend fun requireMinimumVersion(required: Long) {
+        try {
+            val currentVersion = internalGetENFClientVersion()
+            if (currentVersion < required) {
+                val error = OutdatedENFVersionException(current = currentVersion, required = required)
+                Timber.e(error, "Version requirement not satisfied.")
+                throw error
+            } else {
+                Timber.d("Version requirement satisfied: current=$currentVersion, required=$required")
+            }
+        } catch (apiException: ApiException) {
+            if (apiException.statusCode != CommonStatusCodes.API_NOT_CONNECTED) {
+                throw apiException
+            }
+        }
+    }
+
+    private suspend fun internalGetENFClientVersion(): Long = suspendCoroutine { cont ->
+        client.version
+            .addOnSuccessListener { cont.resume(it) }
+            .addOnFailureListener { cont.resumeWithException(it) }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b7d16994a91e0742d3910f3c22fd7323b31b6857
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.nearby.modules.version
+
+interface ENFVersion {
+    /**
+     * May return null if the API is currently not connected.
+     */
+    suspend fun getENFClientVersion(): Long?
+
+    /**
+     * Throws an [OutdatedENFVersionException] if the client runs an old unsupported version of the ENF
+     * If the API is currently not connected, no exception will be thrown, we expect this to only be a temporary state
+     */
+    suspend fun requireMinimumVersion(required: Long)
+
+    companion object {
+        const val V1_6 = 16000000L
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/OutdatedENFVersionException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/OutdatedENFVersionException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5cf38d6fb2e81becdbce33e9fba99fac2ec6127b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/OutdatedENFVersionException.kt
@@ -0,0 +1,6 @@
+package de.rki.coronawarnapp.nearby.modules.version
+
+class OutdatedENFVersionException(
+    val current: Long,
+    val required: Long
+) : Exception("Client is using an outdated ENF version: current=$current, required=$required")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt
index dab0da7f05f50d7b6b75c1e4d81d2187113f4c00..72a049c16999f42880d6a0d159cf2501ecda5088 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt
@@ -11,12 +11,11 @@ import com.google.android.gms.nearby.exposurenotification.ExposureNotificationCl
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient.EXTRA_TOKEN
 import dagger.android.AndroidInjection
 import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL
-import de.rki.coronawarnapp.exception.NoTokenException
 import de.rki.coronawarnapp.exception.UnknownBroadcastException
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
-import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
+import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection.Result
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import kotlinx.coroutines.CoroutineScope
@@ -30,11 +29,8 @@ import javax.inject.Inject
  * new keys are processed. Then the [ExposureStateUpdateReceiver] will receive the corresponding action in its
  * [onReceive] function.
  *
- * Inside this receiver no further action or calculation will be done but it is rather used to inform the
- * [de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction] that the processing of the diagnosis keys is
- * finished and the Exposure Summary can be retrieved in order to calculate a risk level to show to the user.
- *
- * @see de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
+ * Inside this receiver no further action or calculation will be done but it is rather used to start
+ * a worker that launches the RiskLevelTask which then makes use of the new data this notifies us of.
  *
  */
 class ExposureStateUpdateReceiver : BroadcastReceiver() {
@@ -42,25 +38,33 @@ class ExposureStateUpdateReceiver : BroadcastReceiver() {
     @Inject @AppScope lateinit var scope: CoroutineScope
     @Inject lateinit var dispatcherProvider: DispatcherProvider
     @Inject lateinit var exposureDetectionTracker: ExposureDetectionTracker
-    lateinit var context: Context
+    @Inject lateinit var workManager: WorkManager
 
     override fun onReceive(context: Context, intent: Intent) {
         Timber.tag(TAG).d("onReceive(context=%s, intent=%s)", context, intent)
         AndroidInjection.inject(this, context)
-        this.context = context
 
         val action = intent.action
         Timber.tag(TAG).v("Looking up action: %s", action)
 
         val async = goAsync()
-        scope.launch(context = dispatcherProvider.Default) {
+
+        scope.launch(context = scope.coroutineContext) {
             try {
-                when (action) {
-                    ACTION_EXPOSURE_STATE_UPDATED -> processStateUpdates(intent)
-                    ACTION_EXPOSURE_NOT_FOUND -> processNotFound(intent)
-                    else -> throw UnknownBroadcastException(action)
+                intent.getStringExtra(EXTRA_TOKEN)?.let {
+                    Timber.tag(TAG).w("Received unknown token from ENF: %s", it)
                 }
+
+                trackDetection(action)
+
+                val data = Data.Builder().build()
+                OneTimeWorkRequest
+                    .Builder(ExposureStateUpdateWorker::class.java)
+                    .setInputData(data)
+                    .build()
+                    .let { workManager.enqueue(it) }
             } catch (e: Exception) {
+                Timber.e(e, "Failed to process intent.")
                 e.report(INTERNAL)
             } finally {
                 Timber.tag(TAG).i("Finished processing broadcast.")
@@ -69,45 +73,18 @@ class ExposureStateUpdateReceiver : BroadcastReceiver() {
         }
     }
 
-    private fun processStateUpdates(intent: Intent) {
-        Timber.tag(TAG).i("Processing ACTION_EXPOSURE_STATE_UPDATED")
-
-        val workManager = WorkManager.getInstance(context)
-
-        val token = intent.requireToken()
-
-        val data = Data
-            .Builder()
-            .putString(EXTRA_TOKEN, token)
-            .build()
-
-        OneTimeWorkRequest
-            .Builder(ExposureStateUpdateWorker::class.java)
-            .setInputData(data)
-            .build()
-            .let { workManager.enqueue(it) }
-
-        exposureDetectionTracker.finishExposureDetection(
-            token,
-            TrackedExposureDetection.Result.UPDATED_STATE
-        )
-    }
-
-    private fun processNotFound(intent: Intent) {
-        Timber.tag(TAG).i("Processing ACTION_EXPOSURE_NOT_FOUND")
-
-        val token = intent.requireToken()
-
-        exposureDetectionTracker.finishExposureDetection(
-            token,
-            TrackedExposureDetection.Result.NO_MATCHES
-        )
+    private fun trackDetection(action: String?) {
+        when (action) {
+            ACTION_EXPOSURE_STATE_UPDATED -> {
+                exposureDetectionTracker.finishExposureDetection(identifier = null, result = Result.UPDATED_STATE)
+            }
+            ACTION_EXPOSURE_NOT_FOUND -> {
+                exposureDetectionTracker.finishExposureDetection(identifier = null, result = Result.NO_MATCHES)
+            }
+            else -> throw UnknownBroadcastException(action)
+        }
     }
 
-    private fun Intent.requireToken(): String = getStringExtra(EXTRA_TOKEN).also {
-        Timber.tag(TAG).v("Extracted token: %s", it)
-    } ?: throw NoTokenException(IllegalArgumentException("no token was found in the intent"))
-
     companion object {
         private val TAG: String? = ExposureStateUpdateReceiver::class.simpleName
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
index 68d9b8b15df9a37596fab84e1438c6272dc14954..ab39dda9e64d0849d655603e9c720ff53819c7f8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt
@@ -1,231 +1,325 @@
 package de.rki.coronawarnapp.risk
 
+import android.text.TextUtils
 import androidx.annotation.VisibleForTesting
-import androidx.core.app.NotificationManagerCompat
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
-import de.rki.coronawarnapp.CoronaWarnApplication
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.appconfig.AppConfigProvider
-import de.rki.coronawarnapp.exception.RiskLevelCalculationException
-import de.rki.coronawarnapp.notification.NotificationHelper
-import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL
-import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS
-import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass.AttenuationDuration
-import de.rki.coronawarnapp.storage.LocalData
-import de.rki.coronawarnapp.storage.RiskLevelRepository
-import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import com.google.android.gms.nearby.exposurenotification.Infectiousness
+import com.google.android.gms.nearby.exposurenotification.ReportType
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
+import de.rki.coronawarnapp.risk.result.AggregatedRiskPerDateResult
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+import de.rki.coronawarnapp.risk.result.RiskResult
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import org.joda.time.Instant
 import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
-import kotlin.math.round
 
 @Singleton
-class DefaultRiskLevels @Inject constructor(
-    private val appConfigProvider: AppConfigProvider
-) : RiskLevels {
-
-    override 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)
-            }
+class DefaultRiskLevels @Inject constructor() : RiskLevels {
 
-            // 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.")
-            }
+    override fun determineRisk(
+        appConfig: ExposureWindowRiskCalculationConfig,
+        exposureWindows: List<ExposureWindow>
+    ): AggregatedRiskResult {
+        val riskResultsPerWindow =
+            exposureWindows.mapNotNull { window ->
+                calculateRisk(appConfig, window)?.let { window to it }
+            }.toMap()
 
-            throw error
-        }
+        return aggregateResults(appConfig, riskResultsPerWindow)
     }
 
-    override fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean {
-        // if the last calculation is longer in the past as the defined threshold we return the stale state
-        val timeSinceLastDiagnosisKeyFetchFromServer =
-            TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer()
-                ?: throw RiskLevelCalculationException(
-                    IllegalArgumentException(
-                        "Time since last exposure calculation is null"
-                    )
-                )
-        /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the
-        defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */
-        return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() >
-            TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold()
-    }
+    private fun ExposureWindow.dropDueToMinutesAtAttenuation(
+        attenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>
+    ) =
+        attenuationFilters.any { attenuationFilter ->
+            // Get total seconds at attenuation in exposure window
+            val secondsAtAttenuation: Double = scanInstances
+                .filter { attenuationFilter.attenuationRange.inRange(it.minAttenuationDb) }
+                .fold(.0) { acc, scanInstance -> acc + scanInstance.secondsSinceLastScan }
 
-    override fun calculationNotPossibleBecauseOfNoKeys() =
-        (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also {
-            if (it) {
-                Timber.tag(TAG)
-                    .v("No last time diagnosis keys from server fetch timestamp was found")
-            }
+            val minutesAtAttenuation = secondsAtAttenuation / 60
+            return attenuationFilter.dropIfMinutesInRange.inRange(minutesAtAttenuation)
         }
 
-    override suspend fun isIncreasedRisk(lastExposureSummary: ExposureSummary): Boolean {
-        val appConfiguration = appConfigProvider.getAppConfig()
-        Timber.tag(TAG).v("Retrieved configuration from backend")
-        // custom attenuation parameters to weigh the attenuation
-        // values provided by the Google API
-        val attenuationParameters = appConfiguration.attenuationDuration
-        // these are the defined risk classes. They will divide the calculated
-        // risk score into the low and increased risk
-        val riskScoreClassification = appConfiguration.riskScoreClasses
-
-        // calculate the risk score based on the values collected by the Google EN API and
-        // the backend configuration
-        val riskScore = calculateRiskScore(
-            attenuationParameters,
-            lastExposureSummary
-        ).also {
-            Timber.tag(TAG).v("Calculated risk with the given config: $it")
-        }
+    private fun ExposureWindow.determineTransmissionRiskLevel(
+        transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding
+    ): Int {
 
-        // get the high risk score class
-        val highRiskScoreClass =
-            riskScoreClassification.riskClassesList.find { it.label == "HIGH" }
-                ?: throw RiskLevelCalculationException(IllegalStateException("No high risk score class found"))
+        val reportTypeOffset = when (reportType) {
+            ReportType.RECURSIVE -> transmissionRiskLevelEncoding
+                .reportTypeOffsetRecursive
+            ReportType.SELF_REPORT -> transmissionRiskLevelEncoding
+                .reportTypeOffsetSelfReport
+            ReportType.CONFIRMED_CLINICAL_DIAGNOSIS -> transmissionRiskLevelEncoding
+                .reportTypeOffsetConfirmedClinicalDiagnosis
+            ReportType.CONFIRMED_TEST -> transmissionRiskLevelEncoding
+                .reportTypeOffsetConfirmedTest
+            else -> throw UnknownReportTypeException()
+        }
 
-        // if the calculated risk score is above the defined level threshold we return the high level risk score
-        if (withinDefinedLevelThreshold(
-                riskScore,
-                highRiskScoreClass.min,
-                highRiskScoreClass.max
-            )
-        ) {
-            Timber.tag(TAG)
-                .v("$riskScore is above the defined min value ${highRiskScoreClass.min}")
-            return true
-        } else if (riskScore > highRiskScoreClass.max) {
-            throw RiskLevelCalculationException(
-                IllegalStateException("Risk score is above the max threshold for score class")
-            )
+        val infectiousnessOffset = when (infectiousness) {
+            Infectiousness.HIGH -> transmissionRiskLevelEncoding
+                .infectiousnessOffsetHigh
+            else -> transmissionRiskLevelEncoding
+                .infectiousnessOffsetStandard
         }
 
-        return false
+        return reportTypeOffset + infectiousnessOffset
     }
 
-    override fun isActiveTracingTimeAboveThreshold(): Boolean {
-        val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration()
-        val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours()
-        val durationTracingIsActiveThreshold =
-            TimeVariables.getMinActivatedTracingTime().toLong()
+    private fun dropDueToTransmissionRiskLevel(
+        transmissionRiskLevel: Int,
+        transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>
+    ) =
+        transmissionRiskLevelFilters.any {
+            it.dropIfTrlInRange.inRange(transmissionRiskLevel)
+        }
 
-        return (activeTracingDurationInHours >= durationTracingIsActiveThreshold).also {
-            Timber.tag(TAG).v(
-                "Active tracing time ($activeTracingDurationInHours h) is above threshold " +
-                    "($durationTracingIsActiveThreshold h): $it"
-            )
-            if (it) {
-                Timber.tag(TAG).v("Active tracing time is not enough")
-            }
+    private fun ExposureWindow.determineWeightedSeconds(
+        minutesAtAttenuationWeight: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>
+    ): Double =
+        scanInstances.fold(.0) { seconds, scanInstance ->
+            val weight: Double =
+                minutesAtAttenuationWeight
+                    .filter { it.attenuationRange.inRange(scanInstance.minAttenuationDb) }
+                    .map { it.weight }
+                    .firstOrNull() ?: .0
+            seconds + scanInstance.secondsSinceLastScan * weight
         }
-    }
 
-    override fun calculateRiskScore(
-        attenuationParameters: AttenuationDuration,
-        exposureSummary: ExposureSummary
-    ): Double {
-        /** all attenuation values are capped to [TimeVariables.MAX_ATTENUATION_DURATION] */
-        val weightedAttenuationLow =
-            attenuationParameters.weights.low
-                .times(exposureSummary.attenuationDurationsInMinutes[0].capped())
-        val weightedAttenuationMid =
-            attenuationParameters.weights.mid
-                .times(exposureSummary.attenuationDurationsInMinutes[1].capped())
-        val weightedAttenuationHigh =
-            attenuationParameters.weights.high
-                .times(exposureSummary.attenuationDurationsInMinutes[2].capped())
-
-        val maximumRiskScore = exposureSummary.maximumRiskScore.toDouble()
-
-        val defaultBucketOffset = attenuationParameters.defaultBucketOffset.toDouble()
-        val normalizationDivisor = attenuationParameters.riskScoreNormalizationDivisor.toDouble()
-
-        val attenuationStrings =
-            "Weighted Attenuation: ($weightedAttenuationLow + $weightedAttenuationMid + " +
-                "$weightedAttenuationHigh + $defaultBucketOffset)"
-        Timber.v(attenuationStrings)
-
-        val weightedAttenuationDuration =
-            weightedAttenuationLow
-                .plus(weightedAttenuationMid)
-                .plus(weightedAttenuationHigh)
-                .plus(defaultBucketOffset)
-
-        Timber.v("Formula used: ($maximumRiskScore / $normalizationDivisor) * $weightedAttenuationDuration")
-
-        val riskScore = (maximumRiskScore / normalizationDivisor) * weightedAttenuationDuration
-
-        return round(riskScore.times(DECIMAL_MULTIPLIER)).div(DECIMAL_MULTIPLIER)
-    }
+    private fun determineRiskLevel(
+        normalizedTime: Double,
+        timeToRiskLevelMapping: List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>
+    ): ProtoRiskLevel? =
+        timeToRiskLevelMapping
+            .filter { it.normalizedTimeRange.inRange(normalizedTime) }
+            .map { it.riskLevel }
+            .firstOrNull()
 
-    @VisibleForTesting
-    internal fun Int.capped() =
-        if (this > TimeVariables.getMaxAttenuationDuration()) {
-            TimeVariables.getMaxAttenuationDuration()
-        } else {
-            this
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal fun calculateRisk(
+        appConfig: ExposureWindowRiskCalculationConfig,
+        exposureWindow: ExposureWindow
+    ): RiskResult? {
+        if (exposureWindow.dropDueToMinutesAtAttenuation(appConfig.minutesAtAttenuationFilters)) {
+            Timber.d("%s dropped due to minutes at attenuation filter", exposureWindow)
+            return null
         }
 
-    @VisibleForTesting
-    internal fun withinDefinedLevelThreshold(riskScore: Double, min: Int, max: Int) =
-        riskScore >= min && riskScore <= max
-
-    /**
-     * 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()
-        ) {
+        val transmissionRiskLevel: Int = exposureWindow.determineTransmissionRiskLevel(
+            appConfig.transmissionRiskLevelEncoding
+        )
+
+        if (dropDueToTransmissionRiskLevel(transmissionRiskLevel, appConfig.transmissionRiskLevelFilters)) {
             Timber.d(
-                "Notification Permission = ${
-                    NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled()
-                }"
+                "%s dropped due to transmission risk level filter, level is %s",
+                exposureWindow,
+                transmissionRiskLevel
             )
+            return null
+        }
 
-            NotificationHelper.sendNotification(
-                CoronaWarnApplication.getAppContext().getString(R.string.notification_body)
-            )
+        val transmissionRiskValue: Double =
+            transmissionRiskLevel * appConfig.transmissionRiskLevelMultiplier
+
+        Timber.d("%s's transmissionRiskValue is: %s", exposureWindow, transmissionRiskValue)
+
+        val weightedMinutes: Double = exposureWindow.determineWeightedSeconds(
+            appConfig.minutesAtAttenuationWeights
+        ) / 60f
+
+        Timber.d("%s's weightedMinutes are: %s", exposureWindow, weightedMinutes)
+
+        val normalizedTime: Double = transmissionRiskValue * weightedMinutes
+
+        Timber.d("%s's normalizedTime is: %s", exposureWindow, normalizedTime)
+
+        val riskLevel: ProtoRiskLevel? = determineRiskLevel(
+            normalizedTime,
+            appConfig.normalizedTimePerExposureWindowToRiskLevelMapping
+        )
+
+        if (riskLevel == null) {
+            Timber.e("Exposure Window: $exposureWindow could not be mapped to a risk level")
+            throw NormalizedTimePerExposureWindowToRiskLevelMappingMissingException()
+        }
+
+        Timber.d("%s's riskLevel is: %s", exposureWindow, riskLevel)
+
+        return RiskResult(
+            transmissionRiskLevel = transmissionRiskLevel,
+            normalizedTime = normalizedTime,
+            riskLevel = riskLevel
+        )
+    }
 
-            Timber.d("Risk level changed and notification sent. Current Risk level is ${riskLevel.raw}")
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal fun aggregateResults(
+        appConfig: ExposureWindowRiskCalculationConfig,
+        exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
+    ): AggregatedRiskResult {
+        val uniqueDatesMillisSinceEpoch = exposureWindowsAndResult.keys
+            .map { it.dateMillisSinceEpoch }
+            .toSet()
+
+        Timber.d(
+            "uniqueDates: %s", { TextUtils.join(System.lineSeparator(), uniqueDatesMillisSinceEpoch) }
+        )
+        val exposureHistory = uniqueDatesMillisSinceEpoch.map {
+            aggregateRiskPerDate(appConfig, it, exposureWindowsAndResult)
         }
-        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}")
+        Timber.d("exposureHistory size: ${exposureHistory.size}")
+
+        // 6. Determine `Total Risk`
+        val totalRiskLevel =
+            if (exposureHistory.any {
+                    it.riskLevel == RiskCalculationParametersOuterClass
+                        .NormalizedTimeToRiskLevelMapping
+                        .RiskLevel
+                        .HIGH
+                }) {
+                RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH
+            } else {
+                RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW
+            }
+
+        Timber.d("totalRiskLevel: ${totalRiskLevel.name} (${totalRiskLevel.ordinal})")
+
+        // 7. Determine `Date of Most Recent Date with Low Risk`
+        val mostRecentDateWithLowRisk =
+            exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.LOW)
+
+        Timber.d("mostRecentDateWithLowRisk: $mostRecentDateWithLowRisk")
+
+        // 8. Determine `Date of Most Recent Date with High Risk`
+        val mostRecentDateWithHighRisk =
+            exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.HIGH)
+
+        Timber.d("mostRecentDateWithHighRisk: $mostRecentDateWithHighRisk")
+
+        // 9. Determine `Total Minimum Distinct Encounters With Low Risk`
+        val totalMinimumDistinctEncountersWithLowRisk = exposureHistory
+            .sumBy { it.minimumDistinctEncountersWithLowRisk }
+
+        Timber.d("totalMinimumDistinctEncountersWithLowRisk: $totalMinimumDistinctEncountersWithLowRisk")
+
+        // 10. Determine `Total Minimum Distinct Encounters With High Risk`
+        val totalMinimumDistinctEncountersWithHighRisk = exposureHistory
+            .sumBy { it.minimumDistinctEncountersWithHighRisk }
+
+        Timber.d("totalMinimumDistinctEncountersWithHighRisk: $totalMinimumDistinctEncountersWithHighRisk")
+
+        // 11. Determine `Number of Days With Low Risk`
+        val numberOfDaysWithLowRisk =
+            exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.LOW)
+
+        Timber.d("numberOfDaysWithLowRisk: $numberOfDaysWithLowRisk")
+
+        // 12. Determine `Number of Days With High Risk`
+        val numberOfDaysWithHighRisk =
+            exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.HIGH)
+
+        Timber.d("numberOfDaysWithHighRisk: $numberOfDaysWithHighRisk")
+
+        return AggregatedRiskResult(
+            totalRiskLevel = totalRiskLevel,
+            totalMinimumDistinctEncountersWithLowRisk = totalMinimumDistinctEncountersWithLowRisk,
+            totalMinimumDistinctEncountersWithHighRisk = totalMinimumDistinctEncountersWithHighRisk,
+            mostRecentDateWithLowRisk = mostRecentDateWithLowRisk,
+            mostRecentDateWithHighRisk = mostRecentDateWithHighRisk,
+            numberOfDaysWithLowRisk = numberOfDaysWithLowRisk,
+            numberOfDaysWithHighRisk = numberOfDaysWithHighRisk
+        )
+    }
+
+    private fun List<AggregatedRiskPerDateResult>.mostRecentDateForRisk(riskLevel: ProtoRiskLevel): Instant? =
+        filter { it.riskLevel == riskLevel }
+            .maxOfOrNull { it.dateMillisSinceEpoch }
+            ?.let { Instant.ofEpochMilli(it) }
+
+    private fun List<AggregatedRiskPerDateResult>.numberOfDaysForRisk(riskLevel: ProtoRiskLevel): Int =
+        filter { it.riskLevel == riskLevel }
+            .size
+
+    private fun aggregateRiskPerDate(
+        appConfig: ExposureWindowRiskCalculationConfig,
+        dateMillisSinceEpoch: Long,
+        exposureWindowsAndResult: Map<ExposureWindow, RiskResult>
+    ): AggregatedRiskPerDateResult {
+        // 1. Group `Exposure Windows by Date`
+        val exposureWindowsAndResultForDate = exposureWindowsAndResult
+            .filter { it.key.dateMillisSinceEpoch == dateMillisSinceEpoch }
+
+        // 2. Determine `Normalized Time per Date`
+        val normalizedTime = exposureWindowsAndResultForDate.values
+            .sumOf { it.normalizedTime }
+
+        Timber.d("Aggregating result for date $dateMillisSinceEpoch - ${Instant.ofEpochMilli(dateMillisSinceEpoch)}")
+
+        // 3. Determine `Risk Level per Date`
+        val riskLevel = try {
+            appConfig.normalizedTimePerDayToRiskLevelMappingList
+                .filter { it.normalizedTimeRange.inRange(normalizedTime) }
+                .map { it.riskLevel }
+                .first()
+        } catch (e: Exception) {
+            throw NormalizedTimePerDayToRiskLevelMappingMissingException()
         }
-        RiskLevelRepository.setRiskLevelScore(riskLevel)
+
+        Timber.d("riskLevel: ${riskLevel.name} (${riskLevel.ordinal})")
+
+        // 4. Determine `Minimum Distinct Encounters With Low Risk per Date`
+        val minimumDistinctEncountersWithLowRisk =
+            exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.LOW)
+
+        Timber.d("minimumDistinctEncountersWithLowRisk: $minimumDistinctEncountersWithLowRisk")
+
+        // 5. Determine `Minimum Distinct Encounters With High Risk per Date`
+        val minimumDistinctEncountersWithHighRisk =
+            exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.HIGH)
+
+        Timber.d("minimumDistinctEncountersWithHighRisk: $minimumDistinctEncountersWithHighRisk")
+
+        return AggregatedRiskPerDateResult(
+            dateMillisSinceEpoch = dateMillisSinceEpoch,
+            riskLevel = riskLevel,
+            minimumDistinctEncountersWithLowRisk = minimumDistinctEncountersWithLowRisk,
+            minimumDistinctEncountersWithHighRisk = minimumDistinctEncountersWithHighRisk
+        )
     }
 
+    private fun Map<ExposureWindow, RiskResult>.minimumDistinctEncountersForRisk(riskLevel: ProtoRiskLevel): Int =
+        filter { it.value.riskLevel == riskLevel }
+            .map { "${it.value.transmissionRiskLevel}_${it.key.calibrationConfidence}" }
+            .distinct()
+            .size
+
     companion object {
-        private val TAG = DefaultRiskLevels::class.java.simpleName
-        private const val DECIMAL_MULTIPLIER = 100
+
+        open class RiskLevelMappingMissingException(msg: String) : Exception(msg)
+
+        class NormalizedTimePerExposureWindowToRiskLevelMappingMissingException : RiskLevelMappingMissingException(
+            "Failed to map the normalized Time per Exposure Window to a Risk Level"
+        )
+
+        class NormalizedTimePerDayToRiskLevelMappingMissingException : RiskLevelMappingMissingException(
+            "Failed to map the normalized Time per Day to a Risk Level"
+        )
+
+        class UnknownReportTypeException : Exception(
+            "The Report Type returned by the ENF is not known"
+        )
+
+        private fun <T : Number> RiskCalculationParametersOuterClass.Range.inRange(value: T): Boolean =
+            when {
+                minExclusive && value.toDouble() <= min -> false
+                !minExclusive && value.toDouble() < min -> false
+                maxExclusive && value.toDouble() >= max -> false
+                !maxExclusive && value.toDouble() > max -> false
+                else -> true
+            }
     }
 }
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
new file mode 100644
index 0000000000000000000000000000000000000000..3b26fc49702912b12b519529d3df6e2eced4c749
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt
@@ -0,0 +1,30 @@
+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/ProtoRiskLevel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ProtoRiskLevel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ff0013d8e28a58ced5e842815589e6b79196af97
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ProtoRiskLevel.kt
@@ -0,0 +1,5 @@
+package de.rki.coronawarnapp.risk
+
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+
+typealias ProtoRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
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/RiskLevelData.kt
new file mode 100644
index 0000000000000000000000000000000000000000..83372c3f5d5d671f21c29ceb1c478e23df6fed79
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt
@@ -0,0 +1,31 @@
+package de.rki.coronawarnapp.risk
+
+import android.content.Context
+import androidx.core.content.edit
+import de.rki.coronawarnapp.util.di.AppContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RiskLevelData @Inject constructor(
+    @AppContext private val context: Context
+) {
+
+    private val prefs by lazy {
+        context.getSharedPreferences(NAME_SHARED_PREFS, Context.MODE_PRIVATE)
+    }
+
+    /**
+     * The identifier of the config used during the last risklevel calculation
+     */
+    var lastUsedConfigIdentifier: String?
+        get() = prefs.getString(PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID, null)
+        set(value) = prefs.edit(true) {
+            putString(PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID, value)
+        }
+
+    companion object {
+        private const val NAME_SHARED_PREFS = "risklevel_localdata"
+        private const val PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID = "risklevel.config.identifier.last"
+    }
+}
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 81959b959a242b1a4640ffd23c399780f48cea1a..8069454c2b5225e5b157f831777d184519d483d7 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,12 +1,18 @@
 package de.rki.coronawarnapp.risk
 
 import android.content.Context
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
+import androidx.annotation.VisibleForTesting
+import androidx.core.app.NotificationManagerCompat
+import de.rki.coronawarnapp.CoronaWarnApplication
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.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.nearby.InternalExposureNotificationClient
+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
@@ -22,6 +28,7 @@ import de.rki.coronawarnapp.task.TaskFactory
 import de.rki.coronawarnapp.task.common.DefaultProgress
 import de.rki.coronawarnapp.util.BackgroundModeStatus
 import de.rki.coronawarnapp.util.ConnectivityHelper.isNetworkEnabled
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.di.AppContext
 import kotlinx.coroutines.channels.ConflatedBroadcastChannel
@@ -38,7 +45,10 @@ class RiskLevelTask @Inject constructor(
     @AppContext private val context: Context,
     private val enfClient: ENFClient,
     private val timeStamper: TimeStamper,
-    private val backgroundModeStatus: BackgroundModeStatus
+    private val backgroundModeStatus: BackgroundModeStatus,
+    private val riskLevelData: RiskLevelData,
+    private val appConfigProvider: AppConfigProvider,
+    private val exposureResultStore: ExposureResultStore
 ) : Task<DefaultProgress, RiskLevelTask.Result> {
 
     private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>()
@@ -49,8 +59,7 @@ class RiskLevelTask @Inject constructor(
     override suspend fun run(arguments: Task.Arguments): Result {
         try {
             Timber.d("Running with arguments=%s", arguments)
-            // If there is no connectivity the transaction will set the last calculated
-            // risk level
+            // If there is no connectivity the transaction will set the last calculated risk level
             if (!isNetworkEnabled(context)) {
                 RiskLevelRepository.setLastCalculatedRiskLevelAsCurrent()
                 return Result(UNDETERMINED)
@@ -60,36 +69,37 @@ class RiskLevelTask @Inject constructor(
                 return Result(NO_CALCULATION_POSSIBLE_TRACING_OFF)
             }
 
-            with(riskLevels) {
-                return Result(
-                    when {
-                        calculationNotPossibleBecauseOfNoKeys().also {
-                            checkCancel()
-                        } -> UNKNOWN_RISK_INITIAL
-
-                        calculationNotPossibleBecauseOfOutdatedResults().also {
-                            checkCancel()
-                        } -> if (backgroundJobsEnabled()) {
-                            UNKNOWN_RISK_OUTDATED_RESULTS
-                        } else {
-                            UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL
-                        }
-
-                        isIncreasedRisk(getNewExposureSummary()).also {
-                            checkCancel()
-                        } -> INCREASED_RISK
-
-                        !isActiveTracingTimeAboveThreshold().also {
-                            checkCancel()
-                        } -> UNKNOWN_RISK_INITIAL
-
-                        else -> LOW_LEVEL_RISK
-                    }.also {
+            val configData: ConfigData = appConfigProvider.getAppConfig()
+
+            return Result(
+                when {
+                    calculationNotPossibleBecauseOfNoKeys().also {
+                        checkCancel()
+                    } -> UNKNOWN_RISK_INITIAL
+
+                    calculationNotPossibleBecauseOfOutdatedResults().also {
                         checkCancel()
-                        updateRepository(it, timeStamper.nowUTC.millis)
+                    } -> if (backgroundJobsEnabled()) {
+                        UNKNOWN_RISK_OUTDATED_RESULTS
+                    } else {
+                        UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL
                     }
-                )
-            }
+
+                    isIncreasedRisk(configData).also {
+                        checkCancel()
+                    } -> INCREASED_RISK
+
+                    !isActiveTracingTimeAboveThreshold().also {
+                        checkCancel()
+                    } -> UNKNOWN_RISK_INITIAL
+
+                    else -> LOW_LEVEL_RISK
+                }.also {
+                    checkCancel()
+                    updateRepository(it, timeStamper.nowUTC.millis)
+                    riskLevelData.lastUsedConfigIdentifier = configData.identifier
+                }
+            )
         } catch (error: Exception) {
             Timber.tag(TAG).e(error)
             error.report(ExceptionCategory.EXPOSURENOTIFICATION)
@@ -100,22 +110,119 @@ class RiskLevelTask @Inject constructor(
         }
     }
 
+    private fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean {
+        // if the last calculation is longer in the past as the defined threshold we return the stale state
+        val timeSinceLastDiagnosisKeyFetchFromServer =
+            TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer()
+                ?: throw RiskLevelCalculationException(
+                    IllegalArgumentException("Time since last exposure calculation is null")
+                )
+        /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the
+        defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */
+        return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() >
+            TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold()
+    }
+
+    private fun calculationNotPossibleBecauseOfNoKeys() =
+        (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also {
+            if (it) {
+                Timber.tag(TAG)
+                    .v("No last time diagnosis keys from server fetch timestamp was found")
+            }
+        }
+
+    private fun isActiveTracingTimeAboveThreshold(): Boolean {
+        val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration()
+        val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours()
+        val durationTracingIsActiveThreshold = TimeVariables.getMinActivatedTracingTime().toLong()
+
+        return (activeTracingDurationInHours >= durationTracingIsActiveThreshold).also {
+            Timber.tag(TAG).v(
+                "Active tracing time ($activeTracingDurationInHours h) is above threshold " +
+                    "($durationTracingIsActiveThreshold h): $it"
+            )
+        }
+    }
+
+    private suspend fun isIncreasedRisk(configData: ExposureWindowRiskCalculationConfig): Boolean {
+        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
+            } 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.")
+            }
+
+            throw error
+        }
+    }
+
     /**
-     * If there is no persisted exposure summary we try to get a new one with the last persisted
-     * Google API token that was used in the [de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction]
+     * Updates the Risk Level Score in the repository with the calculated Risk Level
      *
-     * @return a exposure summary from the Google Exposure Notification API
+     * @param riskLevel
      */
-    private suspend fun getNewExposureSummary(): ExposureSummary {
-        val googleToken = LocalData.googleApiToken()
-            ?: throw RiskLevelCalculationException(IllegalStateException("Exposure summary is not persisted"))
+    @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()
+                }"
+            )
 
-        val exposureSummary =
-            InternalExposureNotificationClient.asyncGetExposureSummary(googleToken)
+            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
 
-        return exposureSummary.also {
-            Timber.tag(TAG).v("Generated new exposure summary with $googleToken")
+            Timber.d("Risk level changed LocalData is updated. Current Risk level is ${riskLevel.raw}")
         }
+        RiskLevelRepository.setRiskLevelScore(riskLevel)
     }
 
     private fun checkCancel() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt
index b8cd2f00c6b4ea61636c4593b03d43b65520078d..a3ee1addcbcb891f02a737e1e913732362049617 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt
@@ -1,29 +1,13 @@
 package de.rki.coronawarnapp.risk
 
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
-import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
 
 interface RiskLevels {
 
-    fun calculationNotPossibleBecauseOfNoKeys(): Boolean
-
-    fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean
-
-    /**
-     * true if threshold is reached / if the duration of the activated tracing time is above the
-     * defined value
-     */
-    fun isActiveTracingTimeAboveThreshold(): Boolean
-
-    suspend fun isIncreasedRisk(lastExposureSummary: ExposureSummary): Boolean
-
-    fun updateRepository(
-        riskLevel: RiskLevel,
-        time: Long
-    )
-
-    fun calculateRiskScore(
-        attenuationParameters: AttenuationDurationOuterClass.AttenuationDuration,
-        exposureSummary: ExposureSummary
-    ): Double
+    fun determineRisk(
+        appConfig: ExposureWindowRiskCalculationConfig,
+        exposureWindows: List<ExposureWindow>
+    ): AggregatedRiskResult
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..99c140888f23c365690f3671430e015cf966d96b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.risk.result
+
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+
+data class AggregatedRiskPerDateResult(
+    val dateMillisSinceEpoch: Long,
+    val riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel,
+    val minimumDistinctEncountersWithLowRisk: Int,
+    val minimumDistinctEncountersWithHighRisk: Int
+)
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
new file mode 100644
index 0000000000000000000000000000000000000000..07595cd56e098af5055339c2ce3cbfcbf07ed19b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.risk.result
+
+import de.rki.coronawarnapp.risk.ProtoRiskLevel
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import org.joda.time.Instant
+
+data class AggregatedRiskResult(
+    val totalRiskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel,
+    val totalMinimumDistinctEncountersWithLowRisk: Int,
+    val totalMinimumDistinctEncountersWithHighRisk: Int,
+    val mostRecentDateWithLowRisk: Instant?,
+    val mostRecentDateWithHighRisk: Instant?,
+    val numberOfDaysWithLowRisk: Int,
+    val numberOfDaysWithHighRisk: Int
+) {
+
+    fun isIncreasedRisk(): Boolean = totalRiskLevel == ProtoRiskLevel.HIGH
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ee04a41efb0ae2a353bb2dcd7e232ca31cd9059c
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.risk.result
+
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+
+data class RiskResult(
+    val transmissionRiskLevel: Int,
+    val normalizedTime: Double,
+    val riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt
index 66fc87e9d7b3b18e41d0a565b070c6c8e4eb2ad4..1f37983a9d50edb4ab09fa0b658c65833b3d1e77 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt
@@ -62,7 +62,6 @@ abstract class AppDatabase : RoomDatabase() {
             val keyRepository = AppInjector.component.keyCacheRepository
             runBlocking { keyRepository.clear() } // TODO this is not nice
             TracingIntervalRepository.resetInstance()
-            ExposureSummaryRepository.resetInstance()
         }
 
         private fun buildDatabase(context: Context): AppDatabase {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/ExposureSummaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/ExposureSummaryRepository.kt
deleted file mode 100644
index bd02029445025f01ca0deee0ece67037445c4874..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/ExposureSummaryRepository.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package de.rki.coronawarnapp.storage
-
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
-import de.rki.coronawarnapp.CoronaWarnApplication
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-
-class ExposureSummaryRepository(private val exposureSummaryDao: ExposureSummaryDao) {
-    companion object {
-        @Volatile
-        private var instance: ExposureSummaryRepository? = null
-
-        private fun getInstance(exposureSummaryDao: ExposureSummaryDao) =
-            instance ?: synchronized(this) {
-                instance ?: ExposureSummaryRepository(exposureSummaryDao).also { instance = it }
-            }
-
-        fun resetInstance() = synchronized(this) {
-            instance = null
-        }
-
-        fun getExposureSummaryRepository(): ExposureSummaryRepository {
-            return getInstance(
-                AppDatabase.getInstance(CoronaWarnApplication.getAppContext())
-                    .exposureSummaryDao()
-            )
-        }
-
-        private val internalMatchedKeyCount = MutableStateFlow(0)
-        val matchedKeyCount: Flow<Int> = internalMatchedKeyCount
-
-        private val internalDaysSinceLastExposure = MutableStateFlow(0)
-        val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure
-    }
-
-    suspend fun getExposureSummaryEntities() = exposureSummaryDao.getExposureSummaryEntities()
-        .map { it.convertToExposureSummary() }
-
-    suspend fun insertExposureSummaryEntity(exposureSummary: ExposureSummary) =
-        ExposureSummaryEntity().apply {
-            this.daysSinceLastExposure = exposureSummary.daysSinceLastExposure
-            this.matchedKeyCount = exposureSummary.matchedKeyCount
-            this.maximumRiskScore = exposureSummary.maximumRiskScore
-            this.summationRiskScore = exposureSummary.summationRiskScore
-            this.attenuationDurationsInMinutes =
-                exposureSummary.attenuationDurationsInMinutes.toTypedArray().toList()
-        }.run {
-            exposureSummaryDao.insertExposureSummaryEntity(this)
-            internalMatchedKeyCount.value = matchedKeyCount
-            internalDaysSinceLastExposure.value = daysSinceLastExposure
-        }
-
-    suspend fun getLatestExposureSummary(token: String) {
-        if (InternalExposureNotificationClient.asyncIsEnabled())
-            InternalExposureNotificationClient.asyncGetExposureSummary(token).also {
-                internalMatchedKeyCount.value = it.matchedKeyCount
-                internalDaysSinceLastExposure.value = it.daysSinceLastExposure
-            }
-    }
-
-    private fun ExposureSummaryEntity.convertToExposureSummary() =
-        ExposureSummary.ExposureSummaryBuilder()
-            .setAttenuationDurations(this.attenuationDurationsInMinutes.toIntArray())
-            .setDaysSinceLastExposure(this.daysSinceLastExposure)
-            .setMatchedKeyCount(this.matchedKeyCount)
-            .setMaximumRiskScore(this.maximumRiskScore)
-            .setSummationRiskScore(this.summationRiskScore)
-            .build()
-}
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 04b08cdd10b8ef86ebcb046d27c86077fdc6fbf9..fd533993e2ecbee697a3ec5fc5c2215ce056b7c1 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
@@ -5,9 +5,11 @@ import androidx.core.content.edit
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.risk.RiskLevel
+import de.rki.coronawarnapp.util.preferences.createFlowPreference
 import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
 import java.util.Date
 
 /**
@@ -381,40 +383,24 @@ object LocalData {
      * SERVER FETCH DATA
      ****************************************************/
 
-    /**
-     * Gets the last time the server fetched the diagnosis keys from the server as Date object
-     * from the EncryptedSharedPrefs
-     *
-     * @return timestamp as Date
-     */
-    // TODO should be changed to Long as well to align with other timestamps
-    fun lastTimeDiagnosisKeysFromServerFetch(): Date? {
-        val time = getSharedPreferenceInstance().getLong(
-            CoronaWarnApplication.getAppContext()
-                .getString(R.string.preference_timestamp_diagnosis_keys_fetch),
-            0L
-        )
-        if (time == 0L) return null
-
-        return Date(time)
+    private val dateMapperForFetchTime: (Long) -> Date? = {
+        if (it != 0L) Date(it) else null
     }
 
-    /**
-     * Sets the last time the server fetched the diagnosis keys from the server as Date object
-     * from the EncryptedSharedPrefs
-     *
-     * @param value timestamp as Date
-     */
-    fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) {
-        getSharedPreferenceInstance().edit(true) {
-            putLong(
-                CoronaWarnApplication.getAppContext()
-                    .getString(R.string.preference_timestamp_diagnosis_keys_fetch),
-                value?.time ?: 0L
-            )
-        }
+    private val lastTimeDiagnosisKeysFetchedFlowPref by lazy {
+        getSharedPreferenceInstance()
+            .createFlowPreference<Long>(key = "preference_timestamp_diagnosis_keys_fetch", 0L)
     }
 
+    fun lastTimeDiagnosisKeysFromServerFetchFlow() = lastTimeDiagnosisKeysFetchedFlowPref.flow
+        .map { dateMapperForFetchTime(it) }
+
+    fun lastTimeDiagnosisKeysFromServerFetch() =
+        dateMapperForFetchTime(lastTimeDiagnosisKeysFetchedFlowPref.value)
+
+    fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) =
+        lastTimeDiagnosisKeysFetchedFlowPref.update { value?.time ?: 0L }
+
     /**
      * Gets the last time of successful risk level calculation as long
      * from the EncryptedSharedPrefs
@@ -446,34 +432,6 @@ object LocalData {
         }
     }
 
-    /****************************************************
-     * EXPOSURE NOTIFICATION DATA
-     ****************************************************/
-
-    /**
-     * Gets the last token that was used to provide the diagnosis keys to the Exposure Notification API
-     *
-     * @return UUID as string
-     */
-    fun googleApiToken(): String? = getSharedPreferenceInstance().getString(
-        CoronaWarnApplication.getAppContext()
-            .getString(R.string.preference_string_google_api_token),
-        null
-    )
-
-    /**
-     * Sets the last token that was used to provide the diagnosis keys to the Exposure Notification API
-     *
-     * @param value UUID as string
-     */
-    fun googleApiToken(value: String?) = getSharedPreferenceInstance().edit(true) {
-        putString(
-            CoronaWarnApplication.getAppContext()
-                .getString(R.string.preference_string_google_api_token),
-            value
-        )
-    }
-
     /****************************************************
      * SETTINGS DATA
      ****************************************************/
@@ -705,19 +663,6 @@ object LocalData {
         CoronaWarnApplication.getAppContext().getString(R.string.preference_teletan), null
     )
 
-    fun backgroundNotification(value: Boolean) = getSharedPreferenceInstance().edit(true) {
-        putBoolean(
-            CoronaWarnApplication.getAppContext()
-                .getString(R.string.preference_background_notification),
-            value
-        )
-    }
-
-    fun backgroundNotification(): Boolean = getSharedPreferenceInstance().getBoolean(
-        CoronaWarnApplication.getAppContext()
-            .getString(R.string.preference_background_notification), false
-    )
-
     /****************************************************
      * ENCRYPTED SHARED PREFERENCES HANDLING
      ****************************************************/
@@ -740,4 +685,8 @@ object LocalData {
                 putBoolean(PREFERENCE_INTEROPERABILITY_IS_USED_AT_LEAST_ONCE, value)
             }
         }
+
+    fun clear() {
+        lastTimeDiagnosisKeysFetchedFlowPref.update { 0L }
+    }
 }
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
index 559318598d7d747f8e540e6f3bcf389a2cdcf8d5..a4d580da04fefd742dce2756adef1343e28f14ff 100644
--- 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
@@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
 
 object RiskLevelRepository {
 
-    private val internalRisklevelScore = MutableStateFlow(RiskLevelConstants.UNKNOWN_RISK_INITIAL)
+    private val internalRisklevelScore = MutableStateFlow(getLastSuccessfullyCalculatedScore().raw)
     val riskLevelScore: Flow<Int> = internalRisklevelScore
 
     private val internalRiskLevelScoreLastSuccessfulCalculated =
@@ -78,7 +78,7 @@ object RiskLevelRepository {
      *
      * @return
      */
-    private fun getLastSuccessfullyCalculatedScore(): RiskLevel =
+    fun getLastSuccessfullyCalculatedScore(): RiskLevel =
         LocalData.lastSuccessfullyCalculatedRiskLevel()
 
     /**
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt
index 21d0216e4ee4e69161d478dc9d1830b709f03294..9fa27a5ef59fa1b7d671376196553a5986027741 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt
@@ -1,8 +1,6 @@
 package de.rki.coronawarnapp.storage
 
 import androidx.annotation.VisibleForTesting
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException
 import de.rki.coronawarnapp.exception.http.CwaWebException
@@ -10,9 +8,9 @@ import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.playbook.BackgroundNoise
 import de.rki.coronawarnapp.service.submission.SubmissionService
 import de.rki.coronawarnapp.submission.SubmissionSettings
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.util.DeviceUIState
-import de.rki.coronawarnapp.util.Event
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
+import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.formatter.TestResult
@@ -45,17 +43,12 @@ class SubmissionRepository @Inject constructor(
         }
     }
 
-    private val uiStateErrorInternal = MutableLiveData<Event<CwaWebException>>(null)
-    val uiStateError: LiveData<Event<CwaWebException>> = uiStateErrorInternal
-
-    private val uiStateStateFlowInternal = MutableStateFlow(ApiRequestState.IDLE)
-    val uiStateStateFlow: Flow<ApiRequestState> = uiStateStateFlowInternal
-
     private val testResultReceivedDateFlowInternal = MutableStateFlow(Date())
     val testResultReceivedDateFlow: Flow<Date> = testResultReceivedDateFlowInternal
 
-    private val deviceUIStateFlowInternal = MutableStateFlow(DeviceUIState.UNPAIRED)
-    val deviceUIStateFlow: Flow<DeviceUIState> = deviceUIStateFlowInternal
+    private val deviceUIStateFlowInternal =
+        MutableStateFlow<NetworkRequestWrapper<DeviceUIState, Throwable>>(NetworkRequestWrapper.RequestIdle)
+    val deviceUIStateFlow: Flow<NetworkRequestWrapper<DeviceUIState, Throwable>> = deviceUIStateFlowInternal
 
     // to be used by new submission flow screens
     val hasGivenConsentToSubmission = submissionSettings.hasGivenConsent.flow
@@ -88,29 +81,29 @@ class SubmissionRepository @Inject constructor(
     fun refreshDeviceUIState(refreshTestResult: Boolean = true) {
         var refresh = refreshTestResult
 
-        deviceUIStateFlowInternal.value.let {
+        deviceUIStateFlowInternal.value.withSuccess {
             if (it != DeviceUIState.PAIRED_NO_RESULT && it != DeviceUIState.UNPAIRED) {
                 refresh = false
                 Timber.d("refreshDeviceUIState: Change refresh, state ${it.name} doesn't require refresh")
             }
         }
 
-        uiStateStateFlowInternal.value = ApiRequestState.STARTED
+        deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestStarted
+
         scope.launch {
             try {
                 deviceUIStateFlowInternal.value = refreshUIState(refresh)
-                uiStateStateFlowInternal.value = ApiRequestState.SUCCESS
             } catch (err: CwaWebException) {
-                uiStateErrorInternal.postValue(Event(err))
-                uiStateStateFlowInternal.value = ApiRequestState.FAILED
+                deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestFailed(err)
             } catch (err: Exception) {
+                deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestFailed(err)
                 err.report(ExceptionCategory.INTERNAL)
             }
         }
     }
 
     // TODO this should be more UI agnostic
-    suspend fun refreshUIState(refreshTestResult: Boolean): DeviceUIState {
+    suspend fun refreshUIState(refreshTestResult: Boolean): NetworkRequestWrapper<DeviceUIState, Throwable> {
         var uiState = DeviceUIState.UNPAIRED
 
         if (LocalData.submissionWasSuccessful()) {
@@ -129,31 +122,30 @@ class SubmissionRepository @Inject constructor(
                 }
             }
         }
-        return uiState
+        return NetworkRequestWrapper.RequestSuccessful(uiState)
     }
 
-    suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult {
-        val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid)
+    suspend fun asyncRegisterDeviceViaTAN(tan: String) {
+        val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan)
         LocalData.registrationToken(registrationData.registrationToken)
-        LocalData.testGUID(null)
+        LocalData.teletan(null)
         updateTestResult(registrationData.testResult)
         LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis)
         BackgroundNoise.getInstance().scheduleDummyPattern()
-        return registrationData.testResult
     }
 
-    suspend fun asyncRegisterDeviceViaTAN(tan: String) {
-        val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan)
+    suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult {
+        val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid)
         LocalData.registrationToken(registrationData.registrationToken)
-        LocalData.teletan(null)
+        LocalData.testGUID(null)
         updateTestResult(registrationData.testResult)
         LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis)
         BackgroundNoise.getInstance().scheduleDummyPattern()
+        return registrationData.testResult
     }
 
     fun reset() {
-        uiStateStateFlowInternal.value = ApiRequestState.IDLE
-        deviceUIStateFlowInternal.value = DeviceUIState.UNPAIRED
+        deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestIdle
         revokeConsentToSubmission()
     }
 
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 0658b10604134435f471c5ef6f351372f18bdcb0..75be4b8cf6503bb73bdf1a9dbda5c92e1fad2206 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
@@ -2,8 +2,6 @@ package de.rki.coronawarnapp.storage
 
 import android.content.Context
 import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
 import de.rki.coronawarnapp.risk.RiskLevelTask
@@ -15,18 +13,20 @@ import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.timer.TimerHelper
 import de.rki.coronawarnapp.tracing.TracingProgress
 import de.rki.coronawarnapp.util.ConnectivityHelper
+import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.di.AppContext
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
-import org.joda.time.DateTime
-import org.joda.time.DateTimeZone
-import org.joda.time.Instant
+import org.joda.time.Duration
 import timber.log.Timber
 import java.util.Date
+import java.util.NoSuchElementException
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -43,30 +43,18 @@ class TracingRepository @Inject constructor(
     @AppContext private val context: Context,
     @AppScope private val scope: CoroutineScope,
     private val taskController: TaskController,
-    enfClient: ENFClient
+    enfClient: ENFClient,
+    private val timeStamper: TimeStamper
 ) {
 
-    private val internalLastTimeDiagnosisKeysFetched = MutableStateFlow<Date?>(null)
-    val lastTimeDiagnosisKeysFetched: Flow<Date?> = internalLastTimeDiagnosisKeysFetched
+    val lastTimeDiagnosisKeysFetched: Flow<Date?> = LocalData.lastTimeDiagnosisKeysFromServerFetchFlow()
 
     private val internalActiveTracingDaysInRetentionPeriod = MutableStateFlow(0L)
     val activeTracingDaysInRetentionPeriod: Flow<Long> = internalActiveTracingDaysInRetentionPeriod
 
-    /**
-     * Refresh the last time diagnosis keys fetched date with the current shared preferences state.
-     *
-     * @see LocalData
-     */
-    fun refreshLastTimeDiagnosisKeysFetchedDate() {
-        internalLastTimeDiagnosisKeysFetched.value =
-            LocalData.lastTimeDiagnosisKeysFromServerFetch()
-    }
-
-    private val retrievingDiagnosisKeys = MutableStateFlow(false)
     private val internalIsRefreshing =
-        retrievingDiagnosisKeys.combine(taskController.tasks) { retrievingDiagnosisKeys, tasks ->
-            retrievingDiagnosisKeys || tasks.isRiskLevelTaskRunning()
-        }
+        taskController.tasks.map { it.isDownloadDiagnosisKeysTaskRunning() || it.isRiskLevelTaskRunning() }
+
     val tracingProgress: Flow<TracingProgress> = combine(
         internalIsRefreshing,
         enfClient.isPerformingExposureDetection()
@@ -82,27 +70,31 @@ class TracingRepository @Inject constructor(
         it.taskState.isActive && it.taskState.request.type == RiskLevelTask::class
     }
 
+    private fun List<TaskInfo>.isDownloadDiagnosisKeysTaskRunning() = any {
+        it.taskState.isActive && it.taskState.request.type == DownloadDiagnosisKeysTask::class
+    }
+
     /**
      * Refresh the diagnosis keys. For that isRefreshing is set to true which is displayed in the ui.
      * Afterwards the RetrieveDiagnosisKeysTransaction and the RiskLevelTransaction are started.
      * Regardless of whether the transactions where successful or not the
      * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a
      * successful go through from the RetrievelDiagnosisKeysTransaction.
-     *
-     * @see RiskLevelRepository
      */
     fun refreshDiagnosisKeys() {
         scope.launch {
-            retrievingDiagnosisKeys.value = true
             taskController.submitBlocking(
                 DefaultTaskRequest(
                     DownloadDiagnosisKeysTask::class,
-                    DownloadDiagnosisKeysTask.Arguments()
+                    DownloadDiagnosisKeysTask.Arguments(),
+                    originTag = "TracingRepository.refreshDiagnosisKeys()"
+                )
+            )
+            taskController.submit(
+                DefaultTaskRequest(
+                    RiskLevelTask::class, originTag = "TracingRepository.refreshDiagnosisKeys()"
                 )
             )
-            taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
-            refreshLastTimeDiagnosisKeysFetchedDate()
-            retrievingDiagnosisKeys.value = false
             TimerHelper.startManualKeyRetrievalTimer()
         }
     }
@@ -126,19 +118,6 @@ class TracingRepository @Inject constructor(
      */
     // TODO temp place, this needs to go somewhere better
     fun refreshRiskLevel() {
-
-        // get the current date and the date the diagnosis keys were fetched the last time
-        val currentDate = DateTime(Instant.now(), DateTimeZone.UTC)
-        val lastFetch = DateTime(
-            LocalData.lastTimeDiagnosisKeysFromServerFetch(),
-            DateTimeZone.UTC
-        )
-
-        // check if the keys were not already retrieved today
-        val keysWereNotRetrievedToday =
-            LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
-                currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()
-
         // check if the network is enabled to make the server fetch
         val isNetworkEnabled = ConnectivityHelper.isNetworkEnabled(context)
 
@@ -146,56 +125,50 @@ class TracingRepository @Inject constructor(
         // model the keys are only fetched on button press of the user
         val isBackgroundJobEnabled = ConnectivityHelper.autoModeEnabled(context)
 
-        Timber.tag(TAG).v("Keys were not retrieved today $keysWereNotRetrievedToday")
+        val wasNotYetFetched = LocalData.lastTimeDiagnosisKeysFromServerFetch() == null
+
         Timber.tag(TAG).v("Network is enabled $isNetworkEnabled")
         Timber.tag(TAG).v("Background jobs are enabled $isBackgroundJobEnabled")
+        Timber.tag(TAG).v("Was not yet fetched from server $wasNotYetFetched")
 
-        if (keysWereNotRetrievedToday && isNetworkEnabled && isBackgroundJobEnabled) {
-            // TODO shouldn't access this directly
-            retrievingDiagnosisKeys.value = true
-
-            // start the fetching and submitting of the diagnosis keys
+        if (isNetworkEnabled && isBackgroundJobEnabled) {
             scope.launch {
-                taskController.submitBlocking(
-                    DefaultTaskRequest(
-                        DownloadDiagnosisKeysTask::class,
-                        DownloadDiagnosisKeysTask.Arguments()
+                if (wasNotYetFetched || downloadDiagnosisKeysTaskDidNotRunRecently()) {
+                    Timber.tag(TAG).v("Start the fetching and submitting of the diagnosis keys")
+
+                    taskController.submitBlocking(
+                        DefaultTaskRequest(
+                            DownloadDiagnosisKeysTask::class,
+                            DownloadDiagnosisKeysTask.Arguments(),
+                            originTag = "TracingRepository.refreshRisklevel()"
+                        )
                     )
-                )
-                refreshLastTimeDiagnosisKeysFetchedDate()
-                TimerHelper.checkManualKeyRetrievalTimer()
+                    TimerHelper.checkManualKeyRetrievalTimer()
 
-                taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
-                // TODO shouldn't access this directly
-                retrievingDiagnosisKeys.value = false
+                    taskController.submit(
+                        DefaultTaskRequest(RiskLevelTask::class, originTag = "TracingRepository.refreshRiskLevel()")
+                    )
+                }
             }
         }
     }
 
-    /**
-     * Exposure summary
-     * Refresh the following variables in TracingRepository
-     * - daysSinceLastExposure
-     * - matchedKeysCount
-     *
-     * @see TracingRepository
-     */
-    fun refreshExposureSummary() {
-        scope.launch {
-            try {
-                val token = LocalData.googleApiToken()
-                if (token != null) {
-                    ExposureSummaryRepository.getExposureSummaryRepository()
-                        .getLatestExposureSummary(token)
-                }
-                Timber.tag(TAG).v("retrieved latest exposure summary from db")
-            } catch (e: Exception) {
-                e.report(
-                    ExceptionCategory.EXPOSURENOTIFICATION,
-                    TAG,
-                    null
-                )
-            }
+    private suspend fun downloadDiagnosisKeysTaskDidNotRunRecently(): Boolean {
+        val currentDate = timeStamper.nowUTC
+        val taskLastFinishedAt = try {
+            taskController.tasks.first()
+                .filter { it.taskState.type == DownloadDiagnosisKeysTask::class }
+                .mapNotNull { it.taskState.finishedAt }
+                .sortedDescending()
+                .first()
+        } catch (e: NoSuchElementException) {
+            Timber.tag(TAG).v("download did not run recently - no task with a finishedAt date found")
+            return true
+        }
+
+        return currentDate.isAfter(taskLastFinishedAt.plus(Duration.standardHours(1))).also {
+            Timber.tag(TAG)
+                .v("download did not run recently: %s (last=%s, now=%s)", it, taskLastFinishedAt, currentDate)
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt
index 769df6264d8d24e3abd186fd2acfd57e110e30e4..0743eb1aaa05b52dccde5cde3b2f29e057bded17 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt
@@ -6,6 +6,9 @@ import org.joda.time.LocalDate
 
 @Parcelize
 data class Symptoms(
+    /**
+     * this is null if there are no symptoms or there is no information
+     */
     val startOfSymptoms: StartOf?,
     val symptomIndication: Indication
 ) : Parcelable {
@@ -36,7 +39,7 @@ data class Symptoms(
 
     companion object {
         val NO_INFO_GIVEN = Symptoms(
-            startOfSymptoms = null, // FIXME  should this be null?
+            startOfSymptoms = null,
             symptomIndication = Indication.NO_INFORMATION
         )
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt
index df111a364ba660bc00759312bd44578ea3c371b0..e34681d7755b32565a6865a90c69a688e2184391 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt
@@ -8,7 +8,8 @@ import kotlin.reflect.KClass
 data class DefaultTaskRequest(
     override val type: KClass<out Task<Task.Progress, Task.Result>>,
     override val arguments: Task.Arguments = object : Task.Arguments {},
-    override val id: UUID = UUID.randomUUID()
+    override val id: UUID = UUID.randomUUID(),
+    val originTag: String? = null
 ) : TaskRequest {
 
     fun toNewTask(): DefaultTaskRequest = copy(id = UUID.randomUUID())
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt
index e6b1683ce430f4073e569959a96785f79ae6eec2..caac006b32562d2d7ccffa0881ad81d6eb3eec9a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt
@@ -1,27 +1,58 @@
 package de.rki.coronawarnapp.ui.information
 
+import android.content.Intent
 import android.os.Bundle
 import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityNodeInfo
 import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.findNavController
+import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentInformationBinding
 import de.rki.coronawarnapp.ui.doNavigate
 import de.rki.coronawarnapp.ui.main.MainActivity
 import de.rki.coronawarnapp.util.ExternalActionHelper
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.ui.observe2
+import de.rki.coronawarnapp.util.ui.setGone
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
+import timber.log.Timber
+import javax.inject.Inject
 
 /**
  * Basic Fragment which links to static and web content.
  */
-class InformationFragment : Fragment(R.layout.fragment_information) {
+class InformationFragment : Fragment(R.layout.fragment_information), AutoInject {
+
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val vm: InformationFragmentViewModel by cwaViewModels { viewModelFactory }
 
     private val binding: FragmentInformationBinding by viewBindingLazy()
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+
+        vm.currentENFVersion.observe2(this) {
+            binding.informationEnfVersion.apply {
+                setGone(it == null)
+                text = it
+            }
+        }
+        vm.appVersion.observe2(this) {
+            binding.informationVersion.text = it
+        }
+
+        binding.informationEnfVersion.setOnClickListener {
+            try {
+                startActivity(Intent(ExposureNotificationClient.ACTION_EXPOSURE_NOTIFICATION_SETTINGS))
+            } catch (e: Exception) {
+                Timber.e(e, "Can't open ENF settings.")
+            }
+        }
+
         setButtonOnClickListener()
         setAccessibilityDelegate()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7b7473491c6c067a89555f75535491ffbdd81b27
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt
@@ -0,0 +1,22 @@
+package de.rki.coronawarnapp.ui.information
+
+import dagger.Binds
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
+
+@Module
+abstract class InformationFragmentModule {
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(InformationFragmentViewModel::class)
+    abstract fun informationFragmentViewModel(
+        factory: InformationFragmentViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+
+    @ContributesAndroidInjector
+    abstract fun informationFragment(): InformationFragment
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..27f53ae57b79ddf2ea0c09a62fed833f2125a3b4
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentViewModel.kt
@@ -0,0 +1,34 @@
+package de.rki.coronawarnapp.ui.information
+
+import android.content.Context
+import androidx.lifecycle.asLiveData
+import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.BuildConfig
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.nearby.ENFClient
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.di.AppContext
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+
+class InformationFragmentViewModel @AssistedInject constructor(
+    dispatcherProvider: DispatcherProvider,
+    enfClient: ENFClient,
+    @AppContext private val context: Context
+) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
+
+    val currentENFVersion = flow {
+        val enfVersion = enfClient.getENFClientVersion()
+            ?.let { "ENF ${context.getString(R.string.information_version).format(it)}" }
+        emit(enfVersion)
+    }.asLiveData(context = dispatcherProvider.Default)
+
+    val appVersion = flowOf(
+        context.getString(R.string.information_version).format(BuildConfig.VERSION_NAME)
+    ).asLiveData(context = dispatcherProvider.Default)
+
+    @AssistedInject.Factory
+    interface Factory : SimpleCWAViewModelFactory<InformationFragmentViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
index efb78a5cbf81f040fe8b6cac1c78e64785a86303..fd0e01406f0c89425fac3b81ffb0a5bc0bbcbc8e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
@@ -10,13 +10,11 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentManager
 import androidx.lifecycle.ViewModelProviders
-import androidx.lifecycle.lifecycleScope
 import dagger.android.AndroidInjector
 import dagger.android.DispatchingAndroidInjector
 import dagger.android.HasAndroidInjector
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
-import de.rki.coronawarnapp.playbook.BackgroundNoise
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.ui.base.startActivitySafely
 import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel
@@ -30,7 +28,6 @@ import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
-import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 /**
@@ -105,16 +102,10 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
         settingsViewModel.updateBackgroundJobEnabled(ConnectivityHelper.autoModeEnabled(this))
         scheduleWork()
         checkShouldDisplayBackgroundWarning()
-        doBackgroundNoiseCheck()
+        vm.doBackgroundNoiseCheck()
         deadmanScheduler.schedulePeriodic()
     }
 
-    private fun doBackgroundNoiseCheck() {
-        lifecycleScope.launch {
-            BackgroundNoise.getInstance().foregroundScheduleCheck()
-        }
-    }
-
     private fun showEnergyOptimizedEnabledForBackground() {
         val dialog = DialogHelper.DialogInstance(
             this,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt
index 7a596bfc0666c156fe94d05749be586282b531e7..998c4e89dd36104eba97220c65164608a4961f36 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt
@@ -4,6 +4,7 @@ import dagger.Binds
 import dagger.Module
 import dagger.android.ContributesAndroidInjector
 import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.ui.information.InformationFragmentModule
 import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment
 import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragmentModule
 import de.rki.coronawarnapp.ui.main.home.HomeFragmentModule
@@ -23,7 +24,8 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
         HomeFragmentModule::class,
         RiskDetailsFragmentModule::class,
         SettingFragmentsModule::class,
-        SubmissionFragmentModule::class
+        SubmissionFragmentModule::class,
+        InformationFragmentModule::class
     ]
 )
 abstract class MainActivityModule {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt
index 8345f33c5aafac4b55a784fd5090d4ebb8a6779d..02dbea80d6c3e99fe272b5c8980600a2a8f35a70 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.main
 
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.environment.EnvironmentSetup
+import de.rki.coronawarnapp.playbook.BackgroundNoise
 import de.rki.coronawarnapp.util.CWADebug
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
@@ -28,6 +29,12 @@ class MainActivityViewModel @AssistedInject constructor(
         }
     }
 
+    fun doBackgroundNoiseCheck() {
+        launch {
+            BackgroundNoise.getInstance().foregroundScheduleCheck()
+        }
+    }
+
     @AssistedInject.Factory
     interface Factory : SimpleCWAViewModelFactory<MainActivityViewModel>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt
index aba0e9850f7be7eb0f6945755d7259da9ff4d1b9..af99a6380e0446e1338420c9a1e973b67b68eb96 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt
@@ -5,7 +5,6 @@ import android.os.Bundle
 import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentHomeBinding
 import de.rki.coronawarnapp.util.DialogHelper
@@ -18,7 +17,6 @@ import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
-import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 /**
@@ -100,7 +98,7 @@ class HomeFragment : Fragment(R.layout.fragment_home), AutoInject {
             }
         }
 
-        lifecycleScope.launch { vm.observeTestResultToSchedulePositiveTestResultReminder() }
+        vm.observeTestResultToSchedulePositiveTestResultReminder()
     }
 
     override fun onResume() {
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 5b08c3235b18f20546e4a9c8bd4995d91c528f1f..e1f8d5f6098612b854678ffbb93b89cf3c675b8d 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
@@ -76,10 +76,11 @@ class HomeFragmentViewModel @AssistedInject constructor(
 
     private var isLoweredRiskLevelDialogBeingShown = false
 
-    suspend fun observeTestResultToSchedulePositiveTestResultReminder() =
+    fun observeTestResultToSchedulePositiveTestResultReminder() = launch {
         submissionCardsStateProvider.state
             .first { it.isPositiveSubmissionCardVisible() }
             .also { testResultNotificationService.schedulePositiveTestResultReminder() }
+    }
 
     // TODO only lazy to keep tests going which would break because of LocalData access
     val showLoweredRiskLevelDialog: LiveData<Boolean> by lazy {
@@ -103,8 +104,6 @@ class HomeFragmentViewModel @AssistedInject constructor(
         submissionRepository.refreshDeviceUIState()
         // TODO the ordering here is weird, do we expect these to run in sequence?
         tracingRepository.refreshRiskLevel()
-        tracingRepository.refreshExposureSummary()
-        tracingRepository.refreshLastTimeDiagnosisKeysFetchedDate()
         tracingRepository.refreshActiveTracingDaysInRetentionPeriod()
         TimerHelper.checkManualKeyRetrievalTimer()
         tracingRepository.refreshLastSuccessfullyCalculatedScore()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt
index 6592ad0bae0059a1eb86540ff09d9f3f94515d2a..517a077b37f4a65a2c9b01b65737da156353a039 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt
@@ -3,7 +3,7 @@ package de.rki.coronawarnapp.ui.main.home
 import android.content.Context
 import android.graphics.drawable.Drawable
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
+import de.rki.coronawarnapp.exception.http.CwaServerError
 import de.rki.coronawarnapp.util.DeviceUIState
 import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_ERROR
 import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_NEGATIVE
@@ -12,72 +12,113 @@ import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE
 import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE_TELETAN
 import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_REDEEMED
 import de.rki.coronawarnapp.util.DeviceUIState.SUBMITTED_FINAL
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
+import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
 
 data class SubmissionCardState(
-    val deviceUiState: DeviceUIState,
-    val isDeviceRegistered: Boolean,
-    val uiStateState: ApiRequestState
+    val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>,
+    val isDeviceRegistered: Boolean
 ) {
 
-    fun isRiskCardVisible(): Boolean = deviceUiState != PAIRED_POSITIVE &&
-        deviceUiState != PAIRED_POSITIVE_TELETAN &&
-        deviceUiState != SUBMITTED_FINAL
+    fun isRiskCardVisible(): Boolean =
+        deviceUiState.withSuccess(true) {
+            when (it) {
+                PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN, SUBMITTED_FINAL -> false
+                else -> true
+            }
+        }
 
     fun isUnregisteredCardVisible(): Boolean = !isDeviceRegistered
 
     fun isFetchingCardVisible(): Boolean =
-        isDeviceRegistered && (uiStateState == ApiRequestState.STARTED || uiStateState == ApiRequestState.FAILED)
+        isDeviceRegistered && when (deviceUiState) {
+            is NetworkRequestWrapper.RequestFailed -> deviceUiState.error is CwaServerError
+            is NetworkRequestWrapper.RequestStarted -> true
+            else -> false
+        }
 
     fun isFailedCardVisible(): Boolean =
-        isDeviceRegistered && uiStateState == ApiRequestState.SUCCESS && deviceUiState == PAIRED_REDEEMED
+        isDeviceRegistered && when (deviceUiState) {
+            is NetworkRequestWrapper.RequestFailed -> deviceUiState.error !is CwaServerError
+            is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == PAIRED_REDEEMED
+            else -> false
+        }
 
-    fun isPositiveSubmissionCardVisible(): Boolean = uiStateState == ApiRequestState.SUCCESS &&
-        (deviceUiState == PAIRED_POSITIVE ||
-            deviceUiState == PAIRED_POSITIVE_TELETAN)
+    fun isPositiveSubmissionCardVisible(): Boolean =
+        deviceUiState.withSuccess(false) {
+            when (it) {
+                PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN -> true
+                else -> false
+            }
+        }
 
     fun isSubmissionDoneCardVisible(): Boolean =
-        uiStateState == ApiRequestState.SUCCESS && deviceUiState == SUBMITTED_FINAL
+        when (deviceUiState) {
+            is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == SUBMITTED_FINAL
+            else -> false
+        }
 
     fun isContentCardVisible(): Boolean =
-        uiStateState == ApiRequestState.SUCCESS && (deviceUiState == PAIRED_ERROR ||
-            deviceUiState == PAIRED_NEGATIVE ||
-            deviceUiState == PAIRED_NO_RESULT)
+        deviceUiState.withSuccess(false) {
+            when (it) {
+                PAIRED_ERROR, PAIRED_NEGATIVE, PAIRED_NO_RESULT -> true
+                else -> false
+            }
+        }
 
-    fun getContentCardTitleText(c: Context): String = when (deviceUiState) {
-        PAIRED_ERROR, PAIRED_REDEEMED, PAIRED_NEGATIVE -> R.string.submission_status_card_title_available
-        PAIRED_NO_RESULT -> R.string.submission_status_card_title_pending
-        else -> R.string.submission_status_card_title_pending
-    }.let { c.getString(it) }
+    fun getContentCardTitleText(c: Context): String =
+        deviceUiState.withSuccess(R.string.submission_status_card_title_pending) {
+            when (it) {
+                PAIRED_ERROR, PAIRED_REDEEMED, PAIRED_NEGATIVE -> R.string.submission_status_card_title_available
+                PAIRED_NO_RESULT -> R.string.submission_status_card_title_pending
+                else -> R.string.submission_status_card_title_pending
+            }
+        }.let { c.getString(it) }
 
-    fun getContentCardSubTitleText(c: Context): String = when (deviceUiState) {
-        PAIRED_NEGATIVE -> R.string.submission_status_card_subtitle_negative
-        PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_subtitle_invalid
-        else -> null
-    }?.let { c.getString(it) } ?: ""
+    fun getContentCardSubTitleText(c: Context): String =
+        deviceUiState.withSuccess(null) {
+            when (it) {
+                PAIRED_NEGATIVE -> R.string.submission_status_card_subtitle_negative
+                PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_subtitle_invalid
+                else -> null
+            }
+        }?.let { c.getString(it) } ?: ""
 
-    fun getContentCardSubTitleTextColor(c: Context): Int = when (deviceUiState) {
-        PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen
-        PAIRED_ERROR, PAIRED_REDEEMED -> R.color.colorTextSemanticNeutral
-        else -> R.color.colorTextPrimary1
-    }.let { c.getColor(it) }
+    fun getContentCardSubTitleTextColor(c: Context): Int =
+        deviceUiState.withSuccess(R.color.colorTextPrimary1) {
+            when (it) {
+                PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen
+                PAIRED_ERROR, PAIRED_REDEEMED -> R.color.colorTextSemanticNeutral
+                else -> R.color.colorTextPrimary1
+            }
+        }.let { c.getColor(it) }
 
-    fun isContentCardStatusTextVisible(): Boolean = when (deviceUiState) {
-        PAIRED_NEGATIVE, PAIRED_REDEEMED, PAIRED_ERROR -> true
-        else -> false
-    }
+    fun isContentCardStatusTextVisible(): Boolean =
+        deviceUiState.withSuccess(false) {
+            when (it) {
+                PAIRED_NEGATIVE, PAIRED_REDEEMED, PAIRED_ERROR -> true
+                else -> false
+            }
+        }
 
-    fun getContentCardBodyText(c: Context): String = when (deviceUiState) {
-        PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_body_invalid
-        PAIRED_NEGATIVE -> R.string.submission_status_card_body_negative
-        PAIRED_NO_RESULT -> R.string.submission_status_card_body_pending
-        else -> R.string.submission_status_card_body_pending
-    }.let { c.getString(it) }
+    fun getContentCardBodyText(c: Context): String =
+        deviceUiState.withSuccess(R.string.submission_status_card_body_pending) {
+            when (it) {
+                PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_body_invalid
+                PAIRED_NEGATIVE -> R.string.submission_status_card_body_negative
+                PAIRED_NO_RESULT -> R.string.submission_status_card_body_pending
+                else -> R.string.submission_status_card_body_pending
+            }
+        }.let { c.getString(it) }
 
-    fun getContentCardIcon(c: Context): Drawable? = when (deviceUiState) {
-        PAIRED_NO_RESULT -> R.drawable.ic_main_illustration_pending
-        PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN -> R.drawable.ic_main_illustration_pending
-        PAIRED_NEGATIVE -> R.drawable.ic_main_illustration_negative
-        PAIRED_ERROR, PAIRED_REDEEMED -> R.drawable.ic_main_illustration_invalid
-        else -> R.drawable.ic_main_illustration_invalid
-    }.let { c.getDrawable(it) }
+    fun getContentCardIcon(c: Context): Drawable? =
+        deviceUiState.withSuccess(R.drawable.ic_main_illustration_invalid) {
+            when (it) {
+                PAIRED_NO_RESULT -> R.drawable.ic_main_illustration_pending
+                PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN -> R.drawable.ic_main_illustration_pending
+                PAIRED_NEGATIVE -> R.drawable.ic_main_illustration_negative
+                PAIRED_ERROR, PAIRED_REDEEMED -> R.drawable.ic_main_illustration_invalid
+                else -> R.drawable.ic_main_illustration_invalid
+            }
+        }.let { c.getDrawable(it) }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt
index 0b4e70ab4a2cbd1943e0335df14278e5f0a65e6e..56a9f6fef075385e8e5debda3f8746ea44900fe9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt
@@ -3,8 +3,6 @@ package de.rki.coronawarnapp.ui.main.home
 import dagger.Reusable
 import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.SubmissionRepository
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
-import de.rki.coronawarnapp.util.DeviceUIState
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.onCompletion
@@ -19,12 +17,10 @@ class SubmissionCardsStateProvider @Inject constructor(
 ) {
 
     val state: Flow<SubmissionCardState> = combine(
-        submissionRepository.deviceUIStateFlow,
-        submissionRepository.uiStateStateFlow
+        submissionRepository.deviceUIStateFlow
     ) { args ->
         SubmissionCardState(
-            deviceUiState = args[0] as DeviceUIState,
-            uiStateState = args[1] as ApiRequestState,
+            deviceUiState = args[0],
             isDeviceRegistered = LocalData.registrationToken() != null
         )
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt
index 9d996c01ebf7532defd6ebd9235a603fa05040d7..a710675c0b0b106baf0222a0ccc1355063e5c3ec 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.ui.onboarding
 
-import androidx.lifecycle.viewModelScope
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
@@ -10,7 +9,6 @@ import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import kotlinx.coroutines.launch
 
 class OnboardingTracingFragmentViewModel @AssistedInject constructor(
     private val interoperabilityRepository: InteroperabilityRepository
@@ -25,7 +23,7 @@ class OnboardingTracingFragmentViewModel @AssistedInject constructor(
 
     // Reset tracing state in onboarding
     fun resetTracing() {
-        viewModelScope.launch {
+        launch {
             try {
                 if (InternalExposureNotificationClient.asyncIsEnabled()) {
                     InternalExposureNotificationClient.asyncStop()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt
index 65158317199da7f4678075a8ff16336f16a74176..7f84c13b03b80093910b1d6bf50a9364696aa39a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt
@@ -6,7 +6,6 @@ import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import androidx.activity.OnBackPressedCallback
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultBinding
 import de.rki.coronawarnapp.exception.http.CwaClientError
@@ -14,14 +13,13 @@ import de.rki.coronawarnapp.exception.http.CwaServerError
 import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withFailure
 import de.rki.coronawarnapp.util.di.AutoInject
-import de.rki.coronawarnapp.util.observeEvent
 import de.rki.coronawarnapp.util.ui.doNavigate
 import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
-import kotlinx.coroutines.launch
 import javax.inject.Inject
 
 class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_result),
@@ -78,6 +76,11 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_
 
         viewModel.uiState.observe2(this) {
             binding.uiState = it
+            it.deviceUiState.withFailure {
+                if (it is CwaWebException) {
+                    DialogHelper.showDialog(buildErrorDialog(it))
+                }
+            }
         }
 
         // registers callback when the os level back is pressed
@@ -98,10 +101,6 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_
             DialogHelper.showDialog(tracingRequiredDialog)
         }
 
-        viewModel.uiStateError.observeEvent(viewLifecycleOwner) {
-            DialogHelper.showDialog(buildErrorDialog(it))
-        }
-
         viewModel.showRedeemedTokenWarning.observe2(this) {
             val dialog = DialogHelper.DialogInstance(
                 requireActivity(),
@@ -134,7 +133,7 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_
             }
         }
 
-        lifecycleScope.launch { viewModel.observeTestResultToSchedulePositiveTestResultReminder() }
+        viewModel.observeTestResultToSchedulePositiveTestResultReminder()
     }
 
     override fun onResume() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt
index 08114cc0ed73d83167e804bb258824f66ab6d601..42423b2d532959556bc51559cfdfc82235fd1e47 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt
@@ -3,7 +3,6 @@ package de.rki.coronawarnapp.ui.submission.testresult
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.asLiveData
 import com.squareup.inject.assisted.AssistedInject
-import de.rki.coronawarnapp.exception.http.CwaWebException
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.notification.TestResultNotificationService
 import de.rki.coronawarnapp.storage.LocalData
@@ -11,7 +10,7 @@ import de.rki.coronawarnapp.storage.SubmissionRepository
 import de.rki.coronawarnapp.submission.Symptoms
 import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents
 import de.rki.coronawarnapp.util.DeviceUIState
-import de.rki.coronawarnapp.util.Event
+import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -37,31 +36,36 @@ class SubmissionTestResultViewModel @AssistedInject constructor(
     private val tokenErrorMutex = Mutex()
 
     val uiState: LiveData<TestResultUIState> = combineTransform(
-        submissionRepository.uiStateStateFlow,
         submissionRepository.deviceUIStateFlow,
         submissionRepository.testResultReceivedDateFlow
-    ) { apiRequestState, deviceUiState, resultDate ->
+    ) { deviceUiState, resultDate ->
 
         tokenErrorMutex.withLock {
-            if (!wasRedeemedTokenErrorShown && deviceUiState == DeviceUIState.PAIRED_REDEEMED) {
-                wasRedeemedTokenErrorShown = true
-                showRedeemedTokenWarning.postValue(Unit)
+            if (!wasRedeemedTokenErrorShown) {
+                deviceUiState.withSuccess {
+                    if (it == DeviceUIState.PAIRED_REDEEMED) {
+                        wasRedeemedTokenErrorShown = true
+                        showRedeemedTokenWarning.postValue(Unit)
+                    }
+                }
             }
         }
 
         TestResultUIState(
-            apiRequestState = apiRequestState,
             deviceUiState = deviceUiState,
             testResultReceivedDate = resultDate
         ).let { emit(it) }
     }.asLiveData(context = dispatcherProvider.Default)
 
-    suspend fun observeTestResultToSchedulePositiveTestResultReminder() =
+    fun observeTestResultToSchedulePositiveTestResultReminder() = launch {
         submissionRepository.deviceUIStateFlow
-            .first { it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN }
+            .first { request ->
+                request.withSuccess(false) {
+                    it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN
+                }
+            }
             .also { testResultNotificationService.schedulePositiveTestResultReminder() }
-
-    val uiStateError: LiveData<Event<CwaWebException>> = submissionRepository.uiStateError
+    }
 
     fun onBackPressed() {
         routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt
index b1909eb805d6cbaa923b41336f14d60cb30aa45b..a34a02cad13d8a16e1c550ef47aa4c2ce27dfd2d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt
@@ -1,11 +1,10 @@
 package de.rki.coronawarnapp.ui.submission.testresult
 
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.util.DeviceUIState
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
 import java.util.Date
 
 data class TestResultUIState(
-    val apiRequestState: ApiRequestState,
-    val deviceUiState: DeviceUIState,
+    val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>,
     val testResultReceivedDate: Date?
 )
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 7e2f05d5beb481da9c76f9bcaacf0eb5f77f90ac..80db0f28aba95ec7aa073aa399fdaf769fdf3f4a 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,7 +1,7 @@
 package de.rki.coronawarnapp.ui.tracing.card
 
 import dagger.Reusable
-import de.rki.coronawarnapp.storage.ExposureSummaryRepository
+import de.rki.coronawarnapp.risk.ExposureResultStore
 import de.rki.coronawarnapp.storage.RiskLevelRepository
 import de.rki.coronawarnapp.storage.SettingsRepository
 import de.rki.coronawarnapp.storage.TracingRepository
@@ -20,7 +20,8 @@ class TracingCardStateProvider @Inject constructor(
     tracingStatus: GeneralTracingStatus,
     backgroundModeStatus: BackgroundModeStatus,
     settingsRepository: SettingsRepository,
-    tracingRepository: TracingRepository
+    tracingRepository: TracingRepository,
+    exposureResultStore: ExposureResultStore
 ) {
 
     // TODO Refactor these singletons away
@@ -37,10 +38,10 @@ class TracingCardStateProvider @Inject constructor(
         tracingRepository.tracingProgress.onEach {
             Timber.v("tracingProgress: $it")
         },
-        ExposureSummaryRepository.matchedKeyCount.onEach {
+        exposureResultStore.matchedKeyCount.onEach {
             Timber.v("matchedKeyCount: $it")
         },
-        ExposureSummaryRepository.daysSinceLastExposure.onEach {
+        exposureResultStore.daysSinceLastExposure.onEach {
             Timber.v("daysSinceLastExposure: $it")
         },
         tracingRepository.activeTracingDaysInRetentionPeriod.onEach {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt
index 2075f82ed79aa0788b3aeec2707de2a86c338597..6008444b1d0cb5c7906a92b1cb25014c9dc07d5b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt
@@ -36,8 +36,6 @@ class RiskDetailsFragmentViewModel @AssistedInject constructor(
 
     fun refreshData() {
         tracingRepository.refreshRiskLevel()
-        tracingRepository.refreshExposureSummary()
-        tracingRepository.refreshLastTimeDiagnosisKeysFetchedDate()
         TimerHelper.checkManualKeyRetrievalTimer()
         tracingRepository.refreshActiveTracingDaysInRetentionPeriod()
     }
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 f07401a32682540a0227eaacdc45e854a11d78df..388bca23b501d24bda54b33b85110df0bceb0826 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,7 +1,7 @@
 package de.rki.coronawarnapp.ui.tracing.details
 
 import dagger.Reusable
-import de.rki.coronawarnapp.storage.ExposureSummaryRepository
+import de.rki.coronawarnapp.risk.ExposureResultStore
 import de.rki.coronawarnapp.storage.RiskLevelRepository
 import de.rki.coronawarnapp.storage.SettingsRepository
 import de.rki.coronawarnapp.storage.TracingRepository
@@ -21,7 +21,8 @@ class TracingDetailsStateProvider @Inject constructor(
     tracingStatus: GeneralTracingStatus,
     backgroundModeStatus: BackgroundModeStatus,
     settingsRepository: SettingsRepository,
-    tracingRepository: TracingRepository
+    tracingRepository: TracingRepository,
+    exposureResultStore: ExposureResultStore
 ) {
 
     // TODO Refactore these singletons away
@@ -30,8 +31,8 @@ class TracingDetailsStateProvider @Inject constructor(
         RiskLevelRepository.riskLevelScore,
         RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated,
         tracingRepository.tracingProgress,
-        ExposureSummaryRepository.matchedKeyCount,
-        ExposureSummaryRepository.daysSinceLastExposure,
+        exposureResultStore.matchedKeyCount,
+        exposureResultStore.daysSinceLastExposure,
         tracingRepository.activeTracingDaysInRetentionPeriod,
         tracingRepository.lastTimeDiagnosisKeysFetched,
         backgroundModeStatus.isAutoModeEnabled,
@@ -53,8 +54,8 @@ class TracingDetailsStateProvider @Inject constructor(
         )
         val isInformationBodyNoticeVisible =
             riskDetailPresenter.isInformationBodyNoticeVisible(
-            riskLevelScore
-        )
+                riskLevelScore
+            )
 
         TracingDetailsState(
             tracingStatus = status,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt
index 56ef3c2d2747d7f329aa3a1eb04e769ed12f9ece..489f83c59a0bbe31475fd18214d8637bd4cb2752 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt
@@ -5,17 +5,14 @@ import android.os.Bundle
 import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
 import androidx.navigation.fragment.findNavController
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentSettingsTracingBinding
-import de.rki.coronawarnapp.exception.ExceptionCategory
-import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
 import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper
-import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.ui.doNavigate
 import de.rki.coronawarnapp.ui.main.MainActivity
+import de.rki.coronawarnapp.ui.tracing.settings.SettingsTracingFragmentViewModel.Event
 import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.ExternalActionHelper
@@ -25,7 +22,6 @@ import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
-import kotlinx.coroutines.launch
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -47,7 +43,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
 
     private val binding: FragmentSettingsTracingBinding by viewBindingLazy()
 
-    private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper
+    private lateinit var exposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
@@ -63,11 +59,21 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
                     TracingSettingsState.BluetoothDisabled,
                     TracingSettingsState.LocationDisabled -> setOnClickListener(null)
                     TracingSettingsState.TracingInActive,
-                    TracingSettingsState.TracingActive -> setOnClickListener { startStopTracing() }
+                    TracingSettingsState.TracingActive -> setOnClickListener { vm.startStopTracing() }
                 }
             }
         }
 
+        exposureNotificationPermissionHelper = InternalExposureNotificationPermissionHelper(this, this)
+
+        vm.events.observe2(this) {
+            when (it) {
+                Event.RequestPermissions -> exposureNotificationPermissionHelper.requestPermissionToStartTracing()
+                Event.ShowConsentDialog -> showConsentDialog()
+                Event.ManualCheckingDialog -> showManualCheckingRequiredDialog()
+            }
+        }
+
         setButtonOnClickListener()
     }
 
@@ -77,7 +83,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
     }
 
     override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
-        internalExposureNotificationPermissionHelper.onResolutionComplete(
+        exposureNotificationPermissionHelper.onResolutionComplete(
             requestCode,
             resultCode
         )
@@ -98,13 +104,10 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
         val location = binding.settingsTracingStatusLocation.tracingStatusCardButton
         val interoperability = binding.settingsInteroperabilityRow.settingsPlainRow
 
-        internalExposureNotificationPermissionHelper =
-            InternalExposureNotificationPermissionHelper(this, this)
         switch.setOnCheckedChangeListener { view, _ ->
-
             // Make sure that listener is called by user interaction
             if (view.isPressed) {
-                startStopTracing()
+                vm.startStopTracing()
                 // Focus on the body text after to announce the tracing status for accessibility reasons
                 binding.settingsTracingSwitchRow.settingsSwitchRowHeaderBody
                     .sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED)
@@ -131,39 +134,6 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
             )
     }
 
-    private fun startStopTracing() {
-        // if tracing is enabled when listener is activated it should be disabled
-        lifecycleScope.launch {
-            try {
-                if (InternalExposureNotificationClient.asyncIsEnabled()) {
-                    InternalExposureNotificationClient.asyncStop()
-                    BackgroundWorkScheduler.stopWorkScheduler()
-                } else {
-                    // tracing was already activated
-                    if (LocalData.initialTracingActivationTimestamp() != null) {
-                        internalExposureNotificationPermissionHelper.requestPermissionToStartTracing()
-                    } else {
-                        // tracing was never activated
-                        // ask for consent via dialog for initial tracing activation when tracing was not
-                        // activated during onboarding
-                        showConsentDialog()
-                        // check if background processing is switched off, if it is, show the manual calculation dialog explanation before turning on.
-                        val activity = requireActivity() as MainActivity
-                        if (!activity.backgroundPrioritization.isBackgroundActivityPrioritized) {
-                            showManualCheckingRequiredDialog()
-                        }
-                    }
-                }
-            } catch (exception: Exception) {
-                exception.report(
-                    ExceptionCategory.EXPOSURENOTIFICATION,
-                    TAG,
-                    null
-                )
-            }
-        }
-    }
-
     private fun showManualCheckingRequiredDialog() {
         val dialog = DialogHelper.DialogInstance(
             requireActivity(),
@@ -186,7 +156,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
             R.string.onboarding_button_enable,
             R.string.onboarding_button_cancel,
             true, {
-                internalExposureNotificationPermissionHelper.requestPermissionToStartTracing()
+                exposureNotificationPermissionHelper.requestPermissionToStartTracing()
             }, {
                 // Declined
             })
@@ -194,6 +164,6 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing),
     }
 
     companion object {
-        private val TAG: String? = SettingsTracingFragment::class.simpleName
+        internal val TAG: String? = SettingsTracingFragment::class.simpleName
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt
index c9a5462932dcd507732537d002b785f6a7b659fe..6b2e57dfa22ec73593740d62705248d4ce98e6fe 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt
@@ -4,13 +4,20 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.asLiveData
 import androidx.lifecycle.viewModelScope
 import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
+import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus
 import de.rki.coronawarnapp.ui.tracing.details.TracingDetailsState
 import de.rki.coronawarnapp.ui.tracing.details.TracingDetailsStateProvider
+import de.rki.coronawarnapp.util.BackgroundPrioritization
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.flow.shareLatest
+import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
@@ -20,7 +27,8 @@ import timber.log.Timber
 class SettingsTracingFragmentViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
     tracingDetailsStateProvider: TracingDetailsStateProvider,
-    tracingStatus: GeneralTracingStatus
+    tracingStatus: GeneralTracingStatus,
+    private val backgroundPrioritization: BackgroundPrioritization
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val tracingDetailsState: LiveData<TracingDetailsState> = tracingDetailsStateProvider.state
@@ -36,6 +44,47 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
         )
         .asLiveData(dispatcherProvider.Main)
 
+    val events = SingleLiveEvent<Event>()
+
+    fun startStopTracing() {
+        // if tracing is enabled when listener is activated it should be disabled
+        launch {
+            try {
+                if (InternalExposureNotificationClient.asyncIsEnabled()) {
+                    InternalExposureNotificationClient.asyncStop()
+                    BackgroundWorkScheduler.stopWorkScheduler()
+                } else {
+                    // tracing was already activated
+                    if (LocalData.initialTracingActivationTimestamp() != null) {
+                        events.postValue(Event.RequestPermissions)
+                    } else {
+                        // tracing was never activated
+                        // ask for consent via dialog for initial tracing activation when tracing was not
+                        // activated during onboarding
+                        events.postValue(Event.ShowConsentDialog)
+                        // check if background processing is switched off,
+                        // if it is, show the manual calculation dialog explanation before turning on.
+                        if (!backgroundPrioritization.isBackgroundActivityPrioritized) {
+                            events.postValue(Event.ManualCheckingDialog)
+                        }
+                    }
+                }
+            } catch (exception: Exception) {
+                exception.report(
+                    ExceptionCategory.EXPOSURENOTIFICATION,
+                    SettingsTracingFragment.TAG,
+                    null
+                )
+            }
+        }
+    }
+
+    sealed class Event {
+        object RequestPermissions : Event()
+        object ShowConsentDialog : Event()
+        object ManualCheckingDialog : Event()
+    }
+
     @AssistedInject.Factory
     interface Factory : SimpleCWAViewModelFactory<SettingsTracingFragmentViewModel>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
index a806825282e67f110262017d748c2485ca9d51ea..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt
@@ -1,19 +0,0 @@
-package de.rki.coronawarnapp.ui.viewmodel
-
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.asLiveData
-import com.squareup.inject.assisted.AssistedInject
-import de.rki.coronawarnapp.storage.SubmissionRepository
-import de.rki.coronawarnapp.util.DeviceUIState
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
-import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-
-class SubmissionViewModel @AssistedInject constructor(
-    submissionRepository: SubmissionRepository
-) : CWAViewModel() {
-
-    val deviceUiState: LiveData<DeviceUIState> = submissionRepository.deviceUIStateFlow.asLiveData()
-
-    @AssistedInject.Factory
-    interface Factory : SimpleCWAViewModelFactory<SubmissionViewModel>
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt
index 42dfb2f60231fc8461b4b164d8d14bad74a37160..695e297a1d7d12d223abc79a9303a4a69890d62a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt
@@ -7,8 +7,7 @@ import androidx.core.content.ContextCompat.startActivity
 import de.rki.coronawarnapp.BuildConfig
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.appconfig.CWAConfig
-import de.rki.coronawarnapp.appconfig.download.ApplicationConfigurationCorruptException
-import de.rki.coronawarnapp.server.protocols.internal.AppVersionConfig.SemanticVersion
+import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException
 import de.rki.coronawarnapp.ui.LauncherActivity
 import de.rki.coronawarnapp.util.di.AppInjector
 import timber.log.Timber
@@ -69,30 +68,19 @@ class UpdateChecker(private val activity: LauncherActivity) {
     private suspend fun checkIfUpdatesNeededFromServer(): Boolean {
         val cwaAppConfig: CWAConfig = AppInjector.component.appConfigProvider.getAppConfig()
 
-        val minVersionFromServer = cwaAppConfig.appVersion.android.min
-        val minVersionFromServerString =
-            constructSemanticVersionString(minVersionFromServer)
+        val minVersionFromServer = cwaAppConfig.minVersionCode
 
-        Timber.e(
-            "minVersionStringFromServer:%s", constructSemanticVersionString(
-                minVersionFromServer
-            )
+        Timber.d(
+            "minVersionFromServer:%s",
+            minVersionFromServer
         )
-        Timber.e("Current app version:%s", BuildConfig.VERSION_NAME)
+        Timber.d("Current app version:%s", BuildConfig.VERSION_CODE)
 
         val needsImmediateUpdate = VersionComparator.isVersionOlder(
-            BuildConfig.VERSION_NAME,
-            minVersionFromServerString
+            BuildConfig.VERSION_CODE.toLong(),
+            minVersionFromServer
         )
         Timber.e("needs update:$needsImmediateUpdate")
         return needsImmediateUpdate
     }
-
-    private fun constructSemanticVersionString(
-        semanticVersion: SemanticVersion
-    ): String {
-        return semanticVersion.major.toString() + "." +
-                semanticVersion.minor.toString() + "." +
-                semanticVersion.patch.toString()
-    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt
index 2a706d3e1a09f5e3f1084ea6328d39da63f231a8..117932d4f43adfabbcf0a59aa825f0ec2a3e8b34 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt
@@ -15,32 +15,7 @@ object VersionComparator {
      * @param versionToCompareTo
      * @return true if currentVersion is older than versionToCompareTo, else false
      */
-    fun isVersionOlder(currentVersion: String, versionToCompareTo: String): Boolean {
-        var isVersionOlder = false
-
-        val delimiter = "."
-
-        val currentVersionParts = currentVersion.split(delimiter)
-        val currentVersionMajor = currentVersionParts[0].toInt()
-        val currentVersionMinor = currentVersionParts[1].toInt()
-        val currentVersionPatch = currentVersionParts[2].toInt()
-
-        val versionToCompareParts = versionToCompareTo.split(delimiter)
-        val versionToCompareMajor = versionToCompareParts[0].toInt()
-        val versionToCompareMinor = versionToCompareParts[1].toInt()
-        val versionToComparePatch = versionToCompareParts[2].toInt()
-
-        if (versionToCompareMajor > currentVersionMajor) {
-            isVersionOlder = true
-        } else if (versionToCompareMajor == currentVersionMajor) {
-            if (versionToCompareMinor > currentVersionMinor) {
-                isVersionOlder = true
-            } else if ((versionToCompareMinor == currentVersionMinor) &&
-                (versionToComparePatch > currentVersionPatch)
-            ) {
-                isVersionOlder = true
-            }
-        }
-        return isVersionOlder
+    fun isVersionOlder(currentVersion: Long, versionToCompareTo: Long): Boolean {
+        return currentVersion < versionToCompareTo
     }
 }
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 13ea08dfd0c431dc596b9bf024d90788dd3fac2f..2e26c1991f2aae2889e89542223fe0116dd341ed 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
@@ -6,8 +6,6 @@ import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.flow.shareLatest
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.channels.sendBlocking
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.callbackFlow
@@ -23,18 +21,19 @@ class BackgroundModeStatus @Inject constructor(
     @AppScope private val appScope: CoroutineScope
 ) {
 
-    val isBackgroundRestricted: Flow<Boolean> = callbackFlow<Boolean> {
-        var isRunning = true
-        while (isRunning && isActive) {
+    val isBackgroundRestricted: Flow<Boolean?> = callbackFlow<Boolean> {
+        while (true) {
             try {
-                sendBlocking(pollIsBackgroundRestricted())
+                send(pollIsBackgroundRestricted())
             } catch (e: Exception) {
                 Timber.w(e, "isBackgroundRestricted failed.")
                 cancel("isBackgroundRestricted failed", e)
             }
+
+            if (!isActive) break
+
             delay(POLLING_DELAY_MS)
         }
-        awaitClose { isRunning = false }
     }
         .distinctUntilChanged()
         .shareLatest(
@@ -43,17 +42,18 @@ class BackgroundModeStatus @Inject constructor(
         )
 
     val isAutoModeEnabled: Flow<Boolean> = callbackFlow<Boolean> {
-        var isRunning = true
-        while (isRunning && isActive) {
+        while (true) {
             try {
-                sendBlocking(pollIsAutoMode())
+                send(pollIsAutoMode())
             } catch (e: Exception) {
                 Timber.w(e, "autoModeEnabled failed.")
                 cancel("autoModeEnabled failed", e)
             }
+
+            if (!isActive) break
+
             delay(POLLING_DELAY_MS)
         }
-        awaitClose { isRunning = false }
     }
         .distinctUntilChanged()
         .shareLatest(
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 cade97a58323736139c16c3898b04b26944a21fe..195bd3551aae9a2ba6a3d5306e2faeb64bcbb52c 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
@@ -11,10 +11,10 @@ object CWADebug {
     fun init(application: Application) {
         if (isDebugBuildOrMode) System.setProperty("kotlinx.coroutines.debug", "on")
 
-        if (BuildConfig.DEBUG) {
+        if (isDeviceForTestersBuild) {
             Timber.plant(Timber.DebugTree())
         }
-        if ((buildFlavor == BuildFlavor.DEVICE_FOR_TESTERS || BuildConfig.DEBUG)) {
+        if (isDeviceForTestersBuild) {
             fileLogger = FileLogger(application)
         }
     }
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 6e9d54d3353ecebdf76145d9fc7de6d2e6252c1d..6317d11ee81ca9f42ebe779abbbd69a37d5262b5 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
@@ -22,8 +22,11 @@ package de.rki.coronawarnapp.util
 import android.annotation.SuppressLint
 import android.content.Context
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.diagnosiskeys.download.KeyPackageSyncSettings
 import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
+import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.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
@@ -44,7 +47,9 @@ class DataReset @Inject constructor(
     private val keyCacheRepository: KeyCacheRepository,
     private val appConfigProvider: AppConfigProvider,
     private val interoperabilityRepository: InteroperabilityRepository,
-    private val submissionRepository: SubmissionRepository
+    private val submissionRepository: SubmissionRepository,
+    private val exposureDetectionTracker: ExposureDetectionTracker,
+    private val keyPackageSyncSettings: KeyPackageSyncSettings
 ) {
 
     private val mutex = Mutex()
@@ -57,6 +62,8 @@ class DataReset @Inject constructor(
         Timber.w("CWA LOCAL DATA DELETION INITIATED.")
         // Database Reset
         AppDatabase.reset(context)
+        // Because LocalData does not behave like a normal shared preference
+        LocalData.clear()
         // Shared Preferences Reset
         SecurityHelper.resetSharedPrefs()
         // Reset the current risk level stored in LiveData
@@ -66,6 +73,8 @@ class DataReset @Inject constructor(
         keyCacheRepository.clear()
         appConfigProvider.clear()
         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/DefaultBackgroundPrioritization.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt
index 7cfd0d07b2c5fbbc01e76b1002709c5bc06b92ca..eab3573bc450f2792c8ed8fa085f16cdd13f0ce6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt
@@ -1,8 +1,10 @@
 package de.rki.coronawarnapp.util
 
+import dagger.Reusable
 import de.rki.coronawarnapp.util.device.PowerManagement
 import javax.inject.Inject
 
+@Reusable
 class DefaultBackgroundPrioritization @Inject constructor(
     private val powerManagement: PowerManagement
 ) : BackgroundPrioritization {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt
deleted file mode 100644
index 6af00068b370a93acc562d74cf16b23d8819617c..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package de.rki.coronawarnapp.util
-
-import com.google.android.gms.common.api.ApiException
-import com.google.android.gms.common.api.CommonStatusCodes
-import dagger.Reusable
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
-import javax.inject.Inject
-import kotlin.math.abs
-
-@Reusable
-class GoogleAPIVersion @Inject constructor() {
-    /**
-     * Indicates if the client runs above a certain version
-     *
-     * @return isAboveVersion, if connected to an old unsupported version, return false
-     */
-    suspend fun isAtLeast(compareVersion: Long): Boolean {
-        if (!compareVersion.isCorrectVersionLength) {
-            throw IllegalArgumentException("given version has incorrect length")
-        }
-        return try {
-            val currentVersion = InternalExposureNotificationClient.getVersion()
-            currentVersion >= compareVersion
-        } catch (apiException: ApiException) {
-            if (apiException.statusCode != CommonStatusCodes.API_NOT_CONNECTED) {
-                throw apiException
-            }
-            return false
-        }
-    }
-
-    // check if a raw long has the correct length to be considered an API version
-    private val Long.isCorrectVersionLength
-        get(): Boolean = abs(this).toString().length == GOOGLE_API_VERSION_FIELD_LENGTH
-
-    companion object {
-        private const val GOOGLE_API_VERSION_FIELD_LENGTH = 8
-        const val V16 = 16000000L
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/NetworkRequestWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/NetworkRequestWrapper.kt
new file mode 100644
index 0000000000000000000000000000000000000000..47d1c57e48abe6bf8d816ad9305c0d9366ea74ee
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/NetworkRequestWrapper.kt
@@ -0,0 +1,30 @@
+package de.rki.coronawarnapp.util
+
+sealed class NetworkRequestWrapper<out T, out U> {
+    object RequestIdle : NetworkRequestWrapper<Nothing, Nothing>()
+    object RequestStarted : NetworkRequestWrapper<Nothing, Nothing>()
+    data class RequestSuccessful<T, U>(val data: T) : NetworkRequestWrapper<T, U>()
+    data class RequestFailed<T, U>(val error: U) : NetworkRequestWrapper<T, U>()
+
+    companion object {
+        fun <T, U, W> NetworkRequestWrapper<T, U>?.withSuccess(without: W, block: (data: T) -> W): W {
+            return if (this is RequestSuccessful) {
+                block(this.data)
+            } else {
+                without
+            }
+        }
+
+        fun <T, U> NetworkRequestWrapper<T, U>?.withSuccess(block: (data: T) -> Unit) {
+            if (this is RequestSuccessful) {
+                block(this.data)
+            }
+        }
+
+        fun <T, U> NetworkRequestWrapper<T, U>?.withFailure(block: (error: U) -> Unit) {
+            if (this is RequestFailed) {
+                block(this.error)
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
index 00d62d17067f43ef080520518ba36bf2d4ee5d8f..8a5729554f5166ee7d9eb192d72e899374ca2bcf 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
@@ -11,7 +11,6 @@ import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.util.di.AppContext
-import de.rki.coronawarnapp.worker.BackgroundWorkHelper
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import kotlinx.coroutines.launch
 import timber.log.Timber
@@ -45,22 +44,18 @@ class WatchdogService @Inject constructor(
             val wakeLock = createWakeLock()
             // A wifi lock to wake up the wifi connection in case the device is dozing
             val wifiLock = createWifiLock()
-            BackgroundWorkHelper.sendDebugNotification(
-                "Automatic mode is on", "Check if we have downloaded keys already today"
-            )
+
+            Timber.d("Automatic mode is on, check if we have downloaded keys already today")
+
             val state = taskController.submitBlocking(
                 DefaultTaskRequest(
                     DownloadDiagnosisKeysTask::class,
-                    DownloadDiagnosisKeysTask.Arguments(null, true)
+                    DownloadDiagnosisKeysTask.Arguments(),
+                    originTag = "WatchdogService"
                 )
             )
             if (state.isFailed) {
-                BackgroundWorkHelper.sendDebugNotification(
-                    "RetrieveDiagnosisKeysTransaction failed",
-                    (state.error?.localizedMessage
-                        ?: "Unknown exception occurred in onCreate") + "\n\n" + (state.error?.cause
-                        ?: "Cause is unknown").toString()
-                )
+                Timber.e(state.error, "RetrieveDiagnosisKeysTransaction failed")
                 // retry the key retrieval in case of an error with a scheduled work
                 BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
             }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt
index eb3b29093e4a6c772d65dd7167ecbeda941588e0..e8d283d9c7695095ffa9995d76aa3cb2c232873a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt
@@ -1,39 +1,45 @@
 package de.rki.coronawarnapp.util.debug
 
 import android.content.Context
+import de.rki.coronawarnapp.util.CWADebug
 import timber.log.Timber
 import java.io.File
 
-class FileLogger constructor(private val context: Context) {
+class FileLogger constructor(context: Context) {
 
     val logFile = File(context.cacheDir, "FileLoggerTree.log")
-    val triggerFile = File(context.filesDir, "FileLoggerTree.trigger")
+
+    private val blockerFile = File(context.filesDir, "FileLoggerTree.blocker")
     private var loggerTree: FileLoggerTree? = null
 
     val isLogging: Boolean
         get() = loggerTree != null
 
     init {
-        if (triggerFile.exists()) {
+        if (!blockerFile.exists()) {
             start()
         }
     }
 
     fun start() {
+        if (!CWADebug.isDeviceForTestersBuild) return
+
         if (loggerTree != null) return
 
         loggerTree = FileLoggerTree(logFile).also {
             Timber.plant(it)
             it.start()
-            triggerFile.createNewFile()
+            blockerFile.delete()
         }
     }
 
     fun stop() {
+        if (!CWADebug.isDeviceForTestersBuild) return
+
         loggerTree?.let {
             it.stop()
             logFile.delete()
-            triggerFile.delete()
+            blockerFile.createNewFile()
             loggerTree = null
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
index af2cb89a00f2cbb07c2f27e5abdff7d7728abafd..6a869aa15a472ceb4d54b28ebbb2413f016556d4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt
@@ -22,7 +22,6 @@ import de.rki.coronawarnapp.receiver.ReceiverBinder
 import de.rki.coronawarnapp.risk.RiskModule
 import de.rki.coronawarnapp.service.ServiceBinder
 import de.rki.coronawarnapp.storage.SettingsRepository
-import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.submission.SubmissionModule
 import de.rki.coronawarnapp.submission.SubmissionTaskModule
 import de.rki.coronawarnapp.task.TaskController
@@ -89,8 +88,6 @@ interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
 
     val playbook: Playbook
 
-    val interoperabilityRepository: InteroperabilityRepository
-
     val taskController: TaskController
 
     @AppScope val appScope: AppCoroutineScope
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 701dc3239b455bff16edc0b2d656f64f1e8c43ad..947b6d55c29ea2894a9050b31a16069d2440c3f3 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
@@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
@@ -17,7 +17,7 @@ import timber.log.Timber
  * Helper method to create a new flow without suspending and without initial value
  * The flow collector will just wait for the first value
  */
-fun <T> Flow<T>.shareLatest(
+fun <T : Any> Flow<T>.shareLatest(
     tag: String? = null,
     scope: CoroutineScope,
     started: SharingStarted = SharingStarted.WhileSubscribed(replayExpirationMillis = 0)
@@ -40,7 +40,7 @@ fun <T> Flow<T>.shareLatest(
         started = started,
         initialValue = null
     )
-    .mapNotNull { it }
+    .filterNotNull()
 
 @Suppress("UNCHECKED_CAST", "LongParameterList")
 inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R> combine(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt
index 91086980e2bd33336558e889a15272962e37bd83..1fe9db2d964a52c0bc216428d4a5e6fe1ec415c4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt
@@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.plus
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
 import timber.log.Timber
 import kotlin.coroutines.CoroutineContext
 
@@ -37,20 +39,32 @@ class HotDataFlow<T : Any>(
         extraBufferCapacity = Int.MAX_VALUE,
         onBufferOverflow = BufferOverflow.SUSPEND
     )
+    private val valueGuard = Mutex()
 
     private val internalProducer: Flow<Holder<T>> = channelFlow {
-        var currentValue = startValueProvider().also {
-            Timber.tag(tag).v("startValue=%s", it)
-            val updatedBy: suspend T.() -> T = { it }
-            send(Holder.Data(value = it, updatedBy = updatedBy))
+        var currentValue = valueGuard.withLock {
+            startValueProvider().also {
+                Timber.tag(tag).v("startValue=%s", it)
+                val updatedBy: suspend T.() -> T = { it }
+                send(Holder.Data(value = it, updatedBy = updatedBy))
+            }
         }
+        Timber.tag(tag).v("startValue=%s", currentValue)
 
-        updateActions.collect { updateAction ->
-            currentValue = updateAction(currentValue).also {
-                currentValue = it
-                send(Holder.Data(value = it, updatedBy = updateAction))
+        updateActions
+            .onCompletion {
+                Timber.tag(tag).v("updateActions onCompletion -> resetReplayCache()")
+                updateActions.resetReplayCache()
             }
-        }
+            .collect { updateAction ->
+                currentValue = valueGuard.withLock {
+                    updateAction(currentValue).also {
+                        send(Holder.Data(value = it, updatedBy = updateAction))
+                    }
+                }
+            }
+
+        Timber.tag(tag).v("internal channelFlow finished.")
     }
 
     private val internalFlow = internalProducer
@@ -65,7 +79,10 @@ class HotDataFlow<T : Any>(
                 throw it
             }
         }
-        .onCompletion { Timber.tag(tag).v("Internal onCompletion") }
+        .onCompletion { err ->
+            if (err != null) Timber.tag(tag).w(err, "internal onCompletion due to error")
+            else Timber.tag(tag).v("internal onCompletion")
+        }
         .shareIn(
             scope = scope + coroutineContext,
             replay = 1,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterInformationHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterInformationHelper.kt
deleted file mode 100644
index 913424b0671ddfef1a1a9a00e79304ab5b6486f7..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterInformationHelper.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-@file:JvmName("FormatterInformationHelper")
-
-package de.rki.coronawarnapp.util.formatter
-
-import de.rki.coronawarnapp.BuildConfig
-import de.rki.coronawarnapp.CoronaWarnApplication
-import de.rki.coronawarnapp.R
-
-fun formatVersion(): String {
-    val appContext = CoronaWarnApplication.getAppContext()
-    val versionName: String = BuildConfig.VERSION_NAME
-    return appContext.getString(R.string.information_version).format(versionName)
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt
index 156f8efee0289c727be6c85d0aa6e4a96b116674..e573e202356a4b6faef650422585624285eb8c6d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt
@@ -8,11 +8,13 @@ import android.text.Spannable
 import android.text.SpannableString
 import android.text.SpannableStringBuilder
 import android.text.style.ForegroundColorSpan
+import android.view.View
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.submission.Symptoms
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.util.DeviceUIState
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
+import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUIFormat
 import java.util.Date
 import java.util.Locale
@@ -49,33 +51,37 @@ fun isEnableSymptomCalendarButtonByState(currentState: Symptoms.StartOf?): Boole
     return currentState != null
 }
 
-fun formatTestResultSpinnerVisible(uiStateState: ApiRequestState?): Int =
-    formatVisibility(uiStateState != ApiRequestState.SUCCESS)
-
-fun formatTestResultVisible(uiStateState: ApiRequestState?): Int =
-    formatVisibility(uiStateState == ApiRequestState.SUCCESS)
-
-fun formatTestResultStatusText(uiState: DeviceUIState?): String {
-    val appContext = CoronaWarnApplication.getAppContext()
-    return when (uiState) {
-        DeviceUIState.PAIRED_NEGATIVE -> appContext.getString(R.string.test_result_card_status_negative)
-        DeviceUIState.PAIRED_POSITIVE,
-        DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getString(R.string.test_result_card_status_positive)
-        else -> appContext.getString(R.string.test_result_card_status_invalid)
+fun formatTestResultSpinnerVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.VISIBLE) {
+        View.GONE
     }
-}
 
-fun formatTestResultStatusColor(uiState: DeviceUIState?): Int {
-    val appContext = CoronaWarnApplication.getAppContext()
-    return when (uiState) {
-        DeviceUIState.PAIRED_NEGATIVE -> appContext.getColor(R.color.colorTextSemanticGreen)
-        DeviceUIState.PAIRED_POSITIVE,
-        DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getColor(R.color.colorTextSemanticRed)
-        else -> appContext.getColor(R.color.colorTextSemanticRed)
+fun formatTestResultVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.GONE) {
+        View.VISIBLE
     }
-}
 
-fun formatTestResult(uiState: DeviceUIState?): Spannable {
+fun formatTestResultStatusText(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): String =
+    uiState.withSuccess(R.string.test_result_card_status_invalid) {
+        when (it) {
+            DeviceUIState.PAIRED_NEGATIVE -> R.string.test_result_card_status_negative
+            DeviceUIState.PAIRED_POSITIVE,
+            DeviceUIState.PAIRED_POSITIVE_TELETAN -> R.string.test_result_card_status_positive
+            else -> R.string.test_result_card_status_invalid
+        }
+    }.let { CoronaWarnApplication.getAppContext().getString(it) }
+
+fun formatTestResultStatusColor(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(R.color.colorTextSemanticRed) {
+        when (it) {
+            DeviceUIState.PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen
+            DeviceUIState.PAIRED_POSITIVE,
+            DeviceUIState.PAIRED_POSITIVE_TELETAN -> R.color.colorTextSemanticRed
+            else -> R.color.colorTextSemanticRed
+        }
+    }.let { CoronaWarnApplication.getAppContext().getColor(it) }
+
+fun formatTestResult(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Spannable {
     val appContext = CoronaWarnApplication.getAppContext()
     return SpannableStringBuilder()
         .append(appContext.getString(R.string.test_result_card_virus_name_text))
@@ -87,33 +93,36 @@ fun formatTestResult(uiState: DeviceUIState?): Spannable {
         )
 }
 
-fun formatTestResultCardContent(uiState: DeviceUIState?): Spannable {
-    val appContext = CoronaWarnApplication.getAppContext()
-    return when (uiState) {
-        DeviceUIState.PAIRED_NO_RESULT ->
-            SpannableString(appContext.getString(R.string.test_result_card_status_pending))
-        DeviceUIState.PAIRED_ERROR,
-        DeviceUIState.PAIRED_REDEEMED ->
-            SpannableString(appContext.getString(R.string.test_result_card_status_invalid))
-
-        DeviceUIState.PAIRED_POSITIVE,
-        DeviceUIState.PAIRED_POSITIVE_TELETAN,
-        DeviceUIState.PAIRED_NEGATIVE -> formatTestResult(uiState)
-        else -> SpannableString("")
+fun formatTestResultCardContent(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Spannable {
+    return uiState.withSuccess(SpannableString("")) {
+        val appContext = CoronaWarnApplication.getAppContext()
+        when (it) {
+            DeviceUIState.PAIRED_NO_RESULT ->
+                SpannableString(appContext.getString(R.string.test_result_card_status_pending))
+            DeviceUIState.PAIRED_ERROR,
+            DeviceUIState.PAIRED_REDEEMED ->
+                SpannableString(appContext.getString(R.string.test_result_card_status_invalid))
+
+            DeviceUIState.PAIRED_POSITIVE,
+            DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            DeviceUIState.PAIRED_NEGATIVE -> formatTestResult(uiState)
+            else -> SpannableString("")
+        }
     }
 }
 
-fun formatTestStatusIcon(uiState: DeviceUIState?): Drawable? {
-    val appContext = CoronaWarnApplication.getAppContext()
-    return when (uiState) {
-        DeviceUIState.PAIRED_NO_RESULT -> appContext.getDrawable(R.drawable.ic_test_result_illustration_pending)
-        DeviceUIState.PAIRED_POSITIVE_TELETAN,
-        DeviceUIState.PAIRED_POSITIVE -> appContext.getDrawable(R.drawable.ic_test_result_illustration_positive)
-        DeviceUIState.PAIRED_NEGATIVE -> appContext.getDrawable(R.drawable.ic_test_result_illustration_negative)
-        DeviceUIState.PAIRED_ERROR,
-        DeviceUIState.PAIRED_REDEEMED -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid)
-        else -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid)
-    }
+fun formatTestStatusIcon(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Drawable? {
+    return uiState.withSuccess(R.drawable.ic_test_result_illustration_invalid) {
+        when (it) {
+            DeviceUIState.PAIRED_NO_RESULT -> R.drawable.ic_test_result_illustration_pending
+            DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            DeviceUIState.PAIRED_POSITIVE -> R.drawable.ic_test_result_illustration_positive
+            DeviceUIState.PAIRED_NEGATIVE -> R.drawable.ic_test_result_illustration_negative
+            DeviceUIState.PAIRED_ERROR,
+            DeviceUIState.PAIRED_REDEEMED -> R.drawable.ic_test_result_illustration_invalid
+            else -> R.drawable.ic_test_result_illustration_invalid
+        }
+    }.let { CoronaWarnApplication.getAppContext().getDrawable(it) }
 }
 
 fun formatTestResultRegisteredAtText(registeredAt: Date?): String {
@@ -122,24 +131,30 @@ fun formatTestResultRegisteredAtText(registeredAt: Date?): String {
         .format(registeredAt?.toUIFormat(appContext))
 }
 
-fun formatTestResultPendingStepsVisible(uiState: DeviceUIState?): Int =
-    formatVisibility(uiState == DeviceUIState.PAIRED_NO_RESULT)
+fun formatTestResultPendingStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.GONE) { formatVisibility(it == DeviceUIState.PAIRED_NO_RESULT) }
 
-fun formatTestResultNegativeStepsVisible(uiState: DeviceUIState?): Int =
-    formatVisibility(uiState == DeviceUIState.PAIRED_NEGATIVE)
+fun formatTestResultNegativeStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.GONE) { formatVisibility(it == DeviceUIState.PAIRED_NEGATIVE) }
 
-fun formatTestResultPositiveStepsVisible(uiState: DeviceUIState?): Int =
-    formatVisibility(uiState == DeviceUIState.PAIRED_POSITIVE || uiState == DeviceUIState.PAIRED_POSITIVE_TELETAN)
+fun formatTestResultPositiveStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.GONE) {
+        formatVisibility(it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN)
+    }
 
-fun formatTestResultInvalidStepsVisible(uiState: DeviceUIState?): Int =
-    formatVisibility(uiState == DeviceUIState.PAIRED_ERROR || uiState == DeviceUIState.PAIRED_REDEEMED)
+fun formatTestResultInvalidStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.GONE) {
+        formatVisibility(it == DeviceUIState.PAIRED_ERROR || it == DeviceUIState.PAIRED_REDEEMED)
+    }
 
-fun formatShowRiskStatusCard(deviceUiState: DeviceUIState?): Int =
-    formatVisibility(
-        deviceUiState != DeviceUIState.PAIRED_POSITIVE &&
-                deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN &&
-                deviceUiState != DeviceUIState.SUBMITTED_FINAL
-    )
+fun formatShowRiskStatusCard(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int =
+    uiState.withSuccess(View.GONE) {
+        formatVisibility(
+            it != DeviceUIState.PAIRED_POSITIVE &&
+                it != DeviceUIState.PAIRED_POSITIVE_TELETAN &&
+                it != DeviceUIState.SUBMITTED_FINAL
+        )
+    }
 
 fun formatCountryIsoTagToLocalizedName(isoTag: String?): String {
     val country = if (isoTag != null) Locale("", isoTag).displayCountry else ""
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt
index c64cbedfde89f3c164c26e3eb1ea2204102d2bd3..2e97359373b6de42a333ade72b7b4542c5e42a73 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt
@@ -6,6 +6,7 @@ import com.google.gson.Gson
 import de.rki.coronawarnapp.util.serialization.fromJson
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
+import timber.log.Timber
 
 class FlowPreference<T> constructor(
     private val preferences: SharedPreferences,
@@ -17,6 +18,21 @@ class FlowPreference<T> constructor(
     private val flowInternal = MutableStateFlow(internalValue)
     val flow: Flow<T> = flowInternal
 
+    private val preferenceChangeListener =
+        SharedPreferences.OnSharedPreferenceChangeListener { changedPrefs, changedKey ->
+            if (changedKey != key) return@OnSharedPreferenceChangeListener
+
+            val newValue = reader(changedPrefs, changedKey)
+            val currentvalue = flowInternal.value
+            if (currentvalue != newValue && flowInternal.compareAndSet(currentvalue, newValue)) {
+                Timber.v("%s:%s changed to %s", changedPrefs, changedKey, newValue)
+            }
+        }
+
+    init {
+        preferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
+    }
+
     private var internalValue: T
         get() = reader(preferences, key)
         set(newValue) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0469b3840d2b9417a4ad728314f57cfa871a63ec
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt
@@ -0,0 +1,17 @@
+package de.rki.coronawarnapp.util.preferences
+
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import timber.log.Timber
+
+fun SharedPreferences.clearAndNotify() {
+    val currentKeys = this.all.keys.toSet()
+    Timber.v("%s clearAndNotify(): %s", this, currentKeys)
+    edit {
+        currentKeys.forEach { remove(it) }
+    }
+    // Clear does not notify anyone using registerOnSharedPreferenceChangeListener
+    edit(commit = true) {
+        clear()
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt
index 370ab58fd4ce02e2358bc1dd9f66557020403546..c1c145e4965c2cd874b65a4c107f0976e59557dd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt
@@ -27,6 +27,7 @@ import androidx.annotation.VisibleForTesting
 import de.rki.coronawarnapp.exception.CwaSecurityException
 import de.rki.coronawarnapp.util.di.AppInjector
 import de.rki.coronawarnapp.util.di.ApplicationComponent
+import de.rki.coronawarnapp.util.preferences.clearAndNotify
 import de.rki.coronawarnapp.util.security.SecurityConstants.CWA_APP_SQLITE_DB_PW
 import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MAX_LENGTH
 import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MIN_LENGTH
@@ -80,7 +81,7 @@ object SecurityHelper {
 
     @SuppressLint("ApplySharedPref")
     fun resetSharedPrefs() {
-        globalEncryptedSharedPreferencesInstance.edit().clear().commit()
+        globalEncryptedSharedPreferencesInstance.clearAndNotify()
     }
 
     private fun getStoredDbPassword(): ByteArray? =
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt
index 601f833c3ded2777122e62d6814e07edff3380a3..f014dc54c28ca112b2a49db88917add6c83fedca 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt
@@ -11,12 +11,13 @@ inline fun <reified T> Gson.fromJson(json: String): T = fromJson(
     object : TypeToken<T>() {}.type
 )
 
-inline fun <reified T> Gson.fromJson(file: File): T = file.reader().use {
+inline fun <reified T> Gson.fromJson(file: File): T = file.bufferedReader().use {
     fromJson(it, object : TypeToken<T>() {}.type)
 }
 
-inline fun <reified T> Gson.toJson(data: T, file: File) = file.writer().use { writer ->
+inline fun <reified T> Gson.toJson(data: T, file: File) = file.bufferedWriter().use { writer ->
     toJson(data, writer)
+    writer.flush()
 }
 
 fun <T : Any> KClass<T>.getDefaultGsonTypeAdapter(): TypeAdapter<T> = Gson().getAdapter(this.java)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
index 9f3eddff208662fb70a1996c9756b4fcec245f27..40cd6f7685f0ef272a58f763ef318c676be1d5c2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt
@@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import timber.log.Timber
 import kotlin.coroutines.CoroutineContext
@@ -29,7 +29,13 @@ abstract class CWAViewModel constructor(
     fun launch(
         context: CoroutineContext = dispatcherProvider.Default,
         block: suspend CoroutineScope.() -> Unit
-    ): Job = viewModelScope.launch(context = context, block = block)
+    ) {
+        try {
+            viewModelScope.launch(context = context, block = block)
+        } catch (e: CancellationException) {
+            Timber.w(e, "launch()ed coroutine was canceled.")
+        }
+    }
 
     @CallSuper
     override fun onCleared() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt
index 2b8f2cebec6728c11a56ad81194fdb577c10720d..8f1569b31291269029d022e4da11633af2177d4b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt
@@ -2,9 +2,6 @@ package de.rki.coronawarnapp.worker
 
 import androidx.work.Constraints
 import androidx.work.NetworkType
-import de.rki.coronawarnapp.notification.NotificationHelper
-import de.rki.coronawarnapp.storage.LocalData
-import timber.log.Timber
 import kotlin.random.Random
 
 /**
@@ -57,18 +54,4 @@ object BackgroundWorkHelper {
             .Builder()
             .setRequiredNetworkType(NetworkType.CONNECTED)
             .build()
-
-    /**
-     * Send debug notification to check background jobs execution
-     *
-     * @param title: String
-     * @param content: String
-     *
-     * @see LocalData.backgroundNotification()
-     */
-    fun sendDebugNotification(title: String, content: String) {
-        Timber.d("sendDebugNotification(title=%s, content=%s)", title, content)
-        if (!LocalData.backgroundNotification()) return
-        NotificationHelper.sendNotification(title, content, true)
-    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
index 59631fb84cbdf88ac69cf8dd916eb2412224fb68..cc7b277a69444e7cb9dd6298fa1c612cf75a477a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
@@ -3,9 +3,8 @@ package de.rki.coronawarnapp.worker
 import androidx.work.ExistingPeriodicWorkPolicy
 import androidx.work.ExistingWorkPolicy
 import androidx.work.Operation
-import androidx.work.WorkManager
 import androidx.work.WorkInfo
-import de.rki.coronawarnapp.BuildConfig
+import androidx.work.WorkManager
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.storage.LocalData
 import timber.log.Timber
@@ -91,8 +90,7 @@ object BackgroundWorkScheduler {
             LocalData.initialPollingForTestResultTimeStamp(System.currentTimeMillis())
             notificationBody.append("[DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER]")
         }
-        BackgroundWorkHelper.sendDebugNotification(
-            "Background Job Starting", notificationBody.toString())
+        Timber.d("Background Job Starting: %s", notificationBody)
     }
 
     /**
@@ -144,8 +142,7 @@ object BackgroundWorkScheduler {
             workManager.cancelAllWorkByTag(workTag.tag)
                 .also { it.logOperationCancelByTag(workTag) }
         }
-        BackgroundWorkHelper.sendDebugNotification(
-            "All Background Jobs Stopped", "All Background Jobs Stopped")
+        Timber.d("All Background Jobs Stopped")
     }
 
     /**
@@ -277,28 +274,24 @@ object BackgroundWorkScheduler {
      * Log operation schedule
      */
     private fun Operation.logOperationSchedule(workType: WorkType) =
-        this.result.addListener({
-            Timber.d("${workType.uniqueName} completed.")
-            BackgroundWorkHelper.sendDebugNotification(
-                "Background Job Started", "${workType.uniqueName} scheduled")
-        }, { it.run() })
-            .also { if (BuildConfig.DEBUG) Timber.d("${workType.uniqueName} scheduled.") }
+        this.result.addListener(
+            { Timber.d("${workType.uniqueName} completed.") },
+            { it.run() }
+        ).also { Timber.d("${workType.uniqueName} scheduled.") }
 
     /**
      * Log operation cancellation
      */
     private fun Operation.logOperationCancelByTag(workTag: WorkTag) =
-        this.result.addListener({
-            Timber.d("All work with tag ${workTag.tag} canceled.")
-            BackgroundWorkHelper.sendDebugNotification(
-                "Background Job canceled", "${workTag.tag} canceled")
-        }, { it.run() })
-            .also { if (BuildConfig.DEBUG) Timber.d("Canceling all work with tag ${workTag.tag}") }
+        this.result.addListener(
+            { Timber.d("All work with tag ${workTag.tag} canceled.") },
+            { it.run() }
+        ).also { Timber.d("Canceling all work with tag ${workTag.tag}") }
 
     /**
      * Log work active status
      */
     private fun logWorkActiveStatus(tag: String, active: Boolean) {
-        if (BuildConfig.DEBUG) Timber.d("Work type $tag is active: $active")
+        Timber.d("Work type $tag is active: $active")
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
index ef5f51077634e914630f793378753b17336e4432..5546e8947deebec1c3536ed4e206da4e22fbfbc3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
@@ -32,29 +32,19 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
     override suspend fun doWork(): Result {
         Timber.d("$id: doWork() started. Run attempt: $runAttemptCount")
 
-        BackgroundWorkHelper.sendDebugNotification(
-            "KeyOneTime Executing: Start", "KeyOneTime started. Run attempt: $runAttemptCount "
-        )
-
         var result = Result.success()
         taskController.submitBlocking(
             DefaultTaskRequest(
                 DownloadDiagnosisKeysTask::class,
-                DownloadDiagnosisKeysTask.Arguments(null, true)
+                DownloadDiagnosisKeysTask.Arguments(),
+                originTag = "DiagnosisKeyRetrievalOneTimeWorker"
             )
         ).error?.also { error: Throwable ->
-            Timber.w(
-                error, "$id: Error during startWithConstraints()."
-            )
+            Timber.w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.")
 
             if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
                 Timber.w(error, "$id: Retry attempts exceeded.")
 
-                BackgroundWorkHelper.sendDebugNotification(
-                    "KeyOneTime Executing: Failure",
-                    "KeyOneTime failed with $runAttemptCount attempts"
-                )
-
                 return Result.failure()
             } else {
                 Timber.d(error, "$id: Retrying.")
@@ -62,10 +52,6 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
             }
         }
 
-        BackgroundWorkHelper.sendDebugNotification(
-            "KeyOneTime Executing: End", "KeyOneTime result: $result "
-        )
-
         Timber.d("$id: doWork() finished with %s", result)
         return result
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
index f0dc4742ce5993b5425f8feabb4d67173df09538..4bcd5bc8f1a684461d0a1e0d92cf290487c4741e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
@@ -31,10 +31,6 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor(
     override suspend fun doWork(): Result {
         Timber.d("$id: doWork() started. Run attempt: $runAttemptCount")
 
-        BackgroundWorkHelper.sendDebugNotification(
-            "KeyPeriodic Executing: Start", "KeyPeriodic started. Run attempt: $runAttemptCount"
-        )
-
         var result = Result.success()
         try {
             BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
@@ -46,11 +42,6 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor(
             if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
                 Timber.w(e, "$id: Retry attempts exceeded.")
 
-                BackgroundWorkHelper.sendDebugNotification(
-                    "KeyPeriodic Executing: Failure",
-                    "KeyPeriodic failed with $runAttemptCount attempts"
-                )
-
                 return Result.failure()
             } else {
                 Timber.d(e, "$id: Retrying.")
@@ -58,10 +49,6 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor(
             }
         }
 
-        BackgroundWorkHelper.sendDebugNotification(
-            "KeyPeriodic Executing: End", "KeyPeriodic result: $result "
-        )
-
         Timber.d("$id: doWork() finished with %s", result)
         return result
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
index dd3bcea83189fbf7934533fd8073232917b60b31..1453a32bb33c4e5b207d49b68093c49fd667564b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
@@ -28,10 +28,6 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
     private val submissionService: SubmissionService
 ) : CoroutineWorker(context, workerParams) {
 
-    companion object {
-        private val TAG: String? = DiagnosisTestResultRetrievalPeriodicWorker::class.simpleName
-    }
-
     /**
      * Work execution
      *
@@ -46,16 +42,10 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
     override suspend fun doWork(): Result {
 
         Timber.d("$id: doWork() started. Run attempt: $runAttemptCount")
-        BackgroundWorkHelper.sendDebugNotification(
-            "TestResult Executing: Start", "TestResult started. Run attempt: $runAttemptCount "
-        )
 
         if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
             Timber.d("$id doWork() failed after $runAttemptCount attempts. Rescheduling")
 
-            BackgroundWorkHelper.sendDebugNotification(
-                "TestResult Executing: Failure", "TestResult failed with $runAttemptCount attempts"
-            )
             BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
             Timber.d("$id Rescheduled background worker")
 
@@ -81,9 +71,6 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
             result = Result.retry()
         }
 
-        BackgroundWorkHelper.sendDebugNotification(
-            "TestResult Executing: End", "TestResult result: $result "
-        )
         Timber.d("$id: doWork() finished with %s", result)
 
         return result
@@ -133,9 +120,6 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
         LocalData.initialPollingForTestResultTimeStamp(0L)
         BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop()
         Timber.d("$id: Background worker stopped")
-        BackgroundWorkHelper.sendDebugNotification(
-            "TestResult Stopped", "TestResult Stopped"
-        )
     }
 
     @AssistedInject.Factory
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information.xml b/Corona-Warn-App/src/main/res/layout/fragment_information.xml
index d51c313a9172fd2dba61cbbcdfa0a807b3e55a57..b18582525731b852608527024c79dc44b8c39503 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml
@@ -1,12 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <layout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <data>
-
-        <import type="de.rki.coronawarnapp.util.formatter.FormatterInformationHelper" />
-
-    </data>
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/information_container"
@@ -117,11 +112,27 @@
                     android:layout_height="wrap_content"
                     android:layout_marginTop="@dimen/spacing_small"
                     android:focusable="true"
-                    android:text="@{FormatterInformationHelper.formatVersion()}"
+                    tools:text="v1.8.0-RC1"
                     app:layout_constraintEnd_toEndOf="parent"
                     app:layout_constraintStart_toStartOf="@+id/guideline_body"
                     app:layout_constraintTop_toBottomOf="@+id/information_legal" />
 
+                <TextView
+                    android:id="@+id/information_enf_version"
+                    style="@style/body2Medium"
+                    android:visibility="gone"
+                    tools:visibility="visible"
+                    android:layout_width="@dimen/match_constraint"
+                    android:paddingTop="@dimen/spacing_tiny"
+                    android:paddingBottom="@dimen/spacing_tiny"
+                    android:layout_height="wrap_content"
+                    android:focusable="true"
+                    android:background="?selectableItemBackground"
+                    tools:text="16000000"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="@+id/guideline_body"
+                    app:layout_constraintTop_toBottomOf="@+id/information_version" />
+
                 <androidx.constraintlayout.widget.Guideline
                     android:id="@+id/guideline_body"
                     android:layout_width="wrap_content"
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml
index d5a9a1cd10884ea60e9a489a467157ee8a0f1ccb..1c977477629c010d92caec824dc2ce8c00b41d77 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml
@@ -5,9 +5,9 @@
 
     <androidx.constraintlayout.widget.ConstraintLayout
         android:id="@+id/submission_done_container"
-        android:contentDescription="@string/submission_done_title"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:contentDescription="@string/submission_done_title"
         android:fillViewport="true"
         tools:context=".ui.submission.fragment.SubmissionQRCodeInfo">
 
@@ -17,228 +17,20 @@
             android:layout_width="@dimen/match_constraint"
             android:layout_height="wrap_content"
             app:icon="@{@drawable/ic_back}"
-            app:title="@string/submission_qr_info_headline"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintBottom_toTopOf="@id/submission_qr_code_info_scrollview" />
+            app:layout_constraintTop_toTopOf="parent"
+            app:title="@string/submission_qr_info_headline" />
 
-        <ScrollView
-            android:id="@+id/submission_qr_code_info_scrollview"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
+        <include
+            android:id="@+id/include_submission_qr_code_info"
+            layout="@layout/include_submission_qr_code_info"
+            android:layout_width="@dimen/match_constraint"
+            android:layout_height="@dimen/match_constraint"
+            app:layout_constraintBottom_toTopOf="@id/guideline_action"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@+id/submission_qr_code_info_header">
-
-            <androidx.constraintlayout.widget.ConstraintLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:focusable="true">
-
-                <ImageView
-                    android:id="@+id/submission_qr_info_illustration"
-                    android:layout_width="@dimen/match_constraint"
-                    android:layout_height="wrap_content"
-                    android:focusable="true"
-                    android:src="@drawable/ic_illustrations_qr_code_scan_info"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toTopOf="parent" />
-
-                <androidx.constraintlayout.widget.ConstraintLayout
-                    android:id="@+id/qr_info_step_1"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:focusable="true"
-                    android:layout_marginTop="@dimen/bullet_point_spacing_after"
-                    android:layout_marginHorizontal="@dimen/guideline_card"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@id/submission_qr_info_illustration">
-
-                    <androidx.constraintlayout.widget.ConstraintLayout
-                        android:id="@+id/qr_info_step_1_icon"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:background="@drawable/circle"
-                        android:backgroundTint="@color/card_dark"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintStart_toStartOf="parent"
-                        app:layout_constraintTop_toTopOf="parent">
-
-                        <ImageView
-                            style="@style/icon"
-                            android:layout_width="@dimen/icon_size_risk_details_behavior"
-                            android:layout_height="@dimen/icon_size_risk_details_behavior"
-                            android:layout_margin="@dimen/icon_margin_risk_details_behavior"
-                            android:focusable="false"
-                            android:importantForAccessibility="no"
-                            android:src="@drawable/ic_qr_icon_personal_result"
-                            android:tint="@color/button_primary"
-                            app:layout_constraintBottom_toBottomOf="parent"
-                            app:layout_constraintEnd_toEndOf="parent"
-                            app:layout_constraintStart_toStartOf="parent"
-                            app:layout_constraintTop_toTopOf="parent" />
-                    </androidx.constraintlayout.widget.ConstraintLayout>
-
-                    <TextView
-                        style="@style/subtitle"
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_marginStart="@dimen/spacing_small"
-                        android:text="@string/submission_qr_info_point_1_body"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintEnd_toEndOf="parent"
-                        app:layout_constraintStart_toEndOf="@+id/qr_info_step_1_icon"
-                        app:layout_constraintTop_toTopOf="parent" />
-                </androidx.constraintlayout.widget.ConstraintLayout>
-
-                <androidx.constraintlayout.widget.ConstraintLayout
-                    android:id="@+id/qr_info_step_2"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:focusable="true"
-                    android:layout_marginTop="@dimen/bullet_point_spacing_after"
-                    android:layout_marginHorizontal="@dimen/guideline_card"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@id/qr_info_step_1">
-
-                    <androidx.constraintlayout.widget.ConstraintLayout
-                        android:id="@+id/qr_info_step_2_icon"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:background="@drawable/circle"
-                        android:backgroundTint="@color/card_dark"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintStart_toStartOf="parent"
-                        app:layout_constraintTop_toTopOf="parent">
-
-                        <ImageView
-                            style="@style/icon"
-                            android:layout_width="@dimen/icon_size_risk_details_behavior"
-                            android:layout_height="@dimen/icon_size_risk_details_behavior"
-                            android:layout_margin="@dimen/icon_margin_risk_details_behavior"
-                            android:focusable="false"
-                            android:importantForAccessibility="no"
-                            android:src="@drawable/ic_qr_icon_test_scan"
-                            android:tint="@color/button_primary"
-                            app:layout_constraintBottom_toBottomOf="parent"
-                            app:layout_constraintEnd_toEndOf="parent"
-                            app:layout_constraintStart_toStartOf="parent"
-                            app:layout_constraintTop_toTopOf="parent" />
-                    </androidx.constraintlayout.widget.ConstraintLayout>
-
-                    <TextView
-                        style="@style/subtitle"
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_marginStart="@dimen/spacing_small"
-                        android:text="@string/submission_qr_info_point_2_body"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintEnd_toEndOf="parent"
-                        app:layout_constraintStart_toEndOf="@+id/qr_info_step_2_icon"
-                        app:layout_constraintTop_toTopOf="parent" />
-                </androidx.constraintlayout.widget.ConstraintLayout>
-
-                <androidx.constraintlayout.widget.ConstraintLayout
-                    android:id="@+id/qr_info_step_3"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:focusable="true"
-                    android:layout_marginTop="@dimen/bullet_point_spacing_after"
-                    android:layout_marginHorizontal="@dimen/guideline_card"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@id/qr_info_step_2">
-
-                    <androidx.constraintlayout.widget.ConstraintLayout
-                        android:id="@+id/qr_info_step_3_icon"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:background="@drawable/circle"
-                        android:backgroundTint="@color/card_dark"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintStart_toStartOf="parent"
-                        app:layout_constraintTop_toTopOf="parent">
-
-                        <ImageView
-                            style="@style/icon"
-                            android:layout_width="@dimen/icon_size_risk_details_behavior"
-                            android:layout_height="@dimen/icon_size_risk_details_behavior"
-                            android:layout_margin="@dimen/icon_margin_risk_details_behavior"
-                            android:focusable="false"
-                            android:importantForAccessibility="no"
-                            android:src="@drawable/ic_qr_icon_multiple_tests"
-                            android:tint="@color/button_primary"
-                            app:layout_constraintBottom_toBottomOf="parent"
-                            app:layout_constraintEnd_toEndOf="parent"
-                            app:layout_constraintStart_toStartOf="parent"
-                            app:layout_constraintTop_toTopOf="parent" />
-                    </androidx.constraintlayout.widget.ConstraintLayout>
-
-                    <TextView
-                        style="@style/subtitle"
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_marginStart="@dimen/spacing_small"
-                        android:text="@string/submission_qr_info_point_3_body"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintEnd_toEndOf="parent"
-                        app:layout_constraintStart_toEndOf="@+id/qr_info_step_3_icon"
-                        app:layout_constraintTop_toTopOf="parent" />
-                </androidx.constraintlayout.widget.ConstraintLayout>
-
-                <androidx.constraintlayout.widget.ConstraintLayout
-                    android:id="@+id/qr_info_step_4"
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:focusable="true"
-                    android:layout_marginTop="@dimen/bullet_point_spacing_after"
-                    android:layout_marginHorizontal="@dimen/guideline_card"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@id/qr_info_step_3">
-
-                    <androidx.constraintlayout.widget.ConstraintLayout
-                        android:id="@+id/qr_info_step_4_icon"
-                        android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
-                        android:background="@drawable/circle"
-                        android:backgroundTint="@color/card_dark"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintStart_toStartOf="parent"
-                        app:layout_constraintTop_toTopOf="parent">
-
-                        <ImageView
-                            style="@style/icon"
-                            android:layout_width="@dimen/icon_size_risk_details_behavior"
-                            android:layout_height="@dimen/icon_size_risk_details_behavior"
-                            android:layout_margin="@dimen/icon_margin_risk_details_behavior"
-                            android:focusable="false"
-                            android:importantForAccessibility="no"
-                            android:src="@drawable/ic_qr_icon_info"
-                            android:tint="@color/button_primary"
-                            app:layout_constraintBottom_toBottomOf="parent"
-                            app:layout_constraintEnd_toEndOf="parent"
-                            app:layout_constraintStart_toStartOf="parent"
-                            app:layout_constraintTop_toTopOf="parent" />
-                    </androidx.constraintlayout.widget.ConstraintLayout>
-
-                    <TextView
-                        style="@style/subtitle"
-                        android:layout_width="0dp"
-                        android:layout_height="wrap_content"
-                        android:layout_marginStart="@dimen/spacing_small"
-                        android:text="@string/submission_qr_info_point_4_body"
-                        app:layout_constraintBottom_toBottomOf="parent"
-                        app:layout_constraintEnd_toEndOf="parent"
-                        app:layout_constraintStart_toEndOf="@+id/qr_info_step_4_icon"
-                        app:layout_constraintTop_toTopOf="parent" />
-                </androidx.constraintlayout.widget.ConstraintLayout>
-            </androidx.constraintlayout.widget.ConstraintLayout>
-
-        </ScrollView>
+            app:layout_constraintTop_toBottomOf="@+id/submission_qr_code_info_header" />
 
         <Button
             android:id="@+id/submission_qr_info_button_next"
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml
index a34166dd59df62849ed4df4572518c4ef5ea3e24..ecf73ebc437f034b2ec90d622215160aba1e9a20 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml
@@ -9,10 +9,6 @@
 
         <import type="de.rki.coronawarnapp.submission.Symptoms.StartOf" />
 
-        <variable
-            name="submissionViewModel"
-            type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" />
-
     </data>
 
     <ScrollView
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml
index 0fb5a5e024a58ccb1cb3699b28add5c405dee041..e577341719fc20305e51f0ef2b7ea5eaeb8d37e2 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml
@@ -7,10 +7,6 @@
 
         <import type="de.rki.coronawarnapp.util.formatter.FormatterSubmissionHelper" />
 
-        <variable
-            name="submissionViewModel"
-            type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" />
-
     </data>
 
     <ScrollView
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml
index 3521faa0f64126875ffb9317c6782159fa998fff..9b40a94aa99fca525d7cf72b5313f2b2e1f8cef3 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml
@@ -34,7 +34,7 @@
             style="?android:attr/progressBarStyle"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:visibility="@{FormatterSubmissionHelper.formatTestResultSpinnerVisible(uiState.apiRequestState)}"
+            android:visibility="@{FormatterSubmissionHelper.formatTestResultSpinnerVisible(uiState.deviceUiState)}"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
@@ -48,7 +48,7 @@
             android:layout_width="@dimen/match_constraint"
             android:layout_height="@dimen/match_constraint"
             android:layout_marginBottom="@dimen/button_padding_top_bottom"
-            android:visibility="@{FormatterSubmissionHelper.formatTestResultVisible(uiState.apiRequestState)}"
+            android:visibility="@{FormatterSubmissionHelper.formatTestResultVisible(uiState.deviceUiState)}"
             app:layout_constraintBottom_toTopOf="@+id/include_submission_test_result_buttons"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_qr_code_info.xml b/Corona-Warn-App/src/main/res/layout/include_submission_qr_code_info.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2f68632b6174c70d157e0e067b55b1eca08b06ae
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/include_submission_qr_code_info.xml
@@ -0,0 +1,219 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <ScrollView
+        android:id="@+id/submission_qr_code_info_scrollview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:focusable="true">
+
+            <ImageView
+                android:id="@+id/submission_qr_info_illustration"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:focusable="true"
+                android:src="@drawable/ic_illustrations_qr_code_scan_info"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/qr_info_step_1"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="@dimen/guideline_card"
+                android:layout_marginTop="@dimen/bullet_point_spacing_after"
+                android:focusable="true"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/submission_qr_info_illustration">
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/qr_info_step_1_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/circle"
+                    android:backgroundTint="@color/card_dark"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent">
+
+                    <ImageView
+                        style="@style/icon"
+                        android:layout_width="@dimen/icon_size_risk_details_behavior"
+                        android:layout_height="@dimen/icon_size_risk_details_behavior"
+                        android:layout_margin="@dimen/icon_margin_risk_details_behavior"
+                        android:focusable="false"
+                        android:importantForAccessibility="no"
+                        android:src="@drawable/ic_qr_icon_personal_result"
+                        android:tint="@color/button_primary"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent" />
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <TextView
+                    style="@style/subtitle"
+                    android:layout_width="@dimen/match_constraint"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_small"
+                    android:text="@string/submission_qr_info_point_1_body"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toEndOf="@+id/qr_info_step_1_icon"
+                    app:layout_constraintTop_toTopOf="parent" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/qr_info_step_2"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="@dimen/guideline_card"
+                android:layout_marginTop="@dimen/bullet_point_spacing_after"
+                android:focusable="true"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/qr_info_step_1">
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/qr_info_step_2_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/circle"
+                    android:backgroundTint="@color/card_dark"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent">
+
+                    <ImageView
+                        style="@style/icon"
+                        android:layout_width="@dimen/icon_size_risk_details_behavior"
+                        android:layout_height="@dimen/icon_size_risk_details_behavior"
+                        android:layout_margin="@dimen/icon_margin_risk_details_behavior"
+                        android:focusable="false"
+                        android:importantForAccessibility="no"
+                        android:src="@drawable/ic_qr_icon_test_scan"
+                        android:tint="@color/button_primary"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent" />
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <TextView
+                    style="@style/subtitle"
+                    android:layout_width="@dimen/match_constraint"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_small"
+                    android:text="@string/submission_qr_info_point_2_body"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toEndOf="@+id/qr_info_step_2_icon"
+                    app:layout_constraintTop_toTopOf="parent" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/qr_info_step_3"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="@dimen/guideline_card"
+                android:layout_marginTop="@dimen/bullet_point_spacing_after"
+                android:focusable="true"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/qr_info_step_2">
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/qr_info_step_3_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/circle"
+                    android:backgroundTint="@color/card_dark"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent">
+
+                    <ImageView
+                        style="@style/icon"
+                        android:layout_width="@dimen/icon_size_risk_details_behavior"
+                        android:layout_height="@dimen/icon_size_risk_details_behavior"
+                        android:layout_margin="@dimen/icon_margin_risk_details_behavior"
+                        android:focusable="false"
+                        android:importantForAccessibility="no"
+                        android:src="@drawable/ic_qr_icon_multiple_tests"
+                        android:tint="@color/button_primary"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent" />
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <TextView
+                    style="@style/subtitle"
+                    android:layout_width="@dimen/match_constraint"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_small"
+                    android:text="@string/submission_qr_info_point_3_body"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toEndOf="@+id/qr_info_step_3_icon"
+                    app:layout_constraintTop_toTopOf="parent" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+
+            <androidx.constraintlayout.widget.ConstraintLayout
+                android:id="@+id/qr_info_step_4"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="wrap_content"
+                android:layout_marginHorizontal="@dimen/guideline_card"
+                android:layout_marginTop="@dimen/bullet_point_spacing_after"
+                android:focusable="true"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/qr_info_step_3">
+
+                <androidx.constraintlayout.widget.ConstraintLayout
+                    android:id="@+id/qr_info_step_4_icon"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:background="@drawable/circle"
+                    android:backgroundTint="@color/card_dark"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toTopOf="parent">
+
+                    <ImageView
+                        style="@style/icon"
+                        android:layout_width="@dimen/icon_size_risk_details_behavior"
+                        android:layout_height="@dimen/icon_size_risk_details_behavior"
+                        android:layout_margin="@dimen/icon_margin_risk_details_behavior"
+                        android:focusable="false"
+                        android:importantForAccessibility="no"
+                        android:src="@drawable/ic_qr_icon_info"
+                        android:tint="@color/button_primary"
+                        app:layout_constraintBottom_toBottomOf="parent"
+                        app:layout_constraintEnd_toEndOf="parent"
+                        app:layout_constraintStart_toStartOf="parent"
+                        app:layout_constraintTop_toTopOf="parent" />
+                </androidx.constraintlayout.widget.ConstraintLayout>
+
+                <TextView
+                    style="@style/subtitle"
+                    android:layout_width="@dimen/match_constraint"
+                    android:layout_height="wrap_content"
+                    android:layout_marginStart="@dimen/spacing_small"
+                    android:text="@string/submission_qr_info_point_4_body"
+                    app:layout_constraintBottom_toBottomOf="parent"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toEndOf="@+id/qr_info_step_4_icon"
+                    app:layout_constraintTop_toTopOf="parent" />
+            </androidx.constraintlayout.widget.ConstraintLayout>
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </ScrollView>
+</layout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml b/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml
index d95f4ed9f6f4201980e57caa01ec1e06c2d1ff6e..6cbbd318066a22c37059ae2209bd487f7851b341 100644
--- a/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml
+++ b/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml
@@ -8,10 +8,6 @@
 
         <import type="de.rki.coronawarnapp.submission.Symptoms.StartOf" />
 
-        <variable
-            name="submissionViewModel"
-            type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" />
-
     </data>
 
     <androidx.constraintlayout.widget.ConstraintLayout
diff --git a/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml b/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml
index dcdcdea37b799ecb255d0e23b0b3d68d0499f0ad..01364c7898c6942746da48f840b496513e186caf 100644
--- a/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml
+++ b/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml
@@ -13,7 +13,7 @@
 
         <variable
             name="deviceUIState"
-            type="de.rki.coronawarnapp.util.DeviceUIState" />
+            type="de.rki.coronawarnapp.util.NetworkRequestWrapper&lt;de.rki.coronawarnapp.util.DeviceUIState,java.lang.Throwable>" />
     </data>
 
     <androidx.constraintlayout.widget.ConstraintLayout
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 7b20d46a9c3aa5b49191a72f9d811bc7c7f13a15..469ac6b4b49c3c138f8703f8e2d5db12bedf9726 100644
--- a/Corona-Warn-App/src/main/res/values-bg/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml
@@ -20,12 +20,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -109,6 +105,10 @@
     <string name="notification_headline">"Corona-Warn-App"</string>
     <!-- XTXT: Notification body -->
     <string name="notification_body">"Имате нови съобщения от приложението Corona-Warn-App."</string>
+    <!-- XHED: Notification title - Reminder to share a positive test result-->
+    <string name="notification_headline_share_positive_result">"Можете да помогнете!"</string>
+    <!-- XTXT: Notification body - Reminder to share a positive test result-->
+    <string name="notification_body_share_positive_result">"Моля, споделете резултата от теста си и предупредете останалите потребители."</string>
 
     <!-- ####################################
               App Auto Update
@@ -146,15 +146,13 @@
         <item quantity="many">"%1$s излагания на риск"</item>
     </plurals>
     <!-- XTXT: risk card - tracing active for x out of 14 days -->
-    <string name="risk_card_body_saved_days">"Регистрирането на излагания на риск е било активно през %1$s от изминалите 14 дни."</string>
+    <string name="risk_card_body_saved_days">"Регистрирането на излагания на риск беше активно през %1$s от изминалите 14 дни."</string>
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
     <string name="risk_card_body_saved_days_full">"Няма прекъсване на регистрирането на излагания на риск"</string>
     <!-- XTXT; risk card - no update done yet -->
-    <string name="risk_card_body_not_yet_fetched">"Все още не е извършвана проверка на излаганията на риск."</string>
+    <string name="risk_card_body_not_yet_fetched">"Все още не е извършвана проверка на контактите."</string>
     <!-- XTXT: risk card - last successful update -->
     <string name="risk_card_body_time_fetched">"Актуализирано: %1$s"</string>
-    <!-- XTXT: risk card - next update -->
-    <string name="risk_card_body_next_update">"Ежедневно актуализиране"</string>
     <!-- XTXT: risk card - hint to open the app daily -->
     <string name="risk_card_body_open_daily">"Бележка: Моля, отваряйте приложението всеки ден, за да актуализирате своя статус на риск."</string>
     <!-- XBUT: risk card - update risk -->
@@ -185,13 +183,19 @@
     <!-- XHED: risk card - tracing stopped headline, due to no possible calculation -->
     <string name="risk_card_no_calculation_possible_headline">"В момента не се извършва регистриране на излагания на риск"</string>
     <!-- XTXT: risk card - last successfully calculated risk level -->
-    <string name="risk_card_no_calculation_possible_body_saved_risk">"Последно регистриране на излагане на риск:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
+    <string name="risk_card_no_calculation_possible_body_saved_risk">"Последна проверка за излагане на риск:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
     <!-- XHED: risk card -  outdated risk headline, calculation isn't possible -->
     <string name="risk_card_outdated_risk_headline">"Невъзможно регистриране на излагания на риск"</string>
     <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours -->
     <string name="risk_card_outdated_risk_body">"Регистърът на излаганията на риск не е обновяван повече от 24 часа."</string>
     <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours -->
     <string name="risk_card_outdated_manual_risk_body">"Вашият статус на риск не е обновяван от повече от 48 часа. Моля, актуализирайте го."</string>
+    <!-- XHED: risk card - risk check failed headline, no internet connection -->
+    <string name="risk_card_check_failed_no_internet_headline">"Проверката за излагане на риск е неуспешна"</string>
+    <!-- XTXT: risk card - risk check failed, please check your internet connection -->
+    <string name="risk_card_check_failed_no_internet_body">"Синхронизацията на случайни ИД със сървъра е неуспешна. Можете да я рестартирате ръчно."</string>
+    <!-- XTXT: risk card - risk check failed, restart button -->
+    <string name="risk_card_check_failed_no_internet_restart_button">"Рестартиране"</string>
 
     <!-- ####################################
               Risk Card - Progress
@@ -245,9 +249,9 @@
     <!-- XACT: main overview page title -->
     <string name="main_overview_accessibility_title">"Общ преглед"</string>
     <!-- XHED: App overview subtitle for tracing explanation-->
-    <string name="main_overview_subtitle_tracing">"Регистриране на излаганията на риск"</string>
+    <string name="main_overview_subtitle_tracing">"Регистър на рисковете"</string>
     <!-- YTXT: App overview body text about tracing -->
-    <string name="main_overview_body_tracing">"Регистрирането на излагания на риск е една от трите основни функции на приложението. Когато е активирана, всички осъществени контакти между устройства се записват, без да е необходимо да правите друго."</string>
+    <string name="main_overview_body_tracing">"Регистрирането на излагания на риск е една от трите основни функции на приложението. Когато е активирана, всички осъществени контакти между смартфоните се записват, без да е необходимо да правите друго."</string>
     <!-- XHED: App overview subtitle for risk explanation -->
     <string name="main_overview_subtitle_risk">"Риск от заразяване"</string>
     <!-- YTXT: App overview body text about risk levels -->
@@ -357,7 +361,7 @@
         <item quantity="many">"Изложени сте на повишен риск от заразяване, защото преди %1$s дни сте имали продължителен и близък контакт с поне едно лице, диагностицирано с COVID-19."</item>
     </plurals>
     <!-- YTXT: risk details - risk calculation explanation -->
-    <string name="risk_details_information_body_notice">"Рискът от заразяване се изчислява въз основа на данните за излагане (продължителност и близост на контакта), регистрирани на вашето локално устройство. Никой освен Вас не може да види или да получи данни за Вашето ниво на риск."</string>
+    <string name="risk_details_information_body_notice">"Рискът от заразяване се изчислява въз основа на данните за излагане (продължителност и близост на контакта), регистрирани във вашия смартфон. Никой освен Вас не може да види или да получи данни за Вашето ниво на риск."</string>
     <!-- YTXT: risk details - risk calculation explanation for increased risk -->
     <string name="risk_details_information_body_notice_increased">"Това е причината да определим Вашия риск от заразяване като повишен. Рискът от заразяване се изчислява въз основа на данните за излагане (продължителност и близост на контакта), регистрирани на вашето локално устройство. Никой освен Вас не може да види или да получи данни за Вашето ниво на риск. Когато се приберете у дома, избягвайте близките контакти с членовете на домакинството си."</string>
     <!-- NOTR -->
@@ -415,7 +419,7 @@
     <!-- XHED: onboarding(together) - two/three line headline under an illustration -->
     <string name="onboarding_subtitle">"Повече защита за Вас и за всички нас. С помощта на приложението Corona-Warn-App можем да прекъснем веригите на заразяване много по-бързо."</string>
     <!-- YTXT: onboarding(together) - inform about the app -->
-    <string name="onboarding_body">"Превърнете устройството си в предупредителна система за коронавирус. Прегледайте своя статус на риск и разберете дали през последните 14 дни сте имали близък контакт с лице, диагностицирано с COVID-19."</string>
+    <string name="onboarding_body">"Превърнете смартфона си в предупредителна система за коронавирус. Прегледайте своя статус на риск и разберете дали през последните 14 дни сте имали близък контакт с лице, диагностицирано с COVID-19."</string>
     <!-- YTXT: onboarding(together) - explain application -->
     <string name="onboarding_body_emphasized">"Приложението регистрира контакти на лица посредством обмяна на криптирани случайни ИД кодове между устройствата им, без да осъществява достъп до каквито и да било лични данни."</string>
     <!-- XACT: onboarding(together) - illustraction description, header image -->
@@ -432,13 +436,13 @@
     <!-- XHED: onboarding(tracing) - how to enable tracing -->
     <string name="onboarding_tracing_headline">"Как да активирате регистрирането на излагания на риск"</string>
     <!-- XHED: onboarding(tracing) - two/three line headline under an illustration -->
-    <string name="onboarding_tracing_subtitle">"За да установите дали за Вас съществува риск от заразяване, трябва да активирате функцията за регистриране на излагания на риск."</string>
+    <string name="onboarding_tracing_subtitle">"За да установите дали сте застрашени от заразяване, трябва да активирате функцията за регистриране на излагания на риск."</string>
     <!-- YTXT: onboarding(tracing) - explain tracing -->
     <string name="onboarding_tracing_body">"Регистрирането на излагания на риск се извършва с помощта на Bluetooth връзка, при която Вашият смартфон получава криптираните случайни идентификационни кодове на други потребители и изпраща до техните устройства Вашите случайни ИД. Функцията може да бъде дезактивирана по всяко време. "</string>
     <!-- YTXT: onboarding(tracing) - explain tracing -->
     <string name="onboarding_tracing_body_emphasized">"Криптираните случайни идентификатори предават само информация за дата, продължителност и близост на контакта (изчислена от силата на сигнала). Самоличността Ви не може да бъде установена по случайните ИД."</string>
     <!-- YTXT: onboarding(tracing) - easy language explain tracing link-->
-    <string name="onboarding_tracing_easy_language_explanation"><a href="https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache">"Информация за приложението на опростен и жестомимичен език."</a></string>
+    <string name="onboarding_tracing_easy_language_explanation"><a href="https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache">"Информация за приложението на опростен и жестомимичен език"</a></string>
     <!-- NOTR: onboarding(tracing) - easy language explain tracing link URL-->
     <string name="onboarding_tracing_easy_language_explanation_url">"https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache"</string>
     <!-- XBUT: onboarding(tracing) - button enable tracing -->
@@ -452,7 +456,7 @@
     <!-- XBUT: onboarding(tracing) - negative button (right) -->
     <string name="onboarding_tracing_dialog_button_negative">"Назад"</string>
     <!-- XACT: onboarding(tracing) - dialog about background jobs header text -->
-    <string name="onboarding_background_fetch_dialog_headline">"Фоновите актуализации са дезактивирани"</string>
+    <string name="onboarding_background_fetch_dialog_headline">"Фоновото опресняване за приложението е дезактивирано"</string>
     <!-- YMSI: onboarding(tracing) - dialog about background jobs -->
     <string name="onboarding_background_fetch_dialog_body">"Дезактивирали сте фоновите актуализации за приложението Corona-Warn-App. Моля, активирайте ги, за да използвате автоматичното регистриране на излагания на риск. Ако не го направите, регистрирането на излаганията може да бъде стартирано само ръчно от приложението. Може да активирате фоновите актуализации за приложението от настройките на Вашето устройство."</string>
     <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings -->
@@ -494,7 +498,7 @@
     <!-- XACT: Onboarding (datashare) page title -->
     <string name="onboarding_notifications_accessibility_title">"Въведение - страница 6 от 6: Получаване на предупреждения и идентифициране на рискове"</string>
     <!-- XHED: onboarding(datashare) - about positive tests -->
-    <string name="onboarding_notifications_headline">"Получаване на предупреждения и идентифициране на рискове"</string>
+    <string name="onboarding_notifications_headline">"Предупреждения и рискове"</string>
     <!-- XHED: onboarding(datashare) - two/three line headline under an illustration -->
     <string name="onboarding_notifications_subtitle">"Приложението може да Ви уведомява автоматично за Вашия статус на риск от заразяване и да Ви предупреждава за нови заразявания на хора, с които сте били в контакт. Позволете на приложението да Ви изпраща известия."</string>
     <!-- YTXT: onboarding(datashare) - explain test -->
@@ -524,7 +528,7 @@
     <!-- XTXT: settings - off, like a label next to a setting -->
     <string name="settings_off">"Изключено"</string>
     <!-- XHED: settings(tracing) - page title -->
-    <string name="settings_tracing_title">"Регистриране на излаганията на риск"</string>
+    <string name="settings_tracing_title">"Регистър на рисковете"</string>
     <!-- XHED: settings(tracing) - headline bellow illustration -->
     <string name="settings_tracing_headline">"Как работи регистрирането на излагания на риск"</string>
     <!-- XTXT: settings(tracing) - explain text in settings overview under headline -->
@@ -606,7 +610,7 @@
     <!-- XTXT: settings(notification) - next to a switch -->
     <string name="settings_notifications_subtitle_update_risk">"Промяна на Вашия риск от заразяване"</string>
     <!-- XTXT: settings(notification) - next to a switch -->
-    <string name="settings_notifications_subtitle_update_test">"Статус на Вашия тест за COVID-19"</string>
+    <string name="settings_notifications_subtitle_update_test">"Наличие на резултат от Ваш тест за COVID-19"</string>
     <!-- XBUT: settings(notification) - go to operating settings -->
     <string name="settings_notifications_button_open_settings">"Към настройките за устройството"</string>
     <!-- XACT: main (overview) - illustraction description, explanation image, displays notificatin status, active -->
@@ -652,7 +656,7 @@
     <!-- XTXT: settings(background priority) - explains user what to do on card if background priority is enabled -->
     <string name="settings_background_priority_card_body">"Можете да активирате и дезактивирате приоритетната работа във фонов режим от настройките на устройството."</string>
     <!-- XBUT: settings(background priority) - go to operating system settings button on card -->
-    <string name="settings_background_priority_card_button">"Към настройките за устройството"</string>
+    <string name="settings_background_priority_card_button">"Към настройките на устройството"</string>
     <!-- XHED : settings(background priority) - headline on card about the current status and what to do -->
     <string name="settings_background_priority_card_headline">"Промяна на приоритетната работа във фонов режим"</string>
 
@@ -671,7 +675,7 @@
     <!-- YTXT: Body text for about information page -->
     <string name="information_about_body_emphasized">"Институтът „Роберт Кох“ (RKI) е федералната служба за обществено здравеопазване в Германия. Той е издател на приложението Corona-Warn-App по поръчка на федералното правителство. Приложението е предназначено да бъде дигитално допълнение на вече въведените мерки за опазване на общественото здраве: социално дистанциране, поддържане на висока хигиена и носене на маски."</string>
     <!-- YTXT: Body text for about information page -->
-    <string name="information_about_body">"Хората, които използват приложението, помагат за проследяване и прекъсване на веригите на заразяване. Приложението запазва във Вашето устройство данните за контактите Ви с други хора. Получавате известие, ако сте били в контакт с лица, които впоследствие са били диагностицирани с COVID-19. Вашата самоличност и неприкосновеността на данните Ви са защитени по всяко време."</string>
+    <string name="information_about_body">"Всеки, който използва приложението, помага за проследяване и прекъсване на веригите на заразяване. Приложението запазва във Вашия смартфон данните за контактите Ви с други хора. Получавате известие, ако сте били в контакт с лица, които впоследствие са били диагностицирани с COVID-19. Вашата самоличност и неприкосновеността на данните Ви са защитени по всяко време."</string>
     <!-- XACT: describes illustration -->
     <string name="information_about_illustration_description">"Група лица използват смартфоните си, придвижвайки се из града."</string>
     <!-- XHED: Page title for privacy information page, also menu item / button text -->
@@ -689,13 +693,13 @@
     <!-- XTXT: Path to the full blown terms html, to translate it exchange "_de" to "_en" and provide the corresponding html file -->
     <string name="information_terms_html_path">"terms_en.html"</string>
     <!-- XHED: Page title for technical contact and hotline information page, also menu item / button text -->
-    <string name="information_contact_title">"Гореща линия за технически въпроси"</string>
+    <string name="information_contact_title">"Технически въпроси"</string>
     <!-- XHED: Subtitle for technical contact and hotline information page -->
     <string name="information_contact_headline">"Как можем да Ви помогнем?"</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body">"За технически въпроси относно Corona-Warn-App се обадете на горещата линия.\n\nЛицата със слухови затруднения могат да използват релейните услуги Tess (за превод между писмен немски и жестомимичен език), за да се обаждат на горещата телефонна линия. Може да изтеглите приложението от Google Play."</string>
     <!-- XHED: Subtitle for technical contact and hotline information page -->
-    <string name="information_contact_subtitle_phone">"Гореща линия за технически въпроси:"</string>
+    <string name="information_contact_subtitle_phone">"Технически въпроси:"</string>
     <!-- XLNK: Button / hyperlink to phone call for technical contact and hotline information page -->
     <string name="information_contact_button_phone">"+49 800 7540001"</string>
     <!-- XBUT: CAUTION - ONLY UPDATE THE NUMBER IF NEEDED, ONLY NUMBERS AND NO SPECIAL CHARACTERS EXCEPT "+" and "space" ALLOWED IN THIS FIELD; -->
@@ -703,9 +707,9 @@
     <!-- XTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_phone">"Нашият екип за обслужване на клиенти е готов да Ви помогне."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_open">"Езици: немски, английски, турски\nРаботно време:"<xliff:g id="line_break">"\n"</xliff:g>"понеделник до неделя: 7:00 - 22:00"<xliff:g id="line_break">"\n(с изключение на национални празници)"</xliff:g><xliff:g id="line_break">"\nОбаждането е безплатно."</xliff:g></string>
+    <string name="information_contact_body_open">"Езици: английски, немски, турски\nРаботно време:"<xliff:g id="line_break">"\n"</xliff:g>"понеделник до неделя: 7:00 - 22:00"<xliff:g id="line_break">"\n(с изключение на национални празници)"</xliff:g><xliff:g id="line_break">"\nОбаждането е безплатно."</xliff:g></string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_other">"Ако имате въпроси, свързани със здравето, се обадете на личния си лекар или на горещата линия на службата за медицинска помощ на телефон 116 117"</string>
+    <string name="information_contact_body_other">"Ако имате въпроси, свързани със здравето, се обадете на личния си лекар или на горещата линия на службата за медицинска помощ на телефон 116 117."</string>
     <!-- XACT: describes illustration -->
     <string name="information_contact_illustration_description">"Мъж със слушалки провежда телефонен разговор."</string>
     <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website -->
@@ -788,9 +792,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"Назад"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Грешки в теста"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"QR кодът вече не е валиден"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"Възникнаха грешки при проверката на Вашия тест. Този QR код вече е изтекъл."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Вашият тест е направен преди повече от 21 дни и вече не може да бъде регистриран в приложението. Ако в бъдеще си правите нов тест, моля сканирайте QR кода в момента, в който го получите."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string>
 
@@ -851,7 +855,7 @@
 
     <!-- Submission Test Result -->
     <!-- XHED: Page headline for test result  -->
-    <string name="submission_test_result_headline">"Резултат от теста"</string>
+    <string name="submission_test_result_headline">"Резултат от тест"</string>
     <!-- XHED: Page subheadline for test result  -->
     <string name="submission_test_result_subtitle">"Как работи:"</string>
     <!-- XHED: Page headline for results next steps  -->
@@ -865,15 +869,15 @@
     <!-- XBUT: test result pending : refresh button -->
     <string name="submission_test_result_pending_refresh_button">"Актуализиране"</string>
     <!-- XBUT: test result pending : remove the test button -->
-    <string name="submission_test_result_pending_remove_test_button">"Изтриване на тест"</string>
+    <string name="submission_test_result_pending_remove_test_button">"Изтриване на теста"</string>
     <!-- XHED: Page headline for negative test result next steps  -->
     <string name="submission_test_result_negative_steps_negative_heading">"Резултатът от Вашия тест"</string>
     <!-- YTXT: Body text for next steps section of test negative result -->
     <string name="submission_test_result_negative_steps_negative_body">"Вашият лабораторен резултат не потвърждава заразяване с коронавирус SARS-CoV-2.\n\nМоля, изтрийте теста от приложението Corona-Warn-App, за да можете да запазите нов код на тест, ако е необходимо."</string>
     <!-- XBUT: negative test result : remove the test button -->
-    <string name="submission_test_result_negative_remove_test_button">"Изтриване на тест"</string>
+    <string name="submission_test_result_negative_remove_test_button">"Изтриване на теста"</string>
     <!-- XHED: Page headline for other warnings screen  -->
-    <string name="submission_test_result_positive_steps_warning_others_heading">"Предупреждаване на останалите потребители"</string>
+    <string name="submission_test_result_positive_steps_warning_others_heading">"Предупредете другите"</string>
     <!-- YTXT: Body text for for other warnings screen-->
     <string name="submission_test_result_positive_steps_warning_others_body">"Споделете своите случайни идентификатори и предупредете околните.\nПомогнете ни да определяме риска от заразяване за тях по-точно, като споделите и кога за пръв път сте забелязали симптомите на коронавирусната инфекция."</string>
     <!-- XBUT: positive test result : continue button -->
@@ -952,13 +956,13 @@
     <!-- YTXT: Body text for QR code dispatcher option -->
     <string name="submission_dispatcher_qr_card_text">"Регистрирайте теста си, като сканирате QR кода на документа."</string>
     <!-- XHED: Dialog headline for dispatcher QR prviacy dialog  -->
-    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Съгласие"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Поверителност"</string>
     <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog -->
     <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Accept”, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacy”."</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Приемам"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Разрешавам"</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Не приемам"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Не разрешавам"</string>
     <!-- YTXT: Dispatcher text for TAN code option -->
     <string name="submission_dispatcher_card_tan_code">"ТАН код"</string>
     <!-- YTXT: Body text for TAN code dispatcher option -->
@@ -972,7 +976,7 @@
 
     <!-- Submission Positive Other Warning -->
     <!-- XHED: Page title for the positive result additional warning page-->
-    <string name="submission_positive_other_warning_title">"Предупреждаване на останалите потребители"</string>
+    <string name="submission_positive_other_warning_title">"Предупредете другите"</string>
     <!-- XHED: Page headline for the positive result additional warning page-->
     <string name="submission_positive_other_warning_headline">"Моля, помогнете на всички нас!"</string>
     <!-- YTXT: Body text for the positive result additional warning page-->
@@ -980,7 +984,7 @@
     <!-- XBUT: other warning continue button -->
     <string name="submission_positive_other_warning_button">"Приемам"</string>
     <!-- XACT: other warning - illustration description, explanation image -->
-    <string name="submission_positive_other_illustration_description">"Устройство предава на системата информация за положителен резултат от тест."</string>
+    <string name="submission_positive_other_illustration_description">"Смартфонът предава на системата информация за положителен резултат от тест."</string>
     <!-- XHED: Title for the interop country list-->
     <string name="submission_interoperability_list_title">"В момента в международното регистриране на излаганията участват следните държави:"</string>
 
@@ -1071,9 +1075,9 @@
     <!-- YTXT: Body text for step 2 of contact page-->
     <string name="submission_contact_step_2_body">"Регистрирайте теста, като въведете ТАН кода в приложението."</string>
     <!-- YTXT: Body text for operating hours in contact page-->
-    <string name="submission_contact_operating_hours_body">"Езици: \nнемски, английски, турски\n\nРаботно време:\nот понеделник до неделя: 24 часа\n\nОбажданията са безплатни."</string>
+    <string name="submission_contact_operating_hours_body">"Езици: \nанглийски, немски, турски\n\nРаботно време:\nот понеделник до неделя: 24 часа\n\nОбажданията са безплатни."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="submission_contact_body_other">"Ако имате въпроси, свързани със здравето, се обадете на личния си лекар или на горещата линия на службата за медицинска помощ на телефон 116 117"</string>
+    <string name="submission_contact_body_other">"Ако имате въпроси, свързани със здравето, се обадете на личния си лекар или на горещата линия на службата за медицинска помощ на телефон 116 117."</string>
 
     <!-- XACT: Submission contact page title -->
     <string name="submission_contact_accessibility_title">"Обадете се на горещата линия и поискайте ТАН код"</string>
@@ -1100,7 +1104,7 @@
 
     <!-- Submission Status Card -->
     <!-- XHED: Page title for the various submission status: fetching -->
-    <string name="submission_status_card_title_fetching">"Извършва се извличане на данни"</string>
+    <string name="submission_status_card_title_fetching">"Извършва се извличане на данни..."</string>
     <!-- XHED: Page title for the various submission status: unregistered -->
     <string name="submission_status_card_title_unregistered">"Правили ли сте си тест?"</string>
     <!-- XHED: Page title for the various submission status: pending -->
@@ -1201,7 +1205,10 @@
     <string name="errors_google_update_needed">"Вашето приложение Corona-Warn-App е инсталирано правилно, но „Системата за известяване при излагания на риск от заразяване с COVID-19” не се предлага за операционната система на Вашия смартфон. Това означава, че не можете да използвате приложението Corona-Warn-App. Повече информация може да намерите в страницата „ЧЗВ“ на адрес: https://www.coronawarn.app/en/faq/"</string>
     <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day -->
     <string name="errors_google_api_error">"Приложението Corona-Warn-App работи правилно, но не можем да актуализираме Вашия статус на риск. Регистрирането на излагания на риск все още е активно и функционира правилно. За повече информация посетете страницата „ЧЗВ“ на адрес: https://www.coronawarn.app/en/faq/"</string>
-
+    <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_title">"Лимитът вече е достигнат"</string>
+    <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_description">"Не може да правите повече проверки за излагане на риск до края на деня, тъй като сте достигнали максималния брой проверки, определен от вашата операционна система. Моля, проверете статуса си на риск утре."</string>
     <!-- ####################################
                Generic Error Messages
         ###################################### -->
@@ -1248,17 +1255,7 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
@@ -1359,7 +1356,7 @@
     <!-- YMSG: Onboarding tracing step third section in interoperability after the title. -->
     <string name="interoperability_onboarding_randomid_download_free">"Ежедневното изтегляне на списъка със случайни идентификатори обикновено е безплатно за Вас. Това означава, че мобилните оператори не таксуват използваните от приложението данни и не начисляват за тях такси за роуминг в други държави от ЕС. За повече подробности се обърнете към своя мобилен оператор."</string>
     <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. -->
-    <string name="interoperability_onboarding_list_title">"В момента в обмена участват средните държави:"</string>
+    <string name="interoperability_onboarding_list_title">"В момента в обмена участват следните държави:"</string>
 
     <!-- XTXT: Description of the expanded terms in delta interopoerability screen part 1 -->
     <string name="interoperability_onboarding_delta_expanded_terms_text_part_1">"Условията за ползване също бяха актуализирани, за да отразят разширената функционалност на приложението."</string>
@@ -1389,6 +1386,6 @@
     <!-- YMSW: Subtitle for the interoperability onboarding if country download fails -->
     <string name="interoperability_onboarding_list_subtitle_failrequest_no_network">"Възможно е да нямате достъп до интернет. Моля, проверете дали имате връзка."</string>
     <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available -->
-    <string name="interoperability_onboarding_list_button_title_no_network">"Към настройките за устройството"</string>
+    <string name="interoperability_onboarding_list_button_title_no_network">"Към настройките на устройството"</string>
 
 </resources>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml
index 459ed81835081b14fa6e97f66c4a04b9515ef7c0..588bcf6cec02bc523d15264ca1892eed49da2f3c 100644
--- a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml
@@ -11,5 +11,5 @@
     <!-- XHED: onboarding(tracing) - headline for consent information -->
     <string name="onboarding_tracing_headline_consent">"Einwilligungserklärung"</string>
     <!-- YTXT: onboarding(tracing) - body for consent information -->
-    <string name="onboarding_tracing_body_consent">"Um zu erfahren, ob Sie Risiko-Begegnungen mit App-Nutzern der teilnehmenden Länder hatten und für Sie ein Ansteckungsrisiko besteht, müssen Sie die Risiko-Ermittlung aktivieren. Der Aktivierung der Risiko-Ermittlung und der damit im Zusammenhang stehenden Datenverarbeitung durch die App stimmen Sie mit Antippen des Buttons „Risiko-Ermittlung aktivieren“ zu.\n\nUm die Risiko-Ermittlung nutzen zu können müssen Sie zudem auf Ihrem Android-Smartphone die von Google bereitgestellte Funktion „COVID-19-Benachrichtigungen“ aktivieren und für die Corona-Warn-App freigeben.\n\nAnschließend erzeugt Ihr Android-Smartphone kontinuierlich Zufalls-IDs und versendet diese per Bluetooth, damit diese von Smartphones in Ihrer Umgebung empfangen werden können. Umgekehrt empfängt Ihr Android-Smartphone die Zufalls-IDs von anderen Smartphones. Die eigenen und die von anderen Smartphones empfangenen Zufalls-IDs werden von Ihrem Android-Smartphone aufgezeichnet und dort für 14 Tage gespeichert.\n\nFür die Risiko-Ermittlung lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die ihr Testergebnis (genauer gesagt: die eigenen Zufalls-IDs) zur Warnung anderer über ihre offizielle Corona-App geteilt haben. Diese Liste wird dann mit den von Ihrem Android-Smartphone aufgezeichneten Zufalls-IDs anderer Nutzer verglichen.\n\nWenn dabei eine Risiko-Begegnung festgestellt wird, werden Sie von der App informiert. In diesem Fall erhält die App Zugriff auf die von Ihrem Android-Smartphone zu der Risiko-Begegnung aufgezeichneten Daten (Datum, Dauer und Bluetooth-Signalstärke des Kontakts). Aus der Bluetooth-Signalstärke wird der räumliche Abstand zu dem anderen Nutzer abgeleitet (je stärker das Signal, desto geringer der Abstand). Diese Angaben werden von der App ausgewertet, um Ihr Ansteckungsrisiko zu berechnen und Ihnen Verhaltensempfehlungen zu geben. Diese Auswertung wird ausschließlich lokal auf Ihrem Android-Smartphone durchgeführt.\n\nAußer Ihnen erfährt niemand (auch nicht das RKI oder die Gesundheitsbehörden teilnehmender Länder), ob Sie eine Risiko-Begegnung hatten und welches Ansteckungsrisiko für Sie ermittelt wird.\n\nSie können Ihr Einverständnis zur Risiko-Ermittlung jederzeit zurücknehmen, indem Sie die Funktion über den Schieberegler innerhalb der App deaktivieren oder die App löschen. Wenn Sie die Risiko-Ermittlung wieder nutzen möchten, können Sie den Schieberegler erneut aktivieren oder die App erneut installieren. Wenn Sie die Risiko-Ermittlung deaktivieren, prüft die App nicht mehr, ob Sie Risiko-Begegnungen hatten. Um auch das Aussenden und den Empfang der Zufalls-IDs anzuhalten, müssen Sie die Funktion „COVID-19-Benachrichtigungen“ in den Einstellungen Ihres Android-Smartphones deaktivieren. Bitte beachten Sie, dass die durch diese Funktion von Ihrem Android-Smartphone aufgezeichneten fremden und eigenen Zufalls-IDs nicht von der App gelöscht werden. Diese können Sie nur in den Einstellungen Ihres Android-Smartphones dauerhaft löschen.\n\nDie Datenschutzerklärung der App (einschließlich Informationen zur Datenverarbeitung für die länderübergreifende Risiko-Ermittlung) finden Sie unter dem Menüpunkt „App-Informationen“ > „Datenschutz“."</string>
+    <string name="onboarding_tracing_body_consent">"Um zu erfahren, ob Sie Risiko-Begegnungen mit App-Nutzern der teilnehmenden Länder hatten und für Sie ein Infektionsrisiko besteht, müssen Sie die Risiko-Ermittlung aktivieren. Der Aktivierung der Risiko-Ermittlung und der damit im Zusammenhang stehenden Datenverarbeitung durch die App stimmen Sie mit Antippen des Buttons „Risiko-Ermittlung aktivieren“ zu.\n\nUm die Risiko-Ermittlung nutzen zu können, müssen Sie zudem auf Ihrem Android-Smartphone die von Google bereitgestellte Funktion „COVID-19-Benachrichtigungen“ aktivieren und für die Corona-Warn-App freigeben.\n\nBei aktiviertem COVID-19-Benachrichtigungssystem erzeugt Ihr Android-Smartphone kontinuierlich Zufalls-IDs und versendet diese per Bluetooth, damit diese von Smartphones in Ihrer Umgebung empfangen werden können. Umgekehrt empfängt Ihr Android-Smartphone die Zufalls-IDs von anderen Smartphones. Die eigenen und die von anderen Smartphones empfangenen Zufalls-IDs werden von Ihrem Android-Smartphone aufgezeichnet und dort für 14 Tage gespeichert.\n\nFür die Risiko-Ermittlung lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die diese über ihre offizielle Corona-App geteilt haben. Diese Liste wird dann mit den von Ihrem Smartphone aufgezeichneten Zufalls-IDs anderer Nutzer verglichen.\n\nWenn dabei eine Risiko-Begegnung festgestellt wird, werden Sie von der App informiert. In diesem Fall erhält die App Zugriff auf die von Ihrem Smartphone zu der Risiko-Begegnung aufgezeichneten Daten (Datum, Dauer und Bluetooth-Signalstärke des Kontakts). Aus der Bluetooth-Signalstärke wird der räumliche Abstand zu dem anderen Nutzer abgeleitet (je stärker das Signal, desto geringer der Abstand). Diese Angaben werden von der App ausgewertet, um Ihr Infektionsrisiko zu berechnen und Ihnen Verhaltensempfehlungen zu geben. Diese Auswertung wird ausschließlich lokal auf Ihrem Smartphone durchgeführt.\n\nAußer Ihnen erfährt niemand (auch nicht das RKI oder die Gesundheitsbehörden teilnehmender Länder), ob Sie eine Risiko-Begegnung hatten und welches Infektionsrisiko für Sie ermittelt wird.\n\nZum Widerruf Ihrer Einwilligung in die Risiko-Ermittlung können Sie die Funktion über den Schieberegler innerhalb der App deaktivieren oder die App löschen. Wenn Sie die Risiko-Ermittlung wieder nutzen möchten, können Sie den Schieberegler erneut aktivieren oder die App erneut installieren. Wenn Sie die Risiko-Ermittlung deaktivieren, prüft die App nicht mehr, ob Sie Risiko-Begegnungen hatten. Um auch das Aussenden und den Empfang der Zufalls-IDs anzuhalten, müssen Sie das COVID-19-Benachrichtigungssystem in den Einstellungen Ihres Android-Smartphones deaktivieren. Bitte beachten Sie, dass die vom COVID-19-Benachrichtigungssystem Ihres Android-Smartphones aufgezeichneten fremden und eigenen Zufalls-IDs nicht von der App gelöscht werden. Diese können Sie nur in den Einstellungen Ihres Android-Smartphones dauerhaft löschen.\n\nDie Datenschutzerklärung der App (einschließlich Informationen zur Datenverarbeitung für die länderübergreifende Risiko-Ermittlung) finden Sie unter dem Menüpunkt „App-Informationen“ > „Datenschutz“."</string>
 </resources>
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 8f9708aabee049efb3b21341f28b15daf0f7af92..b46a566c1061247858e8c6c868077170607de0e8 100644
--- a/Corona-Warn-App/src/main/res/values-de/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/strings.xml
@@ -21,12 +21,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -233,11 +229,11 @@
     ###################################### -->
 
     <!-- XHED: Share app link page title -->
-    <string name="main_share_title">"Corona-Warn-App teilen"</string>
+    <string name="main_share_title">"Corona-Warn-App empfehlen"</string>
     <!-- XHED: Share app link page subtitle -->
     <string name="main_share_headline">"Gemeinsam Corona bekämpfen"</string>
     <!-- YTXT: Share app link page body -->
-    <string name="main_share_body">"Je mehr Menschen mitmachen, desto besser durchbrechen wir Infektionsketten. Laden Sie Familie, Freunde und Bekannte ein!"</string>
+    <string name="main_share_body">"Je mehr Menschen mitmachen, desto besser durchbrechen wir Infektionsketten. Verschicken Sie einen Link auf die Corona-Warn-App an Familie, Freunde und Bekannte!"</string>
     <!-- XBUT: Share app link page button -->
     <string name="main_share_button">"Download-Link versenden"</string>
     <!-- YMSG: Message when sharing is executed -->
@@ -282,7 +278,7 @@
     <!-- XHED: App overview subtitle for glossary risk calculation  -->
     <string name="main_overview_subtitle_glossary_calculation">"Risiko-Überprüfung"</string>
     <!-- YTXT: App overview body for glossary risk calculation -->
-    <string name="main_overview_body_glossary_calculation">"Abfrage der Begegnungs-Aufzeichnung und Abgleich mit den gemeldeten Infektionen anderer Nutzerinnen und Nutzer. Die Risiko-Überprüfung erfolgt automatisch ungefähr alle zwei Stunden."</string>
+    <string name="main_overview_body_glossary_calculation">"Abfrage der Begegnungs-Aufzeichnung und Abgleich mit den gemeldeten Infektionen anderer Nutzerinnen und Nutzer. Ihr Risiko wird mehrmals täglich automatisch überprüft."</string>
     <!-- XHED: App overview subtitle for glossary contact  -->
     <string name="main_overview_subtitle_glossary_contact">"Risiko-Begegnung"</string>
     <!-- YTXT: App overview body for glossary contact -->
@@ -1299,17 +1295,7 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
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 568659f0360416649e4005c198a66d174be6ff68..a5e01cdeff99cac3ee50b5509b42354aebefa547 100644
--- a/Corona-Warn-App/src/main/res/values-en/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-en/strings.xml
@@ -20,12 +20,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -109,6 +105,10 @@
     <string name="notification_headline">"Corona-Warn-App"</string>
     <!-- XTXT: Notification body -->
     <string name="notification_body">"You have new messages from your Corona-Warn-App."</string>
+    <!-- XHED: Notification title - Reminder to share a positive test result-->
+    <string name="notification_headline_share_positive_result">"You can help!"</string>
+    <!-- XTXT: Notification body - Reminder to share a positive test result-->
+    <string name="notification_body_share_positive_result">"Please share your test result and warn others."</string>
 
     <!-- ####################################
               App Auto Update
@@ -150,11 +150,9 @@
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
     <string name="risk_card_body_saved_days_full">"Exposure logging permanently active"</string>
     <!-- XTXT; risk card - no update done yet -->
-    <string name="risk_card_body_not_yet_fetched">"Exposures have not yet been checked."</string>
+    <string name="risk_card_body_not_yet_fetched">"Encounters have not yet been checked."</string>
     <!-- XTXT: risk card - last successful update -->
     <string name="risk_card_body_time_fetched">"Updated: %1$s"</string>
-    <!-- XTXT: risk card - next update -->
-    <string name="risk_card_body_next_update">"Updated daily"</string>
     <!-- XTXT: risk card - hint to open the app daily -->
     <string name="risk_card_body_open_daily">"Note: Please open the app daily to update your risk status."</string>
     <!-- XBUT: risk card - update risk -->
@@ -185,13 +183,19 @@
     <!-- XHED: risk card - tracing stopped headline, due to no possible calculation -->
     <string name="risk_card_no_calculation_possible_headline">"Exposure logging stopped"</string>
     <!-- XTXT: risk card - last successfully calculated risk level -->
-    <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure logging:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
+    <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure check:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
     <!-- XHED: risk card -  outdated risk headline, calculation isn't possible -->
     <string name="risk_card_outdated_risk_headline">"Exposure logging is not possible"</string>
     <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours -->
     <string name="risk_card_outdated_risk_body">"Your exposure logging could not be updated for more than 24 hours."</string>
     <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours -->
     <string name="risk_card_outdated_manual_risk_body">"Your risk status has not been updated for more than 48 hours. Please update your risk status."</string>
+    <!-- XHED: risk card - risk check failed headline, no internet connection -->
+    <string name="risk_card_check_failed_no_internet_headline">"Exposure check failed"</string>
+    <!-- XTXT: risk card - risk check failed, please check your internet connection -->
+    <string name="risk_card_check_failed_no_internet_body">"The synchronization of random IDs with the server failed. You can restart the synchronization manually."</string>
+    <!-- XTXT: risk card - risk check failed, restart button -->
+    <string name="risk_card_check_failed_no_internet_restart_button">"Restart"</string>
 
     <!-- ####################################
               Risk Card - Progress
@@ -247,7 +251,7 @@
     <!-- XHED: App overview subtitle for tracing explanation-->
     <string name="main_overview_subtitle_tracing">"Exposure Logging"</string>
     <!-- YTXT: App overview body text about tracing -->
-    <string name="main_overview_body_tracing">"Exposure logging is one of the three central features of the app. When you activate it, encounters with people\'s devices are logged. You don\'t have to do anything else."</string>
+    <string name="main_overview_body_tracing">"Exposure logging is one of three central features of the app. Once you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string>
     <!-- XHED: App overview subtitle for risk explanation -->
     <string name="main_overview_subtitle_risk">"Risk of Infection"</string>
     <!-- YTXT: App overview body text about risk levels -->
@@ -357,9 +361,9 @@
         <item quantity="many">"You have an increased risk of infection because you were last exposed %1$s days ago over a longer period of time and at close proximity to at least one person diagnosed with COVID-19."</item>
     </plurals>
     <!-- YTXT: risk details - risk calculation explanation -->
-    <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else."</string>
+    <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else."</string>
     <!-- YTXT: risk details - risk calculation explanation for increased risk -->
-    <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string>
+    <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string>
     <!-- NOTR -->
     <string name="risk_details_button_update">@string/risk_card_button_update</string>
     <!-- NOTR -->
@@ -415,7 +419,7 @@
     <!-- XHED: onboarding(together) - two/three line headline under an illustration -->
     <string name="onboarding_subtitle">"More protection for you and for us all. By using the Corona-Warn-App we can break infection chains much quicker."</string>
     <!-- YTXT: onboarding(together) - inform about the app -->
-    <string name="onboarding_body">"Turn your device into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string>
+    <string name="onboarding_body">"Turn your smartphone into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string>
     <!-- YTXT: onboarding(together) - explain application -->
     <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their devices, whereby no personal data whatsoever is accessed."</string>
     <!-- XACT: onboarding(together) - illustraction description, header image -->
@@ -452,7 +456,7 @@
     <!-- XBUT: onboarding(tracing) - negative button (right) -->
     <string name="onboarding_tracing_dialog_button_negative">"Back"</string>
     <!-- XACT: onboarding(tracing) - dialog about background jobs header text -->
-    <string name="onboarding_background_fetch_dialog_headline">"Background updates deactivated"</string>
+    <string name="onboarding_background_fetch_dialog_headline">"Background app refresh deactivated"</string>
     <!-- YMSI: onboarding(tracing) - dialog about background jobs -->
     <string name="onboarding_background_fetch_dialog_body">"You have deactivated background updates for the Corona-Warn-App. Please activate background updates to use automatic exposure logging. If you do not activate background updates, you can only start exposure logging manually in the app. You can activate background updates for the app in your device settings."</string>
     <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings -->
@@ -474,7 +478,7 @@
     <!-- XBUT: onboarding(tracing) - dialog about manual checking button -->
     <string name="onboarding_manual_required_dialog_button">"OK"</string>
     <!-- XACT: onboarding(tracing) - illustraction description, header image -->
-    <string name="onboarding_tracing_illustration_description">"Three people have activated exposure logging on their devices, which will log their encounters with each other."</string>
+    <string name="onboarding_tracing_illustration_description">"Three persons have activated exposure logging on their smartphones, which will log their encounters with each other."</string>
     <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline -->
     <string name="onboarding_tracing_location_headline">"Allow location access"</string>
     <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text -->
@@ -671,7 +675,7 @@
     <!-- YTXT: Body text for about information page -->
     <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) is Germany’s federal public health body. The RKI publishes the Corona-Warn-App on behalf of the Federal Government. The app is intended as a digital complement to public health measures already introduced: social distancing, hygiene, and face masks."</string>
     <!-- YTXT: Body text for about information page -->
-    <string name="information_about_body">"People who use the app help to trace and break chains of infection. The app saves encounters with other people locally on your device. You are notified if you have encountered people who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string>
+    <string name="information_about_body">"Whoever uses the app helps to trace and break chains of infection. The app saves encounters with other persons locally on your smartphone. You are notified if you have encountered persons who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string>
     <!-- XACT: describes illustration -->
     <string name="information_about_illustration_description">"A group of persons use their smartphones around town."</string>
     <!-- XHED: Page title for privacy information page, also menu item / button text -->
@@ -703,9 +707,9 @@
     <!-- XTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_phone">"Our customer service is here to help."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_open">"Languages: German, English, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string>
+    <string name="information_contact_body_open">"Languages: English, German, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string>
+    <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string>
     <!-- XACT: describes illustration -->
     <string name="information_contact_illustration_description">"A man wears a headset while making a phone call."</string>
     <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website -->
@@ -788,9 +792,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"Back"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Test has errors"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"QR code no longer valid"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"There was a problem evaluating your test. Your QR code has already expired."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Your test is more than 21 days old and can no longer be registered in the app. If you are tested again in future, please make sure to scan the QR code as soon as you get it."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string>
 
@@ -865,15 +869,15 @@
     <!-- XBUT: test result pending : refresh button -->
     <string name="submission_test_result_pending_refresh_button">"Update"</string>
     <!-- XBUT: test result pending : remove the test button -->
-    <string name="submission_test_result_pending_remove_test_button">"Delete Test"</string>
+    <string name="submission_test_result_pending_remove_test_button">"Remove test"</string>
     <!-- XHED: Page headline for negative test result next steps  -->
     <string name="submission_test_result_negative_steps_negative_heading">"Your Test Result"</string>
     <!-- YTXT: Body text for next steps section of test negative result -->
     <string name="submission_test_result_negative_steps_negative_body">"The laboratory result indicates no verification that you have coronavirus SARS-CoV-2.\n\nPlease delete the test from the Corona-Warn-App, so that you can save a new test code here if necessary."</string>
     <!-- XBUT: negative test result : remove the test button -->
-    <string name="submission_test_result_negative_remove_test_button">"Delete Test"</string>
+    <string name="submission_test_result_negative_remove_test_button">"Remove test"</string>
     <!-- XHED: Page headline for other warnings screen  -->
-    <string name="submission_test_result_positive_steps_warning_others_heading">"Warning Others"</string>
+    <string name="submission_test_result_positive_steps_warning_others_heading">"Warn Others"</string>
     <!-- YTXT: Body text for for other warnings screen-->
     <string name="submission_test_result_positive_steps_warning_others_body">"Share your random IDs and warn others.\nHelp determine the risk of infection for others more accurately by also indicating when you first noticed any coronavirus symptoms."</string>
     <!-- XBUT: positive test result : continue button -->
@@ -952,13 +956,13 @@
     <!-- YTXT: Body text for QR code dispatcher option -->
     <string name="submission_dispatcher_qr_card_text">"Register your test by scanning the QR code of your test document."</string>
     <!-- XHED: Dialog headline for dispatcher QR prviacy dialog  -->
-    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Consent"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Declaration of Consent"</string>
     <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog -->
     <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Accept”, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacy”."</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Accept"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Allow"</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Accept"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Allow"</string>
     <!-- YTXT: Dispatcher text for TAN code option -->
     <string name="submission_dispatcher_card_tan_code">"TAN"</string>
     <!-- YTXT: Body text for TAN code dispatcher option -->
@@ -972,7 +976,7 @@
 
     <!-- Submission Positive Other Warning -->
     <!-- XHED: Page title for the positive result additional warning page-->
-    <string name="submission_positive_other_warning_title">"Warning Others"</string>
+    <string name="submission_positive_other_warning_title">"Warn Others"</string>
     <!-- XHED: Page headline for the positive result additional warning page-->
     <string name="submission_positive_other_warning_headline">"Please help all of us!"</string>
     <!-- YTXT: Body text for the positive result additional warning page-->
@@ -980,7 +984,7 @@
     <!-- XBUT: other warning continue button -->
     <string name="submission_positive_other_warning_button">"Accept"</string>
     <!-- XACT: other warning - illustration description, explanation image -->
-    <string name="submission_positive_other_illustration_description">"A device transmits an encrypted positive test diagnosis to the system."</string>
+    <string name="submission_positive_other_illustration_description">"A smartphone transmits an encrypted positive test diagnosis to the system."</string>
     <!-- XHED: Title for the interop country list-->
     <string name="submission_interoperability_list_title">"The following countries currently participate in transnational exposure logging:"</string>
 
@@ -1046,7 +1050,7 @@
     <!-- XBUT:  symptom initial screen no button -->
     <string name="submission_symptom_negative_button">"No"</string>
     <!-- XBUT:  symptom initial screen no information button -->
-    <string name="submission_symptom_no_information_button">"No answer"</string>
+    <string name="submission_symptom_no_information_button">"No statement"</string>
     <!-- XBUT:  symptom initial screen continue button -->
     <string name="submission_symptom_further_button">"Next"</string>
 
@@ -1071,9 +1075,9 @@
     <!-- YTXT: Body text for step 2 of contact page-->
     <string name="submission_contact_step_2_body">"Register the test by entering the TAN in the app."</string>
     <!-- YTXT: Body text for operating hours in contact page-->
-    <string name="submission_contact_operating_hours_body">"Languages: \nGerman, English, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string>
+    <string name="submission_contact_operating_hours_body">"Languages: \nEnglish, German, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string>
+    <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string>
 
     <!-- XACT: Submission contact page title -->
     <string name="submission_contact_accessibility_title">"Call the hotline and request a TAN"</string>
@@ -1096,7 +1100,7 @@
     <!-- XBUT: symptom calendar screen more than 2 weeks button -->
     <string name="submission_symptom_more_two_weeks">"More than 2 weeks ago"</string>
     <!-- XBUT: symptom calendar screen verify button -->
-    <string name="submission_symptom_verify">"No answer"</string>
+    <string name="submission_symptom_verify">"No statement"</string>
 
     <!-- Submission Status Card -->
     <!-- XHED: Page title for the various submission status: fetching -->
@@ -1157,7 +1161,7 @@
     <!-- YTXT: invalid status text -->
     <string name="test_result_card_status_invalid">"Evaluation is not possible"</string>
     <!-- YTXT: pending status text -->
-    <string name="test_result_card_status_pending">"Your result is not available yet"</string>
+    <string name="test_result_card_status_pending">"Your result is not yet available"</string>
     <!-- XHED: Title for further info of test result negative -->
     <string name="test_result_card_negative_further_info_title">"Other information:"</string>
     <!-- YTXT: Content for further info of test result negative -->
@@ -1201,7 +1205,10 @@
     <string name="errors_google_update_needed">"Your Corona-Warn-App is correctly installed, but the \"COVID-19 Exposure Notifications System\" is not available on your smartphone\'s operating system. This means that you cannot use the Corona-Warn-App. For further information, please see our FAQ page: https://www.coronawarn.app/en/faq/"</string>
     <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day -->
     <string name="errors_google_api_error">"The Corona-Warn-App is running correctly, but we cannot update your current risk status. Exposure logging remains active and is working correctly. For further information, please see our FAQ page: https://www.coronawarn.app/en/faq/"</string>
-
+    <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_title">"Limit already reached"</string>
+    <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_description">"No more exposure checks possible today, as you have reached the maximum number of checks per day defined by your operating system. Please check your risik status again tomorrow."</string>
     <!-- ####################################
                Generic Error Messages
         ###################################### -->
@@ -1248,17 +1255,7 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
@@ -1357,7 +1354,7 @@
     <!-- YMSG: Onboarding tracing step second section in interoperability after the title -->
     <string name="interoperability_onboarding_second_section">"When a user submits their random IDs to the exchange server jointly operated by the participating countries, users of the official corona apps in all these countries can be warned of potential exposure."</string>
     <!-- YMSG: Onboarding tracing step third section in interoperability after the title. -->
-    <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, and nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string>
+    <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string>
     <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. -->
     <string name="interoperability_onboarding_list_title">"The following countries currently participate:"</string>
 
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 0f8a8191a508dc0d04aa93af8d0c2aca7ebc8576..eded35c4be1a262535dce655302b0fba532e50e5 100644
--- a/Corona-Warn-App/src/main/res/values-pl/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml
@@ -20,12 +20,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -109,6 +105,10 @@
     <string name="notification_headline">"Corona-Warn-App"</string>
     <!-- XTXT: Notification body -->
     <string name="notification_body">"Masz nowe wiadomości od Corona-Warn-App."</string>
+    <!-- XHED: Notification title - Reminder to share a positive test result-->
+    <string name="notification_headline_share_positive_result">"Możesz pomóc!"</string>
+    <!-- XTXT: Notification body - Reminder to share a positive test result-->
+    <string name="notification_body_share_positive_result">"Udostępnij swój wynik testu i ostrzeż innych."</string>
 
     <!-- ####################################
               App Auto Update
@@ -132,7 +132,7 @@
         <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żenia z niskim ryzykiem"</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>
@@ -150,13 +150,11 @@
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
     <string name="risk_card_body_saved_days_full">"Rejestrowanie narażenia stale aktywne"</string>
     <!-- XTXT; risk card - no update done yet -->
-    <string name="risk_card_body_not_yet_fetched">"Narażenia nie zostały jeszcze sprawdzone."</string>
+    <string name="risk_card_body_not_yet_fetched">"Kontakty nie zostały jeszcze sprawdzone."</string>
     <!-- XTXT: risk card - last successful update -->
     <string name="risk_card_body_time_fetched">"Zaktualizowano: %1$s"</string>
-    <!-- XTXT: risk card - next update -->
-    <string name="risk_card_body_next_update">"Aktualizowane codziennie"</string>
     <!-- XTXT: risk card - hint to open the app daily -->
-    <string name="risk_card_body_open_daily">"Uwaga: Proszę codziennie otwierać aplikację, aby zaktualizować swój status ryzyka."</string>
+    <string name="risk_card_body_open_daily">"Uwaga: Otwieraj codziennie aplikację, aby aktualizować swój status ryzyka."</string>
     <!-- XBUT: risk card - update risk -->
     <string name="risk_card_button_update">"Aktualizuj"</string>
     <!-- XBUT: risk card - update risk with time display -->
@@ -185,13 +183,19 @@
     <!-- XHED: risk card - tracing stopped headline, due to no possible calculation -->
     <string name="risk_card_no_calculation_possible_headline">"Rejestrowanie narażenia zatrzymane"</string>
     <!-- XTXT: risk card - last successfully calculated risk level -->
-    <string name="risk_card_no_calculation_possible_body_saved_risk">"Ostatnie rejestrowanie narażenia:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
+    <string name="risk_card_no_calculation_possible_body_saved_risk">"Ostatnie sprawdzenie narażenia:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
     <!-- XHED: risk card -  outdated risk headline, calculation isn't possible -->
     <string name="risk_card_outdated_risk_headline">"Rejestrowanie narażenia jest niemożliwe"</string>
     <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours -->
     <string name="risk_card_outdated_risk_body">"Rejestrowanie narażenia nie mogło zostać zaktualizowane przez okres dłuższy niż 24 godziny."</string>
     <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours -->
     <string name="risk_card_outdated_manual_risk_body">"Twój status ryzyka nie był aktualizowany od ponad 48 godzin. Zaktualizuj swój status ryzyka."</string>
+    <!-- XHED: risk card - risk check failed headline, no internet connection -->
+    <string name="risk_card_check_failed_no_internet_headline">"Sprawdzanie narażeń nie powiodło się"</string>
+    <!-- XTXT: risk card - risk check failed, please check your internet connection -->
+    <string name="risk_card_check_failed_no_internet_body">"Synchronizacja losowych identyfikatorów z serwerem nie powiodła się. Możesz ponownie uruchomić synchronizację ręcznie."</string>
+    <!-- XTXT: risk card - risk check failed, restart button -->
+    <string name="risk_card_check_failed_no_internet_restart_button">"Uruchom ponownie"</string>
 
     <!-- ####################################
               Risk Card - Progress
@@ -247,7 +251,7 @@
     <!-- XHED: App overview subtitle for tracing explanation-->
     <string name="main_overview_subtitle_tracing">"Rejestrowanie narażenia"</string>
     <!-- YTXT: App overview body text about tracing -->
-    <string name="main_overview_body_tracing">"Rejestrowanie narażenia jest jedną z trzech głównych funkcji aplikacji. Po jej aktywacji rejestrowane są kontakty z urządzeniami innych osób. Nie musisz robić nic więcej."</string>
+    <string name="main_overview_body_tracing">"Rejestrowanie narażenia jest jedną z trzech głównych funkcji aplikacji. Po jej aktywacji rejestrowane są kontakty ze smartfonami innych osób. Nie musisz robić nic więcej."</string>
     <!-- XHED: App overview subtitle for risk explanation -->
     <string name="main_overview_subtitle_risk">"Ryzyko zakażenia"</string>
     <!-- YTXT: App overview body text about risk levels -->
@@ -357,9 +361,9 @@
         <item quantity="many">"Masz podwyższone ryzyko zakażenia, ponieważ %1$s dni temu byłeś(-aś) narażony(-a) na dłuższy, bliski kontakt z co najmniej jedną osobą, u której zdiagnozowano COVID-19."</item>
     </plurals>
     <!-- YTXT: risk details - risk calculation explanation -->
-    <string name="risk_details_information_body_notice">"Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie w urządzeniu. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane."</string>
+    <string name="risk_details_information_body_notice">"Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie w smartfonie. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane."</string>
     <!-- YTXT: risk details - risk calculation explanation for increased risk -->
-    <string name="risk_details_information_body_notice_increased">"Dlatego Twoje ryzyko zakażenia zostało ocenione jako podwyższone. Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie w urządzeniu. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane. Po powrocie do domu unikaj również bliskiego kontaktu z członkami rodziny lub gospodarstwa domowego."</string>
+    <string name="risk_details_information_body_notice_increased">"Dlatego Twoje ryzyko zakażenia zostało ocenione jako podwyższone. Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie na smartfonie. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane. Po powrocie do domu unikaj również bliskiego kontaktu z członkami rodziny lub gospodarstwa domowego."</string>
     <!-- NOTR -->
     <string name="risk_details_button_update">@string/risk_card_button_update</string>
     <!-- NOTR -->
@@ -415,7 +419,7 @@
     <!-- XHED: onboarding(together) - two/three line headline under an illustration -->
     <string name="onboarding_subtitle">"Większa ochrona dla Ciebie i dla nas wszystkich. Korzystając z aplikacji Corona-Warn-App, możemy znacznie szybciej przerwać łańcuchy zakażeń."</string>
     <!-- YTXT: onboarding(together) - inform about the app -->
-    <string name="onboarding_body">"Zmień swoje urządzenie w system ostrzegania przed koronawirusem. Zapoznaj się ze swoim statusem ryzyka i dowiedz się, czy miałeś(-aś) bliski kontakt z osobą, u której w ciągu ostatnich 14 dni zdiagnozowano COVID-19."</string>
+    <string name="onboarding_body">"Zmień swój smartfon w system ostrzegania przed koronawirusem. Zapoznaj się ze swoim statusem ryzyka i dowiedz się, czy miałeś(-aś) bliski kontakt z osobą, u której w ciągu ostatnich 14 dni zdiagnozowano COVID-19."</string>
     <!-- YTXT: onboarding(together) - explain application -->
     <string name="onboarding_body_emphasized">"Aplikacja rejestruje kontakty między osobami poprzez wymianę zaszyfrowanych, losowych identyfikatorów między ich urządzeniami bez uzyskiwania dostępu do danych osobowych."</string>
     <!-- XACT: onboarding(together) - illustraction description, header image -->
@@ -452,7 +456,7 @@
     <!-- XBUT: onboarding(tracing) - negative button (right) -->
     <string name="onboarding_tracing_dialog_button_negative">"Wstecz"</string>
     <!-- XACT: onboarding(tracing) - dialog about background jobs header text -->
-    <string name="onboarding_background_fetch_dialog_headline">"Aktualizacje w tle dezaktywowane"</string>
+    <string name="onboarding_background_fetch_dialog_headline">"Odświeżanie aplikacji w tle dezaktywowane"</string>
     <!-- YMSI: onboarding(tracing) - dialog about background jobs -->
     <string name="onboarding_background_fetch_dialog_body">"Dezaktywowałeś(-aś) aktualizacje w tle dla aplikacji Corona-Warn-App. Aktywuj aktualizacje w tle, aby korzystać z automatycznego rejestrowania narażenia. Jeśli nie aktywujesz aktualizacji w tle, możliwe będzie tylko ręczne uruchomienie rejestrowania narażenia w aplikacji. Możesz aktywować aktualizacje w tle dla aplikacji w ustawieniach swojego urządzenia."</string>
     <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings -->
@@ -474,7 +478,7 @@
     <!-- XBUT: onboarding(tracing) - dialog about manual checking button -->
     <string name="onboarding_manual_required_dialog_button">"OK"</string>
     <!-- XACT: onboarding(tracing) - illustraction description, header image -->
-    <string name="onboarding_tracing_illustration_description">"Trzy osoby aktywowały rejestrowanie narażenia na swoich urządzeniach, które będą rejestrować ich wzajemne kontakty."</string>
+    <string name="onboarding_tracing_illustration_description">"Trzy osoby aktywowały rejestrowanie narażenia na swoich smartfonach, które będą rejestrować ich wzajemne kontakty."</string>
     <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline -->
     <string name="onboarding_tracing_location_headline">"Zezwól na dostęp do lokalizacji"</string>
     <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text -->
@@ -536,7 +540,7 @@
     <!-- XTXT: settings(tracing) - shows status under header in home, inactive location -->
     <string name="settings_tracing_body_inactive_location">"Usługi lokalizacji dezaktywowane"</string>
     <!-- YTXT: settings(tracing) - explains tracings -->
-    <string name="settings_tracing_body_text">"Musisz włączyć funkcję rejestrowania narażenia, aby aplikacja mogła ustalić, czy dotyczy Cię ryzyko zakażenia po kontakcie z zainfekowanym użytkownikiem aplikacji. Funkcja rejestrowania narażenia działa we wszystkich uczestniczących krajach, co oznacza, że potencjalne narażenie użytkowników jest wykrywane również przez inne oficjalne aplikacje koronawirusowe.\n\nDziałanie funkcji rejestrowania narażenia polega na odbieraniu przez Twój smartfon za pomocą Bluetooth zaszyfrowanych, losowych identyfikatorów innych użytkowników i przekazywaniu Twoich własnych, losowych identyfikatorów do ich urządzeń. Codziennie aplikacja pobiera listę losowych identyfikatorów – wraz z wszelkimi opcjonalnie podawanymi informacjami o wystąpieniu objawów – wszystkich użytkowników, którzy mieli pozytywny wynik testu na wirusa i dobrowolnie udostępnili tę informację poprzez aplikację. Lista jest następnie porównywana z losowymi identyfikatorami innych użytkowników, które zarejestrował Twój smartfon, w celu obliczenia prawdopodobieństwa Twojego zakażenia i ostrzeżenia Cię w razie potrzeby. Funkcję tę można wyłączyć w dowolnym momencie..\n\nAplikacja nigdy nie gromadzi danych osobowych takich jak imię i nazwisko, adres czy lokalizacja. Takie informacje nie są też przekazywane innym użytkownikom. Nie jest możliwe wykorzystanie losowych identyfikatorów w celu ustalenia tożsamości poszczególnych osób."</string>
+    <string name="settings_tracing_body_text">"Musisz włączyć funkcję rejestrowania narażenia, aby aplikacja mogła ustalić, czy dotyczy Cię ryzyko zakażenia po kontakcie z zainfekowanym użytkownikiem aplikacji. Funkcja rejestrowania narażenia działa w skali międzynarodowej, co oznacza, że potencjalne narażenie użytkowników jest wykrywane również przez inne oficjalne aplikacje koronawirusowe.\n\nDziałanie funkcji rejestrowania narażenia polega na odbieraniu przez Twój smartfon za pomocą Bluetooth zaszyfrowanych, losowych identyfikatorów innych użytkowników i przekazywaniu Twoich własnych, losowych identyfikatorów do ich smartfonów. Codziennie aplikacja pobiera listę losowych identyfikatorów – wraz z wszelkimi opcjonalnie podawanymi informacjami o wystąpieniu objawów – wszystkich użytkowników, którzy mieli pozytywny wynik testu na koronawirusa i dobrowolnie udostępnili tę informację poprzez aplikację. Lista jest następnie porównywana z losowymi identyfikatorami innych użytkowników, które zarejestrował Twój smartfon, w celu obliczenia prawdopodobieństwa Twojego zakażenia i ostrzeżenia Cię w razie potrzeby. Funkcję tę można wyłączyć w dowolnym momencie.\n\nAplikacja nigdy nie gromadzi danych osobowych, takich jak imię i nazwisko, adres czy lokalizacja. Takie informacje nie są też przekazywane innym użytkownikom. Nie jest możliwe wykorzystanie losowych identyfikatorów w celu ustalenia tożsamości poszczególnych osób."</string>
     <!-- XTXT: settings(tracing) - status next to switch under title -->
     <string name="settings_tracing_status_active">"Aktywne"</string>
     <!-- XTXT: settings(tracing) - status next to switch under title -->
@@ -570,7 +574,7 @@
     <!-- XTXT: settings(tracing) - explains the circle progress indicator to the right with the current value -->
     <plurals name="settings_tracing_status_body_active">
         <item quantity="one">"Rejestrowanie narażenia jest aktywne od jednego dnia.\nSprawdzanie narażeń jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na stałe."</item>
-        <item quantity="other">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeń jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na stałe."</item>
+        <item quantity="other">"Rejestrowanie narażenia jest aktywne od %1$s dnia.\nSprawdzanie narażeń jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na stałe."</item>
         <item quantity="zero">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeń jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na stałe."</item>
         <item quantity="two">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeń jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na stałe."</item>
         <item quantity="few">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeń jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na stałe."</item>
@@ -671,7 +675,7 @@
     <!-- YTXT: Body text for about information page -->
     <string name="information_about_body_emphasized">"Instytut Roberta Kocha (RKI) to niemiecka federalna instytucja zdrowia publicznego. RKI publikuje aplikację Corona-Warn-App w imieniu rządu federalnego. Aplikacja ta służy jako cyfrowe uzupełnienie już wprowadzonych środków ochrony zdrowia publicznego, takich jak zachowanie dystansu społecznego, dbanie o higienę oraz noszenie maseczek."</string>
     <!-- YTXT: Body text for about information page -->
-    <string name="information_about_body">"Osoby korzystające z aplikacji pomagają w śledzeniu i przerwaniu łańcuchów zakażeń. Aplikacja zapisuje kontakty z innymi osobami lokalnie na Twoim urządzeniu. Otrzymasz powiadomienie, jeśli okaże się, że u osób, z którymi miałeś(-aś) kontakt, zdiagnozowano później COVID-19. Twoja tożsamość i prywatność są zawsze chronione."</string>
+    <string name="information_about_body">"Wszystkie osoby korzystające z aplikacji pomagają w śledzeniu i przerwaniu łańcuchów zakażeń. Aplikacja zapisuje kontakty z innymi osobami lokalnie na Twoim smartfonie. Otrzymasz powiadomienie, jeśli okaże się, że u osób, z którymi miałeś(-aś) kontakt, zdiagnozowano później COVID-19. Twoja tożsamość i prywatność są zawsze chronione."</string>
     <!-- XACT: describes illustration -->
     <string name="information_about_illustration_description">"Grupa osób korzysta ze smartfonów na mieście."</string>
     <!-- XHED: Page title for privacy information page, also menu item / button text -->
@@ -703,7 +707,7 @@
     <!-- XTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_phone">"Nasz zespół obsługi klienta jest gotowy do pomocy."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_open">"Języki: niemiecki, angielski, turecki\nGodziny pracy:"<xliff:g id="line_break">"\n"</xliff:g>"od poniedziałku do soboty: 7:00 - 22:00"<xliff:g id="line_break">"\n(za wyjątkiem świąt państwowych)"</xliff:g><xliff:g id="line_break">"\nPołączenie jest bezpłatne."</xliff:g></string>
+    <string name="information_contact_body_open">"Języki: angielski, niemiecki, turecki\nGodziny pracy:"<xliff:g id="line_break">"\n"</xliff:g>"od poniedziałku do soboty: 7:00 - 22:00"<xliff:g id="line_break">"\n(za wyjątkiem świąt państwowych)"</xliff:g><xliff:g id="line_break">"\nPołączenie jest bezpłatne."</xliff:g></string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_other">"W razie jakichkolwiek pytań związanych ze zdrowiem skontaktuj się z lekarzem rodzinnym lub lekarzem dyżurnym pod numerem: 116 117."</string>
     <!-- XACT: describes illustration -->
@@ -788,9 +792,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"Wstecz"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Test zawiera błędy"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"Kod QR stracił ważność"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"Podczas ustalania wyniku testu pojawił się błąd. Twój kod QR wygasł."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Twój test ma więcej niż 21 dni i nie można go już zarejestrować w aplikacji. Jeśli w przyszłości będziesz ponownie poddawać się testowi, zeskanuj kod QR, gdy tylko go otrzymasz."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string>
 
@@ -869,11 +873,11 @@
     <!-- XHED: Page headline for negative test result next steps  -->
     <string name="submission_test_result_negative_steps_negative_heading">"Twój wynik testu"</string>
     <!-- YTXT: Body text for next steps section of test negative result -->
-    <string name="submission_test_result_negative_steps_negative_body">"Wynik laboratoryjny nie potwierdza zakażenia wirusem SARS-CoV-2.\n\nUsuń test z aplikacji Corona-Warn-App, aby w razie potrzeby móc zapisać w niej kod nowego testu."</string>
+    <string name="submission_test_result_negative_steps_negative_body">"Wynik laboratoryjny nie potwierdza zakażenia koronawirusem SARS-CoV-2.\n\nUsuń test z aplikacji Corona-Warn-App, aby w razie potrzeby móc zapisać w niej kod nowego testu."</string>
     <!-- XBUT: negative test result : remove the test button -->
     <string name="submission_test_result_negative_remove_test_button">"Usuń test"</string>
     <!-- XHED: Page headline for other warnings screen  -->
-    <string name="submission_test_result_positive_steps_warning_others_heading">"Ostrzeganie innych"</string>
+    <string name="submission_test_result_positive_steps_warning_others_heading">"Ostrzegaj innych"</string>
     <!-- YTXT: Body text for for other warnings screen-->
     <string name="submission_test_result_positive_steps_warning_others_body">"Udostępnij swoje losowe identyfikatory, aby ostrzegać innych.\nPomóż innym dokładniej ocenić ryzyko zakażenia, wysyłając informację, gdy dostrzeżesz u siebie objawy koronawirusa."</string>
     <!-- XBUT: positive test result : continue button -->
@@ -952,13 +956,13 @@
     <!-- YTXT: Body text for QR code dispatcher option -->
     <string name="submission_dispatcher_qr_card_text">"Zarejestruj test poprzez zeskanowanie kodu QR dokumentu testu."</string>
     <!-- XHED: Dialog headline for dispatcher QR prviacy dialog  -->
-    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Zgoda"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Oświadczenia o wyrażeniu zgody"</string>
     <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog -->
     <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Accept”, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacy”."</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Akceptuj"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Zezwól"</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nie akceptuj"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nie zezwalaj"</string>
     <!-- YTXT: Dispatcher text for TAN code option -->
     <string name="submission_dispatcher_card_tan_code">"TAN"</string>
     <!-- YTXT: Body text for TAN code dispatcher option -->
@@ -972,7 +976,7 @@
 
     <!-- Submission Positive Other Warning -->
     <!-- XHED: Page title for the positive result additional warning page-->
-    <string name="submission_positive_other_warning_title">"Ostrzeganie innych"</string>
+    <string name="submission_positive_other_warning_title">"Ostrzegaj innych"</string>
     <!-- XHED: Page headline for the positive result additional warning page-->
     <string name="submission_positive_other_warning_headline">"Pomóż nam wszystkim!"</string>
     <!-- YTXT: Body text for the positive result additional warning page-->
@@ -980,7 +984,7 @@
     <!-- XBUT: other warning continue button -->
     <string name="submission_positive_other_warning_button">"Akceptuj"</string>
     <!-- XACT: other warning - illustration description, explanation image -->
-    <string name="submission_positive_other_illustration_description">"Urządzenie przesyła zaszyfrowaną diagnozę zakażenia do systemu."</string>
+    <string name="submission_positive_other_illustration_description">"Zaszyfrowana diagnoza zakażenia jest przesyłana do systemu."</string>
     <!-- XHED: Title for the interop country list-->
     <string name="submission_interoperability_list_title">"Następujące kraje uczestniczą obecnie w międzynarodowym rejestrowaniu narażenia:"</string>
 
@@ -1046,7 +1050,7 @@
     <!-- XBUT:  symptom initial screen no button -->
     <string name="submission_symptom_negative_button">"Nie"</string>
     <!-- XBUT:  symptom initial screen no information button -->
-    <string name="submission_symptom_no_information_button">"Brak odpowiedzi"</string>
+    <string name="submission_symptom_no_information_button">"Bez komentarza"</string>
     <!-- XBUT:  symptom initial screen continue button -->
     <string name="submission_symptom_further_button">"Dalej"</string>
 
@@ -1071,7 +1075,7 @@
     <!-- YTXT: Body text for step 2 of contact page-->
     <string name="submission_contact_step_2_body">"Zarejestruj test poprzez wpisanie numeru TAN w aplikacji."</string>
     <!-- YTXT: Body text for operating hours in contact page-->
-    <string name="submission_contact_operating_hours_body">"Języki: \nniemiecki, angielski, turecki\n\nGodziny pracy:\nod poniedziałku do niedzieli:  24 godziny na dobę\n\nPołączenie jest bezpłatne."</string>
+    <string name="submission_contact_operating_hours_body">"Języki: \nangielski, niemiecki, turecki\n\nGodziny pracy:\nod poniedziałku do niedzieli: całodobowo\n\nPołączenie jest bezpłatne."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
     <string name="submission_contact_body_other">"W razie jakichkolwiek pytań związanych ze zdrowiem skontaktuj się z lekarzem rodzinnym lub lekarzem dyżurnym pod numerem: 116 117."</string>
 
@@ -1086,7 +1090,7 @@
     <!-- XHED: Page title for calendar page in submission symptom flow -->
     <string name="submission_symptom_calendar_title">"Początek wystąpienia objawów"</string>
     <!-- XHED: Page headline for calendar page in symptom submission flow -->
-    <string name="submission_symptom_calendar_headline">"Kiedy zacząłeś(-ęłaś) odczuwać te objawy? "</string>
+    <string name="submission_symptom_calendar_headline">"Kiedy zacząłeś(-łaś) odczuwać te objawy? "</string>
     <!-- YTXT: Body text for calendar page in symptom submission flow-->
     <string name="submission_symptom_calendar_body">"Wybierz dokładną datę w kalendarzu lub, jeśli nie pamiętasz dokładnej daty, wybierz jedną z innych opcji."</string>
     <!-- XBUT: symptom calendar screen less than 7 days button -->
@@ -1096,7 +1100,7 @@
     <!-- XBUT: symptom calendar screen more than 2 weeks button -->
     <string name="submission_symptom_more_two_weeks">"Ponad 2 tygodnie temu"</string>
     <!-- XBUT: symptom calendar screen verify button -->
-    <string name="submission_symptom_verify">"Brak odpowiedzi"</string>
+    <string name="submission_symptom_verify">"Bez komentarza"</string>
 
     <!-- Submission Status Card -->
     <!-- XHED: Page title for the various submission status: fetching -->
@@ -1157,7 +1161,7 @@
     <!-- YTXT: invalid status text -->
     <string name="test_result_card_status_invalid">"Ustalenie wyniku jest niemożliwe"</string>
     <!-- YTXT: pending status text -->
-    <string name="test_result_card_status_pending">"Wynik Twojego testu nie jest jeszcze dostępny"</string>
+    <string name="test_result_card_status_pending">"Twój wynik testu nie jest jeszcze dostępny"</string>
     <!-- XHED: Title for further info of test result negative -->
     <string name="test_result_card_negative_further_info_title">"Inne informacje:"</string>
     <!-- YTXT: Content for further info of test result negative -->
@@ -1201,7 +1205,10 @@
     <string name="errors_google_update_needed">"Twoja aplikacja Corona-Warn-App jest poprawnie zainstalowana, ale system „Powiadomienia o narażeniu na COVID-19” nie jest dostępny w systemie operacyjnym Twojego smartfona. Oznacza to, że nie możesz korzystać z aplikacji Corona-Warn-App. Więcej informacji znajduje się na naszej stronie „Często zadawane pytania”: https://www.coronawarn.app/en/faq/."</string>
     <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day -->
     <string name="errors_google_api_error">"Aplikacja Corona-Warn-App działa prawidłowo, ale nie możemy zaktualizować Twojego aktualnego statusu ryzyka. Rejestrowanie narażenia pozostaje aktywne i działa prawidłowo. Więcej informacji można znaleźć na naszej stronie „Często zadawane pytania”: https://www.coronawarn.app/en/faq/"</string>
-
+    <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_title">"Limit został już osiągnięty"</string>
+    <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_description">"Sprawdzanie narażeń nie jest już dzisiaj możliwe, ponieważ osiągnięto maksymalną liczbę takich kontroli na dzień określoną przez system operacyjny. Sprawdź ponownie swój status ryzyka jutro."</string>
     <!-- ####################################
                Generic Error Messages
         ###################################### -->
@@ -1248,17 +1255,7 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
@@ -1342,7 +1339,7 @@
     <!-- XHED: Header of interoperability information/configuration view -->
     <string name="interoperability_configuration_title">"Rejestrowanie narażenia\nw różnych krajach"</string>
     <!-- XTXT: First section after the header of the interoperability information/configuration view -->
-    <string name="interoperability_configuration_first_section">"Wiele krajów współpracuje ze sobą w celu aktywacji transgranicznych alertów wysyłanych poprzez wspólny serwer wymiany danych. Na przykład podczas rejestrowania narażenia można uwzględnić również kontakty z użytkownikami oficjalnych aplikacji koronawirusowych z innych uczestniczących krajów."</string>
+    <string name="interoperability_configuration_first_section">"Wiele krajów współpracuje ze sobą w celu aktywacji transgranicznych alertów wysyłanych poprzez wspólny serwer wymiany danych. Na przykład przy rejestrowaniu narażenia można wziąć pod uwagę również kontakty z użytkownikami oficjalnych aplikacji koronawirusowych z innych uczestniczących krajów."</string>
     <!-- XTXT: Second section after the header of the interoperability information/configuration view -->
     <string name="interoperability_configuration_second_section">"W tym celu aplikacja pobiera listę, która jest aktualizowana codziennie, z losowymi identyfikatorami wszystkich użytkowników, którzy udostępnili swoje losowe identyfikatory poprzez własną aplikację. Lista jest następnie porównywana z losowymi identyfikatorami zarejestrowanymi przez Twój smartfon. Codzienne pobieranie listy z losowymi identyfikatorami jest z reguły bezpłatne – za dane używane przez aplikację w tym kontekście nie będą pobierane opłaty roamingowe w innych krajach UE."</string>
     <!-- XHED: Header right above the country list in the interoperability information/configuration view -->
@@ -1357,7 +1354,7 @@
     <!-- YMSG: Onboarding tracing step second section in interoperability after the title -->
     <string name="interoperability_onboarding_second_section">"Gdy użytkownik prześle swój losowy identyfikator do serwera wymiany danych obsługiwanego wspólnie przez kraje uczestniczące, o potencjalnym narażeniu mogą zostać ostrzeżeni użytkownicy oficjalnych aplikacji koronawirusowych we wszystkich tych krajach."</string>
     <!-- YMSG: Onboarding tracing step third section in interoperability after the title. -->
-    <string name="interoperability_onboarding_randomid_download_free">"Codzienne pobieranie listy z losowymi identyfikatorami jest z reguły bezpłatne. Oznacza to, że operatorzy sieci mobilnych nie będą pobierać opłat za transmisję danych używanych przez aplikację w tym kontekście ani też nie będą naliczane opłaty roamingowe w innych krajach UE. Aby uzyskać więcej informacji, skontaktuj się ze swoim operatorem sieci mobilnej."</string>
+    <string name="interoperability_onboarding_randomid_download_free">"Codzienne pobieranie listy z losowymi identyfikatorami jest z reguły bezpłatne. Oznacza to, że operatorzy sieci mobilnych nie będą pobierać opłat za transmisję danych używanych przez aplikację w tym kontekście ani też nie będą naliczane opłaty roamingowe z tego tytułu w innych krajach UE. Aby uzyskać więcej informacji, skontaktuj się ze swoim operatorem sieci mobilnej."</string>
     <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. -->
     <string name="interoperability_onboarding_list_title">"Obecnie uczestniczą następujące kraje:"</string>
 
@@ -1377,7 +1374,7 @@
     <string name="interoperability_onboarding_delta_free_download">"Codzienne pobieranie listy z losowymi identyfikatorami jest z reguły bezpłatne. Oznacza to, że operatorzy sieci mobilnych nie będą pobierać opłat za transmisję danych używanych przez aplikację w tym kontekście ani też nie będą naliczane opłaty roamingowe z tego tytułu w innych krajach UE. Aby uzyskać więcej informacji, skontaktuj się ze swoim operatorem sieci mobilnej."</string>
 
     <!-- XACT: interoperability (eu) - illustraction description, explanation image -->
-    <string name="interoperability_eu_illustration_description">"Ręka trzyma smartfon. W tle przedstawiona jest Europa i flaga europejska."</string>
+    <string name="interoperability_eu_illustration_description">"Ręka trzyma smartfon. Europa i flaga europejska są przedstawione w tle."</string>
 
     <!-- XTXT: Title for the interoperability onboarding if country download fails -->
     <string name="interoperability_onboarding_list_title_failrequest">"Kraje uczestniczÄ…ce"</string>
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 6c78674ea18deac47d80a941d4aefde469f79695..0c5a9e4f4e0e8f0252b4a82e4df7ddcaa07dac65 100644
--- a/Corona-Warn-App/src/main/res/values-ro/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml
@@ -20,12 +20,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -109,6 +105,10 @@
     <string name="notification_headline">"Corona-Warn-App"</string>
     <!-- XTXT: Notification body -->
     <string name="notification_body">"Aveți mesaje noi de la aplicația Corona-Warn."</string>
+    <!-- XHED: Notification title - Reminder to share a positive test result-->
+    <string name="notification_headline_share_positive_result">"Puteți fi de ajutor!"</string>
+    <!-- XTXT: Notification body - Reminder to share a positive test result-->
+    <string name="notification_body_share_positive_result">"Vă rugăm să partajați rezultatul testului dvs. pentru a-i avertiza pe ceilalți."</string>
 
     <!-- ####################################
               App Auto Update
@@ -150,11 +150,9 @@
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
     <string name="risk_card_body_saved_days_full">"Înregistrarea în jurnal a expunerilor este permanent activă"</string>
     <!-- XTXT; risk card - no update done yet -->
-    <string name="risk_card_body_not_yet_fetched">"Expunerile nu au fost încă verificate."</string>
+    <string name="risk_card_body_not_yet_fetched">"Întâlnirile nu au fost încă verificate."</string>
     <!-- XTXT: risk card - last successful update -->
     <string name="risk_card_body_time_fetched">"Actualizată: %1$s"</string>
-    <!-- XTXT: risk card - next update -->
-    <string name="risk_card_body_next_update">"Actualizată zilnic"</string>
     <!-- XTXT: risk card - hint to open the app daily -->
     <string name="risk_card_body_open_daily">"Rețineți: Deschideți aplicația zilnic pentru a actualiza starea riscului dvs."</string>
     <!-- XBUT: risk card - update risk -->
@@ -185,13 +183,19 @@
     <!-- XHED: risk card - tracing stopped headline, due to no possible calculation -->
     <string name="risk_card_no_calculation_possible_headline">"Înregistrarea în jurnal a expunerilor a fost oprită"</string>
     <!-- XTXT: risk card - last successfully calculated risk level -->
-    <string name="risk_card_no_calculation_possible_body_saved_risk">"Ultima înregistrare în jurnal a expunerilor:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
+    <string name="risk_card_no_calculation_possible_body_saved_risk">"Ultima verificare a expunerilor:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
     <!-- XHED: risk card -  outdated risk headline, calculation isn't possible -->
     <string name="risk_card_outdated_risk_headline">"Înregistrarea în jurnal a expunerilor nu este posibilă"</string>
     <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours -->
     <string name="risk_card_outdated_risk_body">"Înregistrarea în jurnal a expunerilor dvs. nu a putut fi actualizată timp de peste 24 de ore."</string>
     <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours -->
     <string name="risk_card_outdated_manual_risk_body">"Starea riscului dvs. nu a fost actualizată timp de peste 48 de ore. Vă rugăm să activați starea riscului dvs."</string>
+    <!-- XHED: risk card - risk check failed headline, no internet connection -->
+    <string name="risk_card_check_failed_no_internet_headline">"Verificarea expunerii a eșuat"</string>
+    <!-- XTXT: risk card - risk check failed, please check your internet connection -->
+    <string name="risk_card_check_failed_no_internet_body">"Sincronizarea ID-urilor aleatorii cu serverul a eșuat. Puteți relansa manual sincronizarea."</string>
+    <!-- XTXT: risk card - risk check failed, restart button -->
+    <string name="risk_card_check_failed_no_internet_restart_button">"Relansare"</string>
 
     <!-- ####################################
               Risk Card - Progress
@@ -247,7 +251,7 @@
     <!-- XHED: App overview subtitle for tracing explanation-->
     <string name="main_overview_subtitle_tracing">"Înregistrarea în jurnal a expunerilor"</string>
     <!-- YTXT: App overview body text about tracing -->
-    <string name="main_overview_body_tracing">"Înregistrarea în jurnal a expunerilor este una dintre cele trei caracteristici centrale ale aplicației. Când o activați, sunt înregistrate întâlnirile cu dispozitivele altor persoane. Nu trebuie să faceți nimic altceva."</string>
+    <string name="main_overview_body_tracing">"Înregistrarea în jurnal a expunerilor este una dintre cele trei caracteristici centrale ale aplicației. Când o activați, sunt înregistrate întâlnirile cu smartphone-urile altor persoane. Nu trebuie să faceți nimic altceva."</string>
     <!-- XHED: App overview subtitle for risk explanation -->
     <string name="main_overview_subtitle_risk">"Risc de infectare"</string>
     <!-- YTXT: App overview body text about risk levels -->
@@ -285,7 +289,7 @@
     <!-- XHED: App overview subtitle for glossary keys -->
     <string name="main_overview_subtitle_glossary_keys">"ID aleatoriu"</string>
     <!-- YTXT: App overview body for glossary keys -->
-    <string name="main_overview_body_glossary_keys">"ID-urile aleatorii sunt combinații de cifre și litere generate aleatoriu. Acestea sunt schimbate între dispozitivele aflate în proximitate strânsă. ID-urile aleatorii nu pot fi asociate cu o persoană anume și sunt șterse automat după 14 zile. Persoanele diagnosticate cu COVID-19 pot opta să trimită ID-urile aleatorii din ultimele 14 zile altor utilizatori ai aplicației."</string>
+    <string name="main_overview_body_glossary_keys">"ID-urile aleatorii sunt combinații de cifre și litere generate aleatoriu. Acestea sunt schimbate între dispozitivele aflate în proximitate strânsă. ID-urile aleatorii nu pot fi asociate cu o persoană anume și sunt șterse automat după 14 zile. Persoanele diagnosticate cu COVID-19 pot opta să trimită ID-urile aleatorii din ultimele 14 zile și altor utilizatori ai aplicației."</string>
     <!-- XACT: main (overview) - illustraction description, explanation image -->
     <string name="main_overview_illustration_description">"Un smartphone afișează conținut variat, numerotat de la 1 la 3."</string>
     <!-- XACT: App main page title -->
@@ -344,7 +348,7 @@
     <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors -->
     <string name="risk_details_information_body_outdated_risk">"Înregistrarea în jurnal a expunerilor dvs. nu a putut fi actualizată timp de peste 24 de ore."</string>
     <!-- YTXT: risk details - low risk explanation text -->
-    <string name="risk_details_information_body_low_risk">"Aveți un risc redus de infectare deoarece nu a fost înregistrată nicio expunere la persoane diagnosticate ulterior cu COVID-19 sau întâlnirile au fost limitate la o perioadă scurtă și la o distanță mai mare."</string>
+    <string name="risk_details_information_body_low_risk">"Aveți un risc redus de infectare deoarece nu a fost înregistrată nicio expunere la persoane diagnosticate ulterior cu COVID-19 sau întâlnirile dvs. au fost limitate la o perioadă scurtă și la o distanță mai mare."</string>
     <!-- YTXT: risk details - low risk explanation text with encounter with low risk -->
     <string name="risk_details_information_body_low_risk_with_encounter">"Riscul de infectare este calculat local pe smartphone-ul dvs., utilizând datele de înregistrare în jurnal a expunerilor. Calculul poate ține cont și de distanța și durata expunerii la persoane diagnosticate cu COVID-19, precum și de potențiala contagiozitate a acestora. Riscul dvs. de infectare nu poate fi observat sau transmis mai departe niciunei alte persoane."</string>
     <!-- YTXT: risk details - increased risk explanation text with variable for day(s) since last contact -->
@@ -357,9 +361,9 @@
         <item quantity="many">"Aveți un risc crescut de infectare deoarece ați fost expus ultima dată acum %1$s zile pe o perioadă mai lungă de timp și în strânsă proximitate cu cel puțin o persoană diagnosticată cu COVID-19."</item>
     </plurals>
     <!-- YTXT: risk details - risk calculation explanation -->
-    <string name="risk_details_information_body_notice">"Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata și proximitatea) la nivel local pe dispozitivul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane."</string>
+    <string name="risk_details_information_body_notice">"Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata și proximitatea) la nivel local pe smartphone-ul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane."</string>
     <!-- YTXT: risk details - risk calculation explanation for increased risk -->
-    <string name="risk_details_information_body_notice_increased">"Prin urmare, riscul dvs. de infectare a fost clasificat ca fiind crescut. Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata și proximitatea) la nivel local pe dispozitivul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane. Când ajungeți acasă, evitați contactul strâns cu membrii familiei sau cu cei din gospodăria dvs."</string>
+    <string name="risk_details_information_body_notice_increased">"Prin urmare, riscul dvs. de infectare a fost clasificat ca fiind crescut. Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata și proximitatea) la nivel local pe smartphone-ul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane. Când ajungeți acasă, evitați contactul strâns cu membrii familiei sau cu cei din gospodăria dvs."</string>
     <!-- NOTR -->
     <string name="risk_details_button_update">@string/risk_card_button_update</string>
     <!-- NOTR -->
@@ -415,7 +419,7 @@
     <!-- XHED: onboarding(together) - two/three line headline under an illustration -->
     <string name="onboarding_subtitle">"Mai multă protecție pentru dvs. și pentru noi toți. Utilizând Corona-Warn-App putem întrerupe mai ușor lanțul de infectare."</string>
     <!-- YTXT: onboarding(together) - inform about the app -->
-    <string name="onboarding_body">"Transformați-vă dispozitivul într-un sistem de avertizare împotriva coronavirusului. Obțineți un sumar al stării de risc și aflați dacă ați intrat în contact strâns cu persoane diagnosticate cu COVID-19 în ultimele 14 zile."</string>
+    <string name="onboarding_body">"Transformați-vă smartphone-ul într-un sistem de avertizare împotriva coronavirusului. Obțineți un sumar al stării de risc și aflați dacă ați intrat în contact strâns cu persoane diagnosticate cu COVID-19 în ultimele 14 zile."</string>
     <!-- YTXT: onboarding(together) - explain application -->
     <string name="onboarding_body_emphasized">"Aplicația înregistrează în jurnal întâlnirile dintre persoane prin dispozitivele acestora, care schimbă ID-uri aleatorii criptate, fără a accesa niciun fel de date personale."</string>
     <!-- XACT: onboarding(together) - illustraction description, header image -->
@@ -452,7 +456,7 @@
     <!-- XBUT: onboarding(tracing) - negative button (right) -->
     <string name="onboarding_tracing_dialog_button_negative">"ÃŽnapoi"</string>
     <!-- XACT: onboarding(tracing) - dialog about background jobs header text -->
-    <string name="onboarding_background_fetch_dialog_headline">"Actualizări în fundal dezactivate"</string>
+    <string name="onboarding_background_fetch_dialog_headline">"Împrospătarea aplicației în fundal dezactivată"</string>
     <!-- YMSI: onboarding(tracing) - dialog about background jobs -->
     <string name="onboarding_background_fetch_dialog_body">"Ați dezactivat actualizările în fundal pentru aplicația Corona-Warn. Activați actualizările în fundal pentru a utiliza înregistrarea automată în jurnal a expunerilor. Dacă nu activați actualizările în fundal, puteți porni doar manual din aplicație înregistrarea în jurnal a expunerilor. Puteți activa actualizările în fundal pentru aplicație din setările dispozitivului dvs."</string>
     <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings -->
@@ -474,7 +478,7 @@
     <!-- XBUT: onboarding(tracing) - dialog about manual checking button -->
     <string name="onboarding_manual_required_dialog_button">"OK"</string>
     <!-- XACT: onboarding(tracing) - illustraction description, header image -->
-    <string name="onboarding_tracing_illustration_description">"Trei persoane și-au activat pe dispozitiv înregistrarea în jurnal a expunerilor, ceea ce va duce la înregistrarea întâlnirilor lor."</string>
+    <string name="onboarding_tracing_illustration_description">"Trei persoane și-au activat pe smartphone înregistrarea în jurnal a expunerilor, ceea ce va duce la înregistrarea întâlnirilor lor."</string>
     <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline -->
     <string name="onboarding_tracing_location_headline">"Permiteți accesul la locație"</string>
     <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text -->
@@ -671,7 +675,7 @@
     <!-- YTXT: Body text for about information page -->
     <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) este un organism federal de sănătate publică din Germania. RKI a publicat aplicația Corona-Warn în numele Guvernului Federal. Aplicația are drept scop să completeze sub formă digitală măsurile de sănătate publică deja introduse: distanțarea socială, igiena și purtarea măștii."</string>
     <!-- YTXT: Body text for about information page -->
-    <string name="information_about_body">"Persoanele care utilizează aplicația ajută la urmărirea și întreruperea lanțurilor de infectare. Aplicația salvează local, pe dispozitivul dvs., întâlnirile cu alte persoane. Sunteți notificat dacă ați întâlnit persoane care au fost diagnosticate ulterior cu COVID-19. Identitatea și confidențialitatea dvs. sunt protejate întotdeauna."</string>
+    <string name="information_about_body">"Persoanele care utilizează aplicația ajută la urmărirea și întreruperea lanțurilor de infectare. Aplicația salvează local, pe smartphone-ul dvs., întâlnirile cu alte persoane. Sunteți notificat dacă ați întâlnit persoane care au fost diagnosticate ulterior cu COVID-19. Identitatea și confidențialitatea dvs. sunt protejate întotdeauna."</string>
     <!-- XACT: describes illustration -->
     <string name="information_about_illustration_description">"Un grup de persoane își utilizează smartphone-urile prin oraș."</string>
     <!-- XHED: Page title for privacy information page, also menu item / button text -->
@@ -703,9 +707,9 @@
     <!-- XTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_phone">"Serviciul clienți vă stă la dispoziție."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_open">"Limbi: germană, engleză, turcă\nProgram de lucru:"<xliff:g id="line_break">"\n"</xliff:g>"luni - sâmbătă: 07:00 - 22:00"<xliff:g id="line_break">"\n(exceptând sărbătorile legale)"</xliff:g><xliff:g id="line_break">"\nApelul este gratuit."</xliff:g></string>
+    <string name="information_contact_body_open">"Limbi: engleză, germană, turcă\nProgram de lucru:"<xliff:g id="line_break">"\n"</xliff:g>"luni - sâmbătă: 07:00 - 22:00"<xliff:g id="line_break">"\n(exceptând sărbătorile legale)"</xliff:g><xliff:g id="line_break">"\nApelul este gratuit."</xliff:g></string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_other">"Dacă aveți întrebări legate de starea de sănătate, vă rugăm să contactați medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 112."</string>
+    <string name="information_contact_body_other">"Dacă aveți întrebări legate de starea de sănătate, vă rugăm să contactați medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 116 117 (Germania) sau 112 (România)."</string>
     <!-- XACT: describes illustration -->
     <string name="information_contact_illustration_description">"Un bărbat poartă căști în timpul unei convorbiri telefonice."</string>
     <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website -->
@@ -788,9 +792,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"ÃŽnapoi"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Testul are erori"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"Codul QR nu mai este valabil"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"A apărut o problemă la evaluarea testului dvs. Codul dvs. QR a expirat deja."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Testul dvs. are o vechime de peste 21 de zile și nu mai poate fi înregistrat în aplicație. Dacă sunteți testat din nou în viitor, nu uitați să scanați codul QR imediat ce îl primiți."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string>
 
@@ -865,15 +869,15 @@
     <!-- XBUT: test result pending : refresh button -->
     <string name="submission_test_result_pending_refresh_button">"Actualizare"</string>
     <!-- XBUT: test result pending : remove the test button -->
-    <string name="submission_test_result_pending_remove_test_button">"Ștergere test"</string>
+    <string name="submission_test_result_pending_remove_test_button">"Eliminare test"</string>
     <!-- XHED: Page headline for negative test result next steps  -->
     <string name="submission_test_result_negative_steps_negative_heading">"Rezultatul testului dvs."</string>
     <!-- YTXT: Body text for next steps section of test negative result -->
     <string name="submission_test_result_negative_steps_negative_body">"Rezultatul de laborator nu indică o confirmare a infecției cu coronavirusul SARS-CoV-2.\n\nȘtergeți testul din Corona-Warn-App pentru a salva un nou cod de test aici dacă este necesar."</string>
     <!-- XBUT: negative test result : remove the test button -->
-    <string name="submission_test_result_negative_remove_test_button">"Ștergere test"</string>
+    <string name="submission_test_result_negative_remove_test_button">"Eliminare test"</string>
     <!-- XHED: Page headline for other warnings screen  -->
-    <string name="submission_test_result_positive_steps_warning_others_heading">"Avertizarea altora"</string>
+    <string name="submission_test_result_positive_steps_warning_others_heading">"Avertizați-i pe ceilalți"</string>
     <!-- YTXT: Body text for for other warnings screen-->
     <string name="submission_test_result_positive_steps_warning_others_body">"Partajați ID-urile dvs. aleatorii și avertizați-i pe ceilalți.\nAjutați la stabilirea riscului de infectare pentru ceilalți cu mai multă acuratețe, indicând momentul în care ați observat prima dată simptomele de coronavirus."</string>
     <!-- XBUT: positive test result : continue button -->
@@ -952,13 +956,13 @@
     <!-- YTXT: Body text for QR code dispatcher option -->
     <string name="submission_dispatcher_qr_card_text">"Înregistrați-vă testul scanând codul QR al documentului de testare."</string>
     <!-- XHED: Dialog headline for dispatcher QR prviacy dialog  -->
-    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Consimțământ"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Declarație de consimțământ"</string>
     <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog -->
     <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Accept”, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacy”."</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Accept"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Permiteți"</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nu accept"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nu permiteți"</string>
     <!-- YTXT: Dispatcher text for TAN code option -->
     <string name="submission_dispatcher_card_tan_code">"TAN"</string>
     <!-- YTXT: Body text for TAN code dispatcher option -->
@@ -972,7 +976,7 @@
 
     <!-- Submission Positive Other Warning -->
     <!-- XHED: Page title for the positive result additional warning page-->
-    <string name="submission_positive_other_warning_title">"Avertizarea altor persoane"</string>
+    <string name="submission_positive_other_warning_title">"Avertizați-i pe ceilalți"</string>
     <!-- XHED: Page headline for the positive result additional warning page-->
     <string name="submission_positive_other_warning_headline">"Să ne ajutăm împreună!"</string>
     <!-- YTXT: Body text for the positive result additional warning page-->
@@ -980,7 +984,7 @@
     <!-- XBUT: other warning continue button -->
     <string name="submission_positive_other_warning_button">"Accept"</string>
     <!-- XACT: other warning - illustration description, explanation image -->
-    <string name="submission_positive_other_illustration_description">"Un dispozitiv transmite un diagnostic de test pozitiv criptat către sistem."</string>
+    <string name="submission_positive_other_illustration_description">"Un smartphone transmite un diagnostic de test pozitiv criptat către sistem."</string>
     <!-- XHED: Title for the interop country list-->
     <string name="submission_interoperability_list_title">"Următoarele țări participă în prezent la înregistrarea în jurnal a expunerilor la nivel transnațional:"</string>
 
@@ -1046,7 +1050,7 @@
     <!-- XBUT:  symptom initial screen no button -->
     <string name="submission_symptom_negative_button">"Nu"</string>
     <!-- XBUT:  symptom initial screen no information button -->
-    <string name="submission_symptom_no_information_button">"Nu răspund"</string>
+    <string name="submission_symptom_no_information_button">"Nu comentez"</string>
     <!-- XBUT:  symptom initial screen continue button -->
     <string name="submission_symptom_further_button">"ÃŽnainte"</string>
 
@@ -1071,9 +1075,9 @@
     <!-- YTXT: Body text for step 2 of contact page-->
     <string name="submission_contact_step_2_body">"Înregistrați testul introducând codul TAN în aplicație."</string>
     <!-- YTXT: Body text for operating hours in contact page-->
-    <string name="submission_contact_operating_hours_body">"Limbi: \ngermană, engleză, turcă\n\nProgram de lucru:\nluni - duminică: non-stop\n\nApelul este gratuit."</string>
+    <string name="submission_contact_operating_hours_body">"Limbi: \nengleză, germană, turcă\n\nProgram de lucru:\nluni - duminică: non-stop\n\nApelul este gratuit."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="submission_contact_body_other">"Dacă aveți întrebări legate de starea de sănătate, vă rugăm să contactați medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 112."</string>
+    <string name="submission_contact_body_other">"Dacă aveți întrebări legate de starea de sănătate, vă rugăm să contactați medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 116 117 (Germania) sau 112 (România)."</string>
 
     <!-- XACT: Submission contact page title -->
     <string name="submission_contact_accessibility_title">"Sunați la hotline și solicitați un TAN"</string>
@@ -1096,7 +1100,7 @@
     <!-- XBUT: symptom calendar screen more than 2 weeks button -->
     <string name="submission_symptom_more_two_weeks">"Cu peste 2 săptămâni în urmă"</string>
     <!-- XBUT: symptom calendar screen verify button -->
-    <string name="submission_symptom_verify">"Nu răspund"</string>
+    <string name="submission_symptom_verify">"Nu comentez"</string>
 
     <!-- Submission Status Card -->
     <!-- XHED: Page title for the various submission status: fetching -->
@@ -1201,7 +1205,10 @@
     <string name="errors_google_update_needed">"Aplicația dvs. Corona-Warn este instalată corect, dar serviciul „Notificări privind expunerea la COVID-19” nu este disponibil în sistemul de operare al smartphone-ului dvs. Aceasta înseamnă că nu puteți utiliza aplicația Corona-Warn. Pentru mai multe informații, consultați pagina noastră de întrebări frecvente: https://www.coronawarn.app/en/faq/"</string>
     <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day -->
     <string name="errors_google_api_error">"Aplicația Corona-Warn funcționează corect, dar nu putem actualiza starea curentă a riscului dvs. Înregistrarea în jurnal a expunerilor rămâne activă și funcționează corect. Pentru mai multe informații, consultați pagina noastră de întrebări frecvente: https://www.coronawarn.app/en/faq/"</string>
-
+    <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_title">"Limita a fost deja atinsă"</string>
+    <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_description">"Astăzi nu mai este posibilă verificarea expunerii, deoarece ați atins numărul maxim de verificări pe zi definit de sistemul dvs. de operare. Verificați din nou mâine starea riscului dvs."</string>
     <!-- ####################################
                Generic Error Messages
         ###################################### -->
@@ -1248,17 +1255,7 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
@@ -1357,7 +1354,7 @@
     <!-- YMSG: Onboarding tracing step second section in interoperability after the title -->
     <string name="interoperability_onboarding_second_section">"Când un utilizator transmite ID-urile sale aleatorii la serverul de schimb operat în comun de țările participante, utilizatorii aplicațiilor oficiale anticoronavirus din toate aceste țări pot fi avertizați de posibila expunere."</string>
     <!-- YMSG: Onboarding tracing step third section in interoperability after the title. -->
-    <string name="interoperability_onboarding_randomid_download_free">"Descărcarea zilnică a listei cu ID-urilor aleatorii este, de obicei, gratuită pentru dvs. În mod specific, aceasta înseamnă că operatorii de rețele mobile nu percep costuri pentru datele utilizate de aplicație în acest context și nu aplică niciun fel de costuri de roaming pentru această opțiune în alte țări UE. Contactați operatorul rețelei mobile pentru mai multe informații."</string>
+    <string name="interoperability_onboarding_randomid_download_free">"Descărcarea zilnică a listei cu ID-uri aleatorii este, de obicei, gratuită pentru dvs. În mod specific, aceasta înseamnă că operatorii de rețele mobile nu percep costuri pentru datele utilizate de aplicație în acest context și nu aplică niciun fel de costuri de roaming pentru această opțiune în alte țări UE. Contactați operatorul rețelei mobile pentru mai multe informații."</string>
     <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. -->
     <string name="interoperability_onboarding_list_title">"În prezent participă următoarele țări:"</string>
 
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 02255db8b322545744f9c3fa76f6a5958bb01296..78dc5a455f852fb5ba62c532be47921025386790 100644
--- a/Corona-Warn-App/src/main/res/values-tr/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml
@@ -20,12 +20,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -109,6 +105,10 @@
     <string name="notification_headline">"Corona-Warn-App"</string>
     <!-- XTXT: Notification body -->
     <string name="notification_body">"Corona-Warn-App uygulamasından yeni mesajlarınız var."</string>
+    <!-- XHED: Notification title - Reminder to share a positive test result-->
+    <string name="notification_headline_share_positive_result">"Yardımcı olabilirsiniz!"</string>
+    <!-- XTXT: Notification body - Reminder to share a positive test result-->
+    <string name="notification_body_share_positive_result">"Lütfen test sonucunuzu paylaşın ve diğer kullanıcıları uyarın."</string>
 
     <!-- ####################################
               App Auto Update
@@ -150,11 +150,9 @@
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
     <string name="risk_card_body_saved_days_full">"Maruz kalma günlüğü sürekli etkin"</string>
     <!-- XTXT; risk card - no update done yet -->
-    <string name="risk_card_body_not_yet_fetched">"Maruz kalmalar henüz kontrol edilmedi."</string>
+    <string name="risk_card_body_not_yet_fetched">"Karşılaşmalar henüz kontrol edilmedi."</string>
     <!-- XTXT: risk card - last successful update -->
     <string name="risk_card_body_time_fetched">"Güncelleme: %1$s"</string>
-    <!-- XTXT: risk card - next update -->
-    <string name="risk_card_body_next_update">"Günlük olarak güncellenir"</string>
     <!-- XTXT: risk card - hint to open the app daily -->
     <string name="risk_card_body_open_daily">"Not: Risk durumunuzu güncellemek için lütfen uygulamayı her gün açın."</string>
     <!-- XBUT: risk card - update risk -->
@@ -185,13 +183,19 @@
     <!-- XHED: risk card - tracing stopped headline, due to no possible calculation -->
     <string name="risk_card_no_calculation_possible_headline">"Maruz kalma günlüğü durduruldu"</string>
     <!-- XTXT: risk card - last successfully calculated risk level -->
-    <string name="risk_card_no_calculation_possible_body_saved_risk">"Son maruz kalma günlüğü:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
+    <string name="risk_card_no_calculation_possible_body_saved_risk">"Son maruz kalma kontrolü:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
     <!-- XHED: risk card -  outdated risk headline, calculation isn't possible -->
     <string name="risk_card_outdated_risk_headline">"Maruz kalma günlüğü oluşturulamıyor"</string>
     <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours -->
     <string name="risk_card_outdated_risk_body">"Maruz kalma günlüğünüz 24 saatten uzun süre için güncellenemedi."</string>
     <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours -->
     <string name="risk_card_outdated_manual_risk_body">"Risk durumunuz 48 saatten uzun süredir güncellenmedi. Lütfen risk durumunuzu güncelleyin."</string>
+    <!-- XHED: risk card - risk check failed headline, no internet connection -->
+    <string name="risk_card_check_failed_no_internet_headline">"Maruz kalma kontrolü başarısız oldu"</string>
+    <!-- XTXT: risk card - risk check failed, please check your internet connection -->
+    <string name="risk_card_check_failed_no_internet_body">"Rastgele kimliklerin sunucu ile senkronizasyonu başarısız oldu. Senkronizasyonu manüel olarak başlatabilirsiniz."</string>
+    <!-- XTXT: risk card - risk check failed, restart button -->
+    <string name="risk_card_check_failed_no_internet_restart_button">"Yeniden baÅŸlat"</string>
 
     <!-- ####################################
               Risk Card - Progress
@@ -247,7 +251,7 @@
     <!-- XHED: App overview subtitle for tracing explanation-->
     <string name="main_overview_subtitle_tracing">"Maruz Kalma Günlüğü"</string>
     <!-- YTXT: App overview body text about tracing -->
-    <string name="main_overview_body_tracing">"Maruz kalma günlüğü, uygulamanın üç temel özelliğinden biridir. Bu özelliği etkinleştirdiğinizde, diğer kişilerin cihazlarıyla karşılaşmalarınız günlüğe kaydedilir. Başka bir işlem yapmanız gerekmez."</string>
+    <string name="main_overview_body_tracing">"Maruz kalma günlüğü, uygulamanın üç temel özelliğinden biridir. Bu özelliği etkinleştirdiğinizde, diğer kişilerin akıllı telefonlarıyla karşılaşmalarınız günlüğe kaydedilir. Başka bir işlem yapmanız gerekmez."</string>
     <!-- XHED: App overview subtitle for risk explanation -->
     <string name="main_overview_subtitle_risk">"Enfeksiyon Riski"</string>
     <!-- YTXT: App overview body text about risk levels -->
@@ -357,9 +361,9 @@
         <item quantity="many">"En son %1$s gün önce, COVID-19 tanısı konan en az bir kişiyle daha uzun süreyle ve yakın mesafeden maruz kalma yaşadığınız için enfeksiyon riskiniz daha yüksektir."</item>
     </plurals>
     <!-- YTXT: risk details - risk calculation explanation -->
-    <string name="risk_details_information_body_notice">"Enfeksiyon riskiniz, cihazınızda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz başkaları tarafından görüntülenemez veya başkalarına aktarılmaz."</string>
+    <string name="risk_details_information_body_notice">"Enfeksiyon riskiniz, akıllı telefonunuzda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz başkaları tarafından görüntülenemez veya başkalarına aktarılmaz."</string>
     <!-- YTXT: risk details - risk calculation explanation for increased risk -->
-    <string name="risk_details_information_body_notice_increased">"Bu nedenle enfeksiyon riskiniz artmış olarak derecelendirilmiştir. Enfeksiyon riskiniz, cihazınızda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz başkaları tarafından görüntülenemez veya başkalarına aktarılmaz. Eve gittiğinizde lütfen aile fertleriniz veya ev arkadaşlarınızla yakın temastan kaçının."</string>
+    <string name="risk_details_information_body_notice_increased">"Bu nedenle enfeksiyon riskiniz artmış olarak derecelendirilmiştir. Enfeksiyon riskiniz, akıllı telefonunuzda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz başkaları tarafından görüntülenemez veya başkalarına aktarılmaz. Eve gittiğinizde lütfen aile fertleriniz veya ev arkadaşlarınızla yakın temastan kaçının."</string>
     <!-- NOTR -->
     <string name="risk_details_button_update">@string/risk_card_button_update</string>
     <!-- NOTR -->
@@ -415,7 +419,7 @@
     <!-- XHED: onboarding(together) - two/three line headline under an illustration -->
     <string name="onboarding_subtitle">"Sizin için ve hepimiz için daha fazla koruma. Corona-Warn-App uygulamasını kullanarak enfeksiyon zincirlerini çok daha kısa süre içinde kırabiliriz."</string>
     <!-- YTXT: onboarding(together) - inform about the app -->
-    <string name="onboarding_body">"Cihazınızı koronavirüs uyarı sistemine dönüştürün. Risk durumunuza ilişkin genel bir bakış elde edin ve son 14 gün içinde COVID-19 tanısı konan herhangi biri ile yakın temasa geçip geçmediğinizi öğrenin."</string>
+    <string name="onboarding_body">"Akıllı telefonunuzu koronavirüs uyarı sistemine dönüştürün. Risk durumunuza ilişkin genel bir bakış elde edin ve son 14 gün içinde COVID-19 tanısı konan herhangi biri ile yakın temasa geçip geçmediğinizi öğrenin."</string>
     <!-- YTXT: onboarding(together) - explain application -->
     <string name="onboarding_body_emphasized">"Uygulama kişilerin cihazları arasında şifrelenmiş rastgele kimlikleri paylaşarak karşılaşma günlüğü oluşturur ve bu sırada hiçbir kişisel veriye erişim sağlanmaz."</string>
     <!-- XACT: onboarding(together) - illustraction description, header image -->
@@ -452,7 +456,7 @@
     <!-- XBUT: onboarding(tracing) - negative button (right) -->
     <string name="onboarding_tracing_dialog_button_negative">"Geri"</string>
     <!-- XACT: onboarding(tracing) - dialog about background jobs header text -->
-    <string name="onboarding_background_fetch_dialog_headline">"Arka plan güncellemeleri devre dışı bırakıldı"</string>
+    <string name="onboarding_background_fetch_dialog_headline">"Arka planda uygulamayı yenileme devre dışı bırakıldı"</string>
     <!-- YMSI: onboarding(tracing) - dialog about background jobs -->
     <string name="onboarding_background_fetch_dialog_body">"Corona-Warn-App için arka plan güncellemelerini devre dışı bıraktınız. Otomatik maruz kalma günlüğü özelliğini kullanmak için lütfen arka plan güncellemelerini etkinleştirin. Arka plan güncellemelerini etkinleştirmezseniz maruz kalma günlüğü özelliğini yalnızca uygulamadan manüel olarak başlatabilirsiniz. Uygulamanın arka plan güncellemelerini cihazınızın ayarlarından etkinleştirebilirsiniz."</string>
     <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings -->
@@ -474,7 +478,7 @@
     <!-- XBUT: onboarding(tracing) - dialog about manual checking button -->
     <string name="onboarding_manual_required_dialog_button">"Tamam"</string>
     <!-- XACT: onboarding(tracing) - illustraction description, header image -->
-    <string name="onboarding_tracing_illustration_description">"Üç kişi cihazlarında maruz kalma günlüğünü etkinleştirdi ve birbirleri ile karşılaşmaları günlüğe kaydedilecektir."</string>
+    <string name="onboarding_tracing_illustration_description">"Üç kişi akıllı telefonlarında maruz kalma günlüğünü etkinleştirdi ve birbirleri ile karşılaşmaları günlüğe kaydedilecektir."</string>
     <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline -->
     <string name="onboarding_tracing_location_headline">"Konum eriÅŸimine izin ver"</string>
     <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text -->
@@ -671,7 +675,7 @@
     <!-- YTXT: Body text for about information page -->
     <string name="information_about_body_emphasized">"Robert Koch Institute (RKI), Almanya\'nın federal kamu sağlığı kurumudur. RKI, Federal Hükûmet adına Corona-Warn-App uygulamasını yayınlamaktadır. Uygulama, daha önce açıklanan kamu sağlığı önlemlerine ilişkin dijital bir tamamlayıcı niteliğindedir: sosyal mesafe, hijyen uygulamaları ve yüz maskeleri."</string>
     <!-- YTXT: Body text for about information page -->
-    <string name="information_about_body">"Uygulamayı kullanan kişiler, enfeksiyon zincirlerinin takip edilmesine ve kırılmasına yardımcı olur. Uygulama, diğer kişilerle karşılaşmaları cihazınızda yerel olarak kaydeder. Daha sonra COVID-19 tanısı konan kişilerle karşılaşmışsanız size bildirim gönderilir. Kimliğiniz ve gizliliğiniz daima koruma altındadır."</string>
+    <string name="information_about_body">"Uygulamayı kullanan herkes, enfeksiyon zincirlerinin takip edilmesine ve kırılmasına yardımcı olur. Uygulama, diğer kişilerle karşılaşmaları akıllı telefonunuzda yerel olarak kaydeder. Daha sonra COVID-19 tanısı konan kişilerle karşılaşmışsanız size bildirim gönderilir. Kimliğiniz ve gizliliğiniz daima koruma altındadır."</string>
     <!-- XACT: describes illustration -->
     <string name="information_about_illustration_description">"Bölgedeki bir grup insan akıllı telefonlarını kullanıyor."</string>
     <!-- XHED: Page title for privacy information page, also menu item / button text -->
@@ -703,7 +707,7 @@
     <!-- XTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_phone">"Müşteri hizmetlerimiz size yardımcı olmaya hazır."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_open">"Diller: Almanca, İngilizce, Türkçe\nMesai saatleri:"<xliff:g id="line_break">"\n"</xliff:g>"Pazartesi - Cumartesi: 7.00 - 22.00"<xliff:g id="line_break">"\n(ulusal tatiller hariçtir)"</xliff:g><xliff:g id="line_break">"\nAramalar ücretsizdir."</xliff:g></string>
+    <string name="information_contact_body_open">"Diller: İngilizce, Almanca, Türkçe\nMesai saatleri:"<xliff:g id="line_break">"\n"</xliff:g>"Pazartesi - Cumartesi: 7.00 - 22.00"<xliff:g id="line_break">"\n(ulusal tatiller hariçtir)"</xliff:g><xliff:g id="line_break">"\nAramalar ücretsizdir."</xliff:g></string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_other">"Sağlıkla ilgili tüm sorularınız için lütfen aile hekiminizle veya tıbbi acil servis yardım hattı ile iletişime geçin. Telefon: 116 117."</string>
     <!-- XACT: describes illustration -->
@@ -788,9 +792,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"Geri"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Testte hata var"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"QR kod artık geçerli değil"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"Testinizi değerlendirirken bir problem oluştu. QR kodunuzun süresi dolmuş."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Testiniz 21 günden eski ve artık uygulamaya kaydedilemez. İlerleyen zamanlarda yeniden test yaptırırsanız lütfen QR kodu alır almaz tarayın."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"Tamam"</string>
 
@@ -865,15 +869,15 @@
     <!-- XBUT: test result pending : refresh button -->
     <string name="submission_test_result_pending_refresh_button">"Güncelle"</string>
     <!-- XBUT: test result pending : remove the test button -->
-    <string name="submission_test_result_pending_remove_test_button">"Testi Sil"</string>
+    <string name="submission_test_result_pending_remove_test_button">"Testi kaldır"</string>
     <!-- XHED: Page headline for negative test result next steps  -->
     <string name="submission_test_result_negative_steps_negative_heading">"Test Sonucunuz"</string>
     <!-- YTXT: Body text for next steps section of test negative result -->
     <string name="submission_test_result_negative_steps_negative_body">"Laboratuvar sonucuna göre koronavirüs SARS-CoV-2 olduğunuza dair bir doğrulama yok.\n\nGerekirse yeni bir test kodu kaydedebilmeniz için lütfen testi Corona-Warn-App\'ten silin."</string>
     <!-- XBUT: negative test result : remove the test button -->
-    <string name="submission_test_result_negative_remove_test_button">"Testi Sil"</string>
+    <string name="submission_test_result_negative_remove_test_button">"Testi kaldır"</string>
     <!-- XHED: Page headline for other warnings screen  -->
-    <string name="submission_test_result_positive_steps_warning_others_heading">"Diğer Kullanıcıları Uyarma"</string>
+    <string name="submission_test_result_positive_steps_warning_others_heading">"Diğer Kullanıcıları Uyarın"</string>
     <!-- YTXT: Body text for for other warnings screen-->
     <string name="submission_test_result_positive_steps_warning_others_body">"Rastgele kimliklerinizi paylaşın ve diğer kullanıcıları uyarın.\nHerhangi bir koronavirüs semptomunu ne zaman ilk kez fark ettiğinizi de belirterek diğer kullanıcıların enfeksiyon riskini daha doğru şekilde belirlemeye yardımcı olun."</string>
     <!-- XBUT: positive test result : continue button -->
@@ -952,17 +956,17 @@
     <!-- YTXT: Body text for QR code dispatcher option -->
     <string name="submission_dispatcher_qr_card_text">"Test belgenizin QR kodunu tarayarak testinizi kaydedin."</string>
     <!-- XHED: Dialog headline for dispatcher QR prviacy dialog  -->
-    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Onay"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Kabul Beyanı"</string>
     <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog -->
     <string name="submission_dispatcher_qr_privacy_dialog_body">"\"Kabul Et\" seçeneğine dokunarak Uygulamanın koronavirüs testinizin durumunu sorgulamasına ve Uygulamada görüntülemesine izin verirsiniz. Bu özelliği, QR kod aldıysanız ve test sonucunuzun Uygulamanın sunucu sistemine aktarılmasına onay verdiyseniz kullanabilirsiniz. Testi yapan laboratuvar test sonucunuzu sunucuya kaydettiği anda sonucu Uygulamada görüntüleyebilirsiniz. Ayrıca bildirimleri etkinleştirirseniz Uygulama, kullanmadığınız sırada test sonucunuzun alındığını belirten bir bildirim gönderir. Ancak gizlilik nedenleriyle testin sonucu yalnızca Uygulamada görüntülenecektir. Uygulamada test kaydınızı silerek dilediğiniz zaman bu onayı geri çekebilirsiniz. Onayınızı geri çekmeniz, onayınızı geri çekmeden önce testi işlemenin hukuki niteliğini etkilemeyecektir. Menüde \"Veri Gizliliği\" başlığında daha fazla bilgiye erişebilirsiniz."</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Kabul Et"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Ä°zin Ver"</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Kabul Etme"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Ä°zin Verme"</string>
     <!-- YTXT: Dispatcher text for TAN code option -->
     <string name="submission_dispatcher_card_tan_code">"TAN"</string>
     <!-- YTXT: Body text for TAN code dispatcher option -->
-    <string name="submission_dispatcher_tan_code_card_text">"TAN\'yi manuel olarak girerek testinizi kaydedin."</string>
+    <string name="submission_dispatcher_tan_code_card_text">"TAN\'yi manuel olarak girerek kaydedin."</string>
     <!-- YTXT: Dispatcher text for TELE-TAN option -->
     <string name="submission_dispatcher_card_tan_tele">"TAN Talebi"</string>
     <!-- YTXT: Body text for TELE_TAN dispatcher option -->
@@ -972,7 +976,7 @@
 
     <!-- Submission Positive Other Warning -->
     <!-- XHED: Page title for the positive result additional warning page-->
-    <string name="submission_positive_other_warning_title">"Diğer Kullanıcıları Uyarma"</string>
+    <string name="submission_positive_other_warning_title">"Diğer Kullanıcıları Uyarın"</string>
     <!-- XHED: Page headline for the positive result additional warning page-->
     <string name="submission_positive_other_warning_headline">"Lütfen hepimize yardımcı olun!"</string>
     <!-- YTXT: Body text for the positive result additional warning page-->
@@ -980,7 +984,7 @@
     <!-- XBUT: other warning continue button -->
     <string name="submission_positive_other_warning_button">"Kabul Et"</string>
     <!-- XACT: other warning - illustration description, explanation image -->
-    <string name="submission_positive_other_illustration_description">"Bir cihaz şifrelenmiş pozitif test tanısını sisteme aktarır."</string>
+    <string name="submission_positive_other_illustration_description">"Bir akıllı telefon şifrelenmiş pozitif test tanısını sisteme aktarır."</string>
     <!-- XHED: Title for the interop country list-->
     <string name="submission_interoperability_list_title">"Aşağıdaki ülkeler, ülkeler arası maruz kalma günlüğüne katılmaktadır:"</string>
 
@@ -1046,7 +1050,7 @@
     <!-- XBUT:  symptom initial screen no button -->
     <string name="submission_symptom_negative_button">"Hayır"</string>
     <!-- XBUT:  symptom initial screen no information button -->
-    <string name="submission_symptom_no_information_button">"Bilgi yok"</string>
+    <string name="submission_symptom_no_information_button">"Beyan yok"</string>
     <!-- XBUT:  symptom initial screen continue button -->
     <string name="submission_symptom_further_button">"Sonraki"</string>
 
@@ -1071,7 +1075,7 @@
     <!-- YTXT: Body text for step 2 of contact page-->
     <string name="submission_contact_step_2_body">"Uygulamaya TAN girerek testi kaydedin."</string>
     <!-- YTXT: Body text for operating hours in contact page-->
-    <string name="submission_contact_operating_hours_body">"Diller: \nAlmanca, İngilizce, Türkçe\n\nMesai saatleri:\nPazartesi - Pazar: 24 saat\n\nArama ücretsizdir."</string>
+    <string name="submission_contact_operating_hours_body">"Diller: \nİngilizce, Almanca, Türkçe\n\nMesai saatleri:\nPazartesi - Pazar: 24 saat\n\nArama ücretsizdir."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
     <string name="submission_contact_body_other">"Sağlıkla ilgili tüm sorularınız için lütfen aile hekiminizle veya tıbbi acil servis yardım hattı ile iletişime geçin. Telefon: 116 117."</string>
 
@@ -1096,7 +1100,7 @@
     <!-- XBUT: symptom calendar screen more than 2 weeks button -->
     <string name="submission_symptom_more_two_weeks">"2 haftadan uzun süre önce"</string>
     <!-- XBUT: symptom calendar screen verify button -->
-    <string name="submission_symptom_verify">"Bilgi yok"</string>
+    <string name="submission_symptom_verify">"Beyan yok"</string>
 
     <!-- Submission Status Card -->
     <!-- XHED: Page title for the various submission status: fetching -->
@@ -1201,7 +1205,10 @@
     <string name="errors_google_update_needed">"Corona-Warn-App uygulamanız doğru şekilde yüklendi ancak akıllı telefonunuzun işletim sisteminde \"COVID-19 Maruz Kalma Bildirimleri Sistemi\" yok. Bu, Corona-Warn-App\'i kullanamayacağınız anlamına geliyor. Daha fazla bilgi için lütfen SSS sayfamıza bakın: https://www.coronawarn.app/en/faq/"</string>
     <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day -->
     <string name="errors_google_api_error">"Corona-Warn-App doğru şekilde çalışıyor ancak mevcut risk durumunuzu güncelleyemiyoruz. Maruz kalma günlüğü aktif ve doğru şekilde çalışıyor. Daha fazla bilgi için lütfen SSS sayfamıza bakın: https://www.coronawarn.app/en/faq/"</string>
-
+    <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_title">"Sınıra ulaşıldı"</string>
+    <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. -->
+    <string name="errors_risk_detection_limit_reached_description">"İşletim sisteminizce tanımlanan günlük azami kontrol sayısına ulaştığınız için bugün daha fazla maruz kalma kontrolü yapılamaz. Lütfen risk durumunuzu yarın yeniden kontrol edin."</string>
     <!-- ####################################
                Generic Error Messages
         ###################################### -->
@@ -1248,17 +1255,7 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml
index b396ed0c73d4116aee33669d877b232e7a8b4f9c..09331c944495e4296bb62ebb71f3ec3611eb89e2 100644
--- a/Corona-Warn-App/src/main/res/values/strings.xml
+++ b/Corona-Warn-App/src/main/res/values/strings.xml
@@ -21,12 +21,8 @@
     <!-- NOTR -->
     <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string>
@@ -67,8 +63,6 @@
     <!-- NOTR -->
     <string name="preference_risk_days_explanation_shown"><xliff:g id="preference">"preference_risk_days_explanation_shown"</xliff:g></string>
     <!-- NOTR -->
-    <string name="preference_background_notification"><xliff:g id="preference">"preference_background_notification"</xliff:g></string>
-    <!-- NOTR -->
     <string name="preference_interoperability_selected_country_codes"><xliff:g id="preference">"preference_interoperability_selected_country_codes"</xliff:g></string>
     <!-- NOTR -->
     <string name="preference_interoperability_all_countries_selected">preference_interoperability_all_countries_selected</string>
@@ -117,9 +111,9 @@
     <!-- XTXT: Notification body -->
     <string name="notification_body">"You have new messages from your Corona-Warn-App."</string>
     <!-- XHED: Notification title - Reminder to share a positive test result-->
-    <string name="notification_headline_share_positive_result">"Helfen Sie mit!"</string>
+    <string name="notification_headline_share_positive_result">"You can help!"</string>
     <!-- XTXT: Notification body - Reminder to share a positive test result-->
-    <string name="notification_body_share_positive_result">"Bitte warnen Sie andere und teilen Sie Ihr Testergebnis."</string>
+    <string name="notification_body_share_positive_result">"Please share your test result and warn others."</string>
 
     <!-- ####################################
               App Auto Update
@@ -161,11 +155,9 @@
     <!-- XTXT: risk card- tracing active for 14 out of 14 days -->
     <string name="risk_card_body_saved_days_full">"Exposure logging permanently active"</string>
     <!-- XTXT; risk card - no update done yet -->
-    <string name="risk_card_body_not_yet_fetched">"Exposures have not yet been checked."</string>
+    <string name="risk_card_body_not_yet_fetched">"Encounters have not yet been checked."</string>
     <!-- XTXT: risk card - last successful update -->
     <string name="risk_card_body_time_fetched">"Updated: %1$s"</string>
-    <!-- XTXT: risk card - next update -->
-    <string name="risk_card_body_next_update">"Updated daily"</string>
     <!-- XTXT: risk card - hint to open the app daily -->
     <string name="risk_card_body_open_daily">"Note: Please open the app daily to update your risk status."</string>
     <!-- XBUT: risk card - update risk -->
@@ -196,7 +188,7 @@
     <!-- XHED: risk card - tracing stopped headline, due to no possible calculation -->
     <string name="risk_card_no_calculation_possible_headline">"Exposure logging stopped"</string>
     <!-- XTXT: risk card - last successfully calculated risk level -->
-    <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure logging:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
+    <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure check:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string>
     <!-- XHED: risk card -  outdated risk headline, calculation isn't possible -->
     <string name="risk_card_outdated_risk_headline">"Exposure logging is not possible"</string>
     <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours -->
@@ -204,11 +196,11 @@
     <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours -->
     <string name="risk_card_outdated_manual_risk_body">"Your risk status has not been updated for more than 48 hours. Please update your risk status."</string>
     <!-- XHED: risk card - risk check failed headline, no internet connection -->
-    <string name="risk_card_check_failed_no_internet_headline" />
+    <string name="risk_card_check_failed_no_internet_headline">"Exposure check failed"</string>
     <!-- XTXT: risk card - risk check failed, please check your internet connection -->
-    <string name="risk_card_check_failed_no_internet_body" />
+    <string name="risk_card_check_failed_no_internet_body">"The synchronization of random IDs with the server failed. You can restart the synchronization manually."</string>
     <!-- XTXT: risk card - risk check failed, restart button -->
-    <string name="risk_card_check_failed_no_internet_restart_button" />
+    <string name="risk_card_check_failed_no_internet_restart_button">"Restart"</string>
 
     <!-- ####################################
               Risk Card - Progress
@@ -264,7 +256,7 @@
     <!-- XHED: App overview subtitle for tracing explanation-->
     <string name="main_overview_subtitle_tracing">"Exposure Logging"</string>
     <!-- YTXT: App overview body text about tracing -->
-    <string name="main_overview_body_tracing">"Exposure logging is one of the three central features of the app. When you activate it, encounters with people\'s devices are logged. You don\'t have to do anything else."</string>
+    <string name="main_overview_body_tracing">"Exposure logging is one of three central features of the app. Once you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string>
     <!-- XHED: App overview subtitle for risk explanation -->
     <string name="main_overview_subtitle_risk">"Risk of Infection"</string>
     <!-- YTXT: App overview body text about risk levels -->
@@ -374,9 +366,9 @@
         <item quantity="many">"You have an increased risk of infection because you were last exposed %1$s days ago over a longer period of time and at close proximity to at least one person diagnosed with COVID-19."</item>
     </plurals>
     <!-- YTXT: risk details - risk calculation explanation -->
-    <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else."</string>
+    <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else."</string>
     <!-- YTXT: risk details - risk calculation explanation for increased risk -->
-    <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string>
+    <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string>
     <!-- NOTR -->
     <string name="risk_details_button_update">@string/risk_card_button_update</string>
     <!-- NOTR -->
@@ -432,7 +424,7 @@
     <!-- XHED: onboarding(together) - two/three line headline under an illustration -->
     <string name="onboarding_subtitle">"More protection for you and for us all. By using the Corona-Warn-App we can break infection chains much quicker."</string>
     <!-- YTXT: onboarding(together) - inform about the app -->
-    <string name="onboarding_body">"Turn your device into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string>
+    <string name="onboarding_body">"Turn your smartphone into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string>
     <!-- YTXT: onboarding(together) - explain application -->
     <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their devices, whereby no personal data whatsoever is accessed."</string>
     <!-- XACT: onboarding(together) - illustraction description, header image -->
@@ -469,7 +461,7 @@
     <!-- XBUT: onboarding(tracing) - negative button (right) -->
     <string name="onboarding_tracing_dialog_button_negative">"Back"</string>
     <!-- XACT: onboarding(tracing) - dialog about background jobs header text -->
-    <string name="onboarding_background_fetch_dialog_headline">"Background updates deactivated"</string>
+    <string name="onboarding_background_fetch_dialog_headline">"Background app refresh deactivated"</string>
     <!-- YMSI: onboarding(tracing) - dialog about background jobs -->
     <string name="onboarding_background_fetch_dialog_body">"You have deactivated background updates for the Corona-Warn-App. Please activate background updates to use automatic exposure logging. If you do not activate background updates, you can only start exposure logging manually in the app. You can activate background updates for the app in your device settings."</string>
     <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings -->
@@ -491,7 +483,7 @@
     <!-- XBUT: onboarding(tracing) - dialog about manual checking button -->
     <string name="onboarding_manual_required_dialog_button">"OK"</string>
     <!-- XACT: onboarding(tracing) - illustraction description, header image -->
-    <string name="onboarding_tracing_illustration_description">"Three people have activated exposure logging on their devices, which will log their encounters with each other."</string>
+    <string name="onboarding_tracing_illustration_description">"Three persons have activated exposure logging on their smartphones, which will log their encounters with each other."</string>
     <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline -->
     <string name="onboarding_tracing_location_headline">"Allow location access"</string>
     <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text -->
@@ -688,7 +680,7 @@
     <!-- YTXT: Body text for about information page -->
     <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) is Germany’s federal public health body. The RKI publishes the Corona-Warn-App on behalf of the Federal Government. The app is intended as a digital complement to public health measures already introduced: social distancing, hygiene, and face masks."</string>
     <!-- YTXT: Body text for about information page -->
-    <string name="information_about_body">"People who use the app help to trace and break chains of infection. The app saves encounters with other people locally on your device. You are notified if you have encountered people who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string>
+    <string name="information_about_body">"Whoever uses the app helps to trace and break chains of infection. The app saves encounters with other persons locally on your smartphone. You are notified if you have encountered persons who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string>
     <!-- XACT: describes illustration -->
     <string name="information_about_illustration_description">"A group of persons use their smartphones around town."</string>
     <!-- XHED: Page title for privacy information page, also menu item / button text -->
@@ -720,9 +712,9 @@
     <!-- XTXT: Body text for technical contact and hotline information page -->
     <string name="information_contact_body_phone">"Our customer service is here to help."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_open">"Languages: German, English, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string>
+    <string name="information_contact_body_open">"Languages: English, German, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string>
+    <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string>
     <!-- XACT: describes illustration -->
     <string name="information_contact_illustration_description">"A man wears a headset while making a phone call."</string>
     <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website -->
@@ -805,9 +797,9 @@
     <string name="submission_error_dialog_web_tan_invalid_button_positive">"Back"</string>
 
     <!-- XHED: Dialog title for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_title">"Test has errors"</string>
+    <string name="submission_error_dialog_web_tan_redeemed_title">"QR code no longer valid"</string>
     <!-- XMSG: Dialog body for submission tan redeemed -->
-    <string name="submission_error_dialog_web_tan_redeemed_body">"There was a problem evaluating your test. Your QR code has already expired."</string>
+    <string name="submission_error_dialog_web_tan_redeemed_body">"Your test is more than 21 days old and can no longer be registered in the app. If you are tested again in future, please make sure to scan the QR code as soon as you get it."</string>
     <!-- XBUT: Positive button for submission tan redeemed -->
     <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string>
 
@@ -919,15 +911,15 @@
     <!-- XBUT: test result pending : refresh button -->
     <string name="submission_test_result_pending_refresh_button">"Update"</string>
     <!-- XBUT: test result pending : remove the test button -->
-    <string name="submission_test_result_pending_remove_test_button">"Delete Test"</string>
+    <string name="submission_test_result_pending_remove_test_button">"Remove test"</string>
     <!-- XHED: Page headline for negative test result next steps  -->
     <string name="submission_test_result_negative_steps_negative_heading">"Your Test Result"</string>
     <!-- YTXT: Body text for next steps section of test negative result -->
     <string name="submission_test_result_negative_steps_negative_body">"The laboratory result indicates no verification that you have coronavirus SARS-CoV-2.\n\nPlease delete the test from the Corona-Warn-App, so that you can save a new test code here if necessary."</string>
     <!-- XBUT: negative test result : remove the test button -->
-    <string name="submission_test_result_negative_remove_test_button">"Delete Test"</string>
+    <string name="submission_test_result_negative_remove_test_button">"Remove Test"</string>
     <!-- XHED: Page headline for other warnings screen  -->
-    <string name="submission_test_result_positive_steps_warning_others_heading">"Warning Others"</string>
+    <string name="submission_test_result_positive_steps_warning_others_heading">"Warn Others"</string>
     <!-- YTXT: Body text for for other warnings screen-->
     <string name="submission_test_result_positive_steps_warning_others_body">"Share your random IDs and warn others.\nHelp determine the risk of infection for others more accurately by also indicating when you first noticed any coronavirus symptoms."</string>
     <!-- XBUT: positive test result : continue button -->
@@ -1006,13 +998,13 @@
     <!-- YTXT: Body text for QR code dispatcher option -->
     <string name="submission_dispatcher_qr_card_text">"Register your test by scanning the QR code of your test document."</string>
     <!-- XHED: Dialog headline for dispatcher QR prviacy dialog  -->
-    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Consent"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_headline">"Declaration of Consent"</string>
     <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog -->
     <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Accept”, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacy”."</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Accept"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Allow"</string>
     <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) -->
-    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Accept"</string>
+    <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Allow"</string>
     <!-- YTXT: Dispatcher text for TAN code option -->
     <string name="submission_dispatcher_card_tan_code">"TAN"</string>
     <!-- YTXT: Body text for TAN code dispatcher option -->
@@ -1026,7 +1018,7 @@
 
     <!-- Submission Positive Other Warning -->
     <!-- XHED: Page title for the positive result additional warning page-->
-    <string name="submission_positive_other_warning_title">"Warning Others"</string>
+    <string name="submission_positive_other_warning_title">"Warn Others"</string>
     <!-- XHED: Page headline for the positive result additional warning page-->
     <string name="submission_positive_other_warning_headline">"Please help all of us!"</string>
     <!-- YTXT: Body text for the positive result additional warning page-->
@@ -1034,7 +1026,7 @@
     <!-- XBUT: other warning continue button -->
     <string name="submission_positive_other_warning_button">"Accept"</string>
     <!-- XACT: other warning - illustration description, explanation image -->
-    <string name="submission_positive_other_illustration_description">"A device transmits an encrypted positive test diagnosis to the system."</string>
+    <string name="submission_positive_other_illustration_description">"A smartphone transmits an encrypted positive test diagnosis to the system."</string>
     <!-- XHED: Title for the interop country list-->
     <string name="submission_interoperability_list_title">"The following countries currently participate in transnational exposure logging:"</string>
 
@@ -1100,7 +1092,7 @@
     <!-- XBUT:  symptom initial screen no button -->
     <string name="submission_symptom_negative_button">"No"</string>
     <!-- XBUT:  symptom initial screen no information button -->
-    <string name="submission_symptom_no_information_button">"No answer"</string>
+    <string name="submission_symptom_no_information_button">"No statement"</string>
     <!-- XBUT:  symptom initial screen continue button -->
     <string name="submission_symptom_further_button">"Next"</string>
 
@@ -1125,9 +1117,9 @@
     <!-- YTXT: Body text for step 2 of contact page-->
     <string name="submission_contact_step_2_body">"Register the test by entering the TAN in the app."</string>
     <!-- YTXT: Body text for operating hours in contact page-->
-    <string name="submission_contact_operating_hours_body">"Languages: \nGerman, English, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string>
+    <string name="submission_contact_operating_hours_body">"Languages: \nEnglish, German, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string>
     <!-- YTXT: Body text for technical contact and hotline information page -->
-    <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string>
+    <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string>
 
 
     <!-- XACT: Submission contact page title -->
@@ -1151,7 +1143,7 @@
     <!-- XBUT: symptom calendar screen more than 2 weeks button -->
     <string name="submission_symptom_more_two_weeks">"More than 2 weeks ago"</string>
     <!-- XBUT: symptom calendar screen verify button -->
-    <string name="submission_symptom_verify">"No answer"</string>
+    <string name="submission_symptom_verify">"No statement"</string>
 
     <!-- Submission Status Card -->
     <!-- XHED: Page title for the various submission status: fetching -->
@@ -1212,7 +1204,7 @@
     <!-- YTXT: invalid status text -->
     <string name="test_result_card_status_invalid">"Evaluation is not possible"</string>
     <!-- YTXT: pending status text -->
-    <string name="test_result_card_status_pending">"Your result is not available yet"</string>
+    <string name="test_result_card_status_pending">"Your result is not yet available"</string>
     <!-- XHED: Title for further info of test result negative -->
     <string name="test_result_card_negative_further_info_title">"Other information:"</string>
     <!-- YTXT: Content for further info of test result negative -->
@@ -1257,9 +1249,9 @@
     <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day -->
     <string name="errors_google_api_error">"The Corona-Warn-App is running correctly, but we cannot update your current risk status. Exposure logging remains active and is working correctly. For further information, please see our FAQ page: https://www.coronawarn.app/en/faq/"</string>
     <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. -->
-    <string name="errors_risk_detection_limit_reached_title" />
+    <string name="errors_risk_detection_limit_reached_title">"Limit already reached"</string>
     <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. -->
-    <string name="errors_risk_detection_limit_reached_description" />
+    <string name="errors_risk_detection_limit_reached_description">"No more exposure checks possible today, as you have reached the maximum number of checks per day defined by your operating system. Please check your risik status again tomorrow."</string>
 
     <!-- ####################################
                Generic Error Messages
@@ -1307,25 +1299,13 @@
     <!-- NOTR -->
     <string name="test_api_button_check_exposure">"Check Exposure Summary"</string>
     <!-- NOTR -->
-    <string name="test_api_exposure_summary_headline">"Exposure summary"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string>
-    <!-- NOTR -->
-    <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string>
+    <string name="test_api_exposure_summary_headline">"Exposure Windows"</string>
     <!-- NOTR -->
     <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string>
     <!-- NOTR -->
     <string name="test_api_body_other_keys">"Other key"</string>
     <!-- NOTR -->
     <string name="test_api_calculate_risk_level">"Calculate Risk Level"</string>
-    <!-- NOTR -->
-    <string name="test_api_switch_background_notifications">"Background Notifications"</string>
 
     <!-- XHED: Country Entry for Austria -->
     <string name="country_name_at">"Austria"</string>
@@ -1418,7 +1398,7 @@
     <!-- YMSG: Onboarding tracing step second section in interoperability after the title -->
     <string name="interoperability_onboarding_second_section">"When a user submits their random IDs to the exchange server jointly operated by the participating countries, users of the official corona apps in all these countries can be warned of potential exposure."</string>
     <!-- YMSG: Onboarding tracing step third section in interoperability after the title. -->
-    <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, and nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string>
+    <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string>
     <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. -->
     <string name="interoperability_onboarding_list_title">"The following countries currently participate:"</string>
     <!-- XTXT: Description of the expanded terms in delta interopoerability screen part 1 -->
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigModuleTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigModuleTest.kt
similarity index 92%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigModuleTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigModuleTest.kt
index eae101cd8e6281f82ba2658056819e564473fbe0..fdae6dd05d6b673b95c5b49a3a5edf39d76c9124 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigModuleTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigModuleTest.kt
@@ -1,7 +1,6 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig
 
 import android.content.Context
-import de.rki.coronawarnapp.appconfig.AppConfigModule
 import io.kotest.assertions.throwables.shouldNotThrowAny
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt
index 8f5817cf7441619cc98c5d9296f7bafa0bbf8718..e5b901e0c00837569a2abf24179a8772019a3067 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt
@@ -1,5 +1,7 @@
 package de.rki.coronawarnapp.appconfig
 
+import de.rki.coronawarnapp.appconfig.internal.AppConfigSource
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
 import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
@@ -25,7 +27,7 @@ import java.io.File
 
 class AppConfigProviderTest : BaseIOTest() {
 
-    @MockK lateinit var source: AppConfigSource
+    @MockK lateinit var appConfigSource: AppConfigSource
     @MockK lateinit var configData: ConfigData
     @MockK lateinit var timeStamper: TimeStamper
 
@@ -39,15 +41,16 @@ class AppConfigProviderTest : BaseIOTest() {
         testDir.mkdirs()
         testDir.exists() shouldBe true
 
-        testConfigDownload = DefaultConfigData(
+        testConfigDownload = ConfigDataContainer(
             serverTime = Instant.parse("2020-11-03T05:35:16.000Z"),
             localOffset = Duration.ZERO,
             mappedConfig = configData,
             identifier = "identifier",
-            configType = ConfigData.Type.FROM_SERVER
+            configType = ConfigData.Type.FROM_SERVER,
+            cacheValidity = Duration.standardMinutes(5)
         )
-        coEvery { source.clear() } just Runs
-        coEvery { source.retrieveConfig() } returns testConfigDownload
+        coEvery { appConfigSource.clear() } just Runs
+        coEvery { appConfigSource.getConfigData() } returns testConfigDownload
 
         every { timeStamper.nowUTC } returns Instant.parse("2020-11-03T05:35:16.000Z")
     }
@@ -59,7 +62,7 @@ class AppConfigProviderTest : BaseIOTest() {
     }
 
     private fun createInstance(scope: CoroutineScope) = AppConfigProvider(
-        source = source,
+        appConfigSource = appConfigSource,
         dispatcherProvider = TestDispatcherProvider,
         scope = scope
     )
@@ -67,13 +70,14 @@ class AppConfigProviderTest : BaseIOTest() {
     @Test
     fun `appConfig is observable`() = runBlockingTest2(ignoreActive = true) {
         var counter = 0
-        coEvery { source.retrieveConfig() } answers {
-            DefaultConfigData(
+        coEvery { appConfigSource.getConfigData() } answers {
+            ConfigDataContainer(
                 serverTime = Instant.parse("2020-11-03T05:35:16.000Z"),
                 localOffset = Duration.ZERO,
                 mappedConfig = configData,
                 identifier = "${++counter}",
-                configType = ConfigData.Type.FROM_SERVER
+                configType = ConfigData.Type.FROM_SERVER,
+                cacheValidity = Duration.standardMinutes(5)
             )
         }
 
@@ -90,19 +94,19 @@ class AppConfigProviderTest : BaseIOTest() {
         advanceUntilIdle()
 
         coVerifySequence {
-            source.retrieveConfig()
-            source.retrieveConfig()
-            source.retrieveConfig()
-            source.retrieveConfig()
+            appConfigSource.getConfigData()
+            appConfigSource.getConfigData()
+            appConfigSource.getConfigData()
+            appConfigSource.getConfigData()
         }
     }
 
     @Test
-    fun `appConfig uses WHILE_SUBSCRIBED mode`() = runBlockingTest2(ignoreActive = true) {
+    fun `appConfig uses LAZILY mode`() = runBlockingTest2(ignoreActive = true) {
         val instance = createInstance(this)
 
         val testCollector1 = instance.currentConfig.test(startOnScope = this)
-        coVerify(exactly = 1) { source.retrieveConfig() }
+        coVerify(exactly = 1) { appConfigSource.getConfigData() }
 
         // Was still active
         val testCollector2 = instance.currentConfig.test(startOnScope = this)
@@ -114,7 +118,7 @@ class AppConfigProviderTest : BaseIOTest() {
         advanceUntilIdle()
         testCollector3.cancel()
 
-        coVerify(exactly = 1) { source.retrieveConfig() }
+        coVerify(exactly = 1) { appConfigSource.getConfigData() }
         testCollector1.cancel() // Last subscriber
         advanceUntilIdle()
 
@@ -123,7 +127,7 @@ class AppConfigProviderTest : BaseIOTest() {
         advanceUntilIdle()
         testCollector4.cancel()
 
-        coVerify(exactly = 2) { source.retrieveConfig() }
+        coVerify(exactly = 1) { appConfigSource.getConfigData() }
     }
 
     @Test
@@ -133,7 +137,7 @@ class AppConfigProviderTest : BaseIOTest() {
         instance.clear()
 
         coVerifySequence {
-            source.clear()
+            appConfigSource.clear()
         }
     }
 }
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
new file mode 100644
index 0000000000000000000000000000000000000000..e06b40b3ed38a7e8d3a194f27ad2a003749bd293
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt
@@ -0,0 +1,104 @@
+package de.rki.coronawarnapp.appconfig
+
+import de.rki.coronawarnapp.risk.RiskLevelData
+import de.rki.coronawarnapp.task.TaskController
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+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
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ConfigChangeDetectorTest : BaseTest() {
+
+    @MockK lateinit var appConfigProvider: AppConfigProvider
+    @MockK lateinit var taskController: TaskController
+    @MockK lateinit var riskLevelData: RiskLevelData
+
+    private val currentConfigFake = MutableStateFlow(mockConfigId("initial"))
+
+    @BeforeEach
+    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
+    }
+
+    private fun mockConfigId(id: String): ConfigData {
+        return mockk<ConfigData>().apply {
+            every { identifier } returns id
+        }
+    }
+
+    private fun createInstance() = ConfigChangeDetector(
+        appConfigProvider = appConfigProvider,
+        taskController = taskController,
+        appScope = TestCoroutineScope(),
+        riskLevelData = riskLevelData
+    )
+
+    @Test
+    fun `new identifier without previous one is ignored`() {
+
+        every { riskLevelData.lastUsedConfigIdentifier } returns null
+
+        createInstance().launch()
+
+        verify(exactly = 0) {
+            taskController.submit(any())
+            ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel()
+        }
+    }
+
+    @Test
+    fun `new identifier results in new risk level calculation`() {
+        every { riskLevelData.lastUsedConfigIdentifier } returns "I'm a new identifier"
+
+        createInstance().launch()
+
+        verifySequence {
+            ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel()
+            taskController.submit(any())
+        }
+    }
+
+    @Test
+    fun `same idetifier results in no op`() {
+        every { riskLevelData.lastUsedConfigIdentifier } returns "initial"
+
+        createInstance().launch()
+
+        verify(exactly = 0) {
+            taskController.submit(any())
+            ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel()
+        }
+    }
+
+    @Test
+    fun `new emissions keep triggering the check`() {
+        every { riskLevelData.lastUsedConfigIdentifier } returns "initial"
+
+        createInstance().launch()
+        currentConfigFake.value = mockConfigId("Straw")
+        currentConfigFake.value = mockConfigId("berry")
+
+        verifySequence {
+            ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel()
+            taskController.submit(any())
+            ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel()
+            taskController.submit(any())
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSourceTest.kt
deleted file mode 100644
index 2ddfad5332b90522c39a2f0848db1b3eb6342463..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSourceTest.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-package de.rki.coronawarnapp.appconfig.download
-
-import android.content.Context
-import android.content.res.AssetManager
-import de.rki.coronawarnapp.util.HashExtensions.toSHA256
-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.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import testhelpers.BaseIOTest
-import java.io.File
-
-class DefaultAppConfigSourceTest : BaseIOTest() {
-    @MockK private lateinit var context: Context
-    @MockK private lateinit var assetManager: AssetManager
-
-    private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
-    private val configFile = File(testDir, "default_app_config.bin")
-    private val checksumFile = File(testDir, "default_app_config.sha256")
-
-    @BeforeEach
-    fun setup() {
-        MockKAnnotations.init(this)
-
-        every { context.assets } returns assetManager
-
-        every { assetManager.open("default_app_config.bin") } answers { configFile.inputStream() }
-        every { assetManager.open("default_app_config.sha256") } answers { checksumFile.inputStream() }
-
-        testDir.mkdirs()
-        testDir.exists() shouldBe true
-    }
-
-    @AfterEach
-    fun teardown() {
-        clearAllMocks()
-        testDir.deleteRecursively()
-    }
-
-    private fun createInstance() = DefaultAppConfigSource(context = context)
-
-    @Test
-    fun `config loaded from asset`() {
-        val testData = "The Cake Is A Lie"
-        configFile.writeText(testData)
-        checksumFile.writeText(testData.toSHA256())
-
-        val instance = createInstance()
-        instance.getRawDefaultConfig() shouldBe testData.toByteArray()
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSourceTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8a87d88edf92f9c547a5a5f52b192e5c5d0752d2
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSourceTest.kt
@@ -0,0 +1,137 @@
+package de.rki.coronawarnapp.appconfig.internal
+
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.sources.fallback.DefaultAppConfigSource
+import de.rki.coronawarnapp.appconfig.sources.local.LocalAppConfigSource
+import de.rki.coronawarnapp.appconfig.sources.remote.RemoteAppConfigSource
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerifySequence
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Duration
+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 AppConfigSourceTest : BaseTest() {
+
+    @MockK lateinit var remoteSource: RemoteAppConfigSource
+    @MockK lateinit var localSource: LocalAppConfigSource
+    @MockK lateinit var defaultSource: DefaultAppConfigSource
+    @MockK lateinit var timeStamper: TimeStamper
+
+    private val remoteConfig = ConfigDataContainer(
+        serverTime = Instant.EPOCH,
+        localOffset = Duration.standardHours(1),
+        mappedConfig = mockk(),
+        configType = ConfigData.Type.FROM_SERVER,
+        identifier = "remoteetag",
+        cacheValidity = Duration.standardSeconds(42)
+    )
+
+    private val localConfig = ConfigDataContainer(
+        serverTime = Instant.EPOCH,
+        localOffset = Duration.standardHours(1),
+        mappedConfig = mockk(),
+        configType = ConfigData.Type.LAST_RETRIEVED,
+        identifier = "localetag",
+        cacheValidity = Duration.standardSeconds(300)
+    )
+
+    private val defaultConfig = ConfigDataContainer(
+        serverTime = Instant.EPOCH,
+        localOffset = Duration.standardHours(1),
+        mappedConfig = mockk(),
+        configType = ConfigData.Type.LOCAL_DEFAULT,
+        identifier = "fallback.local",
+        cacheValidity = Duration.ZERO
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        coEvery { remoteSource.getConfigData() } returns remoteConfig
+        coEvery { localSource.getConfigData() } returns localConfig
+        coEvery { defaultSource.getConfigData() } returns defaultConfig
+
+        every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1))
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun createInstance() = AppConfigSource(
+        remoteAppConfigSource = remoteSource,
+        localAppConfigSource = localSource,
+        defaultAppConfigSource = defaultSource,
+        timeStamper = timeStamper
+    )
+
+    @Test
+    fun `local config is used if available and valid`() = runBlockingTest {
+        val instance = createInstance()
+        instance.getConfigData() shouldBe localConfig
+
+        coVerifySequence {
+            localSource.getConfigData()
+            timeStamper.nowUTC
+        }
+    }
+
+    @Test
+    fun `remote config is used if local config is not valid`() = runBlockingTest {
+        every { timeStamper.nowUTC } returns Instant.EPOCH
+            .plus(Duration.standardHours(1))
+            .plus(Duration.standardSeconds(301)) // Local config has 300 seconds validity
+
+        val instance = createInstance()
+        instance.getConfigData() shouldBe remoteConfig
+
+        coVerifySequence {
+            localSource.getConfigData()
+            timeStamper.nowUTC
+            remoteSource.getConfigData()
+        }
+    }
+
+    @Test
+    fun `local config is used despite being invalid if remote config is unavailable`() = runBlockingTest {
+        every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(2))
+        coEvery { remoteSource.getConfigData() } returns null
+
+        val instance = createInstance()
+        instance.getConfigData() shouldBe localConfig
+
+        coVerifySequence {
+            localSource.getConfigData()
+            timeStamper.nowUTC
+            remoteSource.getConfigData()
+        }
+    }
+
+    @Test
+    fun `default config is used if remote and local are unavailable`() = runBlockingTest {
+        coEvery { remoteSource.getConfigData() } returns null
+        coEvery { localSource.getConfigData() } returns null
+
+        val instance = createInstance()
+        instance.getConfigData() shouldBe defaultConfig
+
+        coVerifySequence {
+            localSource.getConfigData()
+            remoteSource.getConfigData()
+            defaultSource.getConfigData()
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt
index 2ef853f468f8e5b58bf7db38802a617b5c0d76b5..edebc20fd1c278450405d3ca202bd8ebd5721ab9 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt
@@ -1,6 +1,6 @@
 package de.rki.coronawarnapp.appconfig.mapping
 
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
 import io.kotest.matchers.shouldBe
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
@@ -11,11 +11,12 @@ class CWAConfigMapperTest : BaseTest() {
 
     @Test
     fun `simple creation`() {
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
             .addAllSupportedCountries(listOf("DE", "NL"))
             .build()
         createInstance().map(rawConfig).apply {
-            this.appVersion shouldBe rawConfig.appVersion
+            this.latestVersionCode shouldBe rawConfig.latestVersionCode
+            this.minVersionCode shouldBe rawConfig.minVersionCode
             this.supportedCountries shouldBe listOf("DE", "NL")
         }
     }
@@ -23,11 +24,12 @@ class CWAConfigMapperTest : BaseTest() {
     @Test
     fun `invalid supported countries are filtered out`() {
         // Could happen due to protobuf scheme missmatch
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
             .addAllSupportedCountries(listOf("plausible deniability"))
             .build()
         createInstance().map(rawConfig).apply {
-            this.appVersion shouldBe rawConfig.appVersion
+            this.latestVersionCode shouldBe rawConfig.latestVersionCode
+            this.minVersionCode shouldBe rawConfig.minVersionCode
             this.supportedCountries shouldBe emptyList()
         }
     }
@@ -35,10 +37,11 @@ class CWAConfigMapperTest : BaseTest() {
     @Test
     fun `if supportedCountryList is empty, we do not insert DE as fallback`() {
         // Because the UI requires this to detect when to show alternative UI elements
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
             .build()
         createInstance().map(rawConfig).apply {
-            this.appVersion shouldBe rawConfig.appVersion
+            this.latestVersionCode shouldBe rawConfig.latestVersionCode
+            this.minVersionCode shouldBe rawConfig.minVersionCode
             this.supportedCountries shouldBe emptyList()
         }
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
index 18ee07f72f83cfdbfccae776f6df5edc21a23083..22f65553bf124132ecd7df87bc67b821afbbbc2e 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt
@@ -2,8 +2,8 @@ package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.appconfig.CWAConfig
 import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig
+import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig
 import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
-import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
 import io.mockk.MockKAnnotations
 import io.mockk.clearAllMocks
 import io.mockk.every
@@ -20,7 +20,7 @@ class ConfigParserTest : BaseTest() {
     @MockK lateinit var cwaConfigMapper: CWAConfig.Mapper
     @MockK lateinit var keyDownloadConfigMapper: KeyDownloadConfig.Mapper
     @MockK lateinit var exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper
-    @MockK lateinit var riskCalculationConfigMapper: RiskCalculationConfig.Mapper
+    @MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper
 
     @BeforeEach
     fun setup() {
@@ -29,7 +29,7 @@ class ConfigParserTest : BaseTest() {
         every { cwaConfigMapper.map(any()) } returns mockk()
         every { keyDownloadConfigMapper.map(any()) } returns mockk()
         every { exposureDetectionConfigMapper.map(any()) } returns mockk()
-        every { riskCalculationConfigMapper.map(any()) } returns mockk()
+        every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk()
     }
 
     @AfterEach
@@ -41,7 +41,7 @@ class ConfigParserTest : BaseTest() {
         cwaConfigMapper = cwaConfigMapper,
         keyDownloadConfigMapper = keyDownloadConfigMapper,
         exposureDetectionConfigMapper = exposureDetectionConfigMapper,
-        riskCalculationConfigMapper = riskCalculationConfigMapper
+        exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper
     )
 
     @Test
@@ -52,19 +52,28 @@ class ConfigParserTest : BaseTest() {
                 cwaConfigMapper.map(any())
                 keyDownloadConfigMapper.map(any())
                 exposureDetectionConfigMapper.map(any())
-                riskCalculationConfigMapper.map(any())
+                exposureWindowRiskCalculationConfigMapper.map(any())
             }
         }
     }
 
     companion object {
         private val APPCONFIG_RAW = (
-            "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" +
-                "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" +
-                "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" +
-                "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" +
-                "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" +
-                "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804"
+            "081f101f1a0e0a0c0a0872657365727665641001220244452a061" +
+                "8c20320e003320508061084073ad4010a0d0a0b1900000000004" +
+                "0524020010a0d120b190000000000002440200112140a1209000" +
+                "000000000f03f1900000000000000401a160a0b1900000000008" +
+                "04b40200111000000000000f03f1a1f0a14090000000000804b4" +
+                "0190000000000804f40200111000000000000e03f220f0a0b190" +
+                "000000000002e402001100122160a12090000000000002e40190" +
+                "00000008087c34010022a0f0a0b190000000000002e402001100" +
+                "12a160a12090000000000002e4019000000008087c3401002320" +
+                "a10041804200328023001399a9999999999c93f420c0a0408011" +
+                "0010a04080210024a750a031e32461220000000000000f03f000" +
+                "000000000f03f000000000000f03f000000000000f03f220b080" +
+                "111000000000000f03f220b080211000000000000f03f320b080" +
+                "111000000000000f03f320b080211000000000000f03f320b080" +
+                "311000000000000f03f320b080411000000000000f03f"
             ).decodeHex()
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt
index 2d0ab66e16af911165826ea1d14c99e651483b62..a048e1468ab76c1ed7717da48d8f423b4cca0a35 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt
@@ -1,9 +1,10 @@
 package de.rki.coronawarnapp.appconfig.mapping
 
 import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import de.rki.coronawarnapp.server.protocols.internal.KeyDownloadParameters
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.KeyDownloadParameters
 import io.kotest.matchers.shouldBe
+import org.joda.time.Duration
 import org.joda.time.LocalDate
 import org.joda.time.LocalTime
 import org.junit.jupiter.api.Test
@@ -22,8 +23,8 @@ class DownloadConfigMapperTest : BaseTest() {
             }.let { addRevokedDayPackages(it) }
         }
 
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setAndroidKeyDownloadParameters(builder)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .setKeyDownloadParameters(builder)
             .build()
 
         createInstance().map(rawConfig).apply {
@@ -46,8 +47,8 @@ class DownloadConfigMapperTest : BaseTest() {
             }.let { addRevokedHourPackages(it) }
         }
 
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setAndroidKeyDownloadParameters(builder)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .setKeyDownloadParameters(builder)
             .build()
 
         createInstance().map(rawConfig).apply {
@@ -59,4 +60,17 @@ class DownloadConfigMapperTest : BaseTest() {
             }
         }
     }
+
+    @Test
+    fun `if the protobuf data structures are null we return defaults`() {
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .build()
+
+        createInstance().map(rawConfig).apply {
+            revokedDayPackages shouldBe emptyList()
+            revokedHourPackages shouldBe emptyList()
+            overallDownloadTimeout shouldBe Duration.standardMinutes(8)
+            individualDownloadTimeout shouldBe Duration.standardSeconds(60)
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt
index 7812c23aba103ae8b16d0b5cbb9d799764fb3776..f39e4f9dee0a89f0d4d073cca27b76c99cabfe1b 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.appconfig.mapping
 
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import de.rki.coronawarnapp.server.protocols.internal.ExposureDetectionParameters.ExposureDetectionParametersAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
+import de.rki.coronawarnapp.server.protocols.internal.v2.ExposureDetectionParameters.ExposureDetectionParametersAndroid
 import io.kotest.matchers.shouldBe
 import org.joda.time.Duration
 import org.junit.jupiter.api.Test
@@ -13,21 +13,18 @@ class ExposureDetectionConfigMapperTest : BaseTest() {
 
     @Test
     fun `simple creation`() {
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setMinRiskScore(1)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
             .build()
         createInstance().map(rawConfig).apply {
-            exposureDetectionConfiguration shouldBe rawConfig.mapRiskScoreToExposureConfiguration()
-            exposureDetectionParameters shouldBe rawConfig.androidExposureDetectionParameters
+            exposureDetectionParameters shouldBe null
         }
     }
 
     @Test
     fun `detection interval 0 defaults to almost infinite delay`() {
         val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder()
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setMinRiskScore(1)
-            .setAndroidExposureDetectionParameters(exposureDetectionParameters)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .setExposureDetectionParameters(exposureDetectionParameters)
             .build()
         createInstance().map(rawConfig).apply {
             minTimeBetweenDetections shouldBe Duration.standardDays(99)
@@ -40,9 +37,8 @@ class ExposureDetectionConfigMapperTest : BaseTest() {
         val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder().apply {
             maxExposureDetectionsPerInterval = 3
         }
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setMinRiskScore(1)
-            .setAndroidExposureDetectionParameters(exposureDetectionParameters)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .setExposureDetectionParameters(exposureDetectionParameters)
             .build()
         createInstance().map(rawConfig).apply {
             minTimeBetweenDetections shouldBe Duration.standardHours(24 / 3)
@@ -55,9 +51,8 @@ class ExposureDetectionConfigMapperTest : BaseTest() {
         val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder().apply {
             overallTimeoutInSeconds = 10 * 60
         }
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setMinRiskScore(1)
-            .setAndroidExposureDetectionParameters(exposureDetectionParameters)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .setExposureDetectionParameters(exposureDetectionParameters)
             .build()
         createInstance().map(rawConfig).apply {
             overallDetectionTimeout shouldBe Duration.standardMinutes(10)
@@ -69,12 +64,22 @@ class ExposureDetectionConfigMapperTest : BaseTest() {
         val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder().apply {
             overallTimeoutInSeconds = 0
         }
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .setMinRiskScore(1)
-            .setAndroidExposureDetectionParameters(exposureDetectionParameters)
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .setExposureDetectionParameters(exposureDetectionParameters)
             .build()
         createInstance().map(rawConfig).apply {
             overallDetectionTimeout shouldBe Duration.standardMinutes(15)
         }
     }
+
+    @Test
+    fun `if protobuf is missing the datastructure we return defaults`() {
+        val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
+            .build()
+        createInstance().map(rawConfig).apply {
+            overallDetectionTimeout shouldBe Duration.standardMinutes(15)
+            minTimeBetweenDetections shouldBe Duration.standardHours(24 / 6)
+            maxExposureDetectionsPerUTCDay shouldBe 6
+        }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapperTest.kt
deleted file mode 100644
index e0cf0e3c09a213d3f214cb292efca309a4824e67..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapperTest.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.rki.coronawarnapp.appconfig.mapping
-
-import de.rki.coronawarnapp.server.protocols.internal.AppConfig
-import io.kotest.matchers.shouldBe
-import org.junit.jupiter.api.Test
-import testhelpers.BaseTest
-
-class RiskCalculationConfigMapperTest : BaseTest() {
-
-    private fun createInstance() = RiskCalculationConfigMapper()
-
-    @Test
-    fun `simple creation`() {
-        val rawConfig = AppConfig.ApplicationConfiguration.newBuilder()
-            .build()
-        createInstance().map(rawConfig).apply {
-            this.attenuationDuration shouldBe rawConfig.attenuationDuration
-            this.minRiskScore shouldBe rawConfig.minRiskScore
-            this.riskScoreClasses shouldBe rawConfig.riskScoreClasses
-        }
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSanityCheck.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
similarity index 97%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSanityCheck.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
index 9b52302936bc78f31632bf44598fd3f701b9bb82..c9459763b063371cb4d0630ac7b7ff0b483dd090 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSanityCheck.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.fallback
 
 import android.content.Context
 import android.os.Build
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9aaeca2fdb1435ce41d157ef3b1e97b4e399f86e
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt
@@ -0,0 +1,103 @@
+package de.rki.coronawarnapp.appconfig.sources.fallback
+
+import android.content.Context
+import android.content.res.AssetManager
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
+import io.kotest.assertions.throwables.shouldThrowAny
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.test.runBlockingTest
+import okio.ByteString.Companion.decodeHex
+import org.joda.time.Duration
+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.BaseIOTest
+import java.io.File
+
+class DefaultAppConfigSourceTest : BaseIOTest() {
+    @MockK private lateinit var context: Context
+    @MockK private lateinit var assetManager: AssetManager
+    @MockK lateinit var configParser: ConfigParser
+    @MockK lateinit var configData: ConfigData
+
+    private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
+    private val configFile = File(testDir, "default_app_config_android.bin")
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { context.assets } returns assetManager
+
+        every { assetManager.open("default_app_config_android.bin") } answers { configFile.inputStream() }
+
+        coEvery { configParser.parse(any()) } returns configData
+
+        testDir.mkdirs()
+        testDir.exists() shouldBe true
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+        testDir.deleteRecursively()
+    }
+
+    private fun createInstance() = DefaultAppConfigSource(
+        context = context,
+        configParser = configParser
+    )
+
+    @Test
+    fun `config loaded from asset`() {
+        val testData = "The Cake Is A Lie"
+        configFile.writeText(testData)
+
+        val instance = createInstance()
+        instance.getRawDefaultConfig() shouldBe testData.toByteArray()
+    }
+
+    @Test
+    fun `loading internal config data from assets`() = runBlockingTest {
+        configFile.writeBytes(APPCONFIG_RAW)
+
+        val instance = createInstance()
+
+        instance.getConfigData() shouldBe ConfigDataContainer(
+            serverTime = Instant.EPOCH,
+            localOffset = Duration.ZERO,
+            mappedConfig = configData,
+            configType = ConfigData.Type.LOCAL_DEFAULT,
+            identifier = "fallback.local",
+            cacheValidity = Duration.ZERO
+        )
+    }
+
+    @Test
+    fun `exceptions when getting the default config are rethrown`() = runBlockingTest {
+        val instance = createInstance()
+
+        shouldThrowAny {
+            instance.getConfigData()
+        }
+    }
+
+    companion object {
+        private val APPCONFIG_RAW = (
+            "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" +
+                "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" +
+                "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" +
+                "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" +
+                "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" +
+                "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804"
+            ).decodeHex().toByteArray()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt
similarity index 67%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorageTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt
index 20ddd9ef77f9e81fc8c08cec1a47d0acc9f23bc2..13b19949d1f28e9aee3ea878d73df6cb34e387dd 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorageTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt
@@ -1,6 +1,7 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.local
 
 import android.content.Context
+import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.serialization.SerializationModule
 import io.kotest.matchers.shouldBe
@@ -8,6 +9,7 @@ import io.mockk.MockKAnnotations
 import io.mockk.clearAllMocks
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
 import kotlinx.coroutines.test.runBlockingTest
 import okio.ByteString.Companion.decodeHex
 import okio.ByteString.Companion.toByteString
@@ -31,11 +33,12 @@ class AppConfigStorageTest : BaseIOTest() {
     private val legacyConfigPath = File(storageDir, "appconfig")
     private val configPath = File(storageDir, "appconfig.json")
 
-    private val testConfigDownload = ConfigDownload(
+    private val testConfigDownload = InternalConfigData(
         rawData = APPCONFIG_RAW,
         serverTime = Instant.parse("2020-11-03T05:35:16.000Z"),
         localOffset = Duration.standardHours(1),
-        etag = "I am an ETag :)!"
+        etag = "I am an ETag :)!",
+        cacheValidity = Duration.standardSeconds(123)
     )
 
     @BeforeEach
@@ -72,13 +75,32 @@ class AppConfigStorageTest : BaseIOTest() {
                 "rawData": "$APPCONFIG_BASE64",
                 "etag": "I am an ETag :)!",
                 "serverTime": 1604381716000,
-                "localOffset": 3600000
+                "localOffset": 3600000,
+                "cacheValidity": 123000
             }
         """.toComparableJson()
 
         storage.getStoredConfig() shouldBe testConfigDownload
     }
 
+    @Test
+    fun `restoring from storage`() = runBlockingTest {
+        configPath.parentFile!!.mkdirs()
+        configPath.writeText(
+            """
+            {
+                "rawData": "$APPCONFIG_BASE64",
+                "etag": "I am an ETag :)!",
+                "serverTime": 1604381716000,
+                "localOffset": 3600000,
+                "cacheValidity": 123000
+            }
+        """.trimIndent()
+        )
+        val storage = createStorage()
+        storage.getStoredConfig() shouldBe testConfigDownload
+    }
+
     @Test
     fun `nulling and overwriting`() = runBlockingTest {
         val storage = createStorage()
@@ -98,7 +120,8 @@ class AppConfigStorageTest : BaseIOTest() {
                 "rawData": "$APPCONFIG_BASE64",
                 "etag": "I am an ETag :)!",
                 "serverTime": 1604381716000,
-                "localOffset": 3600000
+                "localOffset": 3600000,
+                "cacheValidity": 123000
             }
         """.toComparableJson()
 
@@ -117,11 +140,12 @@ class AppConfigStorageTest : BaseIOTest() {
 
         val storage = createStorage()
 
-        storage.getStoredConfig() shouldBe ConfigDownload(
+        storage.getStoredConfig() shouldBe InternalConfigData(
             rawData = APPCONFIG_RAW,
             serverTime = Instant.ofEpochMilli(1234),
             localOffset = Duration.ZERO,
-            etag = "I am an ETag :)!"
+            etag = "I am an ETag :)!",
+            cacheValidity = Duration.standardMinutes(5)
         )
     }
 
@@ -138,6 +162,58 @@ class AppConfigStorageTest : BaseIOTest() {
         configPath.exists() shouldBe true
     }
 
+    @Test
+    fun `return null on errors`() = runBlockingTest {
+        every { timeStamper.nowUTC } throws Exception()
+
+        val storage = createStorage()
+        storage.getStoredConfig() shouldBe null
+    }
+
+    @Test
+    fun `return null on invalid json and delete config file`() = runBlockingTest {
+        configPath.parentFile!!.mkdirs()
+        configPath.writeText(
+            """
+            {
+               
+            }
+        """.trimIndent()
+        )
+        val storage = createStorage()
+        storage.getStoredConfig() shouldBe null
+
+        configPath.exists() shouldBe false
+    }
+
+    @Test
+    fun `return null on empty file and delete config file`() {
+        configPath.parentFile!!.mkdirs()
+        configPath.createNewFile()
+
+        val storage = createStorage()
+
+        runBlockingTest {
+            storage.getStoredConfig() shouldBe null
+        }
+
+        configPath.exists() shouldBe false
+    }
+
+    @Test
+    fun `catch errors when trying to save the config`() {
+        configPath.parentFile!!.mkdirs()
+        configPath.createNewFile()
+
+        val storage = createStorage()
+
+        runBlockingTest {
+            storage.setStoredConfig(mockk())
+        }
+
+        configPath.exists() shouldBe true
+    }
+
     companion object {
         private val APPCONFIG_RAW = (
             "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" +
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSourceTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..155d2f76c5a1293205cf55008a22d9a5308e5869
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSourceTest.kt
@@ -0,0 +1,139 @@
+package de.rki.coronawarnapp.appconfig.sources.local
+
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
+import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerifyOrder
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.test.runBlockingTest
+import okio.ByteString.Companion.decodeHex
+import org.joda.time.Duration
+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.BaseIOTest
+import testhelpers.TestDispatcherProvider
+import testhelpers.coroutines.runBlockingTest2
+import java.io.File
+
+class LocalAppConfigSourceTest : BaseIOTest() {
+
+    @MockK lateinit var configStorage: AppConfigStorage
+    @MockK lateinit var configParser: ConfigParser
+    @MockK lateinit var configData: ConfigData
+    @MockK lateinit var timeStamper: TimeStamper
+
+    private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!)
+
+    private var expectedData = InternalConfigData(
+        rawData = APPCONFIG_RAW,
+        serverTime = Instant.parse("2020-11-03T05:35:16.000Z"),
+        localOffset = Duration.standardHours(1),
+        etag = "etag",
+        cacheValidity = Duration.standardMinutes(5)
+    )
+
+    private var mockConfigStorage: InternalConfigData? = null
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        testDir.mkdirs()
+        testDir.exists() shouldBe true
+
+        coEvery { configStorage.getStoredConfig() } answers { mockConfigStorage }
+        coEvery { configStorage.setStoredConfig(any()) } answers {
+            mockConfigStorage = arg(0)
+        }
+
+        every { configParser.parse(APPCONFIG_RAW) } returns configData
+
+        every { timeStamper.nowUTC } returns Instant.parse("2020-11-03T05:35:16.000Z")
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+        testDir.deleteRecursively()
+    }
+
+    private fun createInstance() = LocalAppConfigSource(
+        storage = configStorage,
+        parser = configParser,
+        dispatcherProvider = TestDispatcherProvider
+    )
+
+    @Test
+    fun `local app config source returns null if storage is empty`() = runBlockingTest {
+        coEvery { configStorage.getStoredConfig() } returns null
+
+        val instance = createInstance()
+
+        instance.getConfigData() shouldBe null
+
+        coVerifyOrder { configStorage.getStoredConfig() }
+    }
+
+    @Test
+    fun `local default config is loaded from storage`() = runBlockingTest {
+        coEvery { configStorage.getStoredConfig() } returns expectedData
+
+        val instance = createInstance()
+
+        instance.getConfigData() shouldBe ConfigDataContainer(
+            serverTime = expectedData.serverTime,
+            localOffset = expectedData.localOffset,
+            mappedConfig = configData,
+            configType = ConfigData.Type.LAST_RETRIEVED,
+            identifier = expectedData.etag,
+            cacheValidity = Duration.standardMinutes(5)
+        )
+
+        coVerifyOrder { configStorage.getStoredConfig() }
+    }
+
+    @Test
+    fun `local app config source returns null if there is any exception`() = runBlockingTest {
+        coEvery { configStorage.getStoredConfig() } returns expectedData.copy(
+            rawData = "I'm not valid protobuf".toByteArray()
+        )
+
+        val instance = createInstance()
+
+        instance.getConfigData() shouldBe null
+
+        coVerifyOrder { configStorage.getStoredConfig() }
+    }
+
+    @Test
+    fun `clear clears caches`() = runBlockingTest2(ignoreActive = true) {
+        val instance = createInstance()
+
+        instance.clear()
+
+        advanceUntilIdle()
+
+        coVerifyOrder {
+            configStorage.setStoredConfig(null)
+        }
+    }
+
+    companion object {
+        private val APPCONFIG_RAW = (
+            "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" +
+                "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" +
+                "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" +
+                "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" +
+                "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" +
+                "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804"
+            ).decodeHex().toByteArray()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt
similarity index 86%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt
index da1ab2f7d7e9dd50d4286a5f55827b76f55e2ad9..f90e0f1b8df65c7bb1de01715375996a488df942 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt
@@ -1,7 +1,8 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.remote
 
 import android.content.Context
 import de.rki.coronawarnapp.appconfig.AppConfigModule
+import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2
 import de.rki.coronawarnapp.environment.download.DownloadCDNModule
 import de.rki.coronawarnapp.http.HttpModule
 import io.kotest.matchers.shouldBe
@@ -49,7 +50,7 @@ class AppConfigApiTest : BaseIOTest() {
         testDir.deleteRecursively()
     }
 
-    private fun createAPI(): AppConfigApiV1 {
+    private fun createAPI(): AppConfigApiV2 {
         val httpModule = HttpModule()
         val defaultHttpClient = httpModule.defaultHttpClient()
         val gsonConverterFactory = httpModule.provideGSONConverter()
@@ -76,14 +77,14 @@ class AppConfigApiTest : BaseIOTest() {
         webServer.enqueue(MockResponse().setBody("~appconfig"))
 
         runBlocking {
-            api.getApplicationConfiguration("DE").apply {
+            api.getApplicationConfiguration().apply {
                 body()!!.string() shouldBe "~appconfig"
             }
         }
 
         val request = webServer.takeRequest(5, TimeUnit.SECONDS)!!
         request.method shouldBe "GET"
-        request.path shouldBe "/version/v1/configuration/country/DE/app_config"
+        request.path shouldBe "/version/v1/app_config_android"
     }
 
     @Test
@@ -97,7 +98,7 @@ class AppConfigApiTest : BaseIOTest() {
 
         webServer.enqueue(configResponse)
         runBlocking {
-            api.getApplicationConfiguration("DE").apply {
+            api.getApplicationConfiguration().apply {
                 body()!!.string() shouldBe "~appconfig"
             }
         }
@@ -106,12 +107,12 @@ class AppConfigApiTest : BaseIOTest() {
 
         webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply {
             method shouldBe "GET"
-            path shouldBe "/version/v1/configuration/country/DE/app_config"
+            path shouldBe "/version/v1/app_config_android"
         }
 
         webServer.enqueue(configResponse)
         runBlocking {
-            api.getApplicationConfiguration("DE").apply {
+            api.getApplicationConfiguration().apply {
                 body()!!.string() shouldBe "~appconfig"
             }
         }
@@ -124,7 +125,7 @@ class AppConfigApiTest : BaseIOTest() {
 
         webServer.enqueue(configResponse)
         runBlocking {
-            api.getApplicationConfiguration("DE").apply {
+            api.getApplicationConfiguration().apply {
                 body()!!.string() shouldBe "~appconfig"
             }
         }
@@ -133,7 +134,7 @@ class AppConfigApiTest : BaseIOTest() {
 
         webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply {
             method shouldBe "GET"
-            path shouldBe "/version/v1/configuration/country/DE/app_config"
+            path shouldBe "/version/v1/app_config_android"
         }
     }
 
@@ -148,7 +149,7 @@ class AppConfigApiTest : BaseIOTest() {
 
         webServer.enqueue(configResponse)
         runBlocking {
-            api.getApplicationConfiguration("DE").apply {
+            api.getApplicationConfiguration().apply {
                 body()!!.string() shouldBe "~appconfig"
             }
         }
@@ -157,13 +158,13 @@ class AppConfigApiTest : BaseIOTest() {
 
         webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply {
             method shouldBe "GET"
-            path shouldBe "/version/v1/configuration/country/DE/app_config"
+            path shouldBe "/version/v1/app_config_android"
         }
 
         webServer.enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY))
 
         runBlocking {
-            api.getApplicationConfiguration("DE").apply {
+            api.getApplicationConfiguration().apply {
                 body()!!.string() shouldBe "~appconfig"
             }
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt
similarity index 82%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigServerTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt
index db28c6d6d7c8456eac64fc06250071511e1df0d8..9ae6713c24fd62c7f1b6a1636b90d1aaf89b4b24 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigServerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt
@@ -1,5 +1,9 @@
-package de.rki.coronawarnapp.appconfig.download
+package de.rki.coronawarnapp.appconfig.sources.remote
 
+import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2
+import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException
+import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException
+import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
 import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.security.VerificationKeys
@@ -28,7 +32,7 @@ import java.io.File
 
 class AppConfigServerTest : BaseIOTest() {
 
-    @MockK lateinit var api: AppConfigApiV1
+    @MockK lateinit var api: AppConfigApiV2
     @MockK lateinit var verificationKeys: VerificationKeys
     @MockK lateinit var timeStamper: TimeStamper
     private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!)
@@ -54,32 +58,33 @@ class AppConfigServerTest : BaseIOTest() {
     private fun createInstance(homeCountry: LocationCode = defaultHomeCountry) = AppConfigServer(
         api = { api },
         verificationKeys = verificationKeys,
-        homeCountry = homeCountry,
         cache = mockk(),
         timeStamper = timeStamper
     )
 
     @Test
     fun `application config download`() = runBlockingTest {
-        coEvery { api.getApplicationConfiguration("DE") } returns Response.success(
+        coEvery { api.getApplicationConfiguration() } returns Response.success(
             APPCONFIG_BUNDLE.toResponseBody(),
             Headers.headersOf(
                 "Date", "Tue, 03 Nov 2020 08:46:03 GMT",
-                "ETag", "I am an ETag :)!"
+                "ETag", "I am an ETag :)!",
+                "Cache-Control", "public,max-age=123"
             )
         )
 
         val downloadServer = createInstance()
 
         val configDownload = downloadServer.downloadAppConfig()
-        configDownload shouldBe ConfigDownload(
+        configDownload shouldBe InternalConfigData(
             rawData = APPCONFIG_RAW,
             serverTime = Instant.parse("2020-11-03T08:46:03.000Z"),
             localOffset = Duration(
                 Instant.parse("2020-11-03T08:46:03.000Z"),
                 Instant.ofEpochMilli(123456789)
             ),
-            etag = "I am an ETag :)!"
+            etag = "I am an ETag :)!",
+            cacheValidity = Duration.standardSeconds(123)
         )
 
         verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) }
@@ -87,7 +92,7 @@ class AppConfigServerTest : BaseIOTest() {
 
     @Test
     fun `application config data is faulty`() = runBlockingTest {
-        coEvery { api.getApplicationConfiguration("DE") } returns Response.success(
+        coEvery { api.getApplicationConfiguration() } returns Response.success(
             "123ABC".decodeHex().toResponseBody()
         )
 
@@ -100,7 +105,7 @@ class AppConfigServerTest : BaseIOTest() {
 
     @Test
     fun `application config verification fails`() = runBlockingTest {
-        coEvery { api.getApplicationConfiguration("DE") } returns Response.success(
+        coEvery { api.getApplicationConfiguration() } returns Response.success(
             APPCONFIG_BUNDLE.toResponseBody()
         )
         every { verificationKeys.hasInvalidSignature(any(), any()) } returns true
@@ -114,7 +119,7 @@ class AppConfigServerTest : BaseIOTest() {
 
     @Test
     fun `missing server date leads to local time fallback`() = runBlockingTest {
-        coEvery { api.getApplicationConfiguration("DE") } returns Response.success(
+        coEvery { api.getApplicationConfiguration() } returns Response.success(
             APPCONFIG_BUNDLE.toResponseBody(),
             Headers.headersOf(
                 "ETag", "I am an ETag :)!"
@@ -124,17 +129,18 @@ class AppConfigServerTest : BaseIOTest() {
         val downloadServer = createInstance()
 
         val configDownload = downloadServer.downloadAppConfig()
-        configDownload shouldBe ConfigDownload(
+        configDownload shouldBe InternalConfigData(
             rawData = APPCONFIG_RAW,
             serverTime = Instant.ofEpochMilli(123456789),
             localOffset = Duration.ZERO,
-            etag = "I am an ETag :)!"
+            etag = "I am an ETag :)!",
+            cacheValidity = Duration.standardSeconds(300)
         )
     }
 
     @Test
     fun `missing server etag leads to exception`() = runBlockingTest {
-        coEvery { api.getApplicationConfiguration("DE") } returns Response.success(
+        coEvery { api.getApplicationConfiguration() } returns Response.success(
             APPCONFIG_BUNDLE.toResponseBody()
         )
 
@@ -147,7 +153,7 @@ class AppConfigServerTest : BaseIOTest() {
 
     @Test
     fun `local offset is the difference between server time and local time`() = runBlockingTest {
-        coEvery { api.getApplicationConfiguration("DE") } returns Response.success(
+        coEvery { api.getApplicationConfiguration() } returns Response.success(
             APPCONFIG_BUNDLE.toResponseBody(),
             Headers.headersOf(
                 "Date", "Tue, 03 Nov 2020 06:35:16 GMT",
@@ -158,11 +164,12 @@ class AppConfigServerTest : BaseIOTest() {
 
         val downloadServer = createInstance()
 
-        downloadServer.downloadAppConfig() shouldBe ConfigDownload(
+        downloadServer.downloadAppConfig() shouldBe InternalConfigData(
             rawData = APPCONFIG_RAW,
             serverTime = Instant.parse("2020-11-03T06:35:16.000Z"),
             localOffset = Duration.standardHours(-1),
-            etag = "I am an ETag :)!"
+            etag = "I am an ETag :)!",
+            cacheValidity = Duration.standardSeconds(300)
         )
     }
 
@@ -183,16 +190,17 @@ class AppConfigServerTest : BaseIOTest() {
         every { mockCacheResponse.sentRequestAtMillis } returns Instant.parse("2020-11-03T04:35:16.000Z").millis
         every { response.raw().cacheResponse } returns mockCacheResponse
 
-        coEvery { api.getApplicationConfiguration("DE") } returns response
+        coEvery { api.getApplicationConfiguration() } returns response
         every { timeStamper.nowUTC } returns Instant.parse("2020-11-03T05:35:16.000Z")
 
         val downloadServer = createInstance()
 
-        downloadServer.downloadAppConfig() shouldBe ConfigDownload(
+        downloadServer.downloadAppConfig() shouldBe InternalConfigData(
             rawData = APPCONFIG_RAW,
             serverTime = Instant.parse("2020-11-03T06:35:16.000Z"),
             localOffset = Duration.standardHours(-2),
-            etag = "I am an ETag :)!"
+            etag = "I am an ETag :)!",
+            cacheValidity = Duration.standardSeconds(300)
         )
     }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSourceTest.kt
similarity index 58%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigSourceTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSourceTest.kt
index 2a3bf75048881cb9f6acb22bcb84e7ee8511abcc..162ae3f5c6d6822ba94496f02c3cd3713eadc4c0 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigSourceTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSourceTest.kt
@@ -1,10 +1,10 @@
-package de.rki.coronawarnapp.appconfig
+package de.rki.coronawarnapp.appconfig.sources.remote
 
-import de.rki.coronawarnapp.appconfig.download.AppConfigServer
-import de.rki.coronawarnapp.appconfig.download.AppConfigStorage
-import de.rki.coronawarnapp.appconfig.download.ConfigDownload
-import de.rki.coronawarnapp.appconfig.download.DefaultAppConfigSource
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
 import de.rki.coronawarnapp.appconfig.mapping.ConfigParser
+import de.rki.coronawarnapp.appconfig.sources.local.AppConfigStorage
 import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
@@ -16,8 +16,6 @@ import io.mockk.coVerifyOrder
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
-import io.mockk.verify
-import kotlinx.coroutines.test.runBlockingTest
 import okio.ByteString.Companion.decodeHex
 import org.joda.time.Duration
 import org.joda.time.Instant
@@ -30,25 +28,25 @@ import testhelpers.coroutines.runBlockingTest2
 import java.io.File
 import java.io.IOException
 
-class AppConfigSourceTest : BaseIOTest() {
+class RemoteAppConfigSourceTest : BaseIOTest() {
 
     @MockK lateinit var configServer: AppConfigServer
     @MockK lateinit var configStorage: AppConfigStorage
     @MockK lateinit var configParser: ConfigParser
     @MockK lateinit var configData: ConfigData
     @MockK lateinit var timeStamper: TimeStamper
-    @MockK lateinit var appConfigDefaultFallback: DefaultAppConfigSource
 
     private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!)
 
-    private var testConfigDownload = ConfigDownload(
+    private var dataFromServer = InternalConfigData(
         rawData = APPCONFIG_RAW,
         serverTime = Instant.parse("2020-11-03T05:35:16.000Z"),
         localOffset = Duration.standardHours(1),
-        etag = "etag"
+        etag = "etag",
+        cacheValidity = Duration.standardSeconds(420)
     )
 
-    private var mockConfigStorage: ConfigDownload? = null
+    private var mockConfigStorage: InternalConfigData? = null
 
     @BeforeEach
     fun setup() {
@@ -61,7 +59,7 @@ class AppConfigSourceTest : BaseIOTest() {
             mockConfigStorage = arg(0)
         }
 
-        coEvery { configServer.downloadAppConfig() } returns testConfigDownload
+        coEvery { configServer.downloadAppConfig() } returns dataFromServer
         every { configServer.clearCache() } just Runs
 
         every { configParser.parse(APPCONFIG_RAW) } returns configData
@@ -75,55 +73,38 @@ class AppConfigSourceTest : BaseIOTest() {
         testDir.deleteRecursively()
     }
 
-    private fun createInstance() = AppConfigSource(
+    private fun createInstance() = RemoteAppConfigSource(
         server = configServer,
         storage = configStorage,
         parser = configParser,
-        defaultAppConfig = appConfigDefaultFallback,
         dispatcherProvider = TestDispatcherProvider
     )
 
     @Test
     fun `successful download stores new config`() = runBlockingTest2(ignoreActive = true) {
         val source = createInstance()
-        source.retrieveConfig() shouldBe DefaultConfigData(
+        source.getConfigData() shouldBe ConfigDataContainer(
             serverTime = mockConfigStorage!!.serverTime,
             localOffset = mockConfigStorage!!.localOffset,
             mappedConfig = configData,
             configType = ConfigData.Type.FROM_SERVER,
-            identifier = "etag"
+            identifier = "etag",
+            cacheValidity = Duration.standardSeconds(420)
         )
 
-        mockConfigStorage shouldBe testConfigDownload
+        mockConfigStorage shouldBe dataFromServer
 
-        coVerify { configStorage.setStoredConfig(testConfigDownload) }
-        verify(exactly = 0) { appConfigDefaultFallback.getRawDefaultConfig() }
-    }
-
-    @Test
-    fun `fallback to last config if download fails`() = runBlockingTest2(ignoreActive = true) {
-        mockConfigStorage = testConfigDownload
-        coEvery { configServer.downloadAppConfig() } throws Exception()
-
-        createInstance().retrieveConfig() shouldBe DefaultConfigData(
-            serverTime = mockConfigStorage!!.serverTime,
-            localOffset = mockConfigStorage!!.localOffset,
-            mappedConfig = configData,
-            configType = ConfigData.Type.LAST_RETRIEVED,
-            identifier = "etag"
-        )
-
-        verify(exactly = 0) { appConfigDefaultFallback.getRawDefaultConfig() }
+        coVerify { configStorage.setStoredConfig(dataFromServer) }
     }
 
     @Test
     fun `failed download doesn't overwrite valid config`() = runBlockingTest2(ignoreActive = true) {
-        mockConfigStorage = testConfigDownload
+        mockConfigStorage = dataFromServer
         coEvery { configServer.downloadAppConfig() } throws IOException()
 
-        createInstance().retrieveConfig()
+        createInstance().getConfigData()
 
-        mockConfigStorage shouldBe testConfigDownload
+        mockConfigStorage shouldBe dataFromServer
 
         coVerify(exactly = 0) { configStorage.setStoredConfig(any()) }
     }
@@ -137,30 +118,10 @@ class AppConfigSourceTest : BaseIOTest() {
         advanceUntilIdle()
 
         coVerifyOrder {
-            configStorage.setStoredConfig(null)
             configServer.clearCache()
         }
     }
 
-    @Test
-    fun `local default config is used as last resort`() = runBlockingTest {
-        coEvery { configServer.downloadAppConfig() } throws IOException()
-        coEvery { configStorage.getStoredConfig() } returns null
-        every { appConfigDefaultFallback.getRawDefaultConfig() } returns APPCONFIG_RAW
-
-        val instance = createInstance()
-
-        instance.retrieveConfig() shouldBe DefaultConfigData(
-            serverTime = Instant.EPOCH,
-            localOffset = Duration.standardHours(12),
-            mappedConfig = configData,
-            configType = ConfigData.Type.LOCAL_DEFAULT,
-            identifier = "fallback.local"
-        )
-
-        verify { appConfigDefaultFallback.getRawDefaultConfig() }
-    }
-
     companion object {
         private val APPCONFIG_RAW = (
             "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" +
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 062d1bfb4c1fbf45e9fb29d274fbd97c0c7ee93a..f6dae8e55b255f6e8cb3d71bd3c8a7577cb88466 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
@@ -204,6 +204,20 @@ class HourPackageSyncToolTest : CommonSyncToolTest() {
         instance.expectNewHourPackages(listOf(cachedKey1, cachedKey2), now) shouldBe true
     }
 
+    @Test
+    fun `EXPECT_NEW_HOUR_PACKAGES does not get confused by same hour on next day`() = runBlockingTest {
+        val cachedKey1 = mockk<CachedKey>().apply {
+            every { info } returns mockk<CachedKeyInfo>().apply {
+                every { toDateTime() } returns Instant.parse("2020-01-01T00:00:03.000Z").toDateTime(DateTimeZone.UTC)
+            }
+        }
+
+        val instance = createInstance()
+
+        val now = Instant.parse("2020-01-02T01:00:03.000Z")
+        instance.expectNewHourPackages(listOf(cachedKey1), now) shouldBe true
+    }
+
     @Test
     fun `if keys were revoked skip the EXPECT packages check`() = runBlockingTest {
         every { timeStamper.nowUTC } returns Instant.parse("2020-01-04T02:00:00.000Z")
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt
index ea7a698c4c4f55f7142598b9982bc197f91a672f..0be9642b7a7afd6310ed242a095c3a2ecdf18ec3 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt
@@ -10,12 +10,12 @@ import de.rki.coronawarnapp.ui.main.home.HomeFragmentViewModel
 import de.rki.coronawarnapp.ui.main.home.SubmissionCardState
 import de.rki.coronawarnapp.ui.main.home.SubmissionCardsStateProvider
 import de.rki.coronawarnapp.ui.main.home.TracingHeaderState
-import de.rki.coronawarnapp.ui.submission.ApiRequestState.SUCCESS
 import de.rki.coronawarnapp.ui.tracing.card.TracingCardState
 import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider
 import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel
 import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE
 import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE_TELETAN
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
 import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
@@ -131,7 +131,7 @@ class HomeFragmentViewModelTest : BaseTest() {
 
     @Test
     fun `positive test result notification is triggered on positive QR code result`() {
-        val state = SubmissionCardState(PAIRED_POSITIVE, true, SUCCESS)
+        val state = SubmissionCardState(NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE), true)
         every { submissionCardsStateProvider.state } returns flowOf(state)
         every { testResultNotificationService.schedulePositiveTestResultReminder() } returns Unit
 
@@ -145,7 +145,7 @@ class HomeFragmentViewModelTest : BaseTest() {
 
     @Test
     fun `positive test result notification is triggered on positive TeleTan code result`() {
-        val state = SubmissionCardState(PAIRED_POSITIVE_TELETAN, true, SUCCESS)
+        val state = SubmissionCardState(NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE_TELETAN), true)
         every { submissionCardsStateProvider.state } returns flowOf(state)
         every { testResultNotificationService.schedulePositiveTestResultReminder() } returns Unit
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt
index 37d908b103393884b0e18d7776bd7eae0d547377..e661eb23ae7b28ae3ee123f96ed067d076dd4e3e 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt
@@ -5,6 +5,7 @@ import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.ui.main.home.SubmissionCardState
 import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.util.DeviceUIState
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.clearAllMocks
@@ -33,12 +34,18 @@ class SubmissionCardStateTest : BaseTest() {
     private fun instance(
         deviceUiState: DeviceUIState = mockk(),
         isDeviceRegistered: Boolean = true,
-        uiStateState: ApiRequestState = mockk()
-    ) = SubmissionCardState(
-        deviceUiState = deviceUiState,
-        isDeviceRegistered = isDeviceRegistered,
-        uiStateState = uiStateState
-    )
+        uiStateState: ApiRequestState = ApiRequestState.SUCCESS
+    ) =
+        when (uiStateState) {
+            ApiRequestState.SUCCESS ->
+                SubmissionCardState(NetworkRequestWrapper.RequestSuccessful(deviceUiState), isDeviceRegistered)
+            ApiRequestState.FAILED ->
+                SubmissionCardState(NetworkRequestWrapper.RequestFailed(mockk()), isDeviceRegistered)
+            ApiRequestState.STARTED ->
+                SubmissionCardState(NetworkRequestWrapper.RequestStarted, isDeviceRegistered)
+            ApiRequestState.IDLE ->
+                SubmissionCardState(NetworkRequestWrapper.RequestIdle, isDeviceRegistered)
+        }
 
     @Test
     fun `risk card visibility`() {
@@ -163,7 +170,7 @@ class SubmissionCardStateTest : BaseTest() {
             isFetchingCardVisible() shouldBe false
         }
         instance(isDeviceRegistered = true, uiStateState = ApiRequestState.FAILED).apply {
-            isFetchingCardVisible() shouldBe true
+            isFetchingCardVisible() shouldBe false
         }
     }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt
index 056cb108b466a39887d2365bfb88c226f6fb0514..65e402345e88790e9a18f1cf3d7470ec3c86b2d0 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt
@@ -5,8 +5,8 @@ import de.rki.coronawarnapp.storage.LocalData
 import de.rki.coronawarnapp.storage.SubmissionRepository
 import de.rki.coronawarnapp.ui.main.home.SubmissionCardState
 import de.rki.coronawarnapp.ui.main.home.SubmissionCardsStateProvider
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.util.DeviceUIState
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.clearAllMocks
@@ -46,20 +46,19 @@ class SubmissionCardsStateProviderTest : BaseTest() {
 
     @Test
     fun `state is combined correctly`() = runBlockingTest {
-        every { submissionRepository.deviceUIStateFlow } returns flow { emit(DeviceUIState.PAIRED_POSITIVE) }
-        every { submissionRepository.uiStateStateFlow } returns flow { emit(ApiRequestState.SUCCESS) }
+        every { submissionRepository.deviceUIStateFlow } returns flow {
+            emit(NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>(DeviceUIState.PAIRED_POSITIVE))
+        }
         every { LocalData.registrationToken() } returns "token"
 
         createInstance().apply {
             state.first() shouldBe SubmissionCardState(
-                deviceUiState = DeviceUIState.PAIRED_POSITIVE,
-                uiStateState = ApiRequestState.SUCCESS,
+                deviceUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
                 isDeviceRegistered = true
             )
 
             verify {
                 submissionRepository.deviceUIStateFlow
-                submissionRepository.uiStateStateFlow
                 LocalData.registrationToken()
             }
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt
index 4880b66aa8e79d52d909cc7e2ecdc9fc47a5ea96..b24f8fc8964adf921e00389250ed379eb54ea4d1 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt
@@ -1,22 +1,24 @@
 package de.rki.coronawarnapp.nearby
 
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
 import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider
+import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider
 import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport
 import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus
+import de.rki.coronawarnapp.nearby.modules.version.ENFVersion
 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.coVerifySequence
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
-import io.mockk.mockk
 import io.mockk.verifySequence
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOf
@@ -29,7 +31,6 @@ import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 import java.io.File
 
-@Suppress("DEPRECATION")
 class ENFClientTest : BaseTest() {
 
     @MockK lateinit var googleENFClient: ExposureNotificationClient
@@ -37,12 +38,14 @@ class ENFClientTest : BaseTest() {
     @MockK lateinit var diagnosisKeyProvider: DiagnosisKeyProvider
     @MockK lateinit var tracingStatus: TracingStatus
     @MockK lateinit var scanningSupport: ScanningSupport
+    @MockK lateinit var exposureWindowProvider: ExposureWindowProvider
     @MockK lateinit var exposureDetectionTracker: ExposureDetectionTracker
+    @MockK lateinit var enfVersion: ENFVersion
 
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
-        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns true
+        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true
         every { exposureDetectionTracker.trackNewExposureDetection(any()) } just Runs
     }
 
@@ -56,6 +59,8 @@ class ENFClientTest : BaseTest() {
         diagnosisKeyProvider = diagnosisKeyProvider,
         tracingStatus = tracingStatus,
         scanningSupport = scanningSupport,
+        enfVersion = enfVersion,
+        exposureWindowProvider = exposureWindowProvider,
         exposureDetectionTracker = exposureDetectionTracker
     )
 
@@ -69,24 +74,20 @@ class ENFClientTest : BaseTest() {
     fun `provide diagnosis key call is forwarded to the right module`() {
         val client = createClient()
         val keyFiles = listOf(File("test"))
-        val configuration = mockk<ExposureConfiguration>()
-        val token = "123"
 
-        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns true
+        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true
         runBlocking {
-            client.provideDiagnosisKeys(keyFiles, configuration, token) shouldBe true
+            client.provideDiagnosisKeys(keyFiles) shouldBe true
         }
 
-        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns false
+        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns false
         runBlocking {
-            client.provideDiagnosisKeys(keyFiles, configuration, token) shouldBe false
+            client.provideDiagnosisKeys(keyFiles) shouldBe false
         }
 
         coVerify(exactly = 2) {
             diagnosisKeyProvider.provideDiagnosisKeys(
-                keyFiles,
-                configuration,
-                token
+                keyFiles
             )
         }
     }
@@ -95,16 +96,14 @@ class ENFClientTest : BaseTest() {
     fun `provide diagnosis key call is only forwarded if there are actually key files`() {
         val client = createClient()
         val keyFiles = emptyList<File>()
-        val configuration = mockk<ExposureConfiguration>()
-        val token = "123"
 
-        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns true
+        coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true
         runBlocking {
-            client.provideDiagnosisKeys(keyFiles, configuration, token) shouldBe true
+            client.provideDiagnosisKeys(keyFiles) shouldBe true
         }
 
         coVerify(exactly = 0) {
-            diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any())
+            diagnosisKeyProvider.provideDiagnosisKeys(any())
         }
     }
 
@@ -265,4 +264,26 @@ class ENFClientTest : BaseTest() {
             createClient().lastSuccessfulTrackedExposureDetection().first()!!.identifier shouldBe "1"
         }
     }
+
+    @Test
+    fun `exposure windows check is forwarded to the right module`() = runBlocking {
+        val exposureWindowList = emptyList<ExposureWindow>()
+        coEvery { exposureWindowProvider.exposureWindows() } returns exposureWindowList
+
+        val client = createClient()
+        client.exposureWindows() shouldBe exposureWindowList
+
+        coVerify(exactly = 1) {
+            exposureWindowProvider.exposureWindows()
+        }
+    }
+
+    @Test
+    fun `enf version check is forwaded to the right module`() = runBlocking {
+        coEvery { enfVersion.getENFClientVersion() } returns Long.MAX_VALUE
+
+        createClient().getENFClientVersion() shouldBe Long.MAX_VALUE
+
+        coVerifySequence { enfVersion.getENFClientVersion() }
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt
index fee9f63fa4177ddbb0fc6819d6788fe67535c514..702291ceaeaacdea384fe8f930a56caec0590ede 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt
@@ -130,4 +130,20 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() {
         storedData.getValue("b2b98400-058d-43e6-b952-529a5255248b").isCalculating shouldBe true
         storedData.getValue("aeb15509-fb34-42ce-8795-7a9ae0c2f389").isCalculating shouldBe false
     }
+
+    @Test
+    fun `we catch empty json data and prevent unsafely initialized maps`() = runBlockingTest {
+        storageDir.mkdirs()
+        storageFile.writeText("")
+
+        storageFile.exists() shouldBe true
+
+        createStorage().apply {
+            val value = load()
+            value.size shouldBe 0
+            value shouldBe emptyMap()
+
+            storageFile.exists() shouldBe false
+        }
+    }
 }
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 24fbd423acacfc4c3ba860da038d9a9b4d447e6a..da15e57215243c887afb317a87160ca618a71619 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt
@@ -1,37 +1,32 @@
-@file:Suppress("DEPRECATION")
-
 package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider
 
-import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
-import de.rki.coronawarnapp.util.GoogleAPIVersion
+import de.rki.coronawarnapp.nearby.modules.version.ENFVersion
+import de.rki.coronawarnapp.nearby.modules.version.OutdatedENFVersionException
+import io.kotest.matchers.shouldBe
+import io.mockk.Called
 import io.mockk.MockKAnnotations
 import io.mockk.clearAllMocks
 import io.mockk.coEvery
 import io.mockk.coVerify
+import io.mockk.coVerifySequence
 import io.mockk.impl.annotations.MockK
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
 import testhelpers.BaseTest
 import testhelpers.gms.MockGMSTask
 import java.io.File
 
 class DefaultDiagnosisKeyProviderTest : BaseTest() {
-    @MockK
-    lateinit var googleENFClient: ExposureNotificationClient
-
-    @MockK
-    lateinit var googleAPIVersion: GoogleAPIVersion
+    @MockK lateinit var googleENFClient: ExposureNotificationClient
+    @MockK lateinit var enfVersion: ENFVersion
+    @MockK lateinit var submissionQuota: SubmissionQuota
 
-    @MockK
-    lateinit var submissionQuota: SubmissionQuota
-
-    @MockK
-    lateinit var exampleConfiguration: ExposureConfiguration
     private val exampleKeyFiles = listOf(File("file1"), File("file2"))
-    private val exampleToken = "123e4567-e89b-12d3-a456-426655440000"
 
     @BeforeEach
     fun setup() {
@@ -39,15 +34,9 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() {
 
         coEvery { submissionQuota.consumeQuota(any()) } returns true
 
-        coEvery {
-            googleENFClient.provideDiagnosisKeys(
-                any(),
-                any(),
-                any()
-            )
-        } returns MockGMSTask.forValue(null)
+        coEvery { googleENFClient.provideDiagnosisKeys(any<List<File>>()) } returns MockGMSTask.forValue(null)
 
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true
+        coEvery { enfVersion.requireMinimumVersion(any()) } returns Unit
     }
 
     @AfterEach
@@ -56,141 +45,65 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() {
     }
 
     private fun createProvider() = DefaultDiagnosisKeyProvider(
-        googleAPIVersion = googleAPIVersion,
+        enfVersion = enfVersion,
         submissionQuota = submissionQuota,
         enfClient = googleENFClient
     )
 
     @Test
-    fun `legacy key provision is used on older ENF versions`() {
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns false
+    fun `provide diagnosis keys with outdated ENF versions`() {
+        coEvery { enfVersion.requireMinimumVersion(any()) } throws OutdatedENFVersionException(
+            current = 9000,
+            required = 5000
+        )
 
         val provider = createProvider()
 
-        runBlocking {
-            provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken)
-        }
-
-        coVerify(exactly = 0) {
-            googleENFClient.provideDiagnosisKeys(
-                exampleKeyFiles, exampleConfiguration, exampleToken
-            )
+        assertThrows<OutdatedENFVersionException> {
+            runBlockingTest { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe false
         }
 
-        coVerify(exactly = 1) {
-            googleENFClient.provideDiagnosisKeys(
-                listOf(exampleKeyFiles[0]), exampleConfiguration, exampleToken
-            )
-            googleENFClient.provideDiagnosisKeys(
-                listOf(exampleKeyFiles[1]), exampleConfiguration, exampleToken
-            )
-            submissionQuota.consumeQuota(2)
+        coVerify {
+            googleENFClient wasNot Called
+            submissionQuota wasNot Called
         }
     }
 
     @Test
-    fun `normal key provision is used on newer ENF versions`() {
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true
-
+    fun `key provision is used on newer ENF versions`() {
         val provider = createProvider()
 
-        runBlocking {
-            provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken)
-        }
+        runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true
 
-        coVerify(exactly = 1) {
-            googleENFClient.provideDiagnosisKeys(any(), any(), any())
-            googleENFClient.provideDiagnosisKeys(
-                exampleKeyFiles, exampleConfiguration, exampleToken
-            )
+        coVerifySequence {
             submissionQuota.consumeQuota(1)
+            googleENFClient.provideDiagnosisKeys(exampleKeyFiles)
         }
     }
 
     @Test
-    fun `passing an a null configuration leads to constructing a fallback from defaults`() {
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true
-
-        val provider = createProvider()
-        val fallback = ExposureConfiguration.ExposureConfigurationBuilder().build()
-
-        runBlocking {
-            provider.provideDiagnosisKeys(exampleKeyFiles, null, exampleToken)
-        }
-
-        coVerify(exactly = 1) {
-            googleENFClient.provideDiagnosisKeys(any(), any(), any())
-            googleENFClient.provideDiagnosisKeys(exampleKeyFiles, fallback, exampleToken)
-        }
-    }
-
-    @Test
-    fun `passing an a null configuration leads to constructing a fallback from defaults, legacy`() {
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns false
-
-        val provider = createProvider()
-        val fallback = ExposureConfiguration.ExposureConfigurationBuilder().build()
-
-        runBlocking {
-            provider.provideDiagnosisKeys(exampleKeyFiles, null, exampleToken)
-        }
-
-        coVerify(exactly = 1) {
-            googleENFClient.provideDiagnosisKeys(
-                listOf(exampleKeyFiles[0]), fallback, exampleToken
-            )
-            googleENFClient.provideDiagnosisKeys(
-                listOf(exampleKeyFiles[1]), fallback, exampleToken
-            )
-            submissionQuota.consumeQuota(2)
-        }
-    }
-
-    @Test
-    fun `quota is consumed silenently`() {
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true
+    fun `quota is just monitored`() {
         coEvery { submissionQuota.consumeQuota(any()) } returns false
 
         val provider = createProvider()
 
-        runBlocking {
-            provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken)
-        }
+        runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true
 
-        coVerify(exactly = 1) {
-            googleENFClient.provideDiagnosisKeys(any(), any(), any())
-            googleENFClient.provideDiagnosisKeys(
-                exampleKeyFiles, exampleConfiguration, exampleToken
-            )
+        coVerifySequence {
             submissionQuota.consumeQuota(1)
+            googleENFClient.provideDiagnosisKeys(exampleKeyFiles)
         }
     }
 
     @Test
-    fun `quota is consumed silently, legacy`() {
-        coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns false
-        coEvery { submissionQuota.consumeQuota(any()) } returns false
-
+    fun `provide empty key list`() {
         val provider = createProvider()
 
-        runBlocking {
-            provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken)
-        }
-
-        coVerify(exactly = 0) {
-            googleENFClient.provideDiagnosisKeys(
-                exampleKeyFiles, exampleConfiguration, exampleToken
-            )
-        }
+        runBlocking { provider.provideDiagnosisKeys(emptyList()) } shouldBe true
 
-        coVerify(exactly = 1) {
-            googleENFClient.provideDiagnosisKeys(
-                listOf(exampleKeyFiles[0]), exampleConfiguration, exampleToken
-            )
-            googleENFClient.provideDiagnosisKeys(
-                listOf(exampleKeyFiles[1]), exampleConfiguration, exampleToken
-            )
-            submissionQuota.consumeQuota(2)
+        coVerify {
+            googleENFClient wasNot Called
+            submissionQuota wasNot Called
         }
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt
index c5d7238a84bd7dea1318173e73841f09ee74bbdc..52c90fc86ad19a59195445262bde726331554c80 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt
@@ -69,26 +69,25 @@ class SubmissionQuotaTest : BaseTest() {
             quota.consumeQuota(5) shouldBe true
         }
 
-        coVerify { enfData.currentQuota = 20 }
+        coVerify { enfData.currentQuota = 6 }
 
         // Reset to 20, then consumed 5
-        testStorageCurrentQuota shouldBe 15
+        testStorageCurrentQuota shouldBe 1
     }
 
     @Test
     fun `quota consumption return true if quota was available`() {
-        testStorageCurrentQuota shouldBe 20
+        testStorageCurrentQuota shouldBe 6
 
         val quota = createQuota()
 
         runBlocking {
-            quota.consumeQuota(10) shouldBe true
-            quota.consumeQuota(10) shouldBe true
-            quota.consumeQuota(10) shouldBe false
+            quota.consumeQuota(3) shouldBe true
+            quota.consumeQuota(3) shouldBe true
             quota.consumeQuota(1) shouldBe false
         }
 
-        verify(exactly = 4) { timeStamper.nowUTC }
+        verify(exactly = 3) { timeStamper.nowUTC }
     }
 
     @Test
@@ -97,7 +96,7 @@ class SubmissionQuotaTest : BaseTest() {
 
         runBlocking {
             quota.consumeQuota(0) shouldBe true
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
             quota.consumeQuota(0) shouldBe true
             quota.consumeQuota(1) shouldBe false
         }
@@ -105,12 +104,12 @@ class SubmissionQuotaTest : BaseTest() {
 
     @Test
     fun `partial consumption is not possible`() {
-        testStorageCurrentQuota shouldBe 20
+        testStorageCurrentQuota shouldBe 6
 
         val quota = createQuota()
 
         runBlocking {
-            quota.consumeQuota(18) shouldBe true
+            quota.consumeQuota(4) shouldBe true
             quota.consumeQuota(1) shouldBe true
             quota.consumeQuota(2) shouldBe false
         }
@@ -124,23 +123,23 @@ class SubmissionQuotaTest : BaseTest() {
         val timeTravelTarget = Instant.parse("2020-12-24T00:00:00.001Z")
 
         runBlocking {
-            quota.consumeQuota(20) shouldBe true
-            quota.consumeQuota(20) shouldBe false
+            quota.consumeQuota(6) shouldBe true
+            quota.consumeQuota(6) shouldBe false
 
             every { timeStamper.nowUTC } returns timeTravelTarget
 
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
             quota.consumeQuota(1) shouldBe false
         }
 
-        coVerify(exactly = 1) { enfData.currentQuota = 20 }
+        coVerify(exactly = 1) { enfData.currentQuota = 6 }
         verify(exactly = 4) { timeStamper.nowUTC }
         verify(exactly = 1) { enfData.lastQuotaResetAt = timeTravelTarget }
     }
 
     @Test
     fun `quota fill up is at midnight`() {
-        testStorageCurrentQuota = 20
+        testStorageCurrentQuota = 6
         testStorageLastQuotaReset = Instant.parse("2020-12-24T23:00:00.000Z")
         val startTime = Instant.parse("2020-12-24T23:59:59.998Z")
         every { timeStamper.nowUTC } returns startTime
@@ -148,7 +147,7 @@ class SubmissionQuotaTest : BaseTest() {
         val quota = createQuota()
 
         runBlocking {
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
             quota.consumeQuota(1) shouldBe false
 
             every { timeStamper.nowUTC } returns startTime.plus(1)
@@ -161,10 +160,10 @@ class SubmissionQuotaTest : BaseTest() {
             quota.consumeQuota(1) shouldBe true
 
             every { timeStamper.nowUTC } returns startTime.plus(4)
-            quota.consumeQuota(20) shouldBe false
+            quota.consumeQuota(6) shouldBe false
 
             every { timeStamper.nowUTC } returns startTime.plus(3).plus(Duration.standardDays(1))
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
         }
     }
 
@@ -175,26 +174,26 @@ class SubmissionQuotaTest : BaseTest() {
         runBlocking {
             every { timeStamper.nowUTC } returns startTime
             val quota = createQuota()
-            quota.consumeQuota(17) shouldBe true
+            quota.consumeQuota(3) shouldBe true
         }
 
         runBlocking {
             every { timeStamper.nowUTC } returns startTime.plus(Duration.standardDays(365))
             val quota = createQuota()
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
             quota.consumeQuota(1) shouldBe false
         }
 
         runBlocking {
             every { timeStamper.nowUTC } returns startTime.plus(Duration.standardDays(365 * 2))
             val quota = createQuota()
-            quota.consumeQuota(17) shouldBe true
+            quota.consumeQuota(3) shouldBe true
         }
         runBlocking {
             every { timeStamper.nowUTC } returns startTime.plus(Duration.standardDays(365 * 3))
             val quota = createQuota()
             quota.consumeQuota(3) shouldBe true
-            quota.consumeQuota(17) shouldBe true
+            quota.consumeQuota(3) shouldBe true
             quota.consumeQuota(1) shouldBe false
         }
     }
@@ -208,12 +207,12 @@ class SubmissionQuotaTest : BaseTest() {
         val quota = createQuota()
 
         runBlocking {
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
             quota.consumeQuota(1) shouldBe false
 
             // Go forward and get a reset
             every { timeStamper.nowUTC } returns startTime.plus(Duration.standardHours(1))
-            quota.consumeQuota(20) shouldBe true
+            quota.consumeQuota(6) shouldBe true
             quota.consumeQuota(1) shouldBe false
 
             // Go backwards and don't gain a reset
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt
index d6c4cc4d37817dc2dfc8023b745f0755f6de0b69..ec0ba8d4a53bec4c79e4629f6a3900fffb1cce48 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt
@@ -8,14 +8,17 @@ import io.mockk.clearAllMocks
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.verify
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.flow.toList
-import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import testhelpers.coroutines.runBlockingTest2
+import testhelpers.coroutines.test
 import testhelpers.gms.MockGMSTask
 
 class DefaultTracingStatusTest : BaseTest() {
@@ -26,7 +29,7 @@ class DefaultTracingStatusTest : BaseTest() {
     fun setup() {
         MockKAnnotations.init(this)
 
-        every { client.isEnabled } returns MockGMSTask.forValue(true)
+        every { client.isEnabled } answers { MockGMSTask.forValue(true) }
     }
 
     @AfterEach
@@ -34,33 +37,60 @@ class DefaultTracingStatusTest : BaseTest() {
         clearAllMocks()
     }
 
-    private fun createInstance(): DefaultTracingStatus = DefaultTracingStatus(
-        client = client
+    private fun createInstance(scope: CoroutineScope): DefaultTracingStatus = DefaultTracingStatus(
+        client = client,
+        scope = scope
     )
 
     @Test
-    fun `init is sideeffect free and lazy`() {
-        createInstance()
+    fun `init is sideeffect free and lazy`() = runBlockingTest2(ignoreActive = true) {
+        createInstance(scope = this)
+
+        advanceUntilIdle()
+
         verify { client wasNot Called }
     }
 
     @Test
-    fun `state emission works`() = runBlockingTest {
-        val instance = createInstance()
+    fun `state emission works`() = runBlockingTest2(ignoreActive = true) {
+        val instance = createInstance(scope = this)
         instance.isTracingEnabled.first() shouldBe true
     }
 
     @Test
-    fun `state is updated and polling stops on collection stop`() = runBlockingTest {
+    fun `state is updated and polling stops on cancel`() = runBlockingTest2(ignoreActive = true) {
         every { client.isEnabled } returnsMany listOf(
             true, false, true, false, true, false, true
         ).map { MockGMSTask.forValue(it) }
 
-        val instance = createInstance()
+        val instance = createInstance(scope = this)
 
         instance.isTracingEnabled.take(6).toList() shouldBe listOf(
             true, false, true, false, true, false
         )
         verify(exactly = 6) { client.isEnabled }
     }
+
+    @Test
+    fun `subscriptions are shared but not cached`() = runBlockingTest2(ignoreActive = true) {
+        val instance = createInstance(scope = this)
+
+        val collector1 = instance.isTracingEnabled.test(tag = "1", startOnScope = this)
+        val collector2 = instance.isTracingEnabled.test(tag = "2", startOnScope = this)
+
+        delay(500)
+
+        collector1.latestValue shouldBe true
+        collector2.latestValue shouldBe true
+
+        collector1.cancel()
+        collector2.cancel()
+
+        advanceUntilIdle()
+
+        verify(exactly = 1) { client.isEnabled }
+
+        every { client.isEnabled } answers { MockGMSTask.forValue(false) }
+        instance.isTracingEnabled.first() shouldBe false
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d9e563b93dda599d64478739717998339f72d63
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt
@@ -0,0 +1,112 @@
+package de.rki.coronawarnapp.nearby.modules.version
+
+import com.google.android.gms.common.api.ApiException
+import com.google.android.gms.common.api.CommonStatusCodes.API_NOT_CONNECTED
+import com.google.android.gms.common.api.CommonStatusCodes.INTERNAL_ERROR
+import com.google.android.gms.common.api.Status
+import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
+import io.kotest.assertions.throwables.shouldNotThrowAny
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.gms.MockGMSTask
+
+@ExperimentalCoroutinesApi
+internal class DefaultENFVersionTest {
+
+    @MockK lateinit var client: ExposureNotificationClient
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this)
+    }
+
+    @AfterEach
+    fun tearDown() {
+        clearAllMocks()
+    }
+
+    fun createInstance() = DefaultENFVersion(
+        client = client
+    )
+
+    @Test
+    fun `current version is newer than the required version`() {
+        every { client.version } returns MockGMSTask.forValue(17000000L)
+
+        runBlockingTest {
+            createInstance().apply {
+                getENFClientVersion() shouldBe 17000000L
+                shouldNotThrowAny {
+                    requireMinimumVersion(ENFVersion.V1_6)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `current version is older than the required version`() {
+        every { client.version } returns MockGMSTask.forValue(15000000L)
+
+        runBlockingTest {
+            createInstance().apply {
+                getENFClientVersion() shouldBe 15000000L
+
+                shouldThrow<OutdatedENFVersionException> {
+                    requireMinimumVersion(ENFVersion.V1_6)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `current version is equal to the required version`() {
+        every { client.version } returns MockGMSTask.forValue(16000000L)
+
+        runBlockingTest {
+            createInstance().apply {
+                getENFClientVersion() shouldBe ENFVersion.V1_6
+                shouldNotThrowAny {
+                    requireMinimumVersion(ENFVersion.V1_6)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `API_NOT_CONNECTED exceptions are not treated as failures`() {
+        every { client.version } returns MockGMSTask.forError(ApiException(Status(API_NOT_CONNECTED)))
+
+        runBlockingTest {
+            createInstance().apply {
+                getENFClientVersion() shouldBe null
+                shouldNotThrowAny {
+                    requireMinimumVersion(ENFVersion.V1_6)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `rethrows unexpected exceptions`() {
+        every { client.version } returns MockGMSTask.forError(ApiException(Status(INTERNAL_ERROR)))
+
+        runBlockingTest {
+            createInstance().apply {
+                getENFClientVersion() shouldBe null
+
+                shouldThrow<ApiException> {
+                    requireMinimumVersion(ENFVersion.V1_6)
+                }
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e56375ebb81f0aa772df5885b9ff5a8436088c19
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt
@@ -0,0 +1,422 @@
+package de.rki.coronawarnapp.nearby.windows
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import com.google.android.gms.nearby.exposurenotification.ScanInstance
+import com.google.gson.Gson
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer
+import de.rki.coronawarnapp.nearby.windows.entities.ExposureWindowsJsonInput
+import de.rki.coronawarnapp.nearby.windows.entities.cases.JsonScanInstance
+import de.rki.coronawarnapp.nearby.windows.entities.cases.JsonWindow
+import de.rki.coronawarnapp.nearby.windows.entities.cases.TestCase
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCalculationConfiguration
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationFilter
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationWeight
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonNormalizedTimeToRiskLevelMapping
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTrlFilter
+import de.rki.coronawarnapp.risk.DefaultRiskLevels
+import de.rki.coronawarnapp.risk.result.AggregatedRiskResult
+import de.rki.coronawarnapp.risk.result.RiskResult
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.serialization.fromJson
+import io.kotest.matchers.ints.shouldBeGreaterThan
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.shouldNotBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.runBlocking
+import org.joda.time.DateTimeConstants
+import org.joda.time.Duration
+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 timber.log.Timber
+import java.io.FileReader
+import java.nio.file.Paths
+
+class ExposureWindowsCalculationTest : BaseTest() {
+
+    @MockK lateinit var appConfigProvider: AppConfigProvider
+    @MockK lateinit var configData: ConfigData
+    @MockK lateinit var timeStamper: TimeStamper
+
+    private lateinit var riskLevels: DefaultRiskLevels
+    private lateinit var testConfig: ConfigData
+
+    // Json file (located in /test/resources/exposure-windows-risk-calculation.json)
+    private val fileName = "exposure-windows-risk-calculation.json"
+
+    // Debug logs
+    private enum class LogLevel {
+        NONE,
+        ONLY_COMPARISON,
+        EXTENDED,
+        ALL
+    }
+
+    private val logLevel = LogLevel.ONLY_COMPARISON
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        every { timeStamper.nowUTC } returns Instant.now()
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun debugLog(s: String, toShow: LogLevel = LogLevel.ALL) {
+        if (logLevel < toShow)
+            return
+        Timber.v(s)
+    }
+
+    @Test
+    fun `one test to rule them all`(): Unit = runBlocking {
+        // 1 - Load and parse json file
+        val jsonFile = Paths.get("src", "test", "resources", fileName).toFile()
+        jsonFile shouldNotBe null
+        val jsonString = FileReader(jsonFile).readText()
+        jsonString.length shouldBeGreaterThan 0
+        val json = Gson().fromJson<ExposureWindowsJsonInput>(jsonString)
+        json shouldNotBe null
+
+        // 2 - Check test cases
+        for (case: TestCase in json.testCases) {
+            checkTestCase(case)
+        }
+        debugLog("Test cases checked. Total count: ${json.testCases.size}")
+
+        // 3 - Mock calculation configuration and create default risk level with it
+        setupTestConfiguration(json.defaultRiskCalculationConfiguration)
+        coEvery { appConfigProvider.getAppConfig() } returns testConfig
+        every { appConfigProvider.currentConfig } returns flow { testConfig }
+        logConfiguration(testConfig)
+
+        riskLevels = DefaultRiskLevels()
+
+        val appConfig = appConfigProvider.getAppConfig()
+
+        // 4 - Mock and log exposure windows
+        val allExposureWindows = mutableListOf<ExposureWindow>()
+        for (case: TestCase in json.testCases) {
+            val exposureWindows: List<ExposureWindow> =
+                case.exposureWindows.map { window -> jsonToExposureWindow(window) }
+            allExposureWindows.addAll(exposureWindows)
+
+            // 5 - Calculate risk level for test case and aggregate results
+            val exposureWindowsAndResult = HashMap<ExposureWindow, RiskResult>()
+            for (exposureWindow: ExposureWindow in exposureWindows) {
+
+                logExposureWindow(exposureWindow, "âž¡âž¡ EXPOSURE WINDOW PASSED âž¡âž¡", LogLevel.EXTENDED)
+                val riskResult = riskLevels.calculateRisk(appConfig, exposureWindow) ?: continue
+                exposureWindowsAndResult[exposureWindow] = riskResult
+            }
+            debugLog("Exposure windows and result: ${exposureWindowsAndResult.size}")
+
+            val aggregatedRiskResult = riskLevels.aggregateResults(appConfig, exposureWindowsAndResult)
+
+            debugLog(
+                "\n" + comparisonDebugTable(aggregatedRiskResult, case),
+                LogLevel.ONLY_COMPARISON
+            )
+
+            // 6 - Check with expected result from test case
+            aggregatedRiskResult.totalRiskLevel.number shouldBe case.expTotalRiskLevel
+            aggregatedRiskResult.mostRecentDateWithHighRisk shouldBe getTestCaseDate(case.expAgeOfMostRecentDateWithHighRiskInDays)
+            aggregatedRiskResult.mostRecentDateWithLowRisk shouldBe getTestCaseDate(case.expAgeOfMostRecentDateWithLowRiskInDays)
+            aggregatedRiskResult.totalMinimumDistinctEncountersWithHighRisk shouldBe case.expTotalMinimumDistinctEncountersWithHighRisk
+            aggregatedRiskResult.totalMinimumDistinctEncountersWithLowRisk shouldBe case.expTotalMinimumDistinctEncountersWithLowRisk
+        }
+    }
+
+    private fun getTestCaseDate(expAge: Long?): Instant? {
+        if (expAge == null) return null
+        return timeStamper.nowUTC - expAge * DateTimeConstants.MILLIS_PER_DAY
+    }
+
+    private fun comparisonDebugTable(aggregated: AggregatedRiskResult, case: TestCase): String {
+        val result = StringBuilder()
+        result.append("\n").append(case.description)
+        result.append("\n").append("+----------------------+--------------------------+--------------------------+")
+        result.append("\n").append("| Property             | Actual                   | Expected                 |")
+        result.append("\n").append("+----------------------+--------------------------+--------------------------+")
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Total Risk",
+                aggregated.totalRiskLevel.number,
+                case.expTotalRiskLevel
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Date With High Risk",
+                aggregated.mostRecentDateWithHighRisk,
+                getTestCaseDate(case.expAgeOfMostRecentDateWithHighRiskInDays)
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Date With Low Risk",
+                aggregated.mostRecentDateWithLowRisk,
+                getTestCaseDate(case.expAgeOfMostRecentDateWithLowRiskInDays)
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Encounters High Risk",
+                aggregated.totalMinimumDistinctEncountersWithHighRisk,
+                case.expTotalMinimumDistinctEncountersWithHighRisk
+            )
+        )
+        result.append(
+            addPropertyCheckToComparisonDebugTable(
+                "Encounters Low Risk",
+                aggregated.totalMinimumDistinctEncountersWithLowRisk,
+                case.expTotalMinimumDistinctEncountersWithLowRisk
+            )
+        )
+        result.append("\n")
+        return result.toString()
+    }
+
+    private fun addPropertyCheckToComparisonDebugTable(propertyName: String, expected: Any?, actual: Any?): String {
+        val format = "| %-20s | %-24s | %-24s |"
+        val result = StringBuilder()
+        result.append("\n").append(String.format(format, propertyName, expected, actual))
+        result.append("\n").append("+----------------------+--------------------------+--------------------------+")
+        return result.toString()
+    }
+
+    private fun checkTestCase(case: TestCase) {
+        debugLog("Checking ${case.description}", LogLevel.ALL)
+        case.expTotalRiskLevel shouldNotBe null
+        case.expTotalMinimumDistinctEncountersWithLowRisk shouldNotBe null
+        case.expTotalMinimumDistinctEncountersWithHighRisk shouldNotBe null
+        case.exposureWindows.map { exposureWindow -> checkExposureWindow(exposureWindow) }
+    }
+
+    private fun checkExposureWindow(jsonWindow: JsonWindow) {
+        jsonWindow.ageInDays shouldNotBe null
+        jsonWindow.reportType shouldNotBe null
+        jsonWindow.infectiousness shouldNotBe null
+        jsonWindow.calibrationConfidence shouldNotBe null
+    }
+
+    private fun logConfiguration(config: ConfigData) {
+        val result = StringBuilder()
+        result.append("\n\n").append("----------------- \uD83D\uDEE0 CONFIGURATION \uD83D\uDEE0 -----------")
+
+        result.append("\n").append("â—¦ Minutes At Attenuation Filters (${config.minutesAtAttenuationFilters.size})")
+        for (filter: RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter in config.minutesAtAttenuationFilters) {
+            result.append("\n\t").append("⇥ Filter")
+            result.append(logRange(filter.attenuationRange, "Attenuation Range"))
+            result.append(logRange(filter.dropIfMinutesInRange, "Drop If Minutes In Range"))
+        }
+
+        result.append("\n").append("â—¦ Minutes At Attenuation Weights (${config.minutesAtAttenuationWeights.size})")
+        for (weight: RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight in config.minutesAtAttenuationWeights) {
+            result.append("\n\t").append("⇥ Weight")
+            result.append(logRange(weight.attenuationRange, "Attenuation Range"))
+            result.append("\n\t\t").append("↳ Weight: ${weight.weight}")
+        }
+
+        result.append("\n")
+            .append("â—¦ Normalized Time Per Day To Risk Level Mapping List (${config.normalizedTimePerDayToRiskLevelMappingList.size})")
+        for (mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping in config.normalizedTimePerDayToRiskLevelMappingList) {
+            result.append("\n\t").append("⇥ Mapping")
+            result.append(logRange(mapping.normalizedTimeRange, "Normalized Time Range"))
+            result.append("\n\t\t").append("↳ Risk Level: ${mapping.riskLevel}")
+        }
+
+        result.append("\n")
+            .append("â—¦ Normalized Time Per Exposure Window To Risk Level Mapping (${config.normalizedTimePerExposureWindowToRiskLevelMapping.size})")
+        for (mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping in config.normalizedTimePerExposureWindowToRiskLevelMapping) {
+            result.append("\n\t").append("⇥ Mapping")
+            result.append(logRange(mapping.normalizedTimeRange, "Normalized Time Range"))
+            result.append("\n\t\t").append("↳ Risk Level: ${mapping.riskLevel}")
+        }
+
+        result.append("\n").append("â—¦ Transmission Risk Level Encoding:")
+        result.append("\n\t")
+            .append("↳ Infectiousness Offset High: ${config.transmissionRiskLevelEncoding.infectiousnessOffsetHigh}")
+        result.append("\n\t")
+            .append("↳ Infectiousness Offset Standard: ${config.transmissionRiskLevelEncoding.infectiousnessOffsetStandard}")
+        result.append("\n\t")
+            .append("↳ Report Type Offset Confirmed Clinical Diagnosis: ${config.transmissionRiskLevelEncoding.reportTypeOffsetConfirmedClinicalDiagnosis}")
+        result.append("\n\t")
+            .append("↳ Report Type Offset Confirmed Test: ${config.transmissionRiskLevelEncoding.reportTypeOffsetConfirmedTest}")
+        result.append("\n\t")
+            .append("↳ Report Type Offset Recursive: ${config.transmissionRiskLevelEncoding.reportTypeOffsetRecursive}")
+        result.append("\n\t")
+            .append("↳ Report Type Offset Self Report: ${config.transmissionRiskLevelEncoding.reportTypeOffsetSelfReport}")
+
+        result.append("\n").append("â—¦ Transmission Risk Level Filters (${config.transmissionRiskLevelFilters.size})")
+        for (filter: RiskCalculationParametersOuterClass.TrlFilter in config.transmissionRiskLevelFilters) {
+            result.append("\n\t").append("⇥ Trl Filter")
+            result.append(logRange(filter.dropIfTrlInRange, "Drop If Trl In Range"))
+        }
+
+        result.append("\n").append("â—¦ Transmission Risk Level Multiplier: ${config.transmissionRiskLevelMultiplier}")
+        result.append("\n").append("-------------------------------------------- âš™ -").append("\n")
+        debugLog(result.toString(), LogLevel.NONE)
+    }
+
+    private fun logRange(range: RiskCalculationParametersOuterClass.Range, rangeName: String): String {
+        val builder = StringBuilder()
+        builder.append("\n\t\t").append("⇥ $rangeName")
+        builder.append("\n\t\t\t").append("↳ Min: ${range.min}")
+        builder.append("\n\t\t\t").append("↳ Max: ${range.max}")
+        builder.append("\n\t\t\t").append("↳ Min Exclusive: ${range.minExclusive}")
+        builder.append("\n\t\t\t").append("↳ Max Exclusive: ${range.maxExclusive}")
+        return builder.toString()
+    }
+
+    private fun logExposureWindow(exposureWindow: ExposureWindow, title: String, logLevel: LogLevel = LogLevel.ALL) {
+        val result = StringBuilder()
+        result.append("\n\n").append("------------ $title -----------")
+        result.append("\n").append("Mocked Exposure window: #${exposureWindow.hashCode()}")
+        result.append("\n").append("â—¦ Calibration Confidence: ${exposureWindow.calibrationConfidence}")
+        result.append("\n").append("â—¦ Date Millis Since Epoch: ${exposureWindow.dateMillisSinceEpoch}")
+        result.append("\n").append("â—¦ Infectiousness: ${exposureWindow.infectiousness}")
+        result.append("\n").append("â—¦ Report type: ${exposureWindow.reportType}")
+
+        result.append("\n").append("‣ Scan Instances (${exposureWindow.scanInstances.size}):")
+        for (scan: ScanInstance in exposureWindow.scanInstances) {
+            result.append("\n\t").append("⇥ Mocked Scan Instance: #${scan.hashCode()}")
+            result.append("\n\t\t").append("↳ Min Attenuation: ${scan.minAttenuationDb}")
+            result.append("\n\t\t").append("↳ Seconds Since Last Scan: ${scan.secondsSinceLastScan}")
+            result.append("\n\t\t").append("↳ Typical Attenuation: ${scan.typicalAttenuationDb}")
+        }
+        result.append("\n").append("-------------------------------------------- ✂ ----").append("\n")
+        debugLog(result.toString(), logLevel)
+    }
+
+    private fun setupTestConfiguration(json: DefaultRiskCalculationConfiguration) {
+
+        testConfig = ConfigDataContainer(
+            serverTime = Instant.now(),
+            cacheValidity = Duration.standardMinutes(5),
+            localOffset = Duration.ZERO,
+            mappedConfig = configData,
+            identifier = "soup",
+            configType = ConfigData.Type.FROM_SERVER
+        )
+
+        val attenuationFilters = mutableListOf<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>()
+        for (jsonFilter: JsonMinutesAtAttenuationFilter in json.minutesAtAttenuationFilters) {
+            val filter: RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter = mockk()
+            every { filter.attenuationRange.min } returns jsonFilter.attenuationRange.min
+            every { filter.attenuationRange.max } returns jsonFilter.attenuationRange.max
+            every { filter.attenuationRange.minExclusive } returns jsonFilter.attenuationRange.minExclusive
+            every { filter.attenuationRange.maxExclusive } returns jsonFilter.attenuationRange.maxExclusive
+            every { filter.dropIfMinutesInRange.min } returns jsonFilter.dropIfMinutesInRange.min
+            every { filter.dropIfMinutesInRange.max } returns jsonFilter.dropIfMinutesInRange.max
+            every { filter.dropIfMinutesInRange.minExclusive } returns jsonFilter.dropIfMinutesInRange.minExclusive
+            every { filter.dropIfMinutesInRange.maxExclusive } returns jsonFilter.dropIfMinutesInRange.maxExclusive
+            attenuationFilters.add(filter)
+        }
+        every { testConfig.minutesAtAttenuationFilters } returns attenuationFilters
+
+        val attenuationWeights = mutableListOf<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>()
+        for (jsonWeight: JsonMinutesAtAttenuationWeight in json.minutesAtAttenuationWeights) {
+            val weight: RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight = mockk()
+            every { weight.attenuationRange.min } returns jsonWeight.attenuationRange.min
+            every { weight.attenuationRange.max } returns jsonWeight.attenuationRange.max
+            every { weight.attenuationRange.minExclusive } returns jsonWeight.attenuationRange.minExclusive
+            every { weight.attenuationRange.maxExclusive } returns jsonWeight.attenuationRange.maxExclusive
+            every { weight.weight } returns jsonWeight.weight
+            attenuationWeights.add(weight)
+        }
+        every { testConfig.minutesAtAttenuationWeights } returns attenuationWeights
+
+        val normalizedTimePerDayToRiskLevelMapping =
+            mutableListOf<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>()
+        for (jsonMapping: JsonNormalizedTimeToRiskLevelMapping in json.normalizedTimePerDayToRiskLevelMapping) {
+            val mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping = mockk()
+            every { mapping.riskLevel } returns RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(
+                jsonMapping.riskLevel
+            )
+            every { mapping.normalizedTimeRange.min } returns jsonMapping.normalizedTimeRange.min
+            every { mapping.normalizedTimeRange.max } returns jsonMapping.normalizedTimeRange.max
+            every { mapping.normalizedTimeRange.minExclusive } returns jsonMapping.normalizedTimeRange.minExclusive
+            every { mapping.normalizedTimeRange.maxExclusive } returns jsonMapping.normalizedTimeRange.maxExclusive
+            normalizedTimePerDayToRiskLevelMapping.add(mapping)
+        }
+        every { testConfig.normalizedTimePerDayToRiskLevelMappingList } returns normalizedTimePerDayToRiskLevelMapping
+
+        val normalizedTimePerExposureWindowToRiskLevelMapping =
+            mutableListOf<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>()
+        for (jsonMapping: JsonNormalizedTimeToRiskLevelMapping in json.normalizedTimePerEWToRiskLevelMapping) {
+            val mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping = mockk()
+            every { mapping.riskLevel } returns RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(
+                jsonMapping.riskLevel
+            )
+            every { mapping.normalizedTimeRange.min } returns jsonMapping.normalizedTimeRange.min
+            every { mapping.normalizedTimeRange.max } returns jsonMapping.normalizedTimeRange.max
+            every { mapping.normalizedTimeRange.minExclusive } returns jsonMapping.normalizedTimeRange.minExclusive
+            every { mapping.normalizedTimeRange.maxExclusive } returns jsonMapping.normalizedTimeRange.maxExclusive
+            normalizedTimePerExposureWindowToRiskLevelMapping.add(mapping)
+        }
+        every { testConfig.normalizedTimePerExposureWindowToRiskLevelMapping } returns normalizedTimePerExposureWindowToRiskLevelMapping
+
+        every { testConfig.transmissionRiskLevelMultiplier } returns json.transmissionRiskLevelMultiplier
+
+        val trlEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding = mockk()
+        every { trlEncoding.infectiousnessOffsetHigh } returns json.trlEncoding.infectiousnessOffsetHigh
+        every { trlEncoding.infectiousnessOffsetStandard } returns json.trlEncoding.infectiousnessOffsetStandard
+        every { trlEncoding.reportTypeOffsetConfirmedClinicalDiagnosis } returns json.trlEncoding.reportTypeOffsetConfirmedClinicalDiagnosis
+        every { trlEncoding.reportTypeOffsetConfirmedTest } returns json.trlEncoding.reportTypeOffsetConfirmedTest
+        every { trlEncoding.reportTypeOffsetRecursive } returns json.trlEncoding.reportTypeOffsetRecursive
+        every { trlEncoding.reportTypeOffsetSelfReport } returns json.trlEncoding.reportTypeOffsetSelfReport
+        every { testConfig.transmissionRiskLevelEncoding } returns trlEncoding
+
+        val trlFilters = mutableListOf<RiskCalculationParametersOuterClass.TrlFilter>()
+        for (jsonFilter: JsonTrlFilter in json.trlFilters) {
+            val filter: RiskCalculationParametersOuterClass.TrlFilter = mockk()
+            every { filter.dropIfTrlInRange.min } returns jsonFilter.dropIfTrlInRange.min
+            every { filter.dropIfTrlInRange.max } returns jsonFilter.dropIfTrlInRange.max
+            every { filter.dropIfTrlInRange.minExclusive } returns jsonFilter.dropIfTrlInRange.minExclusive
+            every { filter.dropIfTrlInRange.maxExclusive } returns jsonFilter.dropIfTrlInRange.maxExclusive
+            trlFilters.add(filter)
+        }
+        every { testConfig.transmissionRiskLevelFilters } returns trlFilters
+    }
+
+    private fun jsonToExposureWindow(json: JsonWindow): ExposureWindow {
+        val exposureWindow: ExposureWindow = mockk()
+
+        every { exposureWindow.calibrationConfidence } returns json.calibrationConfidence
+        every { exposureWindow.dateMillisSinceEpoch } returns timeStamper.nowUTC.millis - (DateTimeConstants.MILLIS_PER_DAY * json.ageInDays).toLong()
+        every { exposureWindow.infectiousness } returns json.infectiousness
+        every { exposureWindow.reportType } returns json.reportType
+        every { exposureWindow.scanInstances } returns json.scanInstances.map { scanInstance ->
+            jsonToScanInstance(
+                scanInstance
+            )
+        }
+
+        logExposureWindow(exposureWindow, "⊞ EXPOSURE WINDOW MOCK ⊞")
+
+        return exposureWindow
+    }
+
+    private fun jsonToScanInstance(json: JsonScanInstance): ScanInstance {
+        val scanInstance: ScanInstance = mockk()
+        every { scanInstance.minAttenuationDb } returns json.minAttenuation
+        every { scanInstance.secondsSinceLastScan } returns json.secondsSinceLastScan
+        every { scanInstance.typicalAttenuationDb } returns json.typicalAttenuation
+        return scanInstance
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3c0a77a3a27bdc197a2ca75cd885303a3ec587eb
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt
@@ -0,0 +1,14 @@
+package de.rki.coronawarnapp.nearby.windows.entities
+
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.nearby.windows.entities.cases.TestCase
+import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCalculationConfiguration
+
+data class ExposureWindowsJsonInput(
+    @SerializedName("__comment__")
+    val comment: String,
+    @SerializedName("defaultRiskCalculationConfiguration")
+    val defaultRiskCalculationConfiguration: DefaultRiskCalculationConfiguration,
+    @SerializedName("testCases")
+    val testCases: List<TestCase>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a9ea714b7e7f440c4e71c5e27f8f8c912a3c575c
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt
@@ -0,0 +1,12 @@
+package de.rki.coronawarnapp.nearby.windows.entities.cases
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonScanInstance(
+    @SerializedName("minAttenuation")
+    val minAttenuation: Int,
+    @SerializedName("secondsSinceLastScan")
+    val secondsSinceLastScan: Int,
+    @SerializedName("typicalAttenuation")
+    val typicalAttenuation: Int
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7394b329509f98a10e6d7c368dc3033b62c442ef
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.nearby.windows.entities.cases
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonWindow(
+    @SerializedName("ageInDays")
+    val ageInDays: Int,
+    @SerializedName("calibrationConfidence")
+    val calibrationConfidence: Int,
+    @SerializedName("infectiousness")
+    val infectiousness: Int,
+    @SerializedName("reportType")
+    val reportType: Int,
+    @SerializedName("scanInstances")
+    val scanInstances: List<JsonScanInstance>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0d28ea6d7b85b963c6e24e08c4e4c906b6a91082
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt
@@ -0,0 +1,24 @@
+package de.rki.coronawarnapp.nearby.windows.entities.cases
+
+import com.google.gson.annotations.SerializedName
+
+data class TestCase(
+    @SerializedName("description")
+    val description: String,
+    @SerializedName("expAgeOfMostRecentDateWithHighRisk")
+    val expAgeOfMostRecentDateWithHighRiskInDays: Long?,
+    @SerializedName("expAgeOfMostRecentDateWithLowRisk")
+    val expAgeOfMostRecentDateWithLowRiskInDays: Long?,
+    @SerializedName("expTotalMinimumDistinctEncountersWithHighRisk")
+    val expTotalMinimumDistinctEncountersWithHighRisk: Int,
+    @SerializedName("expTotalMinimumDistinctEncountersWithLowRisk")
+    val expTotalMinimumDistinctEncountersWithLowRisk: Int,
+    @SerializedName("expTotalRiskLevel")
+    val expTotalRiskLevel: Int,
+    @SerializedName("expNumberOfDaysWithLowRisk")
+    val expNumberOfDaysWithLowRisk: Int,
+    @SerializedName("expNumberOfDaysWithHighRisk")
+    val expNumberOfDaysWithHighRisk: Int,
+    @SerializedName("exposureWindows")
+    val exposureWindows: List<JsonWindow>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7445d86e786d7250f7cf57dc4c5498eba97fabeb
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class DefaultRiskCalculationConfiguration(
+    @SerializedName("minutesAtAttenuationFilters")
+    val minutesAtAttenuationFilters: List<JsonMinutesAtAttenuationFilter>,
+    @SerializedName("minutesAtAttenuationWeights")
+    val minutesAtAttenuationWeights: List<JsonMinutesAtAttenuationWeight>,
+    @SerializedName("normalizedTimePerDayToRiskLevelMapping")
+    val normalizedTimePerDayToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
+    @SerializedName("normalizedTimePerEWToRiskLevelMapping")
+    val normalizedTimePerEWToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>,
+    @SerializedName("transmissionRiskLevelMultiplier")
+    val transmissionRiskLevelMultiplier: Double,
+    @SerializedName("trlEncoding")
+    val trlEncoding: JsonTrlEncoding,
+    @SerializedName("trlFilters")
+    val trlFilters: List<JsonTrlFilter>
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a01efc1f6022a2a68a8a20e25aeefc6320591b2b
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonMinutesAtAttenuationFilter(
+    @SerializedName("attenuationRange")
+    val attenuationRange: Range,
+    @SerializedName("dropIfMinutesInRange")
+    val dropIfMinutesInRange: Range
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3af598da7dc5c6d71bf0bbeeac85788f038a90ee
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonMinutesAtAttenuationWeight(
+    @SerializedName("attenuationRange")
+    val attenuationRange: Range,
+    @SerializedName("weight")
+    val weight: Double
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4f0fc786998558caf43be1d5cee1e6026f7bb919
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt
@@ -0,0 +1,10 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonNormalizedTimeToRiskLevelMapping(
+    @SerializedName("normalizedTimeRange")
+    val normalizedTimeRange: Range,
+    @SerializedName("riskLevel")
+    val riskLevel: Int
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt
new file mode 100644
index 0000000000000000000000000000000000000000..00a3e5af78270ef34fb384909f1f69ddab92f2ba
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonTrlEncoding(
+    @SerializedName("infectiousnessOffsetHigh")
+    val infectiousnessOffsetHigh: Int,
+    @SerializedName("infectiousnessOffsetStandard")
+    val infectiousnessOffsetStandard: Int,
+    @SerializedName("reportTypeOffsetConfirmedClinicalDiagnosis")
+    val reportTypeOffsetConfirmedClinicalDiagnosis: Int,
+    @SerializedName("reportTypeOffsetConfirmedTest")
+    val reportTypeOffsetConfirmedTest: Int,
+    @SerializedName("reportTypeOffsetRecursive")
+    val reportTypeOffsetRecursive: Int,
+    @SerializedName("reportTypeOffsetSelfReport")
+    val reportTypeOffsetSelfReport: Int
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6e529bc6af34385922152903db58c5a1521c725e
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt
@@ -0,0 +1,8 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class JsonTrlFilter(
+    @SerializedName("dropIfTrlInRange")
+    val dropIfTrlInRange: Range
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a4d686a74cd8183a8261ec35bf8487b0ad2db005
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt
@@ -0,0 +1,14 @@
+package de.rki.coronawarnapp.nearby.windows.entities.configuration
+
+import com.google.gson.annotations.SerializedName
+
+data class Range(
+    @SerializedName("min")
+    val min: Double,
+    @SerializedName("minExclusive")
+    val minExclusive: Boolean,
+    @SerializedName("max")
+    val max: Double,
+    @SerializedName("maxExclusive")
+    val maxExclusive: Boolean
+)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt
index fbc170105e025901200d40c8dc3e40d92a95e3c8..4bd4076caffa27abfe8a250730495fbf917eeedc 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt
@@ -12,12 +12,13 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTra
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection
 import de.rki.coronawarnapp.util.di.AppInjector
 import io.mockk.MockKAnnotations
+import io.mockk.Runs
 import io.mockk.clearAllMocks
 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.mockkStatic
 import io.mockk.verifySequence
 import kotlinx.coroutines.test.TestCoroutineScope
 import org.junit.jupiter.api.AfterEach
@@ -36,6 +37,7 @@ class ExposureStateUpdateReceiverTest : BaseTest() {
     @MockK private lateinit var intent: Intent
     @MockK private lateinit var workManager: WorkManager
     @MockK private lateinit var exposureDetectionTracker: ExposureDetectionTracker
+
     private val scope = TestCoroutineScope()
 
     class TestApp : Application(), HasAndroidInjector {
@@ -48,23 +50,25 @@ class ExposureStateUpdateReceiverTest : BaseTest() {
     @BeforeEach
     fun setUp() {
         MockKAnnotations.init(this)
-        mockkStatic(WorkManager::class)
         every { intent.getStringExtra(ExposureNotificationClient.EXTRA_TOKEN) } returns "token"
 
         mockkObject(AppInjector)
 
+        every { workManager.enqueue(any<WorkRequest>()) } answers { mockk() }
+
         val application = mockk<TestApp>()
         every { context.applicationContext } returns application
+
         val broadcastReceiverInjector = AndroidInjector<Any> {
             it as ExposureStateUpdateReceiver
             it.exposureDetectionTracker = exposureDetectionTracker
             it.dispatcherProvider = TestDispatcherProvider
             it.scope = scope
+            it.workManager = workManager
         }
         every { application.androidInjector() } returns broadcastReceiverInjector
 
-        every { WorkManager.getInstance(context) } returns workManager
-        every { workManager.enqueue(any<WorkRequest>()) } answers { mockk() }
+        every { exposureDetectionTracker.finishExposureDetection(any(), any()) } just Runs
     }
 
     @AfterEach
@@ -77,9 +81,11 @@ class ExposureStateUpdateReceiverTest : BaseTest() {
         every { intent.action } returns ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED
         ExposureStateUpdateReceiver().onReceive(context, intent)
 
+        scope.advanceUntilIdle()
+
         verifySequence {
+            exposureDetectionTracker.finishExposureDetection(null, TrackedExposureDetection.Result.UPDATED_STATE)
             workManager.enqueue(any<WorkRequest>())
-            exposureDetectionTracker.finishExposureDetection("token", TrackedExposureDetection.Result.UPDATED_STATE)
         }
     }
 
@@ -88,8 +94,11 @@ class ExposureStateUpdateReceiverTest : BaseTest() {
         every { intent.action } returns ExposureNotificationClient.ACTION_EXPOSURE_NOT_FOUND
         ExposureStateUpdateReceiver().onReceive(context, intent)
 
+        scope.advanceUntilIdle()
+
         verifySequence {
-            exposureDetectionTracker.finishExposureDetection("token", TrackedExposureDetection.Result.NO_MATCHES)
+            exposureDetectionTracker.finishExposureDetection(null, TrackedExposureDetection.Result.NO_MATCHES)
+            workManager.enqueue(any<WorkRequest>())
         }
     }
 }
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/RiskLevelDataTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..41a2b35177a6eab0a08331f6337a9ce6f991554d
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt
@@ -0,0 +1,40 @@
+package de.rki.coronawarnapp.risk
+
+import android.content.Context
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.preferences.MockSharedPreferences
+
+class RiskLevelDataTest : BaseTest() {
+
+    @MockK lateinit var context: Context
+    lateinit var preferences: MockSharedPreferences
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        preferences = MockSharedPreferences()
+        every { context.getSharedPreferences("risklevel_localdata", Context.MODE_PRIVATE) } returns preferences
+    }
+
+    fun createInstance() = RiskLevelData(context = context)
+
+    @Test
+    fun `update last used config identifier`() {
+        createInstance().apply {
+            lastUsedConfigIdentifier shouldBe null
+            lastUsedConfigIdentifier = "Banana"
+            lastUsedConfigIdentifier shouldBe "Banana"
+            preferences.dataMapPeek.containsValue("Banana") shouldBe true
+
+            lastUsedConfigIdentifier = null
+            lastUsedConfigIdentifier shouldBe null
+            preferences.dataMapPeek.isEmpty() shouldBe true
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5e1cbd3839de8c388a5281a9e8084e7b3e1981a3
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt
@@ -0,0 +1,15 @@
+package de.rki.coronawarnapp.risk
+
+import io.kotest.matchers.shouldBe
+import org.joda.time.Duration
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class RiskLevelTaskConfigTest : BaseTest() {
+
+    @Test
+    fun `risk level task max execution time is not above 9 minutes`() {
+        val config = RiskLevelTask.Config()
+        config.executionTimeout.isShorterThan(Duration.standardMinutes(9)) shouldBe true
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..f174600cfd181bc4936dbe935c9195c830be8c82
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt
@@ -0,0 +1,84 @@
+package de.rki.coronawarnapp.risk
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+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.task.Task
+import de.rki.coronawarnapp.util.BackgroundModeStatus
+import de.rki.coronawarnapp.util.TimeStamper
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class RiskLevelTaskTest : BaseTest() {
+
+    @MockK lateinit var riskLevels: RiskLevels
+    @MockK lateinit var context: Context
+    @MockK lateinit var enfClient: ENFClient
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var backgroundModeStatus: BackgroundModeStatus
+    @MockK lateinit var riskLevelData: RiskLevelData
+    @MockK lateinit var configData: ConfigData
+    @MockK lateinit var appConfigProvider: AppConfigProvider
+    @MockK lateinit var exposureResultStore: ExposureResultStore
+
+    private val arguments: Task.Arguments = object : Task.Arguments {}
+
+    private fun createTask() = RiskLevelTask(
+        riskLevels = riskLevels,
+        context = context,
+        enfClient = enfClient,
+        timeStamper = timeStamper,
+        backgroundModeStatus = backgroundModeStatus,
+        riskLevelData = riskLevelData,
+        appConfigProvider = appConfigProvider,
+        exposureResultStore = exposureResultStore
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkObject(TimeVariables)
+        every { TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() } returns null
+
+        coEvery { appConfigProvider.getAppConfig() } returns configData
+        every { configData.identifier } returns "config-identifier"
+
+        every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns mockk<ConnectivityManager>().apply {
+            every { activeNetwork } returns mockk<Network>().apply {
+                every { getNetworkCapabilities(any()) } returns mockk<NetworkCapabilities>().apply {
+                    every { hasCapability(any()) } returns true
+                }
+            }
+        }
+
+        every { enfClient.isTracingEnabled } returns flowOf(true)
+        every { timeStamper.nowUTC } returns Instant.EPOCH
+
+        every { riskLevelData.lastUsedConfigIdentifier = any() } just Runs
+    }
+
+    @Test
+    fun `last used config ID is set after calculation`() = runBlockingTest {
+//        val task = createTask()
+//        task.run(arguments)
+//
+//        verify { riskLevelData.lastUsedConfigIdentifier = "config-identifier" }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt
deleted file mode 100644
index 845722a2c23174f01becc23d183dc78053e30226..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-package de.rki.coronawarnapp.risk
-
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
-import de.rki.coronawarnapp.appconfig.AppConfigProvider
-import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass
-import io.kotest.matchers.shouldBe
-import io.mockk.MockKAnnotations
-import io.mockk.impl.annotations.MockK
-import junit.framework.TestCase.assertEquals
-import org.junit.Before
-import org.junit.Test
-import testhelpers.BaseTest
-
-class RiskLevelsTest : BaseTest() {
-
-    @MockK lateinit var appConfigProvider: AppConfigProvider
-    private lateinit var riskLevels: DefaultRiskLevels
-
-    @Before
-    fun setUp() {
-        MockKAnnotations.init(this)
-        riskLevels = DefaultRiskLevels(appConfigProvider)
-    }
-
-    @Test
-    fun `is within defined level threshold`() {
-        riskLevels.withinDefinedLevelThreshold(2.0, 1, 3) shouldBe true
-    }
-
-    @Test
-    fun `is not within defined level threshold`() {
-        riskLevels.withinDefinedLevelThreshold(4.0, 1, 3) shouldBe false
-    }
-
-    @Test
-    fun `is within defined level threshold - edge cases`() {
-        riskLevels.withinDefinedLevelThreshold(1.0, 1, 3) shouldBe true
-        riskLevels.withinDefinedLevelThreshold(3.0, 1, 3) shouldBe true
-    }
-
-    @Test
-    fun calculateRiskScoreZero() {
-        val riskScore =
-            riskLevels.calculateRiskScore(
-                buildAttenuationDuration(0.5, 0.5, 1.0),
-                buildSummary(0, 0, 0, 0)
-            )
-
-        assertEquals(0.0, riskScore)
-    }
-
-    @Test
-    fun calculateRiskScoreLow() {
-        val riskScore =
-            riskLevels.calculateRiskScore(
-                buildAttenuationDuration(0.5, 0.5, 1.0),
-                buildSummary(156, 10, 10, 10)
-            )
-
-        assertEquals(124.8, riskScore)
-    }
-
-    @Test
-    fun calculateRiskScoreMid() {
-        val riskScore =
-            riskLevels.calculateRiskScore(
-                buildAttenuationDuration(0.5, 0.5, 1.0),
-                buildSummary(256, 15, 15, 15)
-            )
-
-        assertEquals(307.2, riskScore)
-    }
-
-    @Test
-    fun calculateRiskScoreHigh() {
-        val riskScore =
-            riskLevels.calculateRiskScore(
-                buildAttenuationDuration(0.5, 0.5, 1.0),
-                buildSummary(512, 30, 30, 30)
-            )
-
-        assertEquals(1228.8, riskScore)
-    }
-
-    @Test
-    fun calculateRiskScoreMax() {
-        val riskScore =
-            riskLevels.calculateRiskScore(
-                buildAttenuationDuration(0.5, 0.5, 1.0),
-                buildSummary(4096, 30, 30, 30)
-            )
-
-        assertEquals(9830.4, riskScore)
-    }
-
-    @Test
-    fun calculateRiskScoreCapped() {
-        val riskScore =
-            riskLevels.calculateRiskScore(
-                buildAttenuationDuration(0.5, 0.5, 1.0),
-                buildSummary(4096, 45, 45, 45)
-            )
-
-        assertEquals(9830.4, riskScore)
-    }
-
-    private fun buildAttenuationDuration(
-        high: Double,
-        mid: Double,
-        low: Double,
-        norm: Int = 25,
-        offset: Int = 0
-    ): AttenuationDurationOuterClass.AttenuationDuration {
-        return AttenuationDurationOuterClass.AttenuationDuration
-            .newBuilder()
-            .setRiskScoreNormalizationDivisor(norm)
-            .setDefaultBucketOffset(offset)
-            .setWeights(
-                AttenuationDurationOuterClass.Weights
-                    .newBuilder()
-                    .setHigh(high)
-                    .setMid(mid)
-                    .setLow(low)
-                    .build()
-            )
-            .build()
-    }
-
-    private fun buildSummary(
-        maxRisk: Int = 0,
-        lowAttenuation: Int = 0,
-        midAttenuation: Int = 0,
-        highAttenuation: Int = 0
-    ): ExposureSummary {
-        val intArray = IntArray(3)
-        intArray[0] = lowAttenuation
-        intArray[1] = midAttenuation
-        intArray[2] = highAttenuation
-        return ExposureSummary.ExposureSummaryBuilder()
-            .setMaximumRiskScore(maxRisk)
-            .setAttenuationDurations(intArray)
-            .build()
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt
deleted file mode 100644
index b4a6ffd64b8a5ba18289f74bd437da2b0d63e769..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt
+++ /dev/null
@@ -1,112 +0,0 @@
-package de.rki.coronawarnapp.storage
-
-import com.google.android.gms.nearby.exposurenotification.ExposureSummary
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
-import io.mockk.MockKAnnotations
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.every
-import io.mockk.impl.annotations.MockK
-import io.mockk.mockk
-import io.mockk.mockkObject
-import io.mockk.unmockkAll
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import java.util.UUID
-
-/**
- * ExposureSummaryRepository test.
- */
-class ExposureSummaryRepositoryTest {
-
-    @MockK
-    private lateinit var dao: ExposureSummaryDao
-    private lateinit var repository: ExposureSummaryRepository
-
-    @Before
-    fun setUp() {
-        MockKAnnotations.init(this)
-        repository = ExposureSummaryRepository(dao)
-
-        mockkObject(InternalExposureNotificationClient)
-        coEvery { InternalExposureNotificationClient.asyncGetExposureSummary(any()) } returns buildSummary()
-        coEvery { InternalExposureNotificationClient.asyncIsEnabled() } returns true
-
-        coEvery { dao.getExposureSummaryEntities() } returns listOf()
-        coEvery { dao.getLatestExposureSummary() } returns null
-        coEvery { dao.insertExposureSummaryEntity(any()) } returns 0
-    }
-
-    /**
-     * Test DAO is called.
-     */
-    @Test
-    fun testGet() {
-        runBlocking {
-            repository.getExposureSummaryEntities()
-
-            coVerify {
-                dao.getExposureSummaryEntities()
-            }
-        }
-    }
-
-    /**
-     * Test DAO is called.
-     */
-    @Test
-    fun testGetLatest() {
-        runBlocking {
-            val token = UUID.randomUUID().toString()
-            repository.getLatestExposureSummary(token)
-
-            coVerify {
-                InternalExposureNotificationClient.asyncGetExposureSummary(token)
-            }
-        }
-    }
-
-    /**
-     * Test DAO is called.
-     */
-    @Test
-    fun testInsert() {
-        val es = mockk<ExposureSummary>()
-        every { es.attenuationDurationsInMinutes } returns intArrayOf(0)
-        every { es.daysSinceLastExposure } returns 1
-        every { es.matchedKeyCount } returns 1
-        every { es.maximumRiskScore } returns 0
-        every { es.summationRiskScore } returns 0
-
-        runBlocking {
-            repository.insertExposureSummaryEntity(es)
-
-            coVerify {
-                dao.insertExposureSummaryEntity(any())
-            }
-        }
-    }
-
-    private fun buildSummary(
-        maxRisk: Int = 0,
-        lowAttenuation: Int = 0,
-        midAttenuation: Int = 0,
-        highAttenuation: Int = 0
-    ): ExposureSummary {
-        val intArray = IntArray(3)
-        intArray[0] = lowAttenuation
-        intArray[1] = midAttenuation
-        intArray[2] = highAttenuation
-        return ExposureSummary.ExposureSummaryBuilder()
-            .setMaximumRiskScore(maxRisk)
-            .setAttenuationDurations(intArray)
-            .build()
-    }
-
-    @After
-    fun cleanUp() {
-        unmockkAll()
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt
index 5b92e22d28896a9dd1471402f68e39496062d51d..e904e2159df50ee958e28227563b8832e56fc6d6 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt
@@ -8,55 +8,55 @@ class VersionComparatorTest {
 
     @Test
     fun testVersionMajorOlder() {
-        val result = VersionComparator.isVersionOlder("1.0.0", "2.0.0")
+        val result = VersionComparator.isVersionOlder(1000000, 2000000)
         assertThat(result, `is`(true))
     }
 
     @Test
     fun testVersionMinorOlder() {
-        val result = VersionComparator.isVersionOlder("1.0.0", "1.1.0")
+        val result = VersionComparator.isVersionOlder(1000000, 1010000)
         assertThat(result, `is`(true))
     }
 
     @Test
     fun testVersionPatchOlder() {
-        val result = VersionComparator.isVersionOlder("1.0.1", "1.0.2")
+        val result = VersionComparator.isVersionOlder(1000100, 1000200)
         assertThat(result, `is`(true))
     }
 
     @Test
     fun testVersionMajorNewer() {
-        val result = VersionComparator.isVersionOlder("2.0.0", "1.0.0")
+        val result = VersionComparator.isVersionOlder(2000000, 1000000)
         assertThat(result, `is`(false))
     }
 
     @Test
     fun testVersionMinorNewer() {
-        val result = VersionComparator.isVersionOlder("1.2.0", "1.1.0")
+        val result = VersionComparator.isVersionOlder(1020000, 1010000)
         assertThat(result, `is`(false))
     }
 
     @Test
     fun testVersionPatchNewer() {
-        val result = VersionComparator.isVersionOlder("1.0.3", "1.0.2")
+        val result = VersionComparator.isVersionOlder(1000300, 1000200)
         assertThat(result, `is`(false))
     }
 
     @Test
     fun testSameVersion() {
-        val result = VersionComparator.isVersionOlder("1.0.1", "1.0.1")
+        val result = VersionComparator.isVersionOlder(1000100, 1000100)
         assertThat(result, `is`(false))
     }
 
     @Test
     fun testIfMajorIsNewerButMinorSmallerNumber() {
-        val result = VersionComparator.isVersionOlder("3.1.0", "1.2.0")
+        val result = VersionComparator.isVersionOlder(3010000, 1020000)
         assertThat(result, `is`(false))
     }
 
     @Test
     fun testIfMinorIsNewerButPatchSmallerNumber() {
-        val result = VersionComparator.isVersionOlder("1.3.1", "1.2.4")
+        val result = VersionComparator.isVersionOlder(1030100, 1020400)
         assertThat(result, `is`(false))
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt
index 003239700b5cac02e2fc7e0bbcb2e0dccf801133..ec97022501f8487302e33279ca3a5cc1baefac4b 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt
@@ -10,18 +10,18 @@ import io.mockk.impl.annotations.MockK
 import io.mockk.mockkObject
 import io.mockk.verify
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.test.TestCoroutineScope
-import kotlinx.coroutines.test.runBlockingTest
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
+import testhelpers.coroutines.runBlockingTest2
+import testhelpers.coroutines.test
 
 class BackgroundModeStatusTest : BaseTest() {
 
     @MockK lateinit var context: Context
-    private val scope: CoroutineScope = TestCoroutineScope()
 
     @BeforeEach
     fun setup() {
@@ -34,23 +34,21 @@ class BackgroundModeStatusTest : BaseTest() {
         clearAllMocks()
     }
 
-    private fun createInstance(): BackgroundModeStatus = BackgroundModeStatus(
+    private fun createInstance(scope: CoroutineScope): BackgroundModeStatus = BackgroundModeStatus(
         context = context,
         appScope = scope
     )
 
     @Test
-    fun `init is sideeffect free and lazy`() {
-        createInstance()
+    fun `init is sideeffect free and lazy`() = runBlockingTest2(ignoreActive = true) {
+        createInstance(scope = this)
         verify { context wasNot Called }
     }
 
     @Test
-    fun isAutoModeEnabled() = runBlockingTest {
-        every { ConnectivityHelper.autoModeEnabled(any()) } returnsMany listOf(
-            true, false, true, false
-        )
-        createInstance().apply {
+    fun isAutoModeEnabled() = runBlockingTest2(ignoreActive = true) {
+        every { ConnectivityHelper.autoModeEnabled(any()) } returnsMany listOf(true, false, true, false)
+        createInstance(scope = this).apply {
             isAutoModeEnabled.first() shouldBe true
             isAutoModeEnabled.first() shouldBe false
             isAutoModeEnabled.first() shouldBe true
@@ -58,14 +56,60 @@ class BackgroundModeStatusTest : BaseTest() {
     }
 
     @Test
-    fun isBackgroundRestricted() = runBlockingTest {
-        every { ConnectivityHelper.isBackgroundRestricted(any()) } returnsMany listOf(
-            false, true, false
-        )
-        createInstance().apply {
+    fun `isAutoModeEnabled is shared but not cached`() = runBlockingTest2(ignoreActive = true) {
+        every { ConnectivityHelper.autoModeEnabled(any()) } returnsMany listOf(true, false, true, false)
+
+        val instance = createInstance(scope = this)
+
+        val collector1 = instance.isAutoModeEnabled.test(tag = "1", startOnScope = this)
+        val collector2 = instance.isAutoModeEnabled.test(tag = "2", startOnScope = this)
+
+        delay(500)
+
+        collector1.latestValue shouldBe true
+        collector2.latestValue shouldBe true
+
+        collector1.cancel()
+        collector2.cancel()
+
+        advanceUntilIdle()
+
+        verify(exactly = 1) { ConnectivityHelper.autoModeEnabled(any()) }
+
+        instance.isAutoModeEnabled.first() shouldBe false
+    }
+
+    @Test
+    fun isBackgroundRestricted() = runBlockingTest2(ignoreActive = true) {
+        every { ConnectivityHelper.isBackgroundRestricted(any()) } returnsMany listOf(false, true, false)
+        createInstance(scope = this).apply {
             isBackgroundRestricted.first() shouldBe false
             isBackgroundRestricted.first() shouldBe true
             isBackgroundRestricted.first() shouldBe false
         }
     }
+
+    @Test
+    fun `isBackgroundRestricted is shared but not cached`() = runBlockingTest2(ignoreActive = true) {
+        every { ConnectivityHelper.isBackgroundRestricted(any()) } returnsMany listOf(true, false, true, false)
+
+        val instance = createInstance(scope = this)
+
+        val collector1 = instance.isBackgroundRestricted.test(tag = "1", startOnScope = this)
+        val collector2 = instance.isBackgroundRestricted.test(tag = "2", startOnScope = this)
+
+        delay(500)
+
+        collector1.latestValue shouldBe true
+        collector2.latestValue shouldBe true
+
+        collector1.cancel()
+        collector2.cancel()
+
+        advanceUntilIdle()
+
+        verify(exactly = 1) { ConnectivityHelper.isBackgroundRestricted(any()) }
+
+        instance.isBackgroundRestricted.first() shouldBe false
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt
deleted file mode 100644
index 110d8cbeed2aef3b469c0355c3a848025f1faadf..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package de.rki.coronawarnapp.util
-
-import com.google.android.gms.common.api.ApiException
-import com.google.android.gms.common.api.CommonStatusCodes.API_NOT_CONNECTED
-import com.google.android.gms.common.api.Status
-import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
-import io.kotest.matchers.shouldBe
-import io.mockk.Called
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockkObject
-import io.mockk.unmockkObject
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
-
-@ExperimentalCoroutinesApi
-internal class GoogleAPIVersionTest {
-
-    private lateinit var classUnderTest: GoogleAPIVersion
-
-    @BeforeEach
-    fun setUp() {
-        mockkObject(InternalExposureNotificationClient)
-        classUnderTest = GoogleAPIVersion()
-    }
-
-    @AfterEach
-    fun tearDown() {
-        unmockkObject(InternalExposureNotificationClient)
-    }
-
-    @Test
-    fun `isAbove API v16 is true for v17`() {
-        coEvery { InternalExposureNotificationClient.getVersion() } returns 17000000L
-
-        runBlockingTest {
-            classUnderTest.isAtLeast(GoogleAPIVersion.V16) shouldBe true
-        }
-    }
-
-    @Test
-    fun `isAbove API v16 is false for v15`() {
-        coEvery { InternalExposureNotificationClient.getVersion() } returns 15000000L
-
-        runBlockingTest {
-            classUnderTest.isAtLeast(GoogleAPIVersion.V16) shouldBe false
-        }
-    }
-
-    @Test
-    fun `isAbove API v16 throws IllegalArgument for invalid version`() {
-        assertThrows<IllegalArgumentException> {
-            runBlockingTest {
-                classUnderTest.isAtLeast(1L)
-            }
-            coVerify {
-                InternalExposureNotificationClient.getVersion() wasNot Called
-            }
-        }
-    }
-
-    @Test
-    fun `isAbove API v16 false when APIException for too low version`() {
-        coEvery { InternalExposureNotificationClient.getVersion() } throws
-                ApiException(Status(API_NOT_CONNECTED))
-
-        runBlockingTest {
-            classUnderTest.isAtLeast(GoogleAPIVersion.V16) shouldBe false
-        }
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt
index 3de7010f7bf7105aa25a8738dbcd6772f0c140b4..9b23f5eca4035d5da1266bdc020f05dc3bb131fe 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.util.flow
 
+import de.rki.coronawarnapp.util.mutate
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.types.instanceOf
@@ -84,7 +85,7 @@ class HotDataFlowTest : BaseTest() {
         )
 
         testScope.apply {
-            runBlockingTest2(permanentJobs = true) {
+            runBlockingTest2(ignoreActive = true) {
                 hotData.data.first() shouldBe "Test"
                 hotData.data.first() shouldBe "Test"
             }
@@ -107,7 +108,7 @@ class HotDataFlowTest : BaseTest() {
         )
 
         testScope.apply {
-            runBlockingTest2(permanentJobs = true) {
+            runBlockingTest2(ignoreActive = true) {
                 hotData.data.first() shouldBe "Test"
                 hotData.data.first() shouldBe "Test"
             }
@@ -150,6 +151,48 @@ class HotDataFlowTest : BaseTest() {
         coVerify(exactly = 1) { valueProvider.invoke(any()) }
     }
 
+    data class TestData(
+        val number: Long = 1
+    )
+
+    @Test
+    fun `check multi threading value updates with more complex data`() {
+        val testScope = TestCoroutineScope()
+        val valueProvider = mockk<suspend CoroutineScope.() -> Map<String, TestData>>()
+        coEvery { valueProvider.invoke(any()) } returns mapOf("data" to TestData())
+
+        val hotData = HotDataFlow(
+            loggingTag = "tag",
+            scope = testScope,
+            startValueProvider = valueProvider,
+            sharingBehavior = SharingStarted.Lazily
+        )
+
+        val testCollector = hotData.data.test(startOnScope = testScope)
+        testCollector.silent = true
+
+        (1..10).forEach { _ ->
+            thread {
+                (1..400).forEach { _ ->
+                    hotData.updateSafely {
+                        mutate {
+                            this["data"] = getValue("data").copy(
+                                number = getValue("data").number + 1
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        runBlocking {
+            testCollector.await { list, l -> list.size == 4001 }
+            testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList()
+        }
+
+        coVerify(exactly = 1) { valueProvider.invoke(any()) }
+    }
+
     @Test
     fun `only emit new values if they actually changed updates`() {
         val testScope = TestCoroutineScope()
@@ -188,10 +231,10 @@ class HotDataFlowTest : BaseTest() {
             sharingBehavior = SharingStarted.Lazily
         )
 
-        testScope.runBlockingTest2(permanentJobs = true) {
-            val sub1 = hotData.data.test().start(scope = this)
-            val sub2 = hotData.data.test().start(scope = this)
-            val sub3 = hotData.data.test().start(scope = this)
+        testScope.runBlockingTest2(ignoreActive = true) {
+            val sub1 = hotData.data.test(tag = "sub1", startOnScope = this)
+            val sub2 = hotData.data.test(tag = "sub2", startOnScope = this)
+            val sub3 = hotData.data.test(tag = "sub3", startOnScope = this)
 
             hotData.updateSafely { "A" }
             hotData.updateSafely { "B" }
@@ -208,6 +251,49 @@ class HotDataFlowTest : BaseTest() {
         coVerify(exactly = 1) { valueProvider.invoke(any()) }
     }
 
+    @Test
+    fun `update queue is wiped on completion`() = runBlockingTest2(ignoreActive = true) {
+        val valueProvider = mockk<suspend CoroutineScope.() -> Long>()
+        coEvery { valueProvider.invoke(any()) } returns 1
+
+        val hotData = HotDataFlow(
+            loggingTag = "tag",
+            scope = this,
+            coroutineContext = this.coroutineContext,
+            startValueProvider = valueProvider,
+            sharingBehavior = SharingStarted.WhileSubscribed(replayExpirationMillis = 0)
+        )
+
+        val testCollector1 = hotData.data.test(tag = "collector1", startOnScope = this)
+        testCollector1.silent = false
+
+        (1..10).forEach { _ ->
+            hotData.updateSafely {
+                this + 1L
+            }
+        }
+
+        advanceUntilIdle()
+
+        testCollector1.await { list, _ -> list.size == 11 }
+        testCollector1.latestValues shouldBe (1L..11L).toList()
+
+        testCollector1.cancel()
+        testCollector1.awaitFinal()
+
+        val testCollector2 = hotData.data.test(tag = "collector2", startOnScope = this)
+        testCollector2.silent = false
+
+        advanceUntilIdle()
+
+        testCollector2.cancel()
+        testCollector2.awaitFinal()
+
+        testCollector2.latestValues shouldBe listOf(1L)
+
+        coVerify(exactly = 2) { valueProvider.invoke(any()) }
+    }
+
     @Test
     fun `blocking update is actually blocking`() = runBlocking {
         val testScope = TestCoroutineScope()
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt
index c8310865349f45752a771fee79b85b6bfa4e74cc..c7f8e7b15f275e2a9b405de92603fa1531f8f7db 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt
@@ -8,8 +8,8 @@ import android.text.style.ForegroundColorSpan
 import android.view.View
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.ui.submission.ApiRequestState
 import de.rki.coronawarnapp.util.DeviceUIState
+import de.rki.coronawarnapp.util.NetworkRequestWrapper
 import io.mockk.MockKAnnotations
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
@@ -74,52 +74,76 @@ class FormatterSubmissionHelperTest {
         every { context.getDrawable(R.drawable.ic_test_result_illustration_negative) } returns drawable
     }
 
-    private fun formatTestResultSpinnerVisibleBase(oUiStateState: ApiRequestState?, iResult: Int) {
-        val result = formatTestResultSpinnerVisible(uiStateState = oUiStateState)
+    private fun formatTestResultSpinnerVisibleBase(
+        oUiStateState: NetworkRequestWrapper<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
+        val result = formatTestResultSpinnerVisible(uiState = oUiStateState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultVisibleBase(oUiStateState: ApiRequestState?, iResult: Int) {
-        val result = formatTestResultVisible(uiStateState = oUiStateState)
+    private fun formatTestResultVisibleBase(
+        oUiStateState: NetworkRequestWrapper<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
+        val result = formatTestResultVisible(uiState = oUiStateState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultStatusTextBase(oUiState: DeviceUIState?, iResult: String) {
+    private fun formatTestResultStatusTextBase(
+        oUiState: NetworkRequestWrapper<DeviceUIState, Throwable>?,
+        iResult: String
+    ) {
         val result = formatTestResultStatusText(uiState = oUiState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultStatusColorBase(oUiState: DeviceUIState?, iResult: Int) {
+    private fun formatTestResultStatusColorBase(
+        oUiState: NetworkRequestWrapper<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
         val result = formatTestResultStatusColor(uiState = oUiState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestStatusIconBase(oUiState: DeviceUIState?) {
+    private fun formatTestStatusIconBase(oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?) {
         val result = formatTestStatusIcon(uiState = oUiState)
         assertThat(result, `is`(drawable))
     }
 
-    private fun formatTestResultPendingStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) {
+    private fun formatTestResultPendingStepsVisibleBase(
+        oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
         val result = formatTestResultPendingStepsVisible(uiState = oUiState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultNegativeStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) {
+    private fun formatTestResultNegativeStepsVisibleBase(
+        oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
         val result = formatTestResultNegativeStepsVisible(uiState = oUiState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultPositiveStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) {
+    private fun formatTestResultPositiveStepsVisibleBase(
+        oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
         val result = formatTestResultPositiveStepsVisible(uiState = oUiState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultInvalidStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) {
+    private fun formatTestResultInvalidStepsVisibleBase(
+        oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?,
+        iResult: Int
+    ) {
         val result = formatTestResultInvalidStepsVisible(uiState = oUiState)
         assertThat(result, `is`(iResult))
     }
 
-    private fun formatTestResultBase(oUiState: DeviceUIState?) {
+    private fun formatTestResultBase(oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?) {
         mockkConstructor(SpannableStringBuilder::class)
 
         val spannableStringBuilder1 =
@@ -147,19 +171,19 @@ class FormatterSubmissionHelperTest {
     fun formatTestResultSpinnerVisible() {
         formatTestResultSpinnerVisibleBase(oUiStateState = null, iResult = View.VISIBLE)
         formatTestResultSpinnerVisibleBase(
-            oUiStateState = ApiRequestState.FAILED,
+            oUiStateState = NetworkRequestWrapper.RequestFailed(mockk()),
             iResult = View.VISIBLE
         )
         formatTestResultSpinnerVisibleBase(
-            oUiStateState = ApiRequestState.IDLE,
+            oUiStateState = NetworkRequestWrapper.RequestIdle,
             iResult = View.VISIBLE
         )
         formatTestResultSpinnerVisibleBase(
-            oUiStateState = ApiRequestState.STARTED,
+            oUiStateState = NetworkRequestWrapper.RequestStarted,
             iResult = View.VISIBLE
         )
         formatTestResultSpinnerVisibleBase(
-            oUiStateState = ApiRequestState.SUCCESS,
+            oUiStateState = NetworkRequestWrapper.RequestSuccessful(mockk()),
             iResult = View.GONE
         )
     }
@@ -167,10 +191,13 @@ class FormatterSubmissionHelperTest {
     @Test
     fun formatTestResultVisible() {
         formatTestResultVisibleBase(oUiStateState = null, iResult = View.GONE)
-        formatTestResultVisibleBase(oUiStateState = ApiRequestState.FAILED, iResult = View.GONE)
-        formatTestResultVisibleBase(oUiStateState = ApiRequestState.IDLE, iResult = View.GONE)
-        formatTestResultVisibleBase(oUiStateState = ApiRequestState.STARTED, iResult = View.GONE)
-        formatTestResultVisibleBase(oUiStateState = ApiRequestState.SUCCESS, iResult = View.VISIBLE)
+        formatTestResultVisibleBase(oUiStateState = NetworkRequestWrapper.RequestFailed(mockk()), iResult = View.GONE)
+        formatTestResultVisibleBase(oUiStateState = NetworkRequestWrapper.RequestIdle, iResult = View.GONE)
+        formatTestResultVisibleBase(oUiStateState = NetworkRequestWrapper.RequestStarted, iResult = View.GONE)
+        formatTestResultVisibleBase(
+            oUiStateState = NetworkRequestWrapper.RequestSuccessful(mockk()),
+            iResult = View.VISIBLE
+        )
     }
 
     @Test
@@ -180,35 +207,35 @@ class FormatterSubmissionHelperTest {
             iResult = context.getString(R.string.test_result_card_status_invalid)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.PAIRED_NEGATIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE),
             iResult = context.getString(R.string.test_result_card_status_negative)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.PAIRED_ERROR,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR),
             iResult = context.getString(R.string.test_result_card_status_invalid)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.PAIRED_NO_RESULT,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT),
             iResult = context.getString(R.string.test_result_card_status_invalid)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
             iResult = context.getString(R.string.test_result_card_status_positive)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN),
             iResult = context.getString(R.string.test_result_card_status_positive)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.SUBMITTED_FINAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL),
             iResult = context.getString(R.string.test_result_card_status_invalid)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.SUBMITTED_INITIAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL),
             iResult = context.getString(R.string.test_result_card_status_invalid)
         )
         formatTestResultStatusTextBase(
-            oUiState = DeviceUIState.UNPAIRED,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED),
             iResult = context.getString(R.string.test_result_card_status_invalid)
         )
     }
@@ -220,35 +247,35 @@ class FormatterSubmissionHelperTest {
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.PAIRED_NEGATIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE),
             iResult = context.getColor(R.color.colorTextSemanticGreen)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.PAIRED_ERROR,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.PAIRED_NO_RESULT,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.SUBMITTED_FINAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.SUBMITTED_INITIAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
         formatTestResultStatusColorBase(
-            oUiState = DeviceUIState.UNPAIRED,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED),
             iResult = context.getColor(R.color.colorTextSemanticRed)
         )
     }
@@ -256,49 +283,49 @@ class FormatterSubmissionHelperTest {
     @Test
     fun formatTestStatusIcon() {
         formatTestStatusIconBase(oUiState = null)
-        formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_NEGATIVE)
-        formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_ERROR)
-        formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_NO_RESULT)
-        formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_POSITIVE)
-        formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN)
-        formatTestStatusIconBase(oUiState = DeviceUIState.SUBMITTED_FINAL)
-        formatTestStatusIconBase(oUiState = DeviceUIState.SUBMITTED_INITIAL)
-        formatTestStatusIconBase(oUiState = DeviceUIState.UNPAIRED)
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL))
+        formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED))
     }
 
     @Test
     fun formatTestResultPendingStepsVisible() {
         formatTestResultPendingStepsVisibleBase(oUiState = null, iResult = View.GONE)
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NEGATIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE),
             iResult = View.GONE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_ERROR,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR),
             iResult = View.GONE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NO_RESULT,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT),
             iResult = View.VISIBLE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
             iResult = View.GONE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN),
             iResult = View.GONE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_FINAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL),
             iResult = View.GONE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_INITIAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL),
             iResult = View.GONE
         )
         formatTestResultPendingStepsVisibleBase(
-            oUiState = DeviceUIState.UNPAIRED,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED),
             iResult = View.GONE
         )
     }
@@ -307,35 +334,35 @@ class FormatterSubmissionHelperTest {
     fun formatTestResultNegativeStepsVisible() {
         formatTestResultNegativeStepsVisibleBase(oUiState = null, iResult = View.GONE)
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NEGATIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE),
             iResult = View.VISIBLE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_ERROR,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR),
             iResult = View.GONE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NO_RESULT,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT),
             iResult = View.GONE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
             iResult = View.GONE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN),
             iResult = View.GONE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_FINAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL),
             iResult = View.GONE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_INITIAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL),
             iResult = View.GONE
         )
         formatTestResultNegativeStepsVisibleBase(
-            oUiState = DeviceUIState.UNPAIRED,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED),
             iResult = View.GONE
         )
     }
@@ -344,35 +371,35 @@ class FormatterSubmissionHelperTest {
     fun formatTestResultPositiveStepsVisible() {
         formatTestResultPositiveStepsVisibleBase(oUiState = null, iResult = View.GONE)
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NEGATIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE),
             iResult = View.GONE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_ERROR,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR),
             iResult = View.GONE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NO_RESULT,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT),
             iResult = View.GONE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
             iResult = View.VISIBLE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN),
             iResult = View.VISIBLE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_FINAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL),
             iResult = View.GONE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_INITIAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL),
             iResult = View.GONE
         )
         formatTestResultPositiveStepsVisibleBase(
-            oUiState = DeviceUIState.UNPAIRED,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED),
             iResult = View.GONE
         )
     }
@@ -381,35 +408,35 @@ class FormatterSubmissionHelperTest {
     fun formatTestResultInvalidStepsVisible() {
         formatTestResultInvalidStepsVisibleBase(oUiState = null, iResult = View.GONE)
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NEGATIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE),
             iResult = View.GONE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_ERROR,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR),
             iResult = View.VISIBLE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_NO_RESULT,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT),
             iResult = View.GONE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE),
             iResult = View.GONE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN),
             iResult = View.GONE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_FINAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL),
             iResult = View.GONE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.SUBMITTED_INITIAL,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL),
             iResult = View.GONE
         )
         formatTestResultInvalidStepsVisibleBase(
-            oUiState = DeviceUIState.UNPAIRED,
+            oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED),
             iResult = View.GONE
         )
     }
@@ -417,14 +444,14 @@ class FormatterSubmissionHelperTest {
     @Test
     fun formatTestResult() {
         formatTestResultBase(oUiState = null)
-        formatTestResultBase(oUiState = DeviceUIState.PAIRED_NEGATIVE)
-        formatTestResultBase(oUiState = DeviceUIState.PAIRED_ERROR)
-        formatTestResultBase(oUiState = DeviceUIState.PAIRED_NO_RESULT)
-        formatTestResultBase(oUiState = DeviceUIState.PAIRED_POSITIVE)
-        formatTestResultBase(oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN)
-        formatTestResultBase(oUiState = DeviceUIState.SUBMITTED_FINAL)
-        formatTestResultBase(oUiState = DeviceUIState.SUBMITTED_INITIAL)
-        formatTestResultBase(oUiState = DeviceUIState.UNPAIRED)
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL))
+        formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED))
     }
 
     @After
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 468877bbaaa72b0dfc9f3177073a15d4b6e7024c..88976e539bab83ed2fb7aa482ee3f35e26557652 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
@@ -6,7 +6,9 @@ import dagger.Module
 import dagger.Provides
 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.task.TaskController
 import de.rki.coronawarnapp.util.di.AssistedInjectModule
 import io.github.classgraph.ClassGraph
@@ -87,4 +89,11 @@ class MockProvider {
 
     @Provides
     fun taskController(): TaskController = mockk()
+
+    // For ExposureStateUpdateWorker
+    @Provides
+    fun enfClient(): ENFClient = mockk()
+
+    @Provides
+    fun exposureSummaryRepository(): ExposureResultStore = mockk()
 }
diff --git a/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt b/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt
index 3fc56ce9a503dcd65c2afb6f91e657557776c2d8..5a19fa308a2f5ec037373528c26e0ea099810aa1 100644
--- a/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt
+++ b/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt
@@ -16,14 +16,17 @@ import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.test.TestCoroutineScope
+import kotlinx.coroutines.withTimeout
+import org.joda.time.Duration
 import timber.log.Timber
 
 fun <T> Flow<T>.test(
     tag: String? = null,
-    startOnScope: CoroutineScope
-): TestCollector<T> = test(tag ?: "FlowTest").start(scope = startOnScope)
+    startOnScope: CoroutineScope = TestCoroutineScope()
+): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = startOnScope)
 
-fun <T> Flow<T>.test(
+fun <T> Flow<T>.createTest(
     tag: String? = null
 ): TestCollector<T> = TestCollector(this, tag ?: "FlowTest")
 
@@ -74,9 +77,14 @@ class TestCollector<T>(
     val latestValues: List<T>
         get() = collectedValues
 
-    fun await(condition: (List<T>, T) -> Boolean): T = runBlocking {
-        emissions().first {
-            condition(collectedValues, it)
+    fun await(
+        timeout: Duration = Duration.standardSeconds(10),
+        condition: (List<T>, T) -> Boolean
+    ): T = runBlocking {
+        withTimeout(timeMillis = timeout.millis) {
+            emissions().first {
+                condition(collectedValues, it)
+            }
         }
     }
 
@@ -95,6 +103,8 @@ class TestCollector<T>(
     }
 
     fun cancel() {
+        if (job.isCompleted) throw IllegalStateException("Flow is already canceled.")
+
         runBlocking {
             job.cancelAndJoin()
         }
diff --git a/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt b/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt
index 375adf36e462e42753b985160ba5062cbcdc3397..68d1ba19c174d4bb013eb4bcba609566a31cafdf 100644
--- a/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt
+++ b/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt
@@ -11,10 +11,10 @@ import kotlin.coroutines.EmptyCoroutineContext
 
 @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0
 fun TestCoroutineScope.runBlockingTest2(
-    permanentJobs: Boolean = false,
+    ignoreActive: Boolean = false,
     block: suspend TestCoroutineScope.() -> Unit
 ): Unit = runBlockingTest2(
-    ignoreActive = permanentJobs,
+    ignoreActive = ignoreActive,
     context = coroutineContext,
     testBody = block
 )
diff --git a/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt b/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt
index d0ed11f4ec929f0ba4ec6ff7e89bfc832fef9f01..321a733ae210c6fa0dd2600912032fdbb4d08159 100644
--- a/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt
+++ b/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt
@@ -8,12 +8,12 @@ import io.mockk.mockk
 
 object MockGMSTask {
     fun <T> forError(error: Exception): Task<T> = mockk<Task<T>>().apply {
-        every { addOnSuccessListener(any()) } answers {
+        every { addOnFailureListener(any()) } answers {
             val listener = arg<OnFailureListener>(0)
             listener.onFailure(error)
             this@apply
         }
-        every { addOnFailureListener(any()) } returns this
+        every { addOnSuccessListener(any()) } returns this
     }
 
     fun <T> forValue(value: T): Task<T> = mockk<Task<T>>().apply {
diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt
index 33d46578e1a956c645d549edc3f793d2fc9b27df..0b855e1b9938ed9d47650a26c85ba5bbe9ba4927 100644
--- a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt
+++ b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt
@@ -16,5 +16,6 @@ fun <T> mockFlowPreference(
         val updateCall = arg<(T) -> T>(0)
         flow.value = updateCall(flow.value)
     }
+
     return instance
 }
diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt
index a6294b00f54bcdbc96ab6bd18195389fc1a79e51..c094e560e5401f80bbf7bedff36af3537cc8a67e 100644
--- a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt
+++ b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt
@@ -3,6 +3,7 @@ package testhelpers.preferences
 import android.content.SharedPreferences
 
 class MockSharedPreferences : SharedPreferences {
+    private val listeners = mutableListOf<SharedPreferences.OnSharedPreferenceChangeListener>()
     private val dataMap = mutableMapOf<String, Any>()
     val dataMapPeek: Map<String, Any>
         get() = dataMap.toMap()
@@ -36,12 +37,12 @@ class MockSharedPreferences : SharedPreferences {
         dataMap.putAll(newData)
     }
 
-    override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {
-        throw NotImplementedError()
+    override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
+        listeners.add(listener)
     }
 
-    override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) {
-        throw NotImplementedError()
+    override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
+        listeners.remove(listener)
     }
 
     private fun createEditor(
diff --git a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
new file mode 100644
index 0000000000000000000000000000000000000000..405aa42953a5f7a37a9ae6e98748fb95bafc9ee0
--- /dev/null
+++ b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json
@@ -0,0 +1,1065 @@
+{
+  "__comment__": "JSON has been generated from YAML, see README",
+  "defaultRiskCalculationConfiguration": {
+    "minutesAtAttenuationFilters": [
+      {
+        "attenuationRange": {
+          "min": 0,
+          "max": 73,
+          "maxExclusive": true
+        },
+        "dropIfMinutesInRange": {
+          "min": 0,
+          "max": 10,
+          "maxExclusive": true
+        }
+      }
+    ],
+    "trlFilters": [
+      {
+        "dropIfTrlInRange": {
+          "min": 1,
+          "max": 2
+        }
+      }
+    ],
+    "minutesAtAttenuationWeights": [
+      {
+        "attenuationRange": {
+          "min": 0,
+          "max": 55,
+          "maxExclusive": true
+        },
+        "weight": 1
+      },
+      {
+        "attenuationRange": {
+          "min": 55,
+          "max": 63,
+          "maxExclusive": true
+        },
+        "weight": 0.5
+      }
+    ],
+    "normalizedTimePerEWToRiskLevelMapping": [
+      {
+        "normalizedTimeRange": {
+          "min": 0,
+          "max": 15,
+          "maxExclusive": true
+        },
+        "riskLevel": 1
+      },
+      {
+        "normalizedTimeRange": {
+          "min": 15,
+          "max": 9999
+        },
+        "riskLevel": 2
+      }
+    ],
+    "normalizedTimePerDayToRiskLevelMapping": [
+      {
+        "normalizedTimeRange": {
+          "min": 0,
+          "max": 15,
+          "maxExclusive": true
+        },
+        "riskLevel": 1
+      },
+      {
+        "normalizedTimeRange": {
+          "min": 15,
+          "max": 9999
+        },
+        "riskLevel": 2
+      }
+    ],
+    "trlEncoding": {
+      "infectiousnessOffsetStandard": 0,
+      "infectiousnessOffsetHigh": 4,
+      "reportTypeOffsetRecursive": 4,
+      "reportTypeOffsetSelfReport": 3,
+      "reportTypeOffsetConfirmedClinicalDiagnosis": 2,
+      "reportTypeOffsetConfirmedTest": 1
+    },
+    "transmissionRiskLevelMultiplier": 0.2
+  },
+  "testCases": [
+    {
+      "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (< 10 minutes)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 299
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "keep Exposure Windows that match minutesAtAttenuationFilters (>= 10 minutes)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (>= 73 dB)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 73,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 73,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "keep Exposure Windows that match minutesAtAttenuationFilters (< 73 dB)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 72,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 72,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "drop Exposure Windows that do not match trlFilters (<= 2)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "keep Exposure Windows that match trlFilters (> 2)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "identify Exposure Window as Low Risk based on normalizedTime (< 15)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 1,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 299
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "identify Exposure Window as High Risk based on normalizedTime (>= 15)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 1,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 1
+    },
+    {
+      "description": "identify the most recent date with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 3,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 4,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 3,
+      "expAgeOfMostRecentDateWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 3,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 1,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 2,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "determine High Risk in total if there are sufficient Exposure Windows with a Low Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 1
+    },
+    {
+      "description": "identify the most recent date with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 3,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 4,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 2,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 3,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 3
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 1
+    },
+    {
+      "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 1,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 1
+    },
+    {
+      "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 1
+    },
+    {
+      "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        },
+        {
+          "ageInDays": 2,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 2,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 2
+    },
+    {
+      "description": "determine High Risk in total if there is at least one Exposure Window with High Risk",
+      "exposureWindows": [
+        {
+          "ageInDays": 2,
+          "reportType": 3,
+          "infectiousness": 1,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        },
+        {
+          "ageInDays": 1,
+          "reportType": 4,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            },
+            {
+              "minAttenuation": 30,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 420
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 2,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithLowRisk": 2,
+      "expAgeOfMostRecentDateWithHighRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 1,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 1
+    },
+    {
+      "description": "handle empty set of Exposure Windows",
+      "exposureWindows": [],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "handle empty set of Scan Instances (should never happen)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 2,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": []
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 0,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": null,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 0,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "handle a typicalAttenuation: of zero (should never happen)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 0,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 70,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    },
+    {
+      "description": "handle secondsSinceLastScan of zero (should never happen)",
+      "exposureWindows": [
+        {
+          "ageInDays": 1,
+          "reportType": 3,
+          "infectiousness": 2,
+          "calibrationConfidence": 0,
+          "scanInstances": [
+            {
+              "minAttenuation": 70,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 0
+            },
+            {
+              "minAttenuation": 70,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            },
+            {
+              "minAttenuation": 70,
+              "typicalAttenuation": 25,
+              "secondsSinceLastScan": 300
+            }
+          ]
+        }
+      ],
+      "expTotalRiskLevel": 1,
+      "expTotalMinimumDistinctEncountersWithLowRisk": 1,
+      "expTotalMinimumDistinctEncountersWithHighRisk": 0,
+      "expAgeOfMostRecentDateWithLowRisk": 1,
+      "expAgeOfMostRecentDateWithHighRisk": null,
+      "expNumberOfDaysWithLowRisk": 1,
+      "expNumberOfDaysWithHighRisk": 0
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt
index 42ad7c156bb1ceff1fb4a3603f1849edb92f444c..41ea40f28f2074b96d99fb6abd733be53881ce85 100644
--- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt
+++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt
@@ -3,8 +3,6 @@ package de.rki.coronawarnapp.test.debugoptions.ui
 import android.content.Context
 import androidx.lifecycle.Observer
 import de.rki.coronawarnapp.environment.EnvironmentSetup
-import de.rki.coronawarnapp.storage.TestSettings
-import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.test.api.ui.EnvironmentState
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
@@ -30,8 +28,6 @@ class DebugOptionsFragmentViewModelTest : BaseTest() {
 
     @MockK private lateinit var environmentSetup: EnvironmentSetup
     @MockK private lateinit var context: Context
-    @MockK private lateinit var testSettings: TestSettings
-    @MockK lateinit var taskController: TaskController
 
     private var currentEnvironment = EnvironmentSetup.Type.DEV
 
@@ -61,9 +57,7 @@ class DebugOptionsFragmentViewModelTest : BaseTest() {
 
     private fun createViewModel(): DebugOptionsFragmentViewModel = DebugOptionsFragmentViewModel(
         context = context,
-        taskController = taskController,
         envSetup = environmentSetup,
-        testSettings = testSettings,
         dispatcherProvider = TestDispatcherProvider
     )
 
diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt
deleted file mode 100644
index 0413a8515b6db83ef95f76efb6e8af70b46238e7..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package de.rki.coronawarnapp.test.risklevel.ui
-
-import android.content.Context
-import androidx.lifecycle.SavedStateHandle
-import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient
-import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
-import de.rki.coronawarnapp.nearby.ENFClient
-import de.rki.coronawarnapp.risk.RiskLevels
-import de.rki.coronawarnapp.task.TaskController
-import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider
-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 io.mockk.mockk
-import kotlinx.coroutines.flow.flowOf
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.extension.ExtendWith
-import testhelpers.BaseTest
-import testhelpers.TestDispatcherProvider
-import testhelpers.extensions.CoroutinesTestExtension
-import testhelpers.extensions.InstantExecutorExtension
-
-@ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class)
-class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() {
-
-    @MockK lateinit var context: Context
-    @MockK lateinit var savedStateHandle: SavedStateHandle
-    @MockK lateinit var enfClient: ENFClient
-    @MockK lateinit var exposureNotificationClient: ExposureNotificationClient
-    @MockK lateinit var keyCacheRepository: KeyCacheRepository
-    @MockK lateinit var tracingCardStateProvider: TracingCardStateProvider
-    @MockK lateinit var taskController: TaskController
-    @MockK lateinit var riskLevels: RiskLevels
-
-    @BeforeEach
-    fun setup() {
-        MockKAnnotations.init(this)
-
-        coEvery { keyCacheRepository.clear() } returns Unit
-        every { enfClient.internalClient } returns exposureNotificationClient
-        every { tracingCardStateProvider.state } returns flowOf(mockk())
-        every { taskController.submit(any()) } just Runs
-    }
-
-    @AfterEach
-    fun teardown() {
-        clearAllMocks()
-    }
-
-    private fun createViewModel(exampleArgs: String? = null): TestRiskLevelCalculationFragmentCWAViewModel =
-        TestRiskLevelCalculationFragmentCWAViewModel(
-            handle = savedStateHandle,
-            exampleArg = exampleArgs,
-            context = context,
-            enfClient = enfClient,
-            keyCacheRepository = keyCacheRepository,
-            tracingCardStateProvider = tracingCardStateProvider,
-            dispatcherProvider = TestDispatcherProvider,
-            riskLevels = riskLevels,
-            taskController = taskController
-        )
-
-    @Test
-    fun `action clearDiagnosisKeys calls the keyCacheRepo`() {
-        val vm = createViewModel()
-
-        vm.clearKeyCache()
-
-        coVerify(exactly = 1) { keyCacheRepository.clear() }
-    }
-
-    @Test
-    fun `action scanLocalQRCodeAndProvide, triggers event`() {
-        val vm = createViewModel()
-
-        vm.startLocalQRCodeScanEvent.value shouldBe null
-
-        vm.scanLocalQRCodeAndProvide()
-
-        vm.startLocalQRCodeScanEvent.value shouldBe Unit
-    }
-}