diff --git a/.reuse/dep5 b/.reuse/dep5
index b1088ff6d57f1f228fdbe25522ebe3b70c80941e..ae4ac319fb88ee2f0c1dc565ae79e4385f673f2d 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -50,4 +50,8 @@ License: Apache-2.0
 
 Files: Corona-Warn-App/libs/play-services-nearby-exposurenotification-18.0.3.aar
 Copyright: Copyright 2020 Google LLC
+License: Apache-2.0
+
+Files: Corona-Warn-App/src/main/res/font/roboto.ttf
+Copyright: 2011 Google Inc.
 License: Apache-2.0
\ No newline at end of file
diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt
index f8ac9f20dbc3afc5118bb6e262863d392401a67c..92111156ce7451c297cd779ec6333f70f9d2ee0d 100644
--- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt
+++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk.storage
 
 import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import kotlinx.coroutines.CoroutineScope
@@ -13,8 +14,14 @@ import javax.inject.Singleton
 class DefaultRiskLevelStorage @Inject constructor(
     riskResultDatabaseFactory: RiskResultDatabase.Factory,
     presenceTracingRiskRepository: PresenceTracingRiskRepository,
-    @AppScope scope: CoroutineScope
-) : BaseRiskLevelStorage(riskResultDatabaseFactory, presenceTracingRiskRepository, scope) {
+    @AppScope scope: CoroutineScope,
+    riskCombinator: RiskCombinator,
+) : BaseRiskLevelStorage(
+    riskResultDatabaseFactory,
+    presenceTracingRiskRepository,
+    scope,
+    riskCombinator
+) {
 
     // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation
     // Taken from TimeVariables.MAX_STALE_EXPOSURE_RISK_RANGE
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt
index 7fbb56a67d86ed6d29371dec56f158905536068e..23a0450fef17a9260ddcd2b06328d3501d15b7c8 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk.storage
 
 import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance
 import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow
@@ -17,8 +18,14 @@ import javax.inject.Singleton
 class DefaultRiskLevelStorage @Inject constructor(
     riskResultDatabaseFactory: RiskResultDatabase.Factory,
     presenceTracingRiskRepository: PresenceTracingRiskRepository,
-    @AppScope val scope: CoroutineScope
-) : BaseRiskLevelStorage(riskResultDatabaseFactory, presenceTracingRiskRepository, scope) {
+    @AppScope val scope: CoroutineScope,
+    riskCombinator: RiskCombinator,
+) : BaseRiskLevelStorage(
+    riskResultDatabaseFactory,
+    presenceTracingRiskRepository,
+    scope,
+    riskCombinator,
+) {
 
     // 14 days, 6 times per day
     // For testers keep all the results!
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt
index 7e0e674bcee1e3f48cb962d9a1f286e9ef20f8ba..dfb0d221f958bb44cadceaff6600824147254b68 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt
@@ -68,7 +68,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor(
         taskRunTime.postValue(duration)
 
         val warningPackages = traceWarningRepository.allMetaData.first()
-        val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first()
+        val overlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first()
         val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull()
 
         val infoText = when {
@@ -99,7 +99,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor(
                     riskCalculationRuntime.postValue(it)
                 },
                 {
-                    val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first()
+                    val checkInWarningOverlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first()
                     val normalizedTimePerCheckInDayList =
                         presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps)
                     val riskStates =
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml
index 590b0a48c28bc6f1771a2a1063778e0c93b67f41..f6b5098993b199f056ddc6741d156a879e71a93d 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml
@@ -97,6 +97,7 @@
 
             <TextView
                 android:id="@+id/info_text_view"
+                style="@style/PosterTextStyle"
                 android:layout_width="0dp"
                 android:layout_height="wrap_content"
                 android:maxLines="2"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifier.kt
new file mode 100644
index 0000000000000000000000000000000000000000..abd313609ebdd09320116af2a9a86fdeafb125c9
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifier.kt
@@ -0,0 +1,76 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import androidx.annotation.StringRes
+import dagger.Reusable
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import javax.inject.Inject
+
+@Reusable
+class TraceLocationVerifier @Inject constructor() {
+    @Suppress("ReturnCount")
+    fun verifyTraceLocation(protoQrCodePayload: TraceLocationOuterClass.QRCodePayload): VerificationResult {
+        val traceLocation = protoQrCodePayload.traceLocation()
+
+        if (traceLocation.description.isEmpty()) {
+            return VerificationResult.Invalid.Description
+        }
+
+        if (traceLocation.description.length > QR_CODE_DESCRIPTION_MAX_LENGTH) {
+            return VerificationResult.Invalid.Description
+        }
+
+        if (traceLocation.description.lines().size > 1) {
+            return VerificationResult.Invalid.Description
+        }
+
+        if (traceLocation.address.isEmpty()) {
+            return VerificationResult.Invalid.Address
+        }
+
+        if (traceLocation.address.length > QR_CODE_ADDRESS_MAX_LENGTH) {
+            return VerificationResult.Invalid.Address
+        }
+
+        if (traceLocation.address.lines().size > 1) {
+            return VerificationResult.Invalid.Address
+        }
+
+        // If both are 0 do nothing else check start is smaller than end or return error
+        if (!(
+            protoQrCodePayload.locationData.startTimestamp == 0L &&
+                protoQrCodePayload.locationData.endTimestamp == 0L
+            )
+        ) {
+            if (protoQrCodePayload.locationData.startTimestamp > protoQrCodePayload.locationData.endTimestamp) {
+                return VerificationResult.Invalid.StartEndTime
+            }
+        }
+
+        if (traceLocation.cryptographicSeed.size != CROWD_NOTIFIER_CRYPTO_SEED_LENGTH) {
+            return VerificationResult.Invalid.CryptographicSeed
+        }
+
+        return VerificationResult.Valid(
+            VerifiedTraceLocation(protoQrCodePayload)
+        )
+    }
+
+    sealed class VerificationResult {
+        data class Valid(val verifiedTraceLocation: VerifiedTraceLocation) : VerificationResult()
+
+        sealed class Invalid(@StringRes val errorTextRes: Int) : VerificationResult() {
+            object Description : Invalid(R.string.trace_location_checkins_qr_code_invalid_description)
+            object Address : Invalid(R.string.trace_location_checkins_qr_code_invalid_address)
+            object StartEndTime : Invalid(R.string.trace_location_checkins_qr_code_invalid_times)
+            object CryptographicSeed :
+                Invalid(R.string.trace_location_checkins_qr_code_invalid_cryptographic_seed)
+        }
+    }
+
+    companion object {
+        private const val CROWD_NOTIFIER_CRYPTO_SEED_LENGTH = 16
+        private const val QR_CODE_DESCRIPTION_MAX_LENGTH = 100
+        private const val QR_CODE_ADDRESS_MAX_LENGTH = 100
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt
index e553c983471c6c5c523868b2bf95a80a760bf070..ed1f0b0c0eb042105bb16987f3e1f907a6b68528 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt
@@ -12,7 +12,6 @@ import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.firstOrNull
 import org.joda.time.Duration
 import org.joda.time.Seconds
 import org.joda.time.format.DateTimeFormat
@@ -29,8 +28,7 @@ class ContactJournalCheckInEntryCreator @Inject constructor(
         Timber.d("Creating journal entry for %s", checkIn)
 
         // 1. Create location if missing
-        val location: ContactDiaryLocation = diaryRepository.locations.first()
-            .find { it.traceLocationID == checkIn.traceLocationId } ?: checkIn.toLocation()
+        val location = checkIn.createLocationIfMissing()
 
         // 2. Split CheckIn by Midnight UTC
         val splitCheckIns = checkIn.splitByMidnightUTC()
@@ -42,17 +40,24 @@ class ContactJournalCheckInEntryCreator @Inject constructor(
             .forEach { diaryRepository.addLocationVisit(it) }
     }
 
-    private suspend fun CheckIn.toLocation(): ContactDiaryLocation {
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    suspend fun CheckIn.createLocationIfMissing(): ContactDiaryLocation = diaryRepository.locations.first()
+        .find { it.traceLocationID == traceLocationId } ?: createLocationEntry()
+
+    private suspend fun CheckIn.createLocationEntry(): ContactDiaryLocation {
+        Timber.d("Creating new contact diary location from %s", this)
         val location = DefaultContactDiaryLocation(
-            locationName = locationName(),
+            locationName = toLocationName(),
             traceLocationID = traceLocationId
         )
-        Timber.d("Created new location %s and adding it to contact journal db", location)
-        return diaryRepository.addLocation(location) // Get location from db cause we need the id autogenerated by db
+
+        // Get location from db cause we need the id autogenerated by db
+        return diaryRepository.addLocation(location)
+            .also { Timber.d("Created %s and added it to contact journal db", it) }
     }
 
     @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
-    fun CheckIn.locationName(): String {
+    fun CheckIn.toLocationName(): String {
         val nameParts = mutableListOf(description, address)
 
         if (traceLocationStart != null && traceLocationEnd != null) {
@@ -66,7 +71,10 @@ class ContactJournalCheckInEntryCreator @Inject constructor(
         return nameParts.joinToString(separator = ", ")
     }
 
-    private fun CheckIn.toLocationVisit(location: ContactDiaryLocation): ContactDiaryLocationVisit {
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    fun CheckIn.toLocationVisit(location: ContactDiaryLocation): ContactDiaryLocationVisit {
+        // Duration column is set by calculating the time difference in minutes between Check-in StartDate
+        // and Check-in EndDate and rounding it to the closest 15-minute duration
         // Use Seconds for more precision
         val durationInMinutes = Seconds.secondsBetween(checkInStart, checkInEnd).seconds / 60.0
         val duration = (durationInMinutes / 15).roundToLong() * 15
@@ -75,17 +83,18 @@ class ContactJournalCheckInEntryCreator @Inject constructor(
             contactDiaryLocation = location,
             duration = Duration.standardMinutes(duration),
             checkInID = id
-        )
+        ).also { Timber.d("Created %s for %s", it, this) }
     }
 
-    private suspend fun List<CheckIn>.createMissingLocationVisits(location: ContactDiaryLocation):
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    suspend fun List<CheckIn>.createMissingLocationVisits(location: ContactDiaryLocation):
         List<ContactDiaryLocationVisit> {
             Timber.d(
                 "createMissingLocationVisits(location=%s) for %s",
                 location,
                 this.joinToString(prefix = System.lineSeparator(), separator = System.lineSeparator())
             )
-            val existingLocationVisits = diaryRepository.locationVisits.firstOrNull() ?: emptyList()
+            val existingLocationVisits = diaryRepository.locationVisits.first()
             // Existing location visits shall not be updated, so just drop them
             return filter {
                 existingLocationVisits.none { visit ->
@@ -96,7 +105,7 @@ class ContactJournalCheckInEntryCreator @Inject constructor(
                 .map { it.toLocationVisit(location) }
                 .also {
                     Timber.d(
-                        "Created locations visits: %s",
+                        "Created location visits: %s",
                         it.joinToString(prefix = System.lineSeparator(), separator = System.lineSeparator())
                     )
                 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt
index 541496f3bac86227cc1fe7204d8289961871def4..bc9bb66393317c88172eb6889ec8021d322aaab3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt
@@ -7,44 +7,43 @@ import org.joda.time.Instant
 import org.joda.time.LocalDate
 
 /**
- * @param presenceTracingDayRisk Only available for the last calculation, if successful, otherwise null
- * @param checkInWarningOverlaps Only available for the last calculation, if successful, otherwise null
+ * @param presenceTracingDayRisk Only available for the latest calculation, otherwise null
+ * @param checkInWarningOverlaps Only available for the latest calculation, otherwise null
  */
 data class PtRiskLevelResult(
     val calculatedAt: Instant,
     val riskState: RiskState,
-    val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null,
+    private val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null,
     private val checkInWarningOverlaps: List<CheckInWarningOverlap>? = null,
 ) {
 
-    val wasSuccessfullyCalculated: Boolean
-        get() = riskState != RiskState.CALCULATION_FAILED
+    val wasSuccessfullyCalculated: Boolean by lazy {
+        riskState != RiskState.CALCULATION_FAILED
+    }
 
-    val numberOfDaysWithHighRisk: Int
-        get() = presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0
+    val numberOfDaysWithHighRisk: Int by lazy {
+        presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0
+    }
 
-    val numberOfDaysWithLowRisk: Int
-        get() = presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0
+    val numberOfDaysWithLowRisk: Int by lazy {
+        presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0
+    }
 
-    val mostRecentDateWithHighRisk: LocalDate?
-        get() = presenceTracingDayRisk
+    val mostRecentDateWithHighRisk: LocalDate? by lazy {
+        presenceTracingDayRisk
             ?.filter { it.riskState == RiskState.INCREASED_RISK }
             ?.maxByOrNull { it.localDateUtc }
             ?.localDateUtc
+    }
 
-    val mostRecentDateWithLowRisk: LocalDate?
-        get() = presenceTracingDayRisk
+    val mostRecentDateWithLowRisk: LocalDate? by lazy {
+        presenceTracingDayRisk
             ?.filter { it.riskState == RiskState.LOW_RISK }
             ?.maxByOrNull { it.localDateUtc }
             ?.localDateUtc
+    }
 
-    val daysWithEncounters: Int
-        get() = when (riskState) {
-            RiskState.INCREASED_RISK -> numberOfDaysWithHighRisk
-            RiskState.LOW_RISK -> numberOfDaysWithLowRisk
-            else -> 0
-        }
-
-    val checkInOverlapCount: Int
-        get() = checkInWarningOverlaps?.size ?: 0
+    val checkInOverlapCount: Int by lazy {
+        checkInWarningOverlaps?.size ?: 0
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt
index 568273ff125bcc1716ec8bc64d9ead1a201bc339..a2f9f0098e99fa5077a23dd43be7f48e11e76246 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt
@@ -38,18 +38,19 @@ class PresenceTracingRiskCalculator @Inject constructor(
         }
     }
 
-    suspend fun calculateAggregatedRiskPerDay(list: List<CheckInNormalizedTime>):
-        List<PresenceTracingDayRisk> {
-            return list.groupBy { it.localDateUtc }.map {
-                val normalizedTimePerDate = it.value.sumByDouble {
-                    it.normalizedTime
-                }
-                PresenceTracingDayRisk(
-                    localDateUtc = it.key,
-                    riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate)
-                )
+    suspend fun calculateDayRisk(
+        list: List<CheckInNormalizedTime>
+    ): List<PresenceTracingDayRisk> {
+        return list.groupBy { it.localDateUtc }.map {
+            val normalizedTimePerDate = it.value.sumByDouble {
+                it.normalizedTime
             }
+            PresenceTracingDayRisk(
+                localDateUtc = it.key,
+                riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate)
+            )
         }
+    }
 
     suspend fun calculateTotalRisk(list: List<CheckInNormalizedTime>): RiskState {
         if (list.isEmpty()) return RiskState.LOW_RISK
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt
index 9dbccbc3e1b0c6c2a191cccde25f7eff674f2d82..1b356f35b51531d19136a1098fd3706e5b1193d8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt
@@ -46,27 +46,16 @@ class PresenceTracingRiskRepository @Inject constructor(
         database.presenceTracingRiskLevelResultDao()
     }
 
-    private val matchesOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches()
-        .map { timeIntervalMatchEntities ->
-            timeIntervalMatchEntities
-                .map { it.toCheckInWarningOverlap() }
-                .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) }
-        }
-
-    val checkInWarningOverlaps: Flow<List<CheckInWarningOverlap>> =
-        traceTimeIntervalMatchDao.allMatches().map { matchEntities ->
-            matchEntities.map {
-                it.toCheckInWarningOverlap()
-            }
-        }
+    val overlapsOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches().map { entities ->
+        entities
+            .map { it.toCheckInWarningOverlap() }
+            .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) }
+    }
 
-    private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.map {
+    private val normalizedTimeOfLast14DaysPlusToday = overlapsOfLast14DaysPlusToday.map {
         presenceTracingRiskCalculator.calculateNormalizedTime(it)
     }
 
-    private val fifteenDaysAgo: Instant
-        get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration())
-
     val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> =
         normalizedTimeOfLast14DaysPlusToday.map {
             presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it)
@@ -74,7 +63,7 @@ class PresenceTracingRiskRepository @Inject constructor(
 
     val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> =
         normalizedTimeOfLast14DaysPlusToday.map {
-            presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it)
+            presenceTracingRiskCalculator.calculateDayRisk(it)
         }
 
     /**
@@ -120,39 +109,23 @@ class PresenceTracingRiskRepository @Inject constructor(
     }
 
     fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list ->
-        var lastSuccessfulFound = false
-        list.sortedByDescending {
-            it.calculatedAtMillis
-        }
-            .map { entity ->
-                if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) {
-                    lastSuccessfulFound = true
-                    // add risk per day to the last successful result
-                    entity.toRiskLevelResult(
-                        presenceTracingDayRisks = presenceTracingDayRisk.first(),
-                        checkInWarningOverlaps = checkInWarningOverlaps.first(),
-                    )
-                } else {
-                    entity.toRiskLevelResult(
-                        presenceTracingDayRisks = null,
-                        checkInWarningOverlaps = null,
-                    )
-                }
-            }
+        list.sortAndComplementLatestResult()
     }
 
     fun allEntries() = riskLevelResultDao.allEntries().map { list ->
-        var lastSuccessfulFound = false
-        list.sortedByDescending {
+        list.sortAndComplementLatestResult()
+    }
+
+    private suspend fun List<PresenceTracingRiskLevelResultEntity>.sortAndComplementLatestResult() =
+        sortedByDescending {
             it.calculatedAtMillis
         }
-            .map { entity ->
-                if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) {
-                    lastSuccessfulFound = true
-                    // add risk per day to the last successful result
+            .mapIndexed { index, entity ->
+                if (index == 0) {
+                    // add risk per day to the latest result
                     entity.toRiskLevelResult(
                         presenceTracingDayRisks = presenceTracingDayRisk.first(),
-                        checkInWarningOverlaps = checkInWarningOverlaps.first(),
+                        checkInWarningOverlaps = overlapsOfLast14DaysPlusToday.first(),
                     )
                 } else {
                     entity.toRiskLevelResult(
@@ -161,13 +134,15 @@ class PresenceTracingRiskRepository @Inject constructor(
                     )
                 }
             }
-    }
 
     private fun addResult(result: PtRiskLevelResult) {
         Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.")
         riskLevelResultDao.insert(result.toRiskLevelEntity())
     }
 
+    private val fifteenDaysAgo: Instant
+        get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration())
+
     suspend fun clearAllTables() {
         traceTimeIntervalMatchDao.deleteAll()
         riskLevelResultDao.deleteAll()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt
index 1e2a96561f7506bb77d98c6e253feececd26b436..304aa75d3599ce225c70b2eaddd3ed199d84e596 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt
@@ -1,8 +1,7 @@
 package de.rki.coronawarnapp.risk
 
 import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
-import de.rki.coronawarnapp.risk.storage.combine
-import de.rki.coronawarnapp.risk.storage.max
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -18,7 +17,7 @@ data class CombinedEwPtRiskLevelResult(
 ) {
 
     val riskState: RiskState by lazy {
-        combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState)
+        RiskCombinator.combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState)
     }
 
     val wasSuccessfullyCalculated: Boolean by lazy {
@@ -71,3 +70,13 @@ data class LastCombinedRiskResults(
     val lastCalculated: CombinedEwPtRiskLevelResult,
     val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult
 )
+
+internal fun max(left: Instant, right: Instant): Instant {
+    return Instant.ofEpochMilli(kotlin.math.max(left.millis, right.millis))
+}
+
+internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? {
+    if (left == null) return right
+    if (right == null) return left
+    return if (left.isAfter(right)) left else right
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt
index af277a073144c8f1a9aa075768c14e2c18a4013c..0582cc3c9579abb25f40c3ecb13a79bbd617f2c9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt
@@ -1,7 +1,5 @@
 package de.rki.coronawarnapp.risk.storage
 
-import androidx.annotation.VisibleForTesting
-import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
 import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
@@ -11,10 +9,8 @@ import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
 import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult
 import de.rki.coronawarnapp.risk.LastCombinedRiskResults
-import de.rki.coronawarnapp.risk.RiskState
-import de.rki.coronawarnapp.risk.mapToRiskState
-import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedAggregatedRiskPerDateResult
@@ -26,17 +22,14 @@ import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.map
-import org.joda.time.Instant
-import org.joda.time.LocalDate
 import timber.log.Timber
-import java.lang.reflect.Modifier.PRIVATE
-import kotlin.math.max
 import de.rki.coronawarnapp.util.flow.combine as flowCombine
 
 abstract class BaseRiskLevelStorage constructor(
     private val riskResultDatabaseFactory: RiskResultDatabase.Factory,
     private val presenceTracingRiskRepository: PresenceTracingRiskRepository,
-    scope: CoroutineScope
+    scope: CoroutineScope,
+    private val riskCombinator: RiskCombinator,
 ) : RiskLevelStorage {
 
     private val database by lazy { riskResultDatabaseFactory.create() }
@@ -179,7 +172,7 @@ abstract class BaseRiskLevelStorage constructor(
             ptDayRiskStates,
             ewDayRiskStates
         ) { ptRiskList, ewRiskList ->
-            combineRisk(ptRiskList, ewRiskList)
+            riskCombinator.combineRisk(ptRiskList, ewRiskList)
         }
 
     override val latestAndLastSuccessfulEwRiskLevelResult: Flow<List<EwRiskLevelResult>> = riskResultsTables
@@ -197,12 +190,14 @@ abstract class BaseRiskLevelStorage constructor(
             presenceTracingRiskRepository.allEntries()
         ) { ewRiskLevelResults, ptRiskLevelResults ->
 
-            val combinedResults = combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults)
+            val combinedResults = riskCombinator.combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults)
                 .sortedByDescending { it.calculatedAt }
 
             LastCombinedRiskResults(
-                lastCalculated = combinedResults.firstOrNull() ?: currentCombinedLowRisk,
-                lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: initialCombined
+                lastCalculated = combinedResults.firstOrNull() ?: riskCombinator.latestCombinedResult,
+                lastSuccessfullyCalculated = combinedResults.find {
+                    it.wasSuccessfullyCalculated
+                } ?: riskCombinator.initialCombinedResult
             )
         }
 
@@ -217,7 +212,7 @@ abstract class BaseRiskLevelStorage constructor(
             latestEwRiskLevelResults,
             latestPtRiskLevelResults
         ) { ewRiskLevelResults, ptRiskLevelResults ->
-            combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults)
+            riskCombinator.combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults)
                 .sortedByDescending { it.calculatedAt }
                 .take(2)
         }
@@ -236,105 +231,3 @@ abstract class BaseRiskLevelStorage constructor(
         private const val TAG = "RiskLevelStorage"
     }
 }
-
-@VisibleForTesting(otherwise = PRIVATE)
-internal fun combineRisk(
-    ptRiskList: List<PresenceTracingDayRisk>,
-    exposureWindowDayRiskList: List<ExposureWindowDayRisk>
-): List<CombinedEwPtDayRisk> {
-    val allDates = ptRiskList.map { it.localDateUtc }.plus(exposureWindowDayRiskList.map { it.localDateUtc }).distinct()
-    return allDates.map { date ->
-        val ptRisk = ptRiskList.find { it.localDateUtc == date }
-        val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date }
-        CombinedEwPtDayRisk(
-            date,
-            combine(
-                ptRisk?.riskState,
-                ewRisk?.riskLevel?.mapToRiskState()
-            )
-        )
-    }
-}
-
-internal fun combine(vararg states: RiskState?): RiskState {
-    if (states.any { it == RiskState.CALCULATION_FAILED }) return RiskState.CALCULATION_FAILED
-    if (states.any { it == RiskState.INCREASED_RISK }) return RiskState.INCREASED_RISK
-
-    require(states.filterNotNull().all { it == RiskState.LOW_RISK })
-
-    return RiskState.LOW_RISK
-}
-
-internal fun max(left: Instant, right: Instant): Instant {
-    return Instant.ofEpochMilli(max(left.millis, right.millis))
-}
-
-internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? {
-    if (left == null) return right
-    if (right == null) return left
-    return if (left.isAfter(right)) left
-    else right
-}
-
-@VisibleForTesting(otherwise = PRIVATE)
-internal fun combineEwPtRiskLevelResults(
-    ptRiskResults: List<PtRiskLevelResult>,
-    ewRiskResults: List<EwRiskLevelResult>
-): List<CombinedEwPtRiskLevelResult> {
-    val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct()
-    val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt }
-    val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt }
-    return allDates.map { date ->
-        val ptRisk = sortedPtResults.find { it.calculatedAt <= date } ?: ptInitialRiskLevelResult
-        val ewRisk = sortedEwResults.find { it.calculatedAt <= date } ?: EwInitialRiskLevelResult
-        CombinedEwPtRiskLevelResult(
-            ptRisk,
-            ewRisk
-        )
-    }
-}
-
-private object EwInitialRiskLevelResult : EwRiskLevelResult {
-    override val calculatedAt: Instant = Instant.EPOCH
-    override val riskState: RiskState = RiskState.CALCULATION_FAILED
-    override val failureReason: EwRiskLevelResult.FailureReason? = null
-    override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null
-    override val exposureWindows: List<ExposureWindow>? = null
-    override val matchedKeyCount: Int = 0
-    override val daysWithEncounters: Int = 0
-}
-
-private val ptInitialRiskLevelResult: PtRiskLevelResult by lazy {
-    PtRiskLevelResult(
-        calculatedAt = Instant.EPOCH,
-        riskState = RiskState.CALCULATION_FAILED
-    )
-}
-
-private val ewCurrentLowRiskLevelResult
-    get() = object : EwRiskLevelResult {
-        override val calculatedAt: Instant = Instant.now()
-        override val riskState: RiskState = RiskState.LOW_RISK
-        override val failureReason: EwRiskLevelResult.FailureReason? = null
-        override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null
-        override val exposureWindows: List<ExposureWindow>? = null
-        override val matchedKeyCount: Int = 0
-        override val daysWithEncounters: Int = 0
-    }
-
-private val ptCurrentLowRiskLevelResult: PtRiskLevelResult
-    get() = PtRiskLevelResult(
-        calculatedAt = Instant.now(),
-        riskState = RiskState.LOW_RISK
-    )
-
-private val initialCombined = CombinedEwPtRiskLevelResult(
-    ptInitialRiskLevelResult,
-    EwInitialRiskLevelResult
-)
-
-private val currentCombinedLowRisk: CombinedEwPtRiskLevelResult
-    get() = CombinedEwPtRiskLevelResult(
-        ptCurrentLowRiskLevelResult,
-        ewCurrentLowRiskLevelResult
-    )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt
new file mode 100644
index 0000000000000000000000000000000000000000..13aa62d91979693e113b43d430cfd340c44aa13b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt
@@ -0,0 +1,117 @@
+package de.rki.coronawarnapp.risk.storage.internal
+
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
+import dagger.Reusable
+import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
+import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk
+import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult
+import de.rki.coronawarnapp.risk.EwRiskLevelResult
+import de.rki.coronawarnapp.risk.RiskState
+import de.rki.coronawarnapp.risk.mapToRiskState
+import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
+import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
+import de.rki.coronawarnapp.util.TimeStamper
+import org.joda.time.Instant
+import javax.inject.Inject
+
+@Reusable
+class RiskCombinator @Inject constructor(
+    private val timeStamper: TimeStamper
+) {
+
+    private val initialEWRiskLevelResult = object : EwRiskLevelResult {
+        override val calculatedAt: Instant = Instant.EPOCH
+        override val riskState: RiskState = RiskState.LOW_RISK
+        override val failureReason: EwRiskLevelResult.FailureReason? = null
+        override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null
+        override val exposureWindows: List<ExposureWindow>? = null
+        override val matchedKeyCount: Int = 0
+        override val daysWithEncounters: Int = 0
+    }
+
+    private val initialPTRiskLevelResult: PtRiskLevelResult = PtRiskLevelResult(
+        calculatedAt = Instant.EPOCH,
+        riskState = RiskState.LOW_RISK
+    )
+
+    internal val initialCombinedResult = CombinedEwPtRiskLevelResult(
+        ptRiskLevelResult = initialPTRiskLevelResult,
+        ewRiskLevelResult = initialEWRiskLevelResult
+    )
+
+    private val ewCurrentLowRiskLevelResult
+        get() = object : EwRiskLevelResult {
+            override val calculatedAt: Instant = timeStamper.nowUTC
+            override val riskState: RiskState = RiskState.LOW_RISK
+            override val failureReason: EwRiskLevelResult.FailureReason? = null
+            override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null
+            override val exposureWindows: List<ExposureWindow>? = null
+            override val matchedKeyCount: Int = 0
+            override val daysWithEncounters: Int = 0
+        }
+
+    private val ptCurrentLowRiskLevelResult: PtRiskLevelResult
+        get() = PtRiskLevelResult(
+            calculatedAt = timeStamper.nowUTC,
+            riskState = RiskState.LOW_RISK
+        )
+
+    internal val latestCombinedResult: CombinedEwPtRiskLevelResult
+        get() = CombinedEwPtRiskLevelResult(
+            ptCurrentLowRiskLevelResult,
+            ewCurrentLowRiskLevelResult
+        )
+
+    internal fun combineEwPtRiskLevelResults(
+        ptRiskResults: List<PtRiskLevelResult>,
+        ewRiskResults: List<EwRiskLevelResult>
+    ): List<CombinedEwPtRiskLevelResult> {
+        val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct()
+        val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt }
+        val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt }
+        return allDates.map { date ->
+            val ptRisk = sortedPtResults.find {
+                it.calculatedAt <= date
+            } ?: initialPTRiskLevelResult
+            val ewRisk = sortedEwResults.find {
+                it.calculatedAt <= date
+            } ?: initialEWRiskLevelResult
+
+            CombinedEwPtRiskLevelResult(
+                ptRiskLevelResult = ptRisk,
+                ewRiskLevelResult = ewRisk
+            )
+        }
+    }
+
+    internal fun combineRisk(
+        ptRiskList: List<PresenceTracingDayRisk>,
+        exposureWindowDayRiskList: List<ExposureWindowDayRisk>
+    ): List<CombinedEwPtDayRisk> {
+        val allDates =
+            ptRiskList.map { it.localDateUtc }.plus(exposureWindowDayRiskList.map { it.localDateUtc }).distinct()
+        return allDates.map { date ->
+            val ptRisk = ptRiskList.find { it.localDateUtc == date }
+            val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date }
+            CombinedEwPtDayRisk(
+                localDate = date,
+                riskState = combine(
+                    ptRisk?.riskState,
+                    ewRisk?.riskLevel?.mapToRiskState()
+                )
+            )
+        }
+    }
+
+    companion object {
+        fun combine(vararg states: RiskState?): RiskState {
+            if (states.any { it == RiskState.CALCULATION_FAILED }) return RiskState.CALCULATION_FAILED
+            if (states.any { it == RiskState.INCREASED_RISK }) return RiskState.INCREASED_RISK
+
+            require(states.filterNotNull().all { it == RiskState.LOW_RISK })
+
+            return RiskState.LOW_RISK
+        }
+    }
+}
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 6f9fdeed6e2231cc1f4f7bba4c3ea874a405d299..b69db70bb6c049c1119e8f3313ca2157039a49c2 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
@@ -10,6 +10,7 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.lastSubmission
 import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask
 import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTaskProgress
 import de.rki.coronawarnapp.risk.RiskLevelTask
+import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.TaskInfo
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
@@ -45,7 +46,8 @@ class TracingRepository @Inject constructor(
     enfClient: ENFClient,
     private val timeStamper: TimeStamper,
     private val exposureDetectionTracker: ExposureDetectionTracker,
-    private val backgroundModeStatus: BackgroundModeStatus
+    private val backgroundModeStatus: BackgroundModeStatus,
+    private val riskWorkScheduler: RiskWorkScheduler,
 ) {
 
     @SuppressLint("BinaryOperationInTimber")
@@ -91,29 +93,17 @@ class TracingRepository @Inject constructor(
         it.taskState.isActive && it.taskState.request.type == RiskLevelTask::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.
-     */
-    fun refreshDiagnosisKeys() {
-        scope.launch {
-            taskController.submitBlocking(
-                DefaultTaskRequest(
-                    DownloadDiagnosisKeysTask::class,
-                    DownloadDiagnosisKeysTask.Arguments(),
-                    originTag = "TracingRepository.refreshDiagnosisKeys()"
-                )
-            )
-            taskController.submit(
-                DefaultTaskRequest(
-                    RiskLevelTask::class,
-                    originTag = "TracingRepository.refreshDiagnosisKeys()"
-                )
+    fun refreshRiskResult() = scope.launch {
+        Timber.tag(TAG).d("refreshRiskResults()")
+
+        riskWorkScheduler.runRiskTasksNow()
+
+        taskController.submit(
+            DefaultTaskRequest(
+                RiskLevelTask::class,
+                originTag = "TracingRepository.refreshRiskResult()"
             )
-        }
+        )
     }
 
     /**
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt
index bc815e3040b8a41cc7b389d7baf59781f43efea8..179d8edbde1c58dcc8672b20c32f3a2ba9de286d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt
@@ -3,15 +3,10 @@ package de.rki.coronawarnapp.task.internal
 import dagger.Module
 import dagger.Provides
 import de.rki.coronawarnapp.task.TaskCoroutineScope
-import de.rki.coronawarnapp.task.example.QueueingTaskModule
 import kotlinx.coroutines.CoroutineScope
 import javax.inject.Singleton
 
-@Module(
-    includes = [
-        QueueingTaskModule::class
-    ]
-)
+@Module
 class TaskModule {
 
     @Provides
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt
index 5aa675aae2f4b9182fce26433476fb2427f97a6c..55e4b09d7ab90a2ba39ba30e9feec61482065b57 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt
@@ -30,19 +30,19 @@ class TracingStateProvider @AssistedInject constructor(
 ) {
     val state: Flow<TracingState> = combine(
         tracingStatus.generalStatus.onEach {
-            Timber.v("tracingStatus: $it")
+            Timber.tag(TAG).v("tracingStatus: $it")
         },
         tracingRepository.tracingProgress.onEach {
-            Timber.v("tracingProgress: $it")
+            Timber.tag(TAG).v("tracingProgress: $it")
         },
         riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.onEach {
-            Timber.v("riskLevelResults: $it")
+            Timber.tag(TAG).v("riskLevelResults: $it")
         },
         exposureDetectionTracker.latestSubmission().onEach {
-            Timber.v("latestSubmission: $it")
+            Timber.tag(TAG).v("latestSubmission: $it")
         },
         backgroundModeStatus.isAutoModeEnabled.onEach {
-            Timber.v("isAutoModeEnabled: $it")
+            Timber.tag(TAG).v("isAutoModeEnabled: $it")
         }
     ) { tracingStatus,
         tracingProgress,
@@ -88,12 +88,16 @@ class TracingStateProvider @AssistedInject constructor(
             )
         }
     }
-        .onStart { Timber.v("TracingStateProvider FLOW start") }
-        .onEach { Timber.d("TracingStateProvider FLOW emission: %s", it) }
-        .onCompletion { Timber.v("TracingStateProvider FLOW completed.") }
+        .onStart { Timber.tag(TAG).v("TracingStateProvider FLOW start") }
+        .onEach { Timber.tag(TAG).d("TracingStateProvider FLOW emission: %s", it) }
+        .onCompletion { Timber.tag(TAG).v("TracingStateProvider FLOW completed.") }
 
     @AssistedFactory
     interface Factory {
         fun create(isDetailsMode: Boolean): TracingStateProvider
     }
+
+    companion object {
+        const val TAG = "TracingStateProvider"
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt
index dd52cdbc939d76e6c95e680b4982fe33ebd93500..1fbb2d1f2d414fbf604956c3d9ade6e13fa774e3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt
@@ -104,7 +104,7 @@ class TracingDetailsFragmentViewModel @AssistedInject constructor(
     }
 
     fun updateRiskDetails() {
-        tracingRepository.refreshDiagnosisKeys()
+        tracingRepository.refreshRiskResult()
     }
 
     fun onItemClicked(item: DetailsItem) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt
index 5af35e5be9608b289d8430ec27353589a67d2d08..a8c0aba5198cd1c7f7bfa98a90e34dea8566ce5c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins
 
+import androidx.annotation.StringRes
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation
 
@@ -11,6 +12,10 @@ sealed class CheckInEvent {
 
     data class ConfirmCheckIn(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent()
 
+    data class InvalidQrCode(@StringRes val errorTextRes: Int) : CheckInEvent()
+
+    data class ConfirmCheckInWithoutHistory(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent()
+
     data class EditCheckIn(val checkInId: Long, val position: Int) : CheckInEvent()
 
     data class ConfirmSwipeItem(val checkIn: CheckIn, val position: Int) : CheckInEvent()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt
index 4a99d53591edb92a549e77939ecd19b82604adb6..d198e93cc2b4ac82228cc1215d73cec574e7fa7f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt
@@ -7,6 +7,7 @@ import android.os.Bundle
 import android.provider.Settings
 import android.view.View
 import android.widget.Toast
+import androidx.annotation.StringRes
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.widget.Toolbar
 import androidx.core.net.toUri
@@ -16,6 +17,7 @@ import androidx.navigation.fragment.FragmentNavigatorExtras
 import androidx.navigation.fragment.findNavController
 import androidx.navigation.fragment.navArgs
 import androidx.recyclerview.widget.DefaultItemAnimator
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.transition.Hold
 import com.google.android.material.transition.MaterialSharedAxis
 import de.rki.coronawarnapp.BuildConfig
@@ -38,7 +40,6 @@ import de.rki.coronawarnapp.util.ui.viewBindingLazy
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
 import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
 import timber.log.Timber
-import java.lang.Exception
 import java.net.URLEncoder
 import javax.inject.Inject
 
@@ -53,7 +54,8 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
             factory as CheckInsViewModel.Factory
             factory.create(
                 savedState = savedState,
-                deepLink = navArgs.uri
+                deepLink = navArgs.uri,
+                cleanHistory = navArgs.cleanHistory
             )
         }
     )
@@ -97,6 +99,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
                 )
             }
 
+            is CheckInEvent.InvalidQrCode -> showInvalidQrCodeInformation(event.errorTextRes)
+
+            is CheckInEvent.ConfirmCheckInWithoutHistory -> doNavigate(
+                CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragmentCleanHistory(
+                    verifiedTraceLocation = event.verifiedTraceLocation
+                )
+            )
+
             is CheckInEvent.ConfirmSwipeItem -> showRemovalConfirmation(event.checkIn, event.position)
 
             is CheckInEvent.ConfirmRemoveItem -> showRemovalConfirmation(event.checkIn, null)
@@ -125,6 +135,13 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
         }
     }
 
+    private fun showInvalidQrCodeInformation(@StringRes errorTextRes: Int) =
+        MaterialAlertDialogBuilder(requireContext()).apply {
+            setTitle(R.string.errors_generic_headline)
+            setMessage(errorTextRes)
+            setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> }
+        }.show()
+
     private fun updateViews(items: List<CheckInsItem>) {
         checkInsAdapter.update(items)
         binding.apply {
@@ -232,14 +249,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
     }
 
     companion object {
-        fun createCheckInUri(rootUri: String): Uri {
+        fun createCheckInUri(rootUri: String, cleanHistory: Boolean = false): Uri {
             val encodedUrl = try {
                 URLEncoder.encode(rootUri, Charsets.UTF_8.name())
             } catch (e: Exception) {
                 Timber.d(e, "URL Encoding failed url($rootUri)")
                 rootUri // Pass original
             }
-            return "coronawarnapp://check-ins/$encodedUrl".toUri()
+            return "coronawarnapp://check-ins/$encodedUrl/?cleanHistory=$cleanHistory".toUri()
         }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt
index cad63ea577b336f62629b952f6668ac15b133842..94c98114e6d18bbff7bd7932ba116ef723bb7684 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt
@@ -9,7 +9,7 @@ import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser
-import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationVerifier
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler
@@ -28,15 +28,18 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.combine
 import timber.log.Timber
 
+@Suppress("LongParameterList")
 class CheckInsViewModel @AssistedInject constructor(
     @Assisted private val savedState: SavedStateHandle,
     @Assisted private val deepLink: String?,
+    @Assisted private val cleanHistory: Boolean,
     dispatcherProvider: DispatcherProvider,
     @AppScope private val appScope: CoroutineScope,
     private val qrCodeUriParser: QRCodeUriParser,
     private val checkInsRepository: CheckInRepository,
     private val checkOutHandler: CheckOutHandler,
-    private val cameraPermissionProvider: CameraPermissionProvider
+    private val cameraPermissionProvider: CameraPermissionProvider,
+    private val traceLocationVerifier: TraceLocationVerifier
 ) : CWAViewModel(dispatcherProvider) {
 
     val events = SingleLiveEvent<CheckInEvent>()
@@ -142,8 +145,16 @@ class CheckInsViewModel @AssistedInject constructor(
         try {
             Timber.i("uri: $uri")
             val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri)
-            val verifiedTraceLocation = VerifiedTraceLocation(qrCodePayload)
-            events.postValue(CheckInEvent.ConfirmCheckIn(verifiedTraceLocation))
+            when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) {
+                is TraceLocationVerifier.VerificationResult.Valid -> events.postValue(
+                    if (cleanHistory)
+                        CheckInEvent.ConfirmCheckInWithoutHistory(verifyResult.verifiedTraceLocation)
+                    else
+                        CheckInEvent.ConfirmCheckIn(verifyResult.verifiedTraceLocation)
+                )
+                is TraceLocationVerifier.VerificationResult.Invalid ->
+                    events.postValue(CheckInEvent.InvalidQrCode(verifyResult.errorTextRes))
+            }
         } catch (e: Exception) {
             Timber.d(e, "TraceLocation verification failed")
             e.report(ExceptionCategory.INTERNAL)
@@ -162,7 +173,8 @@ class CheckInsViewModel @AssistedInject constructor(
     interface Factory : CWAViewModelFactory<CheckInsViewModel> {
         fun create(
             savedState: SavedStateHandle,
-            deepLink: String?
+            deepLink: String?,
+            cleanHistory: Boolean
         ): CheckInsViewModel
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt
index dce82fc756c047ceedd73a1533318fcc2c203892..619de76483d144439940fa7665fc86d1959f31fa 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt
@@ -81,7 +81,6 @@ class ActiveCheckInVH(parent: ViewGroup) :
         checkoutAction.setOnClickListener { curItem.onCheckout(curItem.checkin) }
 
         itemView.apply {
-            setOnClickListener { curItem.onCardClicked(curItem.checkin, adapterPosition) }
             transitionName = item.checkin.id.toString()
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt
index 1a9eb4cd3eb753544367c454dbdc177f2c9f16c3..3c3cd2f0c9b6a16b76f0cdd46feb621e293573e1 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt
@@ -36,7 +36,12 @@ class CheckInOnboardingFragment : Fragment(R.layout.fragment_trace_location_onbo
         super.onViewCreated(view, savedInstanceState)
 
         if (viewModel.isOnboardingComplete && args.uri != null) {
-            doNavigate(CheckInOnboardingFragmentDirections.actionCheckInOnboardingFragmentToCheckInsFragment(args.uri))
+            doNavigate(
+                CheckInOnboardingFragmentDirections.actionCheckInOnboardingFragmentToCheckInsFragment(
+                    args.uri,
+                    args.cleanHistory
+                )
+            )
         }
 
         with(binding) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt
index 32fccb915730437caa226c162b27f14acfd248d6..6cf9139837b6ba6c194dcd9128985c4652de5de4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt
@@ -116,7 +116,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_
 
                 is TraceLocationEvent.SelfCheckIn -> {
                     findNavController().navigate(
-                        CheckInsFragment.createCheckInUri(it.traceLocation.locationUrl),
+                        CheckInsFragment.createCheckInUri(it.traceLocation.locationUrl, true),
                         NavOptions.Builder()
                             .setPopUpTo(R.id.checkInsFragment, true)
                             .build()
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 c795ab4d5dd99665eded47666d7f0fb7cc73eee1..3e0d93c81d701e3875a42efb2e13c0ad5e4990c5 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
@@ -146,21 +146,21 @@ class HomeFragmentViewModel @AssistedInject constructor(
                 onCardClick = {
                     routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment())
                 },
-                onUpdateClick = { refreshDiagnosisKeys() }
+                onUpdateClick = { refreshRiskResult() }
             )
             is IncreasedRisk -> IncreasedRiskCard.Item(
                 state = tracingState,
                 onCardClick = {
                     routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment())
                 },
-                onUpdateClick = { refreshDiagnosisKeys() }
+                onUpdateClick = { refreshRiskResult() }
             )
             is TracingFailed -> TracingFailedCard.Item(
                 state = tracingState,
                 onCardClick = {
                     routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment())
                 },
-                onRetryClick = { refreshDiagnosisKeys() }
+                onRetryClick = { refreshRiskResult() }
             )
         }
     }.distinctUntilChanged()
@@ -268,7 +268,7 @@ class HomeFragmentViewModel @AssistedInject constructor(
     fun reenableRiskCalculation() {
         deregisterWarningAccepted()
         deadmanNotificationScheduler.schedulePeriodic()
-        refreshDiagnosisKeys()
+        refreshRiskResult()
     }
 
     // TODO only lazy to keep tests going which would break because of LocalData access
@@ -303,8 +303,8 @@ class HomeFragmentViewModel @AssistedInject constructor(
         }
     }
 
-    private fun refreshDiagnosisKeys() {
-        tracingRepository.refreshDiagnosisKeys()
+    private fun refreshRiskResult() {
+        tracingRepository.refreshRiskResult()
     }
 
     fun deregisterWarningAccepted() {
diff --git a/Corona-Warn-App/src/main/res/font/roboto.ttf b/Corona-Warn-App/src/main/res/font/roboto.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..2c97eeadffe1a34bd67d3ff1c3887fd53e22c2ca
Binary files /dev/null and b/Corona-Warn-App/src/main/res/font/roboto.ttf differ
diff --git a/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml
index f2b6d1d139a858278c8c7abcbd497482a39e5a76..903ac942915ebfe2fdb1d7142222ef0b8db30926 100644
--- a/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml
@@ -93,6 +93,7 @@
 
         <TextView
             android:id="@+id/info_text_view"
+            style="@style/PosterTextStyle"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:maxLines="2"
diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml
index bbf7abe775c1afe85f8994a4eba64e519d054a59..bf5f0ba98e89a4ec1d90780dbb2337213e3bb056 100644
--- a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml
@@ -9,7 +9,7 @@
         android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment"
         android:label="CheckInOnboardingFragment"
         tools:layout="@layout/fragment_trace_location_onboarding">
-        <deepLink app:uri="coronawarnapp://check-ins/{uri}" />
+        <deepLink app:uri="coronawarnapp://check-ins/{uri}/?cleanHistory={cleanHistory}" />
         <action
             android:id="@+id/action_checkInOnboardingFragment_to_checkInsFragment"
             app:destination="@id/checkInsFragment"
@@ -22,6 +22,10 @@
             android:name="showBottomNav"
             android:defaultValue="true"
             app:argType="boolean" />
+        <argument
+            android:name="cleanHistory"
+            android:defaultValue="false"
+            app:argType="boolean" />
         <argument
             android:name="uri"
             android:defaultValue="@null"
@@ -68,11 +72,20 @@
         <action
             android:id="@+id/action_checkInsFragment_to_confirmCheckInFragment"
             app:destination="@id/confirmCheckInFragment" />
+        <action
+            android:id="@+id/action_checkInsFragment_to_confirmCheckInFragment_cleanHistory"
+            app:popUpTo="@id/checkInsFragment"
+            app:popUpToInclusive="true"
+            app:destination="@id/confirmCheckInFragment" />
         <argument
             android:name="uri"
             android:defaultValue="@null"
             app:argType="string"
             app:nullable="true" />
+        <argument
+            android:name="cleanHistory"
+            android:defaultValue="false"
+            app:argType="boolean" />
         <action
             android:id="@+id/action_checkInsFragment_to_editCheckInFragment"
             app:destination="@id/editCheckInFragment" />
diff --git a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml
index 732c3f2c57841116a81ebae569f4bdc3f7defcef..758b6ca6e8c166dd4540c89b14c42010f5ffb433 100644
--- a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml
@@ -259,4 +259,14 @@
     <!-- XBUT: Organizer Flow : Title for save as template button -->
     <string name="trace_location_event_detail_save_as_template_button">"Als Vorlage verwenden"</string>
 
+    <!-- Qr Code Validation Messages -->
+    <!-- XTXT: My check-ins: qr code validation wrong description field -->
+    <string name="trace_location_checkins_qr_code_invalid_description">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_DESCRIPTION)."</string>
+    <!-- XTXT: My check-ins: qr code validation wrong address field -->
+    <string name="trace_location_checkins_qr_code_invalid_address">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_ADDRESS)."</string>
+    <!-- XTXT: My check-ins: qr code validation wrong start end time field -->
+    <string name="trace_location_checkins_qr_code_invalid_times">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_TIMESTAMPS)."</string>
+    <!-- XTXT: My check-ins: qr code validation wrong cryptographic seed field -->
+    <string name="trace_location_checkins_qr_code_invalid_cryptographic_seed">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_CRYPTO_SEED)."</string>
+
 </resources>
diff --git a/Corona-Warn-App/src/main/res/values/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values/event_registration_strings.xml
index a326943cfb63867f792db7ac14a3789e7bac4326..9896181c86dbe91f97783d12c58501d797371a41 100644
--- a/Corona-Warn-App/src/main/res/values/event_registration_strings.xml
+++ b/Corona-Warn-App/src/main/res/values/event_registration_strings.xml
@@ -43,7 +43,7 @@
     <!-- XTXT: My check-ins card: Active event, checkin information, automatic checkout info -->
     <string name="trace_location_checkins_card_automatic_checkout_info">"%1$s - check out automatically after %2$s."</string>
     <!-- XTXT: My check-ins card: Active event, checkin information, automatic checkout info -->
-    <string name="trace_location_checkins_card_automatic_checkout_info_format">"%1$s 2$s - check out automatically after %3$s."</string>
+    <string name="trace_location_checkins_card_automatic_checkout_info_format">"%1$s %2$s - check out automatically after %3$s."</string>
     <!-- XHED: Title of the category list screen of the event creation  -->
     <string name="tracelocation_organizer_category_title">"Create QR Code"</string>
 
@@ -257,4 +257,14 @@
     <!-- XBUT: Organizer Flow : Title for save as template button -->
     <string name="trace_location_event_detail_save_as_template_button">"Use as Template"</string>
 
+    <!-- Qr Code Validation Messages -->
+    <!-- XTXT: My check-ins: qr code validation wrong description field -->
+    <string name="trace_location_checkins_qr_code_invalid_description">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_DESCRIPTION)."</string>
+    <!-- XTXT: My check-ins: qr code validation wrong address field -->
+    <string name="trace_location_checkins_qr_code_invalid_address">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_ADDRESS)."</string>
+    <!-- XTXT: My check-ins: qr code validation wrong start end time field -->
+    <string name="trace_location_checkins_qr_code_invalid_times">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_TIMESTAMPS)."</string>
+    <!-- XTXT: My check-ins: qr code validation wrong cryptographic seed field -->
+    <string name="trace_location_checkins_qr_code_invalid_cryptographic_seed">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_CRYPTO_SEED)."</string>
+
 </resources>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml
index 0c94671196cdb387ebe89320c1e148377757b725..ecacc05a638093304b440e6405031fdf834af0ea 100644
--- a/Corona-Warn-App/src/main/res/values/styles.xml
+++ b/Corona-Warn-App/src/main/res/values/styles.xml
@@ -525,4 +525,8 @@
         <item name="android:background">@drawable/circle_ripple</item>
         <item name="android:src">@drawable/ic_baseline_more_vert_24</item>
     </style>
+
+    <style name="PosterTextStyle">
+        <item name="android:fontFamily">@font/roboto</item>
+    </style>
 </resources>
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..6d8df6b978065dd082d5a9d2825af00e11df9a88
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeProvider.kt
@@ -0,0 +1,148 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.util.toProtoByteString
+import okio.ByteString.Companion.decodeBase64
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+import java.util.stream.Stream
+
+class InvalidQRCodeProvider : ArgumentsProvider {
+    private fun baseValidQrCodeBuilder(): TraceLocationOuterClass.QRCodePayload.Builder =
+        TraceLocationOuterClass.QRCodePayload.newBuilder()
+            .setVersion(1)
+            .setCrowdNotifierData(
+                TraceLocationOuterClass.CrowdNotifierData.newBuilder()
+                    .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                    .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                    .setVersion(1)
+            )
+            .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString())
+            .setLocationData(
+                TraceLocationOuterClass.TraceLocation.newBuilder()
+                    .setDescription("Icecream Shop")
+                    .setAddress("Main Street 1")
+                    .setVersion(1)
+                    .build()
+            )
+
+    private fun baseValidLocationData(): TraceLocationOuterClass.TraceLocation.Builder =
+        TraceLocationOuterClass.TraceLocation.newBuilder()
+            .setDescription("Icecream Shop")
+            .setAddress("Main Street 1")
+            .setVersion(1)
+
+    override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
+        return Stream.of(
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setStartTimestamp(2687991)
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.StartEndTime
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setStartTimestamp(2687991)
+                            .setEndTimestamp(2387991)
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.StartEndTime
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setDescription("")
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Description
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .clearDescription()
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Description
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setDescription((0..101).joinToString { "a" })
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Description
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setDescription("A \n B")
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Description
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setAddress("")
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Address
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .clearAddress()
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Address
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setAddress((0..101).joinToString { "a" })
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Address
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setLocationData(
+                        baseValidLocationData()
+                            .setAddress("A \n B")
+                            .build()
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.Address
+            ),
+            Arguments.of(
+                baseValidQrCodeBuilder()
+                    .setCrowdNotifierData(
+                        TraceLocationOuterClass.CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed("WNlQ==".decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    ).build(),
+                TraceLocationVerifier.VerificationResult.Invalid.CryptographicSeed
+            )
+        )
+    }
+
+    companion object {
+        const val CRYPTOGRAPHIC_SEED = "zveDikIfwAXWqI6h4dWNlQ=="
+        const val PUB_KEY =
+            "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" +
+                "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD"
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifierTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1bc7865431213f233211fd4eecaa641f8cc16586
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifierTest.kt
@@ -0,0 +1,32 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ArgumentsSource
+import testhelpers.BaseTest
+
+class TraceLocationVerifierTest : BaseTest() {
+    fun createInstance() = TraceLocationVerifier()
+
+    @ParameterizedTest
+    @ArgumentsSource(ValidQRCodeProvider::class)
+    fun `Valid QR Codes`(
+        protoQrCodePayload: TraceLocationOuterClass.QRCodePayload
+    ) {
+        val validationResult = createInstance().verifyTraceLocation(protoQrCodePayload)
+
+        (validationResult is TraceLocationVerifier.VerificationResult.Valid) shouldBe true
+    }
+
+    @ParameterizedTest
+    @ArgumentsSource(InvalidQRCodeProvider::class)
+    fun `Invalid QR Codes`(
+        protoQrCodePayload: TraceLocationOuterClass.QRCodePayload,
+        expectedFailure: TraceLocationVerifier.VerificationResult.Invalid
+    ) {
+        val validationResult = createInstance().verifyTraceLocation(protoQrCodePayload)
+
+        validationResult shouldBe expectedFailure
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidQRCodeProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidQRCodeProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bd08d284659adf109ec01c0b29834815631f01d1
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidQRCodeProvider.kt
@@ -0,0 +1,104 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.util.toProtoByteString
+import okio.ByteString.Companion.decodeBase64
+import org.junit.jupiter.api.extension.ExtensionContext
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.ArgumentsProvider
+import java.util.stream.Stream
+
+class ValidQRCodeProvider : ArgumentsProvider {
+    override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
+        return Stream.of(
+            Arguments.of(
+                TraceLocationOuterClass.QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        TraceLocationOuterClass.CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocationOuterClass.TraceLocation.newBuilder()
+                            .setDescription("My Birthday Party")
+                            .setAddress("at my place")
+                            .setStartTimestamp(2687955)
+                            .setEndTimestamp(2687991)
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build()
+            ),
+            Arguments.of(
+                TraceLocationOuterClass.QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        TraceLocationOuterClass.CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocationOuterClass.TraceLocation.newBuilder()
+                            .setDescription("My Birthday Party")
+                            .setAddress("at my place")
+                            .setStartTimestamp(2687991)
+                            .setEndTimestamp(2687991)
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build()
+            ),
+            Arguments.of(
+                TraceLocationOuterClass.QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        TraceLocationOuterClass.CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocationOuterClass.TraceLocation.newBuilder()
+                            .setDescription("My Birthday Party")
+                            .setAddress("at my place")
+                            .setEndTimestamp(2687991)
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build()
+            ),
+            Arguments.of(
+                TraceLocationOuterClass.QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        TraceLocationOuterClass.CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocationOuterClass.TraceLocation.newBuilder()
+                            .setDescription("Icecream Shop")
+                            .setAddress("Main Street 1")
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build()
+            )
+        )
+    }
+
+    companion object {
+        const val CRYPTOGRAPHIC_SEED = "zveDikIfwAXWqI6h4dWNlQ=="
+        const val PUB_KEY =
+            "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" +
+                "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD"
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt
index 07007ff99eeed66a36d4c651f91d3a51c8904be0..2c593184e9dc277b021427063294662dbe1a2a51 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt
@@ -97,7 +97,7 @@ class CheckOutHandlerTest : BaseTest() {
             checkOut(42)
         }
 
-        updatedCheckIn?.createJournalEntry shouldBe true
+        updatedCheckIn!!.createJournalEntry shouldBe true
 
         coVerify(exactly = 1) {
             contactJournalCheckInEntryCreator.createEntry(any())
@@ -110,7 +110,7 @@ class CheckOutHandlerTest : BaseTest() {
             checkOut(43)
         }
 
-        updatedCheckIn?.createJournalEntry shouldBe false
+        updatedCheckIn!!.createJournalEntry shouldBe false
 
         coVerify(exactly = 0) {
             contactJournalCheckInEntryCreator.createEntry(any())
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt
index 58834e6c7b70de6ac2f0e5a7452baabbd0c29e86..0726f259a2e346abaf851c492c5ccc0e4fe2d9fd 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt
@@ -1,8 +1,12 @@
 package de.rki.coronawarnapp.presencetracing.checkins.checkout
 
 import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
+import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.coEvery
 import io.mockk.coVerify
@@ -12,8 +16,12 @@ import io.mockk.just
 import io.mockk.runs
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runBlockingTest
+import okio.ByteString.Companion.decodeBase64
 import okio.ByteString.Companion.encode
+import org.joda.time.Days
 import org.joda.time.Instant
+import org.joda.time.Minutes
+import org.joda.time.format.DateTimeFormat
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
@@ -24,7 +32,7 @@ class ContactJournalCheckInEntryCreatorTest : BaseTest() {
 
     private val testCheckIn = CheckIn(
         id = 42L,
-        traceLocationId = "traceLocationId1".encode(),
+        traceLocationId = "traceLocationId1".decodeBase64()!!,
         version = 1,
         type = 1,
         description = "Restaurant",
@@ -42,10 +50,20 @@ class ContactJournalCheckInEntryCreatorTest : BaseTest() {
 
     private val testLocation = DefaultContactDiaryLocation(
         locationId = 123L,
-        locationName = "${testCheckIn.description}, ${testCheckIn.address}, ${testCheckIn.traceLocationStart} - ${testCheckIn.traceLocationEnd}",
+        locationName = "${testCheckIn.description}, ${testCheckIn.address}, ${testCheckIn.traceLocationStart?.toPrettyDate()} - ${testCheckIn.traceLocationEnd?.toPrettyDate()}",
         traceLocationID = testCheckIn.traceLocationId
     )
 
+    private val testLocationVisit = DefaultContactDiaryLocationVisit(
+        id = 0,
+        date = testCheckIn.checkInStart.toLocalDateUtc(),
+        contactDiaryLocation = testLocation,
+        checkInID = testCheckIn.id,
+        duration = Minutes.minutes(60).toStandardDuration()
+    )
+
+    private fun Instant.toPrettyDate(): String = toUserTimeZone().toString(DateTimeFormat.shortDateTime())
+
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
@@ -68,20 +86,136 @@ class ContactJournalCheckInEntryCreatorTest : BaseTest() {
         every { contactDiaryRepo.locations } returns flowOf(emptyList()) andThen flowOf(listOf(testLocation))
 
         // Repo returns an empty list for the first call, so location is missing and a new location should be created and added
-        val instance = createInstance()
-        instance.createEntry(testCheckIn)
+        createInstance().apply {
+            testCheckIn.createLocationIfMissing()
+
+            coVerify(exactly = 1) {
+                contactDiaryRepo.addLocation(any())
+            }
+
+            // Location with trace location id already exists, so that location will be used
+            testCheckIn.createLocationIfMissing()
+            testCheckIn.createLocationIfMissing()
+            testCheckIn.createLocationIfMissing()
+            testCheckIn.createLocationIfMissing()
+
+            coVerify(exactly = 1) {
+                contactDiaryRepo.addLocation(any())
+            }
+
+            testCheckIn.copy(traceLocationId = "traceLocationId2".decodeBase64()!!).createLocationIfMissing()
 
-        coVerify(exactly = 1) {
-            contactDiaryRepo.addLocation(any())
+            coVerify(exactly = 2) {
+                contactDiaryRepo.addLocation(any())
+            }
         }
+    }
 
-        // Location with trace location id already exists, so that location will be used
-        instance.createEntry(testCheckIn)
-        instance.createEntry(testCheckIn)
-        instance.createEntry(testCheckIn)
+    @Test
+    fun `Location name concatenates description, address and if both are set trace location start and end date`() {
+        val testCheckInNoTraceLocationStartDate = testCheckIn.copy(traceLocationStart = null)
+        val testCheckInNoTraceLocationEndDate = testCheckIn.copy(traceLocationEnd = null)
+        val testCheckInNoTraceLocationStartAndEndDate =
+            testCheckIn.copy(traceLocationStart = null, traceLocationEnd = null)
+
+        createInstance().apply {
+            testCheckIn.validateLocationName(testCheckIn.toLocationName())
+            testCheckInNoTraceLocationStartDate.validateLocationName(testCheckInNoTraceLocationStartDate.toLocationName())
+            testCheckInNoTraceLocationEndDate.validateLocationName(testCheckInNoTraceLocationEndDate.toLocationName())
+            testCheckInNoTraceLocationStartAndEndDate.validateLocationName(testCheckInNoTraceLocationStartAndEndDate.toLocationName())
+        }
+    }
+
+    private fun CheckIn.validateLocationName(nameToValidate: String) {
+        nameToValidate shouldBe when (traceLocationStart != null && traceLocationEnd != null) {
+            true -> "$description, $address, ${traceLocationStart?.toPrettyDate()} - ${traceLocationEnd?.toPrettyDate()}"
+            else -> "$description, $address"
+        }
+    }
 
-        coVerify(exactly = 1) {
-            contactDiaryRepo.addLocation(any())
+    @Test
+    fun `CheckIn to ContactDiaryLocationVisit is correct`() {
+        createInstance().apply {
+            testCheckIn.toLocationVisit(testLocation).also {
+                it.checkInID shouldBe testCheckIn.id
+                it.date shouldBe testCheckIn.checkInStart.toLocalDateUtc()
+                it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(60)
+                it.contactDiaryLocation shouldBe testLocation
+            }
+        }
+    }
+
+    @Test
+    fun `CheckIn to ContactDiaryLocationVisit duration mapping is correct`() {
+        createInstance().apply {
+            // Rounds duration to closest 15 minutes
+            testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T23:07:29+01:00")).toLocationVisit(testLocation)
+                .also {
+                    it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(60)
+                }
+
+            testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T23:07:30+01:00")).toLocationVisit(testLocation)
+                .also {
+                    it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(75)
+                }
+
+            testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T22:52:30+01:00")).toLocationVisit(testLocation)
+                .also {
+                    it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(60)
+                }
+
+            testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T22:52:29+01:00")).toLocationVisit(testLocation)
+                .also {
+                    it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(45)
+                }
+        }
+    }
+
+    @Test
+    fun `Creates location visits if missing`() = runBlockingTest {
+        every { contactDiaryRepo.locationVisits } returns flowOf(emptyList()) andThen flowOf(listOf(testLocationVisit))
+
+        createInstance().apply {
+            val checkins = mutableListOf(testCheckIn)
+
+            checkins.createMissingLocationVisits(testLocation).also {
+                it[0] shouldBe testLocationVisit
+            }
+
+            checkins.createMissingLocationVisits(testLocation).also {
+                it.isEmpty() shouldBe true
+            }
+
+            // Create check in for next day which should also create a visit for the next day
+            val testCheckInNextDay = testCheckIn.copy(
+                checkInStart = testCheckIn.checkInStart.plus(Days.ONE.toStandardDuration()),
+                checkInEnd = testCheckIn.checkInEnd.plus(Days.ONE.toStandardDuration())
+            )
+            checkins.add(testCheckInNextDay)
+
+            checkins.createMissingLocationVisits(testLocation).also {
+                it.size shouldBe 1 // and not 2
+                it[0] shouldBe testLocationVisit.copy(date = testLocationVisit.date.plusDays(1))
+            }
+        }
+    }
+
+    @Test
+    fun `Creates 1 location and 2 visits for split check in`() = runBlockingTest {
+        val splitCheckIn = testCheckIn.copy(
+            checkInStart = Instant.parse("2021-03-04T22:00+01:00"),
+            checkInEnd = Instant.parse("2021-03-05T02:00+01:00")
+        )
+        createInstance().apply {
+            createEntry(splitCheckIn)
+
+            coVerify(exactly = 1) {
+                contactDiaryRepo.addLocation(any())
+            }
+
+            coVerify(exactly = 2) {
+                contactDiaryRepo.addLocationVisit(any())
+            }
         }
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt
index f59cd16228473db4a9530fa15857c09c758b048a..b0bfef2136687cb4c6cd327fe96b42c47814a1e3 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt
@@ -103,7 +103,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() {
         )
 
         runBlockingTest {
-            val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime))
+            val result = createInstance().calculateDayRisk(listOf(normTime))
             result.size shouldBe 1
             result[0].riskState shouldBe RiskState.CALCULATION_FAILED
         }
@@ -133,7 +133,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() {
         )
 
         runBlockingTest {
-            val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime, normTime2, normTime3))
+            val result = createInstance().calculateDayRisk(listOf(normTime, normTime2, normTime3))
             result.size shouldBe 2
             result.find { it.localDateUtc == localDate }!!.riskState shouldBe RiskState.INCREASED_RISK
             result.find { it.localDateUtc == localDate2 }!!.riskState shouldBe RiskState.LOW_RISK
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ab4464a2e41681105bb35eda245bdb5a662ba7dd
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt
@@ -0,0 +1,255 @@
+package de.rki.coronawarnapp.presencetracing.risk.storage
+
+import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInNormalizedTime
+import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInRiskPerDay
+import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningOverlap
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator
+import de.rki.coronawarnapp.risk.RiskState
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Days
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class PresenceTracingRiskRepositoryTest : BaseTest() {
+
+    @MockK lateinit var presenceTracingRiskCalculator: PresenceTracingRiskCalculator
+    @MockK lateinit var databaseFactory: PresenceTracingRiskDatabase.Factory
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var traceTimeIntervalMatchDao: TraceTimeIntervalMatchDao
+    @MockK lateinit var riskLevelResultDao: PresenceTracingRiskLevelResultDao
+    @MockK lateinit var database: PresenceTracingRiskDatabase
+
+    private val now = Instant.ofEpochMilli(9999999)
+    private val fifteenDaysAgo = now.minus(Days.days(15).toStandardDuration())
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        every { timeStamper.nowUTC } returns now
+
+        every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(emptyList())
+        coEvery { traceTimeIntervalMatchDao.insert(any()) } just Runs
+        coEvery { traceTimeIntervalMatchDao.deleteMatchesForPackage(any()) } just Runs
+        coEvery { traceTimeIntervalMatchDao.deleteAll() } just Runs
+        coEvery { traceTimeIntervalMatchDao.deleteOlderThan(any()) } just Runs
+
+        every { riskLevelResultDao.insert(any()) } just Runs
+        coEvery { riskLevelResultDao.deleteOlderThan(any()) } just Runs
+
+        coEvery { databaseFactory.create() } returns database
+        every { database.traceTimeIntervalMatchDao() } returns traceTimeIntervalMatchDao
+        every { database.presenceTracingRiskLevelResultDao() } returns riskLevelResultDao
+
+        coEvery { presenceTracingRiskCalculator.calculateNormalizedTime(any()) } returns listOf()
+        coEvery { presenceTracingRiskCalculator.calculateTotalRisk(any()) } returns RiskState.LOW_RISK
+    }
+
+    @Test
+    fun `overlapsOfLast14DaysPlusToday works`() {
+        val entity = TraceTimeIntervalMatchEntity(
+            checkInId = 1L,
+            traceWarningPackageId = "traceWarningPackageId",
+            transmissionRiskLevel = 1,
+            startTimeMillis = fifteenDaysAgo.minus(100000).millis,
+            endTimeMillis = fifteenDaysAgo.millis
+        )
+        val entity2 = TraceTimeIntervalMatchEntity(
+            checkInId = 2L,
+            traceWarningPackageId = "traceWarningPackageId",
+            transmissionRiskLevel = 1,
+            startTimeMillis = now.minus(100000).millis,
+            endTimeMillis = now.minus(80000).millis
+        )
+        every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(entity, entity2))
+        runBlockingTest {
+            val overlaps = createInstance().overlapsOfLast14DaysPlusToday.first()
+            overlaps.size shouldBe 1
+            overlaps[0].checkInId shouldBe 2L
+        }
+    }
+
+    @Test
+    fun `traceLocationCheckInRiskStates works`() {
+        val entity2 = TraceTimeIntervalMatchEntity(
+            checkInId = 2L,
+            traceWarningPackageId = "traceWarningPackageId",
+            transmissionRiskLevel = 1,
+            startTimeMillis = now.minus(100000).millis,
+            endTimeMillis = now.minus(80000).millis
+        )
+        every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(entity2))
+        val time = CheckInNormalizedTime(
+            checkInId = 2L,
+            localDateUtc = now.minus(100000).toLocalDateUtc(),
+            normalizedTime = 20.0
+        )
+        val riskPerDay = CheckInRiskPerDay(
+            checkInId = 2L,
+            localDateUtc = now.minus(100000).toLocalDateUtc(),
+            riskState = RiskState.LOW_RISK
+        )
+        coEvery { presenceTracingRiskCalculator.calculateNormalizedTime(listOf(entity2.toCheckInWarningOverlap())) } returns listOf(
+            time
+        )
+        coEvery { presenceTracingRiskCalculator.calculateCheckInRiskPerDay(listOf(time)) } returns listOf(riskPerDay)
+        runBlockingTest {
+            val riskStates = createInstance().traceLocationCheckInRiskStates.first()
+            riskStates.size shouldBe 1
+            riskStates[0].checkInId shouldBe 2L
+            riskStates[0].riskState shouldBe RiskState.LOW_RISK
+        }
+    }
+
+    @Test
+    fun `presenceTracingDayRisk works`() {
+        val dayRisk = PresenceTracingDayRisk(
+            localDateUtc = now.minus(100000).toLocalDateUtc(),
+            riskState = RiskState.LOW_RISK
+        )
+        coEvery { presenceTracingRiskCalculator.calculateDayRisk(any()) } returns listOf(dayRisk)
+        runBlockingTest {
+            val risks = createInstance().presenceTracingDayRisk.first()
+            risks.size shouldBe 1
+            risks[0].riskState shouldBe RiskState.LOW_RISK
+        }
+    }
+
+    @Test
+    fun `latestEntries works`() {
+        val resultEntity = PresenceTracingRiskLevelResultEntity(
+            calculatedAtMillis = now.minus(100000).millis,
+            riskState = RiskState.LOW_RISK
+        )
+        val resultEntity2 = PresenceTracingRiskLevelResultEntity(
+            calculatedAtMillis = now.minus(10000).millis,
+            riskState = RiskState.LOW_RISK
+        )
+        coEvery { riskLevelResultDao.latestEntries(2) } returns flowOf(listOf(resultEntity, resultEntity2))
+        val matchEntity = TraceTimeIntervalMatchEntity(
+            checkInId = 1L,
+            traceWarningPackageId = "traceWarningPackageId",
+            transmissionRiskLevel = 1,
+            startTimeMillis = now.minus(100000).millis,
+            endTimeMillis = now.millis
+        )
+        val matchEntity2 = TraceTimeIntervalMatchEntity(
+            checkInId = 2L,
+            traceWarningPackageId = "traceWarningPackageId",
+            transmissionRiskLevel = 1,
+            startTimeMillis = now.minus(100000).millis,
+            endTimeMillis = now.minus(80000).millis
+        )
+        every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(matchEntity, matchEntity2))
+        val dayRisk = PresenceTracingDayRisk(
+            localDateUtc = now.minus(100000).toLocalDateUtc(),
+            riskState = RiskState.LOW_RISK
+        )
+        coEvery { presenceTracingRiskCalculator.calculateDayRisk(any()) } returns listOf(dayRisk)
+        runBlockingTest {
+            val latest = createInstance().latestEntries(2).first()
+            latest.size shouldBe 2
+            latest[0].calculatedAt shouldBe now.minus(10000)
+            latest[0].checkInOverlapCount shouldBe 2
+            latest[1].calculatedAt shouldBe now.minus(100000)
+            latest[1].checkInOverlapCount shouldBe 0
+        }
+    }
+
+    @Test
+    fun `deleteStaleData works`() {
+        runBlockingTest {
+            createInstance().deleteStaleData()
+            coVerify {
+                traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis)
+                riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis)
+            }
+        }
+    }
+
+    @Test
+    fun `deleteAllMatches works`() {
+        runBlockingTest {
+            createInstance().deleteAllMatches()
+            coVerify { traceTimeIntervalMatchDao.deleteAll() }
+        }
+    }
+
+    @Test
+    fun `report successful calculation works`() {
+        val traceWarningPackageId = "traceWarningPackageId"
+        val overlap = CheckInWarningOverlap(
+            checkInId = 1L,
+            transmissionRiskLevel = 1,
+            traceWarningPackageId = traceWarningPackageId,
+            startTime = Instant.ofEpochMilli(9991000),
+            endTime = Instant.ofEpochMilli(9997000)
+        )
+
+        val result = PresenceTracingRiskLevelResultEntity(
+            calculatedAtMillis = now.millis,
+            riskState = RiskState.LOW_RISK
+        )
+        runBlockingTest {
+            createInstance().reportCalculation(
+                successful = true,
+                overlaps = listOf(overlap)
+            )
+
+            coVerify {
+                traceTimeIntervalMatchDao.deleteMatchesForPackage(traceWarningPackageId)
+                traceTimeIntervalMatchDao.insert(listOf(overlap.toTraceTimeIntervalMatchEntity()))
+                riskLevelResultDao.insert(result)
+            }
+        }
+    }
+
+    @Test
+    fun `report failed calculation works`() {
+        val traceWarningPackageId = "traceWarningPackageId"
+        val overlap = CheckInWarningOverlap(
+            checkInId = 1L,
+            transmissionRiskLevel = 1,
+            traceWarningPackageId = traceWarningPackageId,
+            startTime = Instant.ofEpochMilli(9991000),
+            endTime = Instant.ofEpochMilli(9997000)
+        )
+
+        val result = PresenceTracingRiskLevelResultEntity(
+            calculatedAtMillis = now.millis,
+            riskState = RiskState.CALCULATION_FAILED
+        )
+        runBlockingTest {
+            createInstance().reportCalculation(
+                successful = false,
+                overlaps = listOf(overlap)
+            )
+
+            coVerify {
+                traceTimeIntervalMatchDao.deleteMatchesForPackage(traceWarningPackageId)
+                traceTimeIntervalMatchDao.insert(any())
+                riskLevelResultDao.insert(result)
+            }
+        }
+    }
+
+    private fun createInstance() = PresenceTracingRiskRepository(
+        presenceTracingRiskCalculator,
+        databaseFactory,
+        timeStamper
+    )
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt
index fdff4062dbc0aefef65a35dc69770f9fb3462280..4ad2bbd28da2b03920fd7d3a785ae6229519c7fd 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt
@@ -13,6 +13,7 @@ import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testPersistedAggreg
 import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRiskLevelResultDao
 import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResult
 import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResultWithAggregatedRiskPerDateResult
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.AggregatedRiskPerDateResultDao
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao
@@ -21,6 +22,7 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResults
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
+import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
@@ -53,11 +55,19 @@ class BaseRiskLevelStorageTest : BaseTest() {
     @MockK lateinit var exposureWindowTables: ExposureWindowsDao
     @MockK lateinit var aggregatedRiskPerDateResultDao: AggregatedRiskPerDateResultDao
     @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository
+    @MockK lateinit var timeStamper: TimeStamper
+
+    private lateinit var riskCombinator: RiskCombinator
 
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
 
+        every { timeStamper.nowUTC } returns Instant.parse("2021-01-01T12:00:00.000Z")
+        riskCombinator = RiskCombinator(
+            timeStamper = timeStamper
+        )
+
         every { databaseFactory.create() } returns database
         every { database.riskResults() } returns riskResultTables
         every { database.exposureWindows() } returns exposureWindowTables
@@ -91,7 +101,8 @@ class BaseRiskLevelStorageTest : BaseTest() {
     ) = object : BaseRiskLevelStorage(
         scope = scope,
         riskResultDatabaseFactory = databaseFactory,
-        presenceTracingRiskRepository = presenceTracingRiskRepository
+        presenceTracingRiskRepository = presenceTracingRiskRepository,
+        riskCombinator = riskCombinator,
     ) {
         override val storedResultLimit: Int = storedResultLimit
 
@@ -256,7 +267,7 @@ class BaseRiskLevelStorageTest : BaseTest() {
 
             // result from the combination with initial ew low risk result
             riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(400L)
-            riskLevelResults[1].riskState shouldBe RiskState.CALCULATION_FAILED
+            riskLevelResults[1].riskState shouldBe RiskState.LOW_RISK
 
             verify {
                 riskResultTables.latestEntries(2)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt
similarity index 63%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt
index 9e1122826a32e6f43db61015975e11d0421c3a17..3d23b8d37e02bafdd04f5ae0f76d094417f07845 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt
@@ -1,45 +1,81 @@
-package de.rki.coronawarnapp.risk.storage
+package de.rki.coronawarnapp.risk.storage.internal
 
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
 import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
 import de.rki.coronawarnapp.risk.RiskState
+import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED
+import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK
+import de.rki.coronawarnapp.risk.RiskState.LOW_RISK
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
+import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
 import org.joda.time.Instant
 import org.joda.time.LocalDate
+import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
 
-class CombineRiskTest {
+class RiskCombinatorTest : BaseTest() {
+
+    @MockK lateinit var timeStamper: TimeStamper
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234567890)
+    }
+
+    private fun createInstance() = RiskCombinator(
+        timeStamper = timeStamper
+    )
+
+    @Test
+    fun `Initial results`() {
+        createInstance().initialCombinedResult.apply {
+            riskState shouldBe LOW_RISK
+        }
+    }
+
+    @Test
+    fun `Fallback results on empty data`() {
+        createInstance().latestCombinedResult.apply {
+            riskState shouldBe LOW_RISK
+        }
+    }
 
     @Test
     fun `combineRisk works`() {
         val ptRisk0 = PresenceTracingDayRisk(
             localDateUtc = LocalDate(2021, 3, 19),
-            riskState = RiskState.LOW_RISK
+            riskState = LOW_RISK
         )
         val ptRisk1 = PresenceTracingDayRisk(
             localDateUtc = LocalDate(2021, 3, 20),
-            riskState = RiskState.INCREASED_RISK
+            riskState = INCREASED_RISK
         )
         val ptRisk2 = PresenceTracingDayRisk(
             localDateUtc = LocalDate(2021, 3, 21),
-            riskState = RiskState.LOW_RISK
+            riskState = LOW_RISK
         )
         val ptRisk3 = PresenceTracingDayRisk(
             localDateUtc = LocalDate(2021, 3, 22),
-            riskState = RiskState.CALCULATION_FAILED
+            riskState = CALCULATION_FAILED
         )
         val ptRisk4 = PresenceTracingDayRisk(
             localDateUtc = LocalDate(2021, 3, 23),
-            riskState = RiskState.LOW_RISK
+            riskState = LOW_RISK
         )
         val ptRisk5 = PresenceTracingDayRisk(
             localDateUtc = LocalDate(2021, 3, 24),
-            riskState = RiskState.INCREASED_RISK
+            riskState = INCREASED_RISK
         )
 
         val ewRisk0 = ExposureWindowDayRisk(
@@ -81,33 +117,33 @@ class CombineRiskTest {
 
         val ptDayRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk0, ptRisk1, ptRisk2, ptRisk3, ptRisk4, ptRisk5)
         val ewDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk0, ewRisk1, ewRisk2, ewRisk3, ewRisk4, ewRisk5)
-        val result = combineRisk(ptDayRiskList, ewDayRiskList)
+        val result = createInstance().combineRisk(ptDayRiskList, ewDayRiskList)
         result.size shouldBe 7
 
         result.single {
             it.localDate == LocalDate(2021, 3, 15)
-        }.riskState shouldBe RiskState.CALCULATION_FAILED
+        }.riskState shouldBe CALCULATION_FAILED
         result.single {
             it.localDate == LocalDate(2021, 3, 19)
-        }.riskState shouldBe RiskState.LOW_RISK
+        }.riskState shouldBe LOW_RISK
         result.single {
             it.localDate == LocalDate(2021, 3, 20)
-        }.riskState shouldBe RiskState.CALCULATION_FAILED
+        }.riskState shouldBe CALCULATION_FAILED
         result.single {
             it.localDate == LocalDate(2021, 3, 21)
-        }.riskState shouldBe RiskState.LOW_RISK
+        }.riskState shouldBe LOW_RISK
         result.single {
             it.localDate == LocalDate(2021, 3, 22)
-        }.riskState shouldBe RiskState.CALCULATION_FAILED
+        }.riskState shouldBe CALCULATION_FAILED
         result.single {
             it.localDate == LocalDate(2021, 3, 22)
-        }.riskState shouldBe RiskState.CALCULATION_FAILED
+        }.riskState shouldBe CALCULATION_FAILED
         result.single {
             it.localDate == LocalDate(2021, 3, 23)
-        }.riskState shouldBe RiskState.INCREASED_RISK
+        }.riskState shouldBe INCREASED_RISK
         result.single {
             it.localDate == LocalDate(2021, 3, 24)
-        }.riskState shouldBe RiskState.INCREASED_RISK
+        }.riskState shouldBe INCREASED_RISK
     }
 
     @Test
@@ -116,69 +152,73 @@ class CombineRiskTest {
 
         val ptResult = PtRiskLevelResult(
             calculatedAt = startInstant.plus(1000L),
-            riskState = RiskState.LOW_RISK
+            riskState = LOW_RISK
         )
         val ptResult2 = PtRiskLevelResult(
             calculatedAt = startInstant.plus(3000L),
-            riskState = RiskState.LOW_RISK
+            riskState = LOW_RISK
         )
         val ptResult3 = PtRiskLevelResult(
             calculatedAt = startInstant.plus(6000L),
-            riskState = RiskState.CALCULATION_FAILED
+            riskState = CALCULATION_FAILED
         )
         val ptResult4 = PtRiskLevelResult(
             calculatedAt = startInstant.plus(7000L),
-            riskState = RiskState.CALCULATION_FAILED
+            riskState = CALCULATION_FAILED
         )
 
         val ptResults = listOf(ptResult, ptResult2, ptResult4, ptResult3)
         val ewResult = createEwRiskLevelResult(
             calculatedAt = startInstant.plus(2000L),
-            riskState = RiskState.LOW_RISK
+            riskState = LOW_RISK
         )
         val ewResult2 = createEwRiskLevelResult(
             calculatedAt = startInstant.plus(4000L),
-            riskState = RiskState.INCREASED_RISK
+            riskState = INCREASED_RISK
         )
         val ewResult3 = createEwRiskLevelResult(
             calculatedAt = startInstant.plus(5000L),
-            riskState = RiskState.CALCULATION_FAILED
+            riskState = CALCULATION_FAILED
         )
         val ewResult4 = createEwRiskLevelResult(
             calculatedAt = startInstant.plus(8000L),
-            riskState = RiskState.CALCULATION_FAILED
+            riskState = CALCULATION_FAILED
         )
         val ewResults = listOf(ewResult, ewResult4, ewResult2, ewResult3)
-        val result = combineEwPtRiskLevelResults(ptResults, ewResults).sortedByDescending { it.calculatedAt }
+        val result = createInstance().combineEwPtRiskLevelResults(
+            ptRiskResults = ptResults,
+            ewRiskResults = ewResults
+        ).sortedByDescending { it.calculatedAt }
+
         result.size shouldBe 8
-        result[0].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[0].riskState shouldBe CALCULATION_FAILED
         result[0].calculatedAt shouldBe startInstant.plus(8000L)
-        result[1].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[1].riskState shouldBe CALCULATION_FAILED
         result[1].calculatedAt shouldBe startInstant.plus(7000L)
-        result[2].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[2].riskState shouldBe CALCULATION_FAILED
         result[2].calculatedAt shouldBe startInstant.plus(6000L)
-        result[3].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[3].riskState shouldBe CALCULATION_FAILED
         result[3].calculatedAt shouldBe startInstant.plus(5000L)
-        result[4].riskState shouldBe RiskState.INCREASED_RISK
+        result[4].riskState shouldBe INCREASED_RISK
         result[4].calculatedAt shouldBe startInstant.plus(4000L)
-        result[5].riskState shouldBe RiskState.LOW_RISK
+        result[5].riskState shouldBe LOW_RISK
         result[5].calculatedAt shouldBe startInstant.plus(3000L)
-        result[6].riskState shouldBe RiskState.LOW_RISK
+        result[6].riskState shouldBe LOW_RISK
         result[6].calculatedAt shouldBe startInstant.plus(2000L)
-        result[7].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[7].riskState shouldBe LOW_RISK
         result[7].calculatedAt shouldBe startInstant.plus(1000L)
     }
 
     @Test
     fun `max RiskState works`() {
-        combine(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
-        combine(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK
-        combine(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED
-        combine(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
-        combine(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.CALCULATION_FAILED
-        combine(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK
-        combine(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.CALCULATION_FAILED
-        combine(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED
+        RiskCombinator.combine(INCREASED_RISK, INCREASED_RISK) shouldBe INCREASED_RISK
+        RiskCombinator.combine(INCREASED_RISK, LOW_RISK) shouldBe INCREASED_RISK
+        RiskCombinator.combine(INCREASED_RISK, CALCULATION_FAILED) shouldBe CALCULATION_FAILED
+        RiskCombinator.combine(LOW_RISK, INCREASED_RISK) shouldBe INCREASED_RISK
+        RiskCombinator.combine(CALCULATION_FAILED, INCREASED_RISK) shouldBe CALCULATION_FAILED
+        RiskCombinator.combine(LOW_RISK, LOW_RISK) shouldBe LOW_RISK
+        RiskCombinator.combine(CALCULATION_FAILED, LOW_RISK) shouldBe CALCULATION_FAILED
+        RiskCombinator.combine(CALCULATION_FAILED, CALCULATION_FAILED) shouldBe CALCULATION_FAILED
     }
 }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt
index 22595f0c3df4cbee7984b097e5528d19d122db34..71370335eeb30b6f17ba1c479cb12a3d0c467da3 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt
@@ -3,10 +3,10 @@ package de.rki.coronawarnapp.task
 import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
-import de.rki.coronawarnapp.task.example.QueueingTask
 import de.rki.coronawarnapp.task.testtasks.SkippingTask
 import de.rki.coronawarnapp.task.testtasks.alerterror.AlertErrorTask
 import de.rki.coronawarnapp.task.testtasks.precondition.PreconditionTask
+import de.rki.coronawarnapp.task.testtasks.queue.QueueingTask
 import de.rki.coronawarnapp.task.testtasks.silenterror.SilentErrorTask
 import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTask
 import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTask2
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt
index cb0848c9fbbb770cbb8baa2f7c3ee8b4b073b65e..1b0b3c944725d2499b6f232044132929c6ff569e 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt
@@ -3,7 +3,7 @@ package de.rki.coronawarnapp.task.testtasks
 import de.rki.coronawarnapp.task.Task
 import de.rki.coronawarnapp.task.TaskFactory
 import de.rki.coronawarnapp.task.common.DefaultProgress
-import de.rki.coronawarnapp.task.example.QueueingTask
+import de.rki.coronawarnapp.task.testtasks.queue.QueueingTask
 import org.joda.time.Duration
 import javax.inject.Inject
 import javax.inject.Provider
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTask.kt
similarity index 98%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTask.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTask.kt
index d34f0e8890919b779fb7a51dc598213b1a255f5b..0c7d67d9dab2f9243f9043d362741b39614948c2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTask.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTask.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.task.example
+package de.rki.coronawarnapp.task.testtasks.queue
 
 import de.rki.coronawarnapp.task.Task
 import de.rki.coronawarnapp.task.TaskCancellationException
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTaskModule.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTaskModule.kt
similarity index 89%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTaskModule.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTaskModule.kt
index 3b15fdb8f85de48ffef00ac92f3fd0f17186d7f8..ff14228d2d669b0f647ff5fc6795d17b08f3c00d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTaskModule.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTaskModule.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.task.example
+package de.rki.coronawarnapp.task.testtasks.queue
 
 import dagger.Binds
 import dagger.Module
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt
index c1adf45da1f8b75e2e530e7cd7bb86f1d120bf61..7efbed5de4b31e4457ff92bac769a757a49b9eaa 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationVerifier
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler
 import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH
@@ -41,6 +42,7 @@ class CheckInsViewModelTest : BaseTest() {
     @MockK lateinit var checkInsRepository: CheckInRepository
     @MockK lateinit var checkOutHandler: CheckOutHandler
     @MockK lateinit var cameraPermissionProvider: CameraPermissionProvider
+    @MockK lateinit var traceLocationVerifier: TraceLocationVerifier
 
     @BeforeEach
     fun setup() {
@@ -48,6 +50,8 @@ class CheckInsViewModelTest : BaseTest() {
         every { savedState.set(any(), any<String>()) } just Runs
         every { checkInsRepository.checkInsWithinRetention } returns flowOf()
         every { cameraPermissionProvider.deniedPermanently } returns flowOf(false)
+        every { traceLocationVerifier.verifyTraceLocation(any()) } returns
+            TraceLocationVerifier.VerificationResult.Valid(mockk())
     }
 
     @Test
@@ -185,7 +189,9 @@ class CheckInsViewModelTest : BaseTest() {
             qrCodeUriParser = qrCodeUriParser,
             checkInsRepository = checkInsRepository,
             checkOutHandler = checkOutHandler,
-            cameraPermissionProvider = cameraPermissionProvider
+            cameraPermissionProvider = cameraPermissionProvider,
+            traceLocationVerifier = traceLocationVerifier,
+            cleanHistory = false
         )
 
     companion object {
diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
index 274a5e5f941e266f6db258ecf57cad71bd51e806..673ac7b96c843f1c2ed9909a24b0a68e7d383ff9 100644
--- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
+++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
@@ -5,9 +5,11 @@ import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepo
 import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -97,7 +99,8 @@ class DefaultRiskLevelStorageTest : BaseTest() {
     ) = DefaultRiskLevelStorage(
         scope = scope,
         riskResultDatabaseFactory = databaseFactory,
-        presenceTracingRiskRepository = presenceTracingRiskRepository
+        presenceTracingRiskRepository = presenceTracingRiskRepository,
+        riskCombinator = RiskCombinator(TimeStamper()),
     )
 
     @Test
diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
index 72eae29914ebfe630caea23002104922563ca4dd..5502ead593df03320642893e077e6a6201216d88 100644
--- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
+++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt
@@ -5,9 +5,11 @@ import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepo
 import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage
+import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -94,7 +96,8 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() {
     private fun createInstance() = DefaultRiskLevelStorage(
         scope = TestCoroutineScope(),
         riskResultDatabaseFactory = databaseFactory,
-        presenceTracingRiskRepository = presenceTracingRiskRepository
+        presenceTracingRiskRepository = presenceTracingRiskRepository,
+        riskCombinator = RiskCombinator(TimeStamper())
     )
 
     @Test