diff --git a/.circleci/config.yml b/.circleci/config.yml
index e368d3a47e261dc557e1e2802beb3da54d7ac4e7..20f3f4a0c66333127c4e0480a8e3b4a3daca7be0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -396,7 +396,7 @@ jobs:
       - run:
           name: Download Environment Properties
           command: |
-            curl --header "Authorization: token $keystore_download_token" --header "Accept: application/vnd.github.v3.raw" --remote-name --location "$keystore_download_url$env_prop_download_filename"
+            curl --header "Authorization: token $keystore_download_token" --header "Accept: application/vnd.github.v3.raw" --remote-name --location "$environments_download_url$env_prop_download_filename"
       - run:
           name: Decrypt Keystore
           command: openssl enc -aes-256-cbc -d -pbkdf2 -iter 100000 -in $keystore_download_filename -out $keystore_filename -k $keystore_encrypt_key
diff --git a/.reuse/dep5 b/.reuse/dep5
index f04f0807befd63a90e2e5b9c54f6c1fdb7d3d465..b1088ff6d57f1f228fdbe25522ebe3b70c80941e 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -40,6 +40,14 @@ Files: Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/ViewMod
 Copyright: 2018 The Android Open Source Project
 License: Apache-2.0
 
+Files: Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt
+Copyright: 2018 The Android Open Source Project
+License: Apache-2.0
+
 Files: gradlew gradlew.bat
 Copyright: Copyright 2015 the original author or authors.
+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
\ No newline at end of file
diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json
deleted file mode 100644
index 1109cd70c0a4675c7b28f92bdd500ac9ca638fad..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.checkins.riskcalculation.PresenceTracingDatabase/1.json
+++ /dev/null
@@ -1,64 +0,0 @@
-{
-  "formatVersion": 1,
-  "database": {
-    "version": 1,
-    "identityHash": "751c249cb9c836add4b0cb663ed13954",
-    "entities": [
-      {
-        "tableName": "TraceTimeIntervalMatchEntity",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `checkInId` INTEGER NOT NULL, `traceWarningPackageId` INTEGER NOT NULL, `transmissionRiskLevel` INTEGER NOT NULL, `startTimeMillis` INTEGER NOT NULL, `endTimeMillis` INTEGER NOT NULL)",
-        "fields": [
-          {
-            "fieldPath": "id",
-            "columnName": "id",
-            "affinity": "INTEGER",
-            "notNull": false
-          },
-          {
-            "fieldPath": "checkInId",
-            "columnName": "checkInId",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "traceWarningPackageId",
-            "columnName": "traceWarningPackageId",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "transmissionRiskLevel",
-            "columnName": "transmissionRiskLevel",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "startTimeMillis",
-            "columnName": "startTimeMillis",
-            "affinity": "INTEGER",
-            "notNull": true
-          },
-          {
-            "fieldPath": "endTimeMillis",
-            "columnName": "endTimeMillis",
-            "affinity": "INTEGER",
-            "notNull": true
-          }
-        ],
-        "primaryKey": {
-          "columnNames": [
-            "id"
-          ],
-          "autoGenerate": true
-        },
-        "indices": [],
-        "foreignKeys": []
-      }
-    ],
-    "views": [],
-    "setupQueries": [
-      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '751c249cb9c836add4b0cb663ed13954')"
-    ]
-  }
-}
\ No newline at end of file
diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json
index 9e78f2a34c5721273cb56e001bd388c5b50decf5..befd229435a5e47f296ec11550b0b1534c07e0cb 100644
--- a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json
+++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/1.json
@@ -2,11 +2,11 @@
   "formatVersion": 1,
   "database": {
     "version": 1,
-    "identityHash": "1dc5f8a56361d50b8bb18050bce59d20",
+    "identityHash": "5117ed4caaa7ecd70051902d844cc665",
     "entities": [
       {
         "tableName": "checkin",
-        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` TEXT NOT NULL, `traceLocationIdHashBase64` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `traceLocationStart` TEXT, `traceLocationEnd` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL)",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `traceLocationStart` TEXT, `traceLocationEnd` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL)",
         "fields": [
           {
             "fieldPath": "id",
@@ -20,12 +20,6 @@
             "affinity": "TEXT",
             "notNull": true
           },
-          {
-            "fieldPath": "traceLocationIdHashBase64",
-            "columnName": "traceLocationIdHashBase64",
-            "affinity": "TEXT",
-            "notNull": true
-          },
           {
             "fieldPath": "version",
             "columnName": "version",
@@ -192,7 +186,7 @@
     "views": [],
     "setupQueries": [
       "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
-      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1dc5f8a56361d50b8bb18050bce59d20')"
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5117ed4caaa7ecd70051902d844cc665')"
     ]
   }
 }
\ No newline at end of file
diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase/1.json
similarity index 100%
rename from Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskDatabase/1.json
rename to Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskDatabase/1.json
diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..71c7f0b1df463c61c0d7ce53f985d3ad898907d1
--- /dev/null
+++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningDatabase/1.json
@@ -0,0 +1,76 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 1,
+    "identityHash": "1775e362b403962906c6d384338b2708",
+    "entities": [
+      {
+        "tableName": "TraceWarningPackageMetadata",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `createdAt` TEXT NOT NULL, `location` TEXT NOT NULL, `hourInterval` INTEGER NOT NULL, `eTag` TEXT, `downloaded` INTEGER NOT NULL, `emptyPkg` INTEGER NOT NULL, `processed` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "packageId",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "location",
+            "columnName": "location",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "hourInterval",
+            "columnName": "hourInterval",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "eTag",
+            "columnName": "eTag",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "isDownloaded",
+            "columnName": "downloaded",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isEmptyPkg",
+            "columnName": "emptyPkg",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isProcessed",
+            "columnName": "processed",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "setupQueries": [
+      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1775e362b403962906c6d384338b2708')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt
index 8bbe73ab7457b385ff6573a73f5ebfff6d902b8d..2e78d0912c047be24b4ab9c53a46da693f0deaf7 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocationTest.kt
@@ -1,13 +1,10 @@
 package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
-import de.rki.coronawarnapp.environment.EnvironmentSetup
+import android.os.Bundle
+import android.os.Parcel
 import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
 import io.kotest.matchers.shouldBe
-import io.mockk.MockKAnnotations
-import io.mockk.every
-import io.mockk.impl.annotations.MockK
 import okio.ByteString.Companion.decodeBase64
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -15,254 +12,68 @@ import testhelpers.BaseTestInstrumentation
 
 @RunWith(JUnit4::class)
 class VerifiedTraceLocationTest : BaseTestInstrumentation() {
-
-    @MockK lateinit var environmentSetup: EnvironmentSetup
-
-    @Before
-    fun setUp() {
-        MockKAnnotations.init(this)
-        every { environmentSetup.appConfigVerificationKey } returns PUB_KEY
-    }
-
-    // TODO: Ugly but kinda works
     @Test
-    fun verifyTraceLocationIdGenerationHash1() {
-        val base64Payload = "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekAT" +
-            "D3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEst" +
-            "cUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3" +
-            "cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg=="
-        val base64LocationID = "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0="
-
-        val qrCodePayload =
-            TraceLocationOuterClass.QRCodePayload.parseFrom(base64Payload.decodeBase64()!!.toByteArray())
-        val instance = VerifiedTraceLocation(qrCodePayload)
+    fun verifyTraceLocationMapping1() {
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            BASE64_PAYLOAD_1.decodeBase64()!!.toByteArray()
+        )
 
-        instance.traceLocationID.sha256().base64() shouldBe base64LocationID
+        VerifiedTraceLocation(qrCodePayload).traceLocation
+            .apply {
+                locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0="
+                qrCodePayload() shouldBe qrCodePayload
+            }
     }
 
     @Test
-    fun verifyTraceLocationIdGenerationHash2() {
-        val base64Payload = "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJ" +
-            "bMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT" +
-            "0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK"
-        val base64LocationID = "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ="
-
-        val qrCodePayload =
-            TraceLocationOuterClass.QRCodePayload.parseFrom(base64Payload.decodeBase64()!!.toByteArray())
-        val instance = VerifiedTraceLocation(qrCodePayload)
-
-        instance.traceLocationID.sha256().base64() shouldBe base64LocationID
-    }
-
-    /* disabled because of incompatibilities due to latest tech spec changes... needs to be re-written anyway
-
-@Test
-fun verifyEventSuccess() = runBlockingTest {
-    val instant = Instant.ofEpochMilli(2687960 * 1_000L)
-    shouldNotThrowAny {
-        val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray())
-        verifyResult.apply {
-            traceLocation.description shouldBe "My Birthday Party"
-            traceLocation.isBeforeStartTime(instant) shouldBe false
-            traceLocation.isAfterEndTime(instant) shouldBe false
-        }
-    }
-}
-
-@Test
-fun verifyParcelization() = runBlockingTest {
-    val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray())
-
-    val expectedTraceLocation = TraceLocation(
-        guid = "3055331c-2306-43f3-9742-6d8fab54e848",
-        version = 1,
-        type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
-        description = "My Birthday Party",
-        address = "at my place",
-        startDate = Instant.ofEpochSecond(2687955),
-        endDate = Instant.ofEpochSecond(2687991),
-        defaultCheckInLengthInMinutes = 0,
-        byteRepresentation = verifyResult.traceLocationBytes,
-        signature = verifyResult.signature.toByteArray().toByteString(),
-    )
-
-    verifyResult.traceLocation shouldBe expectedTraceLocation
-
-    val bundle = Bundle().apply {
-        putParcelable("test", verifyResult.traceLocation)
-    }
-
-    val parcelRaw = Parcel.obtain().apply {
-        writeBundle(bundle)
-    }.marshall()
-
-    val restoredParcel = Parcel.obtain().apply {
-        unmarshall(parcelRaw, 0, parcelRaw.size)
-        setDataPosition(0)
-    }
-
-    val restoredData = restoredParcel.readBundle()!!.run {
-        classLoader = TraceLocation::class.java.classLoader
-        getParcelable<TraceLocation>("test")
-    }
-    restoredData shouldBe expectedTraceLocation
-}
-
-@Test
-fun verifyEventStartTimeWaning() = runBlockingTest {
-    val instant = Instant.ofEpochMilli(2687940 * 1_000L)
-    shouldNotThrowAny {
-        val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray())
-        verifyResult.apply {
-            traceLocation.description shouldBe "My Birthday Party"
-            traceLocation.isBeforeStartTime(instant) shouldBe true
-            traceLocation.isAfterEndTime(instant) shouldBe false
-        }
-    }
-}
+    fun verifyTraceLocationMapping2() {
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            BASE64_PAYLOAD_2.decodeBase64()!!.toByteArray()
+        )
 
-@Test
-fun verifyEventEndTimeWarning() = runBlockingTest {
-    val instant = Instant.now()
-    shouldNotThrowAny {
-        val verifyResult = traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray())
-        verifyResult.apply {
-            traceLocation.description shouldBe "My Birthday Party"
-            traceLocation.isBeforeStartTime(instant) shouldBe false
-            traceLocation.isAfterEndTime(instant) shouldBe true
-        }
+        VerifiedTraceLocation(qrCodePayload).traceLocation
+            .apply {
+                locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ="
+                qrCodePayload() shouldBe qrCodePayload
+            }
     }
-}
 
-@Test
-fun verifyEventWithInvalidKey() = runBlockingTest {
-    every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY
-    shouldThrow<InvalidQRCodeSignatureException> {
-        traceLocationQRCodeVerifier.verify(ENCODED_EVENT1.decodeBase32().toByteArray())
-    }
-}
-
-@Test
-fun eventHasMalformedData() = runBlockingTest {
-    shouldThrow<InvalidQRCodeDataException> {
-        traceLocationQRCodeVerifier.verify(
-            INVALID_ENCODED_EVENT.decodeBase32().toByteArray()
+    @Test
+    fun parcelization() {
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            BASE64_PAYLOAD_2.decodeBase64()!!.toByteArray()
         )
-    }
-}
 
-@Test
-fun decodingTest1() = runBlockingTest {
-    val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom(
-        ENCODED_EVENT1.decodeBase32().toByteArray()
-    )
-    val expectedSignature =
-        "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ=="
+        val expectedVerifiedLocation = VerifiedTraceLocation(qrCodePayload)
 
-    val base32 = signedTraceLocation.toByteArray().toByteString().base32()
-
-    shouldNotThrowAny {
-        val verifyResult = traceLocationQRCodeVerifier.verify(base32.decodeBase32().toByteArray())
-
-        verifyResult.apply {
-            traceLocation.description shouldBe "My Birthday Party"
-            signedTraceLocation.signature.toByteArray().toByteString().base64() shouldBe expectedSignature
+        val bundle = Bundle().apply {
+            putParcelable("verifiedTraceLocation", expectedVerifiedLocation)
         }
-    }
-}
 
-@Test
-fun decodingTest2() = runBlockingTest {
-    val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom(
-        ENCODED_EVENT2.decodeBase32().toByteArray()
-    )
-    val expectedSignature =
-        "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA=="
+        val parcelRaw = Parcel.obtain().apply {
+            writeBundle(bundle)
+        }.marshall()
 
-    val base32 = signedTraceLocation.toByteArray().toByteString().base32()
-
-    shouldNotThrowAny {
-        val verifyResult = traceLocationQRCodeVerifier.verify(base32.decodeBase32().toByteArray())
-
-        verifyResult.apply {
-            traceLocation.description shouldBe "Icecream Shop"
-            signedTraceLocation.signature.toByteArray().toByteString().base64() shouldBe expectedSignature
+        val restoredParcel = Parcel.obtain().apply {
+            unmarshall(parcelRaw, 0, parcelRaw.size)
+            setDataPosition(0)
         }
-    }
-}
 
-@Test
-fun testVerifiedTraceLocationMapping() {
-    shouldNotThrowAny {
-        val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom(
-            ENCODED_EVENT1.decodeBase32().toByteArray()
-        )
-
-        val traceLocation = TraceLocationOuterClass.TraceLocation.parseFrom(
-            ENCODED_EVENT1_LOCATION.decodeBase32().toByteArray()
-        )
-        val verifiedTraceLocation = VerifiedTraceLocation(
-            protoSignedTraceLocation = signedTraceLocation,
-            protoTraceLocation = traceLocation
-        ).traceLocation
-
-        verifiedTraceLocation shouldBe TraceLocation(
-            guid = "3055331c-2306-43f3-9742-6d8fab54e848",
-            version = 1,
-            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
-            description = "My Birthday Party",
-            address = "at my place",
-            startDate = Instant.ofEpochSecond(2687955),
-            endDate = Instant.ofEpochSecond(2687991),
-            defaultCheckInLengthInMinutes = 0,
-            byteRepresentation = signedTraceLocation.location.toByteArray().toByteString(),
-            signature = signedTraceLocation.signature.toByteArray().toByteString()
-        )
+        val restoredData = restoredParcel.readBundle()!!.run {
+            classLoader = VerifiedTraceLocation::class.java.classLoader
+            getParcelable<VerifiedTraceLocation>("verifiedTraceLocation")
+        }
+        restoredData shouldBe expectedVerifiedLocation
     }
-}
-
-*/
 
     companion object {
+        private const val BASE64_PAYLOAD_1 = "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekAT" +
+            "D3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEst" +
+            "cUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3" +
+            "cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg=="
 
-        //   "signedLocation": {
-        //    "location": {
-        //      "guid": "3055331c-2306-43f3-9742-6d8fab54e848",
-        //      "version": 1,
-        //      "type": 2,
-        //      "description": "My Birthday Party",
-        //      "address": "at my place",
-        //      "startTimestamp": 2687955,
-        //      "endTimestamp": 2687991,
-        //      "defaultCheckInLengthInMinutes": 0
-        //    },
-        //    "signature": "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ=="
-        private const val ENCODED_EVENT1 =
-            "BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER"
-        private const val ENCODED_EVENT1_LOCATION =
-            "BISDGMBVGUZTGMLDFUZDGMBWFU2DGZRTFU4TONBSFU3GIODGMFRDKNDFHA2DQEABDABCEEKNPEQEE2LSORUGIYLZEBIGC4TUPEVAWYLUEBWXSIDQNRQWGZJQ2OD2IAJY66D2IAKAAA"
-
-        //   "signedLocation": {
-        //    "location": {
-        //      "guid": "fca84b37-61c0-4a7c-b2f8-825cadd506cf",
-        //      "version": 1,
-        //      "type": 1,
-        //      "description": "Icecream Shop",
-        //      "address": "Main Street 1",
-        //      "startTimestamp": 0,
-        //      "endTimestamp": 0,
-        //      "defaultCheckInLengthInMinutes": 10
-        //    },
-        //    "signature": "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA=="
-        private const val ENCODED_EVENT2 =
-            "BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA"
-
-        private const val INVALID_ENCODED_EVENT =
-            "NB2HI4DTHIXS653XO4XHK4TCMFXGI2LDORUW63TBOJ4S4Y3PNUXWIZLGNFXGKLTQNBYD65DFOJWT2VDIMUSTEMCDN53GSZBFGIYDCOI="
-
-        private const val PUB_KEY =
-            "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw=="
-        private const val INVALID_PUB_KEY =
-            "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="
+        private const val BASE64_PAYLOAD_2 = "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJ" +
+            "bMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT" +
+            "0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK"
     }
 }
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt
index 500e914851840d734dbeb11f16f85fbd1dd5d42c..370276daf4fe3f5f3dae2b3a188cd403f52488b2 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt
@@ -9,7 +9,6 @@ object CheckInDatabaseData {
 
     val testCheckIn = TraceLocationCheckInEntity(
         traceLocationIdBase64 = "traceLocationId1".encode().base64(),
-        traceLocationIdHashBase64 = "traceLocationIdHash1".encode().base64(),
         version = 1,
         type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER.number,
         description = "testDescription1",
@@ -27,7 +26,6 @@ object CheckInDatabaseData {
 
     val testCheckInWithoutCheckOutTime = TraceLocationCheckInEntity(
         traceLocationIdBase64 = "traceLocationId1".encode().base64(),
-        traceLocationIdHashBase64 = "traceLocationIdHash1".encode().base64(),
         version = 1,
         type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER.number,
         description = "testDescription2",
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..039b5d6bd7798fbdcd59c860b03e1f9b9dc08356
--- /dev/null
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/color/ColorTest.kt
@@ -0,0 +1,25 @@
+package de.rki.coronawarnapp.ui.color
+
+import android.graphics.Color
+import io.kotest.matchers.shouldBe
+import org.junit.Test
+
+import testhelpers.BaseTestInstrumentation
+
+class ColorTest : BaseTestInstrumentation() {
+
+    @Test
+    fun parseValidColor() {
+        "#FFFFFF".parseColor() shouldBe Color.WHITE
+    }
+
+    @Test
+    fun parseInvalidColor() {
+        "000000".parseColor() shouldBe Color.BLACK
+    }
+
+    @Test
+    fun defaultColor() {
+        "00".parseColor(Color.GRAY) shouldBe Color.GRAY
+    }
+}
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt
index 3d7bc4e5fbd13ea12bd820192049a5576089e59d..4038ad7448406bca7ff8339defe6b6089175da6f 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt
@@ -16,6 +16,7 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.DiaryOverviewItem
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.DayOverviewItem
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.contact.ContactItem
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.util.TimeStamper
@@ -47,6 +48,7 @@ class ContactDiaryOverviewFragmentTest : BaseUITest() {
     @MockK lateinit var riskLevelStorage: RiskLevelStorage
     @MockK lateinit var timeStamper: TimeStamper
     @MockK lateinit var exporter: ContactDiaryExporter
+    @MockK lateinit var checkInRepository: CheckInRepository
 
     private lateinit var viewModel: ContactDiaryOverviewViewModel
 
@@ -63,6 +65,7 @@ class ContactDiaryOverviewFragmentTest : BaseUITest() {
                 contactDiaryRepository = contactDiaryRepository,
                 riskLevelStorage = riskLevelStorage,
                 timeStamper = timeStamper,
+                checkInRepository = checkInRepository,
                 exporter = exporter
             )
         )
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt
index 49c71ca142afd1334e8f68d8f1e24e26d6de6996..eddad5048b6c161ac5d076c2d3ace5972b35a58e 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt
@@ -85,12 +85,14 @@ object DiaryData {
 
     val HIGH_RISK_EVENT = RiskEventItem.Event(
         name = HIGH_RISK_EVENT_LOCATION.name,
+        description = "2",
         bulledPointColor = R.color.colorBulletPointHighRisk,
         riskInfoAddition = R.string.contact_diary_trace_location_risk_high
     )
 
     val LOW_RISK_EVENT = RiskEventItem.Event(
         name = LOW_RISK_EVENT_LOCATION.name,
+        description = "1",
         bulledPointColor = R.color.colorBulletPointLowRisk,
         riskInfoAddition = R.string.contact_diary_trace_location_risk_low
     )
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt
index 8e8f547182033c77982535486201654ff27f7619..600842aae293c6ced478cda49a7f683f0c290715 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt
@@ -20,7 +20,7 @@ class VerificationKeysTest {
     @Before
     fun setup() {
         MockKAnnotations.init(this)
-        every { environmentSetup.appConfigVerificationKey } returns PUB_KEY
+        every { environmentSetup.appConfigPublicKey } returns PUB_KEY
     }
 
     private fun createTool() = SignatureValidation(environmentSetup)
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt
deleted file mode 100644
index bc8b3025f2bb742d1bb7835ecdfd64c675c0e2ff..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorkerTest.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import android.content.Context
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.work.WorkInfo
-import androidx.work.WorkManager
-import androidx.work.WorkRequest
-import androidx.work.testing.TestDriver
-import androidx.work.testing.WorkManagerTestInitHelper
-import io.mockk.Runs
-import io.mockk.every
-import io.mockk.just
-import io.mockk.mockkObject
-import org.hamcrest.CoreMatchers.`is`
-import org.hamcrest.CoreMatchers.notNullValue
-import org.hamcrest.CoreMatchers.nullValue
-import org.hamcrest.MatcherAssert.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Ignore
-import org.junit.Test
-import org.junit.runner.RunWith
-import testhelpers.BaseTestInstrumentation
-
-/**
- * DiagnosisKeyRetrievalPeriodicWorker test.
- */
-@Ignore("FixMe:DiagnosisKeyRetrievalPeriodicWorkerTest")
-@RunWith(AndroidJUnit4::class)
-class DiagnosisKeyRetrievalPeriodicWorkerTest : BaseTestInstrumentation() {
-    private lateinit var context: Context
-    private lateinit var workManager: WorkManager
-    private lateinit var request: WorkRequest
-    private lateinit var request2: WorkRequest
-
-    // small delay because WorkManager does not run work instantly when delay is off
-    private val delay = 500L
-
-    @Before
-    fun setUp() {
-        mockkObject(BackgroundWorkScheduler)
-        // do not init Test WorkManager instance again between tests
-        // leads to all tests instead of first one to fail
-        context = ApplicationProvider.getApplicationContext()
-        if (WorkManager.getInstance(context) !is TestDriver) {
-            WorkManagerTestInitHelper.initializeTestWorkManager(context)
-        }
-        workManager = WorkManager.getInstance(context)
-
-        every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers {
-            request = this.callOriginal() as WorkRequest
-            request
-        }
-    }
-
-    /**
-     * Test worker for success.
-     */
-    @Test
-    fun testDiagnosisKeyRetrievalPeriodicWorkerSuccess() {
-        every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } just Runs
-
-        BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
-
-        assertThat(request, notNullValue())
-
-        var workInfo = workManager.getWorkInfoById(request.id).get()
-        assertThat(workInfo, notNullValue())
-        assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED)))
-
-        runPeriodicJobInitialDelayMet()
-        assertThat(request, notNullValue())
-        workInfo = workManager.getWorkInfoById(request.id).get()
-        assertThat(workInfo.runAttemptCount, `is`(0))
-
-        runPeriodicJobPeriodDelayMet()
-        assertThat(request, notNullValue())
-        workInfo = workManager.getWorkInfoById(request.id).get()
-        assertThat(workInfo.runAttemptCount, `is`(0))
-    }
-
-    /**
-     * Test worker for retries and fail.
-     */
-    @Test
-    fun testDiagnosisKeyRetrievalPeriodicWorkerRetryAndFail() {
-        every { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } throws Exception("test exception")
-
-        BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
-
-        assertThat(request, notNullValue())
-        var workInfo = workManager.getWorkInfoById(request.id).get()
-        assertThat(workInfo, notNullValue())
-        assertThat(workInfo.state, `is`((WorkInfo.State.ENQUEUED)))
-
-        for (i in 1..BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2) {
-            // run job i times
-            when (i) {
-                BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> {
-                    every { BackgroundWorkScheduler["buildDiagnosisKeyRetrievalPeriodicWork"]() } answers {
-                        request2 = this.callOriginal() as WorkRequest
-                        request2
-                    }
-                    runPeriodicJobInitialDelayMet()
-                }
-                else -> {
-                    runPeriodicJobInitialDelayMet()
-                }
-            }
-
-            // get job run #i result
-            when (i) {
-                BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD + 2 -> {
-                    assertThat(request, notNullValue())
-                    assertThat(request2, notNullValue())
-                    workInfo = workManager.getWorkInfoById(request.id).get()
-                    val workInfo2 = workManager.getWorkInfoById(request2.id).get()
-                    assertThat(workInfo, nullValue())
-                    assertThat(workInfo2.state, `is`(WorkInfo.State.ENQUEUED))
-                    assertThat(workInfo2.runAttemptCount, `is`(0))
-                }
-                else -> {
-                    assertThat(request, notNullValue())
-                    workInfo = workManager.getWorkInfoById(request.id).get()
-                    assertThat(workInfo.runAttemptCount, `is`(i))
-                }
-            }
-        }
-    }
-
-    @After
-    fun cleanUp() {
-        workManager.cancelAllWork()
-    }
-
-    private fun runPeriodicJobInitialDelayMet() {
-        val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
-        testDriver?.setAllConstraintsMet(request.id)
-        testDriver?.setInitialDelayMet(request.id)
-        Thread.sleep(delay)
-    }
-
-    private fun runPeriodicJobPeriodDelayMet() {
-        val testDriver = WorkManagerTestInitHelper.getTestDriver(context)
-        testDriver?.setAllConstraintsMet(request.id)
-        testDriver?.setPeriodDelayMet(request.id)
-        Thread.sleep(delay)
-    }
-}
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 c88e6445be335789386094922c591394762337bf..f8ac9f20dbc3afc5118bb6e262863d392401a67c 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
@@ -1,6 +1,6 @@
 package de.rki.coronawarnapp.risk.storage
 
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.util.coroutine.AppScope
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 ee15ef5abd872e3749674aa965c61f40a35986a2..7fbb56a67d86ed6d29371dec56f158905536068e 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
@@ -1,6 +1,6 @@
 package de.rki.coronawarnapp.risk.storage
 
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt
index 42200d5b710b1ddab8e1923a2b5b16f45693aa37..638755b2c4ddae1e3f0355209ccd4f635023816a 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt
@@ -70,7 +70,9 @@ class DebugOptionsFragment : Fragment(R.layout.fragment_test_debugoptions), Auto
                 environmentCdnurlSubmission.text = "Submission CDN:\n${state.urlSubmission}"
                 environmentCdnurlVerification.text = "Verification CDN:\n${state.urlVerification}"
                 environmentUrlDatadonation.text = "DataDonation:\n${state.urlDataDonation}"
-                environmentUrlQrcodePosterTemplate.text = "QR-Code Poster Template:\n${state.urlQrCodePosterTemplate}"
+                environmentUrlLogUpload.text = "LogUpload:\n${state.urlLogUpload}"
+                environmentPubkeyCrowdnotifier.text = "CrowdNotifierPubKey:\n${state.pubKeyCrowdNotifier}"
+                environmentPubkeyAppconfig.text = "AppConfigPubKey:\n${state.pubKeyAppConfig}"
             }
         }
         vm.environmentChangeEvent.observe2(this) {
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt
index 3d72f93a074ce0bdeffa9323f695944f54aac039..c9b907793ab6812a252f519c71a9bee66af2387a 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/EnvironmentState.kt
@@ -9,8 +9,9 @@ data class EnvironmentState(
     val urlDownload: String,
     val urlVerification: String,
     val urlDataDonation: String,
-    val urlQrCodePosterTemplate: String
-
+    val urlLogUpload: String,
+    val pubKeyCrowdNotifier: String,
+    val pubKeyAppConfig: String,
 ) {
     companion object {
         internal fun EnvironmentSetup.toEnvironmentState() = EnvironmentState(
@@ -20,7 +21,9 @@ data class EnvironmentState(
             urlDownload = downloadCdnUrl,
             urlVerification = verificationCdnUrl,
             urlDataDonation = dataDonationCdnUrl,
-            urlQrCodePosterTemplate = qrCodePosterTemplateCdnUrl
+            urlLogUpload = logUploadServerUrl,
+            pubKeyCrowdNotifier = crowdNotifierPublicKey,
+            pubKeyAppConfig = appConfigPublicKey,
         )
     }
 }
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt
index beb72df52a2358f5258e42221e0db7f1237d8737..95d522e04234c83bbafcb5d659bf3b60cca610e0 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragment.kt
@@ -2,13 +2,19 @@ package de.rki.coronawarnapp.test.eventregistration.ui
 
 import android.annotation.SuppressLint
 import android.os.Bundle
+import android.text.SpannedString
 import android.view.View
-import android.widget.Toast
+import androidx.core.text.bold
+import androidx.core.text.buildSpannedString
+import androidx.core.text.color
+import androidx.core.text.scale
+import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
-import androidx.navigation.fragment.findNavController
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.FragmentTestEventregistrationBinding
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
 import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
+import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.ui.doNavigate
 import de.rki.coronawarnapp.util.ui.observe2
@@ -28,62 +34,96 @@ class EventRegistrationTestFragment : Fragment(R.layout.fragment_test_eventregis
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
-        with(binding) {
-            scanCheckInQrCode.setOnClickListener {
-                doNavigate(
-                    EventRegistrationTestFragmentDirections
-                        .actionEventRegistrationTestFragmentToScanCheckInQrCodeFragment()
-                )
-            }
-
-            testQrCodeCreation.setOnClickListener {
-                doNavigate(
-                    EventRegistrationTestFragmentDirections
-                        .actionEventRegistrationTestFragmentToTestQrCodeCreationFragment()
-                )
-            }
-
-            createEventButton.setOnClickListener {
-                findNavController().navigate(R.id.createEventTestFragment)
-            }
-
-            showEventsButton.setOnClickListener {
-                findNavController().navigate(R.id.showStoredEventsTestFragment)
-            }
-
-            generateTestTraceLocations.setOnClickListener {
-                viewModel.generateTestTraceLocations()
-            }
-        }
-        binding.runMatcher.setOnClickListener {
-            viewModel.runMatcher()
-        }
-
-        binding.downloadReportedCheckIns.setOnClickListener {
-            Toast.makeText(context, "Not implemented", Toast.LENGTH_SHORT).show()
+        binding.resetProcessedWarningPackages.setOnClickListener {
+            viewModel.resetProcessedWarningPackages()
         }
 
         binding.calculateRisk.setOnClickListener {
             viewModel.runRiskCalculationPerCheckInDay()
         }
 
-        viewModel.checkInOverlapsText.observe2(this) {
-            binding.matchingResultText.text = it
+        viewModel.presenceTracingWarningTaskResult.observe2(this) {
+            binding.tracingWarningTaskResult.text = it
         }
 
         viewModel.checkInRiskPerDayText.observe2(this) {
             binding.riskCalculationResultText.text = it
         }
 
-        viewModel.matchingRuntime.observe2(this) {
-            binding.matchingRuntimeText.text = "Matching runtime in millis: $it"
+        viewModel.taskRunTime.observe2(this) {
+            binding.taskRunTime.text = "Task finished in ${it}ms"
         }
 
         viewModel.riskCalculationRuntime.observe2(this) {
             binding.riskCalculationRuntimeText.text = "Risk calculation runtime in millis: $it"
         }
+
+        binding.runPtWarningTask.setOnClickListener {
+            viewModel.runPresenceTracingWarningTask()
+        }
+
+        viewModel.lastOrganiserLocation.observe(viewLifecycleOwner) {
+            binding.lastOrganiserLocationCard.isVisible = it != null
+            it?.let { traceLocation ->
+                with(binding) {
+                    lastOrganiserLocation.text = traceLocationText(traceLocation)
+                    lastOrganiserLocationId.text = styleText("ID", traceLocation.locationId.base64())
+                    lastOrganiserLocationUrl.text = styleText("URL", traceLocation.locationUrl)
+                    qrcodeButton.setOnClickListener {
+                        doNavigate(
+                            EventRegistrationTestFragmentDirections
+                                .actionEventRegistrationTestFragmentToQrCodePosterFragmentTest(traceLocation.id)
+                        )
+                    }
+                }
+            }
+        }
+
+        viewModel.lastAttendeeLocation.observe(viewLifecycleOwner) {
+            binding.lastAttendeeLocationCard.isVisible = it != null
+            it?.let { traceLocation ->
+                with(binding) {
+                    lastAttendeeLocation.text = traceLocationText(traceLocation)
+                    lastAttendeeLocationId.text = styleText("ID", traceLocation.locationId.base64())
+                    lastAttendeeLocationUrl.text = styleText("URL", traceLocation.locationUrl)
+                }
+            }
+        }
+    }
+
+    private fun traceLocationText(traceLocation: TraceLocation): SpannedString = with(traceLocation) {
+        buildSpannedString {
+            append("TraceLocation [\n")
+            append(styleText("Id", id))
+            append(styleText("type", type))
+            append(styleText("version", version))
+            append(styleText("address", address))
+            append(styleText("description", description))
+            append(styleText("startDate", startDate))
+            append(styleText("endDate", endDate))
+            append(styleText("defaultCheckInLengthInMinutes", defaultCheckInLengthInMinutes))
+            append(styleText("cnPublicKey", cnPublicKey))
+            append(styleText("cryptographicSeed", cryptographicSeed.base64()))
+            append("]")
+        }
     }
 
+    private fun styleText(key: String, value: Any?): SpannedString =
+        buildSpannedString {
+            bold {
+                color(requireContext().getColorCompat(R.color.colorAccent)) {
+                    append("$key=")
+                }
+            }
+
+            scale(0.85f) {
+                color(requireContext().getColorCompat(R.color.colorTextPrimary1)) {
+                    append(value.toString())
+                }
+            }
+            append("\n")
+        }
+
     companion object {
         val MENU_ITEM = TestMenuItem(
             title = "Event Registration",
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt
index a87c13dc9c2aadbd6399aad83b2add45605cad64..3913503fe7082eb9597ce0f5b8d136bd9d0513e1 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/EventRegistrationTestFragmentViewModel.kt
@@ -1,65 +1,94 @@
 package de.rki.coronawarnapp.test.eventregistration.ui
 
+import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.asLiveData
 import dagger.assisted.AssistedFactory
 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.TraceLocation
 import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
-import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningMatcher
-import de.rki.coronawarnapp.presencetracing.risk.CheckInWarningOverlap
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskCalculator
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository
 import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.debug.measureTime
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import okio.ByteString.Companion.encode
-import org.joda.time.DateTime
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
 import timber.log.Timber
+import kotlin.system.measureTimeMillis
 
 class EventRegistrationTestFragmentViewModel @AssistedInject constructor(
-    private val dispatcherProvider: DispatcherProvider,
-    private val traceLocationRepository: TraceLocationRepository,
-    private val checkInWarningMatcher: CheckInWarningMatcher,
-    private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator
+    dispatcherProvider: DispatcherProvider,
+    traceLocationRepository: TraceLocationRepository,
+    checkInRepository: CheckInRepository,
+    private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator,
+    private val taskController: TaskController,
+    private val presenceTracingRiskRepository: PresenceTracingRiskRepository,
+    private val traceWarningRepository: TraceWarningRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
-    private val checkInWarningOverlaps = mutableListOf<CheckInWarningOverlap>()
-    val checkInOverlapsText = MutableLiveData<String>()
-    val matchingRuntime = MutableLiveData<Long>()
-    val riskCalculationRuntime = MutableLiveData<Long>()
+    val lastOrganiserLocation: LiveData<TraceLocation?> =
+        traceLocationRepository.allTraceLocations
+            .map { lastLocationData(it) }
+            .asLiveData(dispatcherProvider.Default)
 
-    val checkInRiskPerDayText = MutableLiveData<String>()
+    val lastAttendeeLocation: LiveData<TraceLocation?> =
+        checkInRepository.allCheckIns
+            .map { lastAttendeeLocationData(it) }
+            .asLiveData(dispatcherProvider.Default)
 
-    fun runMatcher() {
-        launch {
-            measureTime(
-                {
-                    Timber.d("Time to find matches: $it millis")
-                    matchingRuntime.postValue(it)
-                },
-                {
-                    checkInWarningOverlaps.clear()
-                    val matches = checkInWarningMatcher.execute()
+    val presenceTracingWarningTaskResult = MutableLiveData<String>()
+    val taskRunTime = MutableLiveData<Long>()
+    val riskCalculationRuntime = MutableLiveData<Long>()
 
-                    checkInWarningOverlaps.addAll(matches)
+    val checkInRiskPerDayText = MutableLiveData<String>()
 
-                    if (checkInWarningOverlaps.size < 100) {
-                        val text = checkInWarningOverlaps.fold(StringBuilder()) { stringBuilder, checkInOverlap ->
-                            stringBuilder
-                                .append("CheckIn Id ${checkInOverlap.checkInId}, ")
-                                .append("Date ${checkInOverlap.localDateUtc}, ")
-                                .append("Min. ${checkInOverlap.overlap.standardMinutes}")
-                                .append("\n")
-                        }
-                        if (text.isBlank()) checkInOverlapsText.postValue("No matches found")
-                        else checkInOverlapsText.postValue(text.toString())
-                    } else {
-                        checkInOverlapsText.postValue("Output too large. ${checkInWarningOverlaps.size} lines")
-                    }
-                }
+    fun runPresenceTracingWarningTask() = launch {
+        Timber.d("runWarningPackageTask()")
+        presenceTracingWarningTaskResult.postValue("Running")
+        taskRunTime.postValue(-1L)
+
+        val duration = measureTimeMillis {
+            taskController.submitBlocking(
+                DefaultTaskRequest(
+                    PresenceTracingWarningTask::class,
+                    originTag = "EventRegistrationTestFragmentViewModel"
+                )
             )
         }
+        taskRunTime.postValue(duration)
+
+        val warningPackages = traceWarningRepository.allMetaData.first()
+        val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first()
+        val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull()
+
+        val infoText = when {
+            !lastResult!!.wasSuccessfullyCalculated -> "Last calculation failed"
+            overlaps.isEmpty() -> "No matches found (${warningPackages.size} warning packages)."
+            overlaps.size > 100 -> "Output too large. ${overlaps.size} lines"
+            overlaps.isNotEmpty() -> overlaps.fold(StringBuilder()) { stringBuilder, checkInOverlap ->
+                stringBuilder
+                    .append("CheckIn Id ${checkInOverlap.checkInId}, ")
+                    .append("Date ${checkInOverlap.localDateUtc}, ")
+                    .append("Min. ${checkInOverlap.overlap.standardMinutes}")
+                    .appendLine()
+            }.toString()
+            else -> "Unknown state"
+        }
+        presenceTracingWarningTaskResult.postValue(infoText)
+    }
+
+    fun resetProcessedWarningPackages() = launch {
+        traceWarningRepository.clear()
     }
 
     fun runRiskCalculationPerCheckInDay() {
@@ -70,6 +99,7 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor(
                     riskCalculationRuntime.postValue(it)
                 },
                 {
+                    val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first()
                     val normalizedTimePerCheckInDayList =
                         presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps)
                     val riskStates =
@@ -93,92 +123,24 @@ class EventRegistrationTestFragmentViewModel @AssistedInject constructor(
         }
     }
 
-    fun generateTestTraceLocations() {
-        launch {
-            val permanent = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE,
-                description = "SAP Kantine WDF20",
-                address = "Hauptstr. 3, 69115 Heidelberg",
-                startDate = null,
-                endDate = null,
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(permanent)
-
-            val oneDayEvent = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CULTURAL_EVENT,
-                description = "Jahrestreffen der deutschen SAP Anwendergruppe (one day)",
-                address = "Hauptstr. 3, 69115 Heidelberg",
-                startDate = DateTime.now().plusHours(2).toInstant(),
-                endDate = DateTime.now().plusHours(3).toInstant(),
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(oneDayEvent)
-
-            val partyHardEvent = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY,
-                description = "Jahrestreffen der deutschen SAP Anwendergruppe (many days)",
-                address = "Hauptstr. 3, 69115 Heidelberg",
-                startDate = DateTime.now().plusHours(2).toInstant(),
-                endDate = DateTime.now().plusDays(5).plusHours(2).toInstant(),
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(partyHardEvent)
-
-            val oldPermanent = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE,
-                description = "SAP Kantine MOW07",
-                address = "Moscow, Kosmodomianskaya 52/7",
-                startDate = null,
-                endDate = null,
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(oldPermanent)
-
-            val oldTemporaryOne = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY,
-                description = "Old temporary 1",
-                address = "Hauptstr. 3, 69115 Heidelberg",
-                startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(),
-                endDate = DateTime.now().minusSeconds(15 * 86400 - 10).toInstant(),
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(oldTemporaryOne)
-
-            val oldTemporaryTwo = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY,
-                description = "Old temporary 2",
-                address = "Hauptstr. 3, 69115 Heidelberg",
-                startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(),
-                endDate = DateTime.now().minusSeconds(15 * 86400).toInstant(),
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(oldTemporaryTwo)
-
-            val oldTemporaryThree = TraceLocation(
-                type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY,
-                description = "Old temporary 3",
-                address = "Hauptstr. 3, 69115 Heidelberg",
-                startDate = DateTime.now().minusSeconds(16 * 86400).toInstant(),
-                endDate = DateTime.now().minusSeconds(15 * 86400 + 10).toInstant(),
-                defaultCheckInLengthInMinutes = 60,
-                cryptographicSeed = "".encode(),
-                cnPublicKey = ""
-            )
-            traceLocationRepository.addTraceLocation(oldTemporaryThree)
-        }
+    private fun lastLocationData(it: List<TraceLocation>): TraceLocation? =
+        it.maxByOrNull { traceLocation -> traceLocation.id }
+
+    private fun lastAttendeeLocationData(it: List<CheckIn>): TraceLocation? {
+        val checkIn = it.maxByOrNull { checkIn -> checkIn.id } ?: return null
+
+        return TraceLocation(
+            id = checkIn.id,
+            type = TraceLocationOuterClass.TraceLocationType.forNumber(checkIn.type),
+            description = checkIn.description,
+            address = checkIn.address,
+            startDate = checkIn.traceLocationStart,
+            endDate = checkIn.traceLocationEnd,
+            defaultCheckInLengthInMinutes = checkIn.defaultCheckInLengthInMinutes,
+            cryptographicSeed = checkIn.cryptographicSeed,
+            cnPublicKey = checkIn.cnPublicKey,
+            version = checkIn.version
+        )
     }
 
     @AssistedFactory
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt
deleted file mode 100644
index 74d32337209ea60d3fe48e5c0b1f13fa15bb110e..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragment.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.createevent
-
-import android.os.Bundle
-import android.view.View
-import android.widget.ArrayAdapter
-import android.widget.AutoCompleteTextView
-import androidx.core.widget.doAfterTextChanged
-import androidx.core.widget.doOnTextChanged
-import androidx.fragment.app.Fragment
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.contactdiary.util.hideKeyboard
-import de.rki.coronawarnapp.databinding.FragmentTestCreateeventBinding
-import de.rki.coronawarnapp.util.di.AutoInject
-import de.rki.coronawarnapp.util.ui.observe2
-import de.rki.coronawarnapp.util.ui.viewBindingLazy
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
-import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
-import timber.log.Timber
-import javax.inject.Inject
-
-class CreateEventTestFragment : Fragment(R.layout.fragment_test_createevent), AutoInject {
-
-    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
-    private val vm: CreateEventTestViewModel by cwaViewModels { viewModelFactory }
-
-    private val binding: FragmentTestCreateeventBinding by viewBindingLazy()
-
-    private val eventString = "Event"
-    private val locationString = "Location"
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-
-        initSpinner()
-        initOnCreateEventClicked()
-        observeViewModelResult()
-    }
-
-    private fun observeViewModelResult() {
-        vm.result.observe2(this) {
-            when (it) {
-                is CreateEventTestViewModel.Result.Success ->
-                    binding.resultText.text = "Successfully stored: ${it.eventEntity}"
-                is CreateEventTestViewModel.Result.Error ->
-                    binding.resultText.text = "There is something wrong with your input values, please check again."
-            }
-        }
-    }
-
-    private fun initOnCreateEventClicked() = with(binding) {
-        createEventButton.setOnClickListener {
-            createEvent()
-            it.hideKeyboard()
-        }
-    }
-
-    private fun FragmentTestCreateeventBinding.createEvent() {
-        vm.createEvent(
-            eventOrLocationSpinner.editText!!.text.toString(),
-            eventDescription.text.toString(),
-            eventAddress.text.toString(),
-            eventStartEditText.text.toString(),
-            eventEndEditText.text.toString(),
-            eventDefaultCheckinLengthInMinutes.text.toString()
-        )
-    }
-
-    private fun initSpinner() {
-        val items = listOf(eventString, locationString)
-        with(binding.eventOrLocationSpinner.editText as AutoCompleteTextView) {
-            setText(items.first(), false)
-            setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_dropdown_item_1line, items))
-            doAfterTextChanged { }
-            doOnTextChanged { text, start, before, count ->
-                Timber.d("text: $text, start: $start, before: $before, count: $count")
-
-                when (text.toString()) {
-                    eventString -> {
-                        binding.eventStart.visibility = View.VISIBLE
-                        binding.eventEnd.visibility = View.VISIBLE
-                    }
-                    locationString -> {
-                        binding.eventStart.visibility = View.GONE
-                        binding.eventEnd.visibility = View.GONE
-                        binding.eventStartEditText.text = null
-                        binding.eventEndEditText.text = null
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt
deleted file mode 100644
index 258a96c7ad463471d8cac73695fcb3e546f3ae5a..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestFragmentModule.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.createevent
-
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.IntoMap
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
-
-@Module
-abstract class CreateEventTestFragmentModule {
-
-    @Binds
-    @IntoMap
-    @CWAViewModelKey(CreateEventTestViewModel::class)
-    abstract fun testCreateEventFragment(
-        factory: CreateEventTestViewModel.Factory
-    ): CWAViewModelFactory<out CWAViewModel>
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt
deleted file mode 100644
index 66a5cabd01d3776d7d5137ff0a2f189b25243e70..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/createevent/CreateEventTestViewModel.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.createevent
-
-import androidx.lifecycle.MutableLiveData
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
-import de.rki.coronawarnapp.eventregistration.events.TraceLocationCreator
-import de.rki.coronawarnapp.eventregistration.events.TraceLocationUserInput
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER
-import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
-import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import org.joda.time.DateTime
-import org.joda.time.format.DateTimeFormat
-import timber.log.Timber
-
-class CreateEventTestViewModel @AssistedInject constructor(
-    dispatcherProvider: DispatcherProvider,
-    private val traceLocationCreator: TraceLocationCreator
-) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
-
-    @AssistedFactory
-    interface Factory : SimpleCWAViewModelFactory<CreateEventTestViewModel>
-
-    val result = MutableLiveData<Result>()
-
-    fun createEvent(
-        type: String,
-        description: String,
-        address: String,
-        start: String,
-        end: String,
-        defaultCheckInLengthInMinutes: String
-    ) {
-        try {
-            val startDate =
-                if (start.isBlank()) null else DateTime.parse(start, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"))
-            val endDate =
-                if (end.isBlank()) null else DateTime.parse(end, DateTimeFormat.forPattern("yyyy-MM-dd HH:mm"))
-
-            val traceLocationType =
-                if (type == "Event") LOCATION_TYPE_TEMPORARY_OTHER else LOCATION_TYPE_PERMANENT_OTHER
-
-            val userInput = TraceLocationUserInput(
-                traceLocationType,
-                description,
-                address,
-                startDate?.toInstant(),
-                endDate?.toInstant(),
-                defaultCheckInLengthInMinutes.toInt()
-            )
-
-            launch {
-                try {
-                    val traceLocation = traceLocationCreator.createTraceLocation(userInput)
-                    result.postValue(Result.Success(traceLocation))
-                } catch (exception: Exception) {
-                    Timber.d("Something went wrong when creating the trace location $exception")
-                    result.postValue(Result.Error)
-                }
-            }
-        } catch (exception: Exception) {
-            Timber.d("Something went wrong when creating the trace location $exception")
-            result.postValue(Result.Error)
-        }
-    }
-
-    sealed class Result {
-        object Error : Result()
-        data class Success(val eventEntity: TraceLocation) : Result()
-    }
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt
deleted file mode 100644
index 0124e0857c06ebe0e76039d8c3cd7bcdefb838f8..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragment.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.qrcode
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.print.PrintAttributes
-import android.print.PrintManager
-import android.view.View
-import android.widget.Toast
-import androidx.core.content.getSystemService
-import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.databinding.FragmentTestQrcodeCreationBinding
-import de.rki.coronawarnapp.test.eventregistration.ui.PrintingAdapter
-import de.rki.coronawarnapp.util.di.AutoInject
-import de.rki.coronawarnapp.util.ui.observe2
-import de.rki.coronawarnapp.util.ui.viewBindingLazy
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
-import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
-import timber.log.Timber
-import javax.inject.Inject
-
-class QrCodeCreationTestFragment : Fragment(R.layout.fragment_test_qrcode_creation), AutoInject {
-
-    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
-
-    private val viewModel: QrCodeCreationTestViewModel by cwaViewModels { viewModelFactory }
-    private val binding: FragmentTestQrcodeCreationBinding by viewBindingLazy()
-
-    @SuppressLint("SetTextI18n")
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-
-        viewModel.sharingIntent.observe2(this) { fileIntent ->
-
-            binding.printPDF.isVisible = true
-            binding.printPDF.setOnClickListener {
-                // Context must be an Activity context
-                val printingManger = context?.getSystemService<PrintManager>()
-                Timber.i("PrintingManager: $printingManger")
-                printingManger?.apply {
-                    val printingJob = print(
-                        "CoronaWarnApp",
-                        PrintingAdapter(fileIntent.file),
-                        PrintAttributes
-                            .Builder()
-                            .setMediaSize(PrintAttributes.MediaSize.ISO_A3)
-                            .build()
-                    )
-
-                    Timber.i("PrintingJob:$printingJob")
-                    Timber.i("PrintingJob isBlocked:${printingJob.isBlocked}")
-                    Timber.i("PrintingJob isCancelled:${printingJob.isCancelled}")
-                    Timber.i("PrintingJob isCompleted:${printingJob.isCompleted}")
-                    Timber.i("PrintingJob isFailed:${printingJob.isFailed}")
-                    Timber.i("PrintingJob info:${printingJob.info}")
-                }
-            }
-            binding.sharePDF.isVisible = true
-            binding.sharePDF.setOnClickListener {
-                startActivity(fileIntent.intent(requireActivity()))
-            }
-        }
-
-        viewModel.qrCodeBitmap.observe2(this) {
-            binding.qrCodeImage.setImageBitmap(it)
-            if (it != null) {
-                viewModel.createPDF(binding.pdfPage)
-            }
-        }
-
-        viewModel.errorMessage.observe2(this) {
-            Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
-        }
-
-        binding.qrCodeText.setText(
-            "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUD" +
-                "BOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFF" +
-                "BU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
-        )
-        binding.generateQrCode.setOnClickListener {
-            viewModel.createQrCode(binding.qrCodeText.text.toString())
-        }
-
-        binding.downloadQrCodePosterTemplate.setOnClickListener {
-            viewModel.downloadQrCodePosterTemplate()
-        }
-
-        viewModel.qrCodePosterTemplate.observe2(this) { vectorDrawableBytes ->
-            binding.downloadedQrCodePoster.text = vectorDrawableBytes.utf8()
-        }
-    }
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt
deleted file mode 100644
index 336f7fc486d6159c2e1e61d17920851a7e55a3aa..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestViewModel.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.qrcode
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.pdf.PdfDocument
-import android.view.View
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer
-import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeGenerator
-import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
-import de.rki.coronawarnapp.util.di.AppContext
-import de.rki.coronawarnapp.util.files.FileSharing
-import de.rki.coronawarnapp.util.ui.SingleLiveEvent
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
-import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import okio.ByteString
-import okio.ByteString.Companion.toByteString
-import timber.log.Timber
-import java.io.File
-import java.io.FileOutputStream
-
-class QrCodeCreationTestViewModel @AssistedInject constructor(
-    private val dispatcher: DispatcherProvider,
-    private val fileSharing: FileSharing,
-    private val qrCodeGenerator: QrCodeGenerator,
-    @AppContext private val context: Context,
-    private val posterTemplateServer: QrCodePosterTemplateServer
-) : CWAViewModel(dispatcher) {
-
-    val qrCodeBitmap = SingleLiveEvent<Bitmap>()
-    val errorMessage = SingleLiveEvent<String>()
-    val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>()
-    val qrCodePosterTemplate = SingleLiveEvent<ByteString>()
-
-    /**
-     * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap]
-     */
-    fun createQrCode(input: String) = launch(context = dispatcher.IO) {
-
-        try {
-            qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input))
-        } catch (e: Exception) {
-            Timber.d(e, "Qr code creation failed")
-            errorMessage.postValue(e.localizedMessage ?: "QR code creation failed")
-        }
-    }
-
-    /**
-     * Create a new PDF file and result is delivered by [sharingIntent]
-     * as a sharing [FileSharing.ShareIntentProvider]
-     */
-    fun createPDF(
-        view: View
-    ) = launch(context = dispatcher.IO) {
-        try {
-            val file = pdfFile()
-            val pageInfo = PdfDocument.PageInfo.Builder(
-                view.width,
-                view.height,
-                1
-            ).create()
-
-            PdfDocument().apply {
-                startPage(pageInfo).apply {
-                    view.draw(canvas)
-                    finishPage(this)
-                }
-
-                FileOutputStream(file).use {
-                    writeTo(it)
-                    close()
-                }
-            }
-
-            sharingIntent.postValue(
-                fileSharing.getFileIntentProvider(file, "Scan and Help")
-            )
-        } catch (e: Exception) {
-            errorMessage.postValue(e.localizedMessage ?: "Creating pdf failed")
-            Timber.d(e, "Creating pdf failed")
-        }
-    }
-
-    private fun pdfFile(): File {
-        val dir = File(context.filesDir, "events")
-        if (!dir.exists()) dir.mkdirs()
-        return File(dir, "CoronaWarnApp-Event.pdf")
-    }
-
-    fun downloadQrCodePosterTemplate() {
-        launch {
-            try {
-                val posterTemplate = posterTemplateServer.downloadQrCodePosterTemplate()
-                qrCodePosterTemplate.postValue(posterTemplate.template.toByteArray().toByteString())
-            } catch (exception: Exception) {
-                errorMessage.postValue("Downloading Poster Template failed: ${exception.message}")
-            }
-        }
-    }
-
-    @AssistedFactory
-    interface Factory : SimpleCWAViewModelFactory<QrCodeCreationTestViewModel>
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt
deleted file mode 100644
index 77b7135aeeff685a1ea950674c655f2a1b006681..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragment.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.showevents
-
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.Fragment
-import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.databinding.FragmentTestShowstoredeventsBinding
-import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
-import de.rki.coronawarnapp.util.di.AutoInject
-import de.rki.coronawarnapp.util.ui.observe2
-import de.rki.coronawarnapp.util.ui.viewBindingLazy
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
-import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
-import javax.inject.Inject
-
-class ShowStoredEventsTestFragment : Fragment(R.layout.fragment_test_showstoredevents), AutoInject {
-
-    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
-    private val vm: ShowStoredEventsTestViewModel by cwaViewModels { viewModelFactory }
-
-    private val binding: FragmentTestShowstoredeventsBinding by viewBindingLazy()
-
-    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        super.onViewCreated(view, savedInstanceState)
-
-        vm.storedEvents.observe2(this) { events ->
-            binding.storedEvents.text = events.joinToString(separator = "\n\n") { it.getSimpleUIString() }
-        }
-
-        binding.deleteAllEvents.setOnClickListener {
-            vm.deleteAllEvents()
-        }
-    }
-
-    private fun TraceLocation.getSimpleUIString(): String {
-        return listOf(
-            "id = $id",
-            "type = $type",
-            "description = $description",
-            "location = $address",
-            "startTime = $startDate",
-            "endTime = $endDate",
-            "defaultCheckInLengthInMinutes = $defaultCheckInLengthInMinutes",
-            "cryptographicSeed = $cryptographicSeed",
-            "cnPublicKey = $cnPublicKey"
-        ).joinToString(separator = "\n")
-    }
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt
deleted file mode 100644
index 45e3c4763e437a8cc30a17c144e77d7e1af7174c..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestFragmentModule.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.showevents
-
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.IntoMap
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
-
-@Module
-abstract class ShowStoredEventsTestFragmentModule {
-
-    @Binds
-    @IntoMap
-    @CWAViewModelKey(ShowStoredEventsTestViewModel::class)
-    abstract fun testStoredEventsFragment(
-        factory: ShowStoredEventsTestViewModel.Factory
-    ): CWAViewModelFactory<out CWAViewModel>
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt
deleted file mode 100644
index f85513da0a4ec166c8df5c5e13973a0b7a2acfc5..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/showevents/ShowStoredEventsTestViewModel.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.showevents
-
-import androidx.lifecycle.asLiveData
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
-import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
-import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
-import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-
-class ShowStoredEventsTestViewModel @AssistedInject constructor(
-    dispatcherProvider: DispatcherProvider,
-    private val traceLocationRepository: TraceLocationRepository
-) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
-
-    @AssistedFactory
-    interface Factory : SimpleCWAViewModelFactory<ShowStoredEventsTestViewModel>
-
-    val storedEvents = traceLocationRepository.allTraceLocations.asLiveData()
-
-    fun deleteAllEvents() {
-        traceLocationRepository.deleteAllTraceLocations()
-    }
-}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
index 10ba1a3b2352801b7cc95471eeb4cb0283f0cc6d..e631969b4290fe76b22c48870f35bb55b96d1b9d 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
@@ -37,7 +37,6 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() {
             DataDonationTestFragment.MENU_ITEM,
             DeltaonboardingFragment.MENU_ITEM,
             EventRegistrationTestFragment.MENU_ITEM,
-            DeltaonboardingFragment.MENU_ITEM
         ).let { MutableLiveData(it) }
     }
     val showTestScreenEvent = SingleLiveEvent<TestMenuItem>()
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
index 4010a857cd5dba62c8bc247485600db5e8b733aa..9728bc8fd07c67ac0dec8dfe6185f066692b91e9 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
@@ -16,12 +16,6 @@ import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaOnboardingFragmentModul
 import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment
 import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragment
 import de.rki.coronawarnapp.test.eventregistration.ui.EventRegistrationTestFragmentModule
-import de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragment
-import de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragmentModule
-import de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragment
-import de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragmentModule
-import de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragment
-import de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragmentModule
 import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment
 import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule
 import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment
@@ -79,15 +73,6 @@ abstract class MainActivityTestModule {
     @ContributesAndroidInjector(modules = [EventRegistrationTestFragmentModule::class])
     abstract fun eventRegistration(): EventRegistrationTestFragment
 
-    @ContributesAndroidInjector(modules = [QrCodeCreationTestFragmentModule::class])
-    abstract fun qrCodeCreation(): QrCodeCreationTestFragment
-
-    @ContributesAndroidInjector(modules = [CreateEventTestFragmentModule::class])
-    abstract fun createEvent(): CreateEventTestFragment
-
-    @ContributesAndroidInjector(modules = [ShowStoredEventsTestFragmentModule::class])
-    abstract fun showStoredEvents(): ShowStoredEventsTestFragment
-
     @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class])
     abstract fun showEventDetail(): QrCodeDetailFragment
 }
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml
index 224249114efbd69384ab29ac449aa49cae7f585c..165e76a5873ac28d6015f1d63ec99802ff2c4a4f 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml
@@ -54,7 +54,8 @@
                     app:layout_constraintTop_toBottomOf="@+id/new_debuglog_screen_explanation" />
             </androidx.constraintlayout.widget.ConstraintLayout>
 
-            <androidx.constraintlayout.widget.ConstraintLayout
+            <LinearLayout
+                android:orientation="vertical"
                 android:id="@+id/environment_container"
                 style="@style/Card"
                 android:layout_width="match_parent"
@@ -66,77 +67,71 @@
                     style="@style/headline6"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:text="Server environment"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toTopOf="parent" />
+                    android:text="Server environments" />
 
                 <TextView
                     android:id="@+id/environment_cdnurl_download"
-                    style="@style/body2"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:layout_marginTop="@dimen/spacing_tiny"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/environment_title"
                     tools:text="Download: ?" />
 
                 <TextView
                     android:id="@+id/environment_cdnurl_submission"
-                    style="@style/body2"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_download"
+                    android:layout_marginTop="4dp"
                     tools:text="Submission: ?" />
 
                 <TextView
                     android:id="@+id/environment_cdnurl_verification"
-                    style="@style/body2"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_submission"
+                    android:layout_marginTop="4dp"
                     tools:text="Verification: ?" />
 
                 <TextView
                     android:id="@+id/environment_url_datadonation"
-                    style="@style/body2"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/environment_cdnurl_verification"
+                    android:layout_marginTop="4dp"
                     tools:text="DataDonation: ?" />
 
                 <TextView
-                    android:id="@+id/environment_url_qrcode_poster_template"
-                    style="@style/body2"
+                    android:id="@+id/environment_url_log_upload"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/environment_url_datadonation"
-                    tools:text="QR-Code poster template: ?" />
+                    android:layout_marginTop="4dp"
+                    tools:text="LogUpload: ?" />
+
+                <TextView
+                    android:id="@+id/environment_pubkey_appconfig"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="4dp"
+                    tools:text="AppConfigPubKey: ?" />
+
+                <TextView
+                    android:id="@+id/environment_pubkey_crowdnotifier"
+                    style="@style/TextAppearance.MaterialComponents.Caption"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="4dp"
+                    tools:text="CrowdNotifierPubKey: ?" />
 
                 <RadioGroup
                     android:id="@+id/environment_toggle_group"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="@dimen/spacing_tiny"
-                    android:orientation="vertical"
-                    app:layout_constraintBottom_toBottomOf="parent"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/environment_url_qrcode_poster_template" />
-            </androidx.constraintlayout.widget.ConstraintLayout>
+                    android:layout_marginTop="16dp"
+                    android:orientation="vertical" />
+            </LinearLayout>
 
         </LinearLayout>
     </androidx.core.widget.NestedScrollView>
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml
index 7cde04273f12098aaf69f92af3986360c3ae31cf..1e4441e000d99e3810c34fcc01a9b9c9c4dd31ff 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_eventregistration.xml
@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="utf-8"?>
-
 <androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/scroller"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     tools:ignore="HardcodedText">
@@ -12,49 +12,6 @@
         android:layout_height="wrap_content"
         android:orientation="vertical">
 
-        <LinearLayout
-            style="@style/Card"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_margin="@dimen/spacing_tiny"
-            android:orientation="vertical">
-
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:text="QRCode Creation, Poster Template download, PDF creation/printing/sharing" />
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/testQrCodeCreation"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/spacing_tiny"
-                android:text="QR Code and Poster Creation" />
-
-        </LinearLayout>
-
-        <LinearLayout
-            style="@style/Card"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginHorizontal="@dimen/spacing_tiny"
-            android:layout_marginBottom="@dimen/spacing_tiny"
-            android:orientation="vertical">
-
-            <TextView
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:text="QRCode scanning" />
-
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/scanCheckInQrCode"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/spacing_tiny"
-                android:text="Scan check in QR code" />
-
-        </LinearLayout>
-
         <LinearLayout
             style="@style/Card"
             android:layout_width="match_parent"
@@ -68,31 +25,30 @@
                 android:text="Download, matching &amp; risk calculation" />
 
             <com.google.android.material.button.MaterialButton
-                android:id="@+id/downloadReportedCheckIns"
+                android:id="@+id/reset_processed_warning_packages"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/spacing_tiny"
-                android:text="download check-ins" />
+                android:text="Reset processed packages" />
 
             <com.google.android.material.button.MaterialButton
-                android:id="@+id/runMatcher"
+                android:id="@+id/run_pt_warning_task"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/spacing_small"
-                android:text="Run matcher" />
+                android:text="Run task (download+matching)" />
 
             <TextView
-                android:id="@+id/matchingRuntimeText"
+                android:id="@+id/task_run_time"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="@dimen/spacing_small"
                 android:text="Matching runtime:" />
 
             <TextView
-                android:id="@+id/matchingResultText"
+                android:id="@+id/tracing_warning_task_result"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/spacing_small"
                 android:text="Matching result:" />
 
             <com.google.android.material.button.MaterialButton
@@ -113,72 +69,103 @@
                 android:id="@+id/riskCalculationResultText"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/spacing_small"
                 android:text="Risk calculation result:" />
 
         </LinearLayout>
 
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:id="@+id/event_container"
+        <LinearLayout
+            android:id="@+id/lastOrganiserLocationCard"
             style="@style/Card"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_margin="@dimen/spacing_tiny">
+            android:layout_marginHorizontal="@dimen/spacing_tiny"
+            android:layout_marginTop="10dp"
+            android:orientation="vertical">
 
             <TextView
-                android:id="@+id/event_title"
                 style="@style/headline6"
-                android:layout_width="0dp"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:layout_marginEnd="8dp"
-                android:text="Events"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent" />
+                android:text="Last organiser location" />
 
             <TextView
-                android:id="@+id/events_body"
-                android:layout_width="match_parent"
+                android:id="@+id/lastOrganiserLocation"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="@dimen/spacing_tiny"
-                android:text="After creating an event in the app, it is sent to the server and returned together with a guid and a signature."
+                android:layout_margin="10dp"
                 android:textIsSelectable="true"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@+id/event_title" />
+                tools:text="Location" />
 
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/create_event_button"
-                android:layout_width="0dp"
+            <TextView
+                android:id="@+id/lastOrganiserLocationUrl"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                android:layout_marginEnd="8dp"
-                android:text="Create Event"
-                app:layout_constraintEnd_toStartOf="@+id/show_events_button"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/events_body" />
+                android:layout_margin="10dp"
+                android:autoLink="web"
+                android:textIsSelectable="true"
+                tools:text="URL" />
 
-            <com.google.android.material.button.MaterialButton
-                android:id="@+id/show_events_button"
-                android:layout_width="0dp"
+            <TextView
+                android:id="@+id/lastOrganiserLocationId"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                android:text="Show stored Events"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toEndOf="@id/create_event_button"
-                app:layout_constraintTop_toBottomOf="@id/events_body" />
+                android:layout_margin="10dp"
+                android:textIsSelectable="true"
+                tools:text="ID" />
 
             <com.google.android.material.button.MaterialButton
-                android:id="@+id/generate_test_trace_locations"
-                android:layout_width="0dp"
+                android:id="@+id/qrcode_button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center"
+                android:text="Qr Code"
+                android:textColor="@android:color/white"
+                app:backgroundTint="@color/colorAccent"
+                app:icon="@drawable/ic_qrcode"
+                app:iconTint="@android:color/white" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/lastAttendeeLocationCard"
+            style="@style/Card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="@dimen/spacing_tiny"
+            android:layout_marginTop="10dp"
+
+            android:orientation="vertical">
+
+            <TextView
+                style="@style/headline6"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Last attendee location" />
+
+            <TextView
+                android:id="@+id/lastAttendeeLocation"
+                android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:layout_marginTop="8dp"
-                android:text="Generate test trace locations"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="@id/show_events_button"
-                app:layout_constraintTop_toBottomOf="@id/show_events_button" />
+                android:layout_margin="10dp"
+                android:textIsSelectable="true"
+                tools:text="Location" />
+
+            <TextView
+                android:id="@+id/lastAttendeeLocationUrl"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:autoLink="web"
+                android:textIsSelectable="true"
+                tools:text="URL" />
 
-        </androidx.constraintlayout.widget.ConstraintLayout>
+            <TextView
+                android:id="@+id/lastAttendeeLocationId"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="10dp"
+                android:textIsSelectable="true"
+                tools:text="ID" />
+        </LinearLayout>
 
     </LinearLayout>
 </androidx.core.widget.NestedScrollView>
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml
deleted file mode 100644
index 7f8e4b10c71683b4cc812dcf7835de7076e9f037..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qrcode_creation.xml
+++ /dev/null
@@ -1,115 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    xmlns:tools="http://schemas.android.com/tools"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fillViewport="true"
-    tools:ignore="HardcodedText">
-
-    <androidx.constraintlayout.widget.ConstraintLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/generateQrCode"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="24dp"
-            android:text="QR Code"
-            app:layout_constraintEnd_toStartOf="@+id/sharePDF"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/sharePDF"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="24dp"
-            android:text="Share PDF"
-            android:visibility="invisible"
-            app:layout_constraintEnd_toStartOf="@+id/printPDF"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toEndOf="@+id/generateQrCode"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/printPDF"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="24dp"
-            android:text="Print PDF"
-            android:visibility="invisible"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintHorizontal_bias="0.5"
-            app:layout_constraintStart_toEndOf="@+id/sharePDF"
-            app:layout_constraintTop_toTopOf="parent" />
-
-        <com.google.android.material.button.MaterialButton
-            android:id="@+id/downloadQrCodePosterTemplate"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="16dp"
-            android:text="Download Poster Template (result below qr code)"
-            app:layout_constraintStart_toStartOf="@id/generateQrCode"
-            app:layout_constraintEnd_toEndOf="@id/printPDF"
-            app:layout_constraintTop_toBottomOf="@id/generateQrCode" />
-
-        <com.google.android.material.textfield.TextInputLayout
-            android:id="@+id/qrCodeTextLayout"
-            style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_margin="16dp"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/downloadQrCodePosterTemplate">
-            <com.google.android.material.textfield.TextInputEditText
-                android:id="@+id/qrCodeText"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:textSize="14sp" />
-        </com.google.android.material.textfield.TextInputLayout>
-
-        <androidx.constraintlayout.widget.ConstraintLayout
-            android:id="@+id/pdfPage"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_marginTop="8dp"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/qrCodeTextLayout">
-            <androidx.appcompat.widget.AppCompatImageView
-                android:id="@+id/pdfTemplateImageView"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:adjustViewBounds="true"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="parent"
-                app:srcCompat="@drawable/qr_code_print_template" />
-
-            <androidx.appcompat.widget.AppCompatImageView
-                android:id="@+id/qrCodeImage"
-                android:layout_width="250dp"
-                android:layout_height="250dp"
-                app:layout_constraintBottom_toBottomOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintTop_toTopOf="@id/pdfTemplateImageView"
-                app:layout_constraintVertical_bias="0.25" />
-        </androidx.constraintlayout.widget.ConstraintLayout>
-
-        <TextView
-            android:id="@+id/downloadedQrCodePoster"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:layout_margin="16dp"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/pdfPage" />
-
-    </androidx.constraintlayout.widget.ConstraintLayout>
-</ScrollView>
diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
index 09b620c70da305446400ea9db26d267a8ae447c1..1586ca6972c5d45da2f811603c2bce38e0fcf812 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
@@ -133,40 +133,17 @@
         android:label="EventRegistrationTestFragment"
         tools:layout="@layout/fragment_test_eventregistration">
         <action
-            android:id="@+id/action_eventRegistrationTestFragment_to_test_qr_code_creation_fragment"
-            app:destination="@id/test_qr_code_creation_fragment" />
-        <action
-            android:id="@+id/action_eventRegistrationTestFragment_to_scanCheckInQrCodeFragment"
-            app:destination="@id/scanCheckInQrCodeFragmentTest" />
-        <action
-            android:id="@+id/action_eventRegistrationTestFragment_to_CreateEventTestFragment"
-            app:destination="@id/createEventTestFragment" />
-        <action
-            android:id="@+id/action_eventRegistrationTestFragment_to_ShowStoredEventsTestFragment"
-            app:destination="@id/showStoredEventsTestFragment" />
-
+            android:id="@+id/action_eventRegistrationTestFragment_to_qrCodePosterFragmentTest"
+            app:destination="@id/qrCodePosterFragmentTest" />
     </fragment>
-
-    <fragment
-        android:id="@+id/test_qr_code_creation_fragment"
-        android:name="de.rki.coronawarnapp.test.eventregistration.ui.qrcode.QrCodeCreationTestFragment"
-        android:label="QrCodeCreationTestFragment"
-        tools:layout="@layout/fragment_test_qrcode_creation" />
     <fragment
-        android:id="@+id/scanCheckInQrCodeFragmentTest"
-        android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment"
-        android:label="ScanCheckInQrCodeFragment"
-        tools:layout="@layout/fragment_submission_qr_code_scan" />
-
-    <fragment
-        android:id="@+id/createEventTestFragment"
-        android:name="de.rki.coronawarnapp.test.eventregistration.ui.createevent.CreateEventTestFragment"
-        android:label="CreateEventTestFragment"
-        tools:layout="@layout/fragment_test_createevent" />
-    <fragment
-        android:id="@+id/showStoredEventsTestFragment"
-        android:name="de.rki.coronawarnapp.test.eventregistration.ui.showevents.ShowStoredEventsTestFragment"
-        android:label="ShowStoredEventsTestFragment"
-        tools:layout="@layout/fragment_test_showstoredevents" />
+        android:id="@+id/qrCodePosterFragmentTest"
+        android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment"
+        android:label="qr_code_poster_fragment"
+        tools:layout="@layout/qr_code_poster_fragment">
 
+        <argument
+            android:name="traceLocationId"
+            app:argType="long" />
+    </fragment>
 </navigation>
diff --git a/Corona-Warn-App/src/main/AndroidManifest.xml b/Corona-Warn-App/src/main/AndroidManifest.xml
index d0ebbbe7886e8262f3367976a3aa836bf26d0f93..2cefbd599e3a740713e55a733c3b04b38949d234 100644
--- a/Corona-Warn-App/src/main/AndroidManifest.xml
+++ b/Corona-Warn-App/src/main/AndroidManifest.xml
@@ -85,7 +85,6 @@
 
                 <data
                     android:host="e.coronawarn.app"
-                    android:pathPrefix="/"
                     android:scheme="https" />
             </intent-filter>
         </activity>
diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.bin b/Corona-Warn-App/src/main/assets/default_app_config_android.bin
index aeac5e2d45c54db350f6219cbc4487ee8c691af0..04b460df4f9c35969a5d8f6cbfdd90da3b7fa170 100644
Binary files a/Corona-Warn-App/src/main/assets/default_app_config_android.bin and b/Corona-Warn-App/src/main/assets/default_app_config_android.bin differ
diff --git a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256 b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256
index bd4fdf7c0da4ec5b13788ea1547f8d91c8fdb278..d5dbe5290741deddf302bd3a00f664f504ea97ce 100644
--- a/Corona-Warn-App/src/main/assets/default_app_config_android.sha256
+++ b/Corona-Warn-App/src/main/assets/default_app_config_android.sha256
@@ -1 +1 @@
-3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d
\ No newline at end of file
+f10bbfb50eae9f304114bded52972832346279776e9b43f9fdf1a39557497119
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.bin b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.bin
new file mode 100644
index 0000000000000000000000000000000000000000..7194fc2074fa06be83064e9d79d62d3f110b25df
Binary files /dev/null and b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.bin differ
diff --git a/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.sha256 b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.sha256
new file mode 100644
index 0000000000000000000000000000000000000000..5a7b398d9c407b66b4f26eb4e51ec18ed96056e1
--- /dev/null
+++ b/Corona-Warn-App/src/main/assets/default_qr_code_poster_template_android.sha256
@@ -0,0 +1 @@
+1e972018100828aa63bc2559713cffa22d8db62f3ce56b3edbe72ad8cb7adb16
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/assets/technical.html b/Corona-Warn-App/src/main/assets/technical.html
index ab77f6194cdfd05a32003406119f4b122e70df65..25a8b08dfe43ff4c3830ac6c8d3c347a0ee07510 100644
--- a/Corona-Warn-App/src/main/assets/technical.html
+++ b/Corona-Warn-App/src/main/assets/technical.html
@@ -6,74 +6,104 @@
     to our attention by contacting us via this email:
     corona-warn-app.opensource@sap.com</p>
 <h2>Components:</h2>
-<p>Component: android-database-sqlcipher<br/>
+<p>
+    Component: android-database-sqlcipher<br/>
     Licensor: Zetetic LLC<br/>
     Website: https://www.zetetic.net/sqlcipher/<br/>
     License: BSD-style license
-<p>Component: guava<br/>
+</p>
+<p>
+    Component: guava<br/>
     Licensor: Google<br/>
     Website: https://github.com/google/guava<br/>
     License: Apache 2.0
+</p>
 <p>Component: joda-time<br/>
     Licensor: Joda.org<br/>
     Website: https://www.joda.org/joda-time/<br/>
     License: Apache 2.0
-<p>Component: MockK<br/>
+</p>
+<p>
+    Component: MockK<br/>
     Licensor: github.com/mockk<br/>
     Website: https://github.com/mockk/mockk<br/>
     License: Apache 2.0
-<p>Component: Robolectric<br/>
+</p>
+<p>
+    Component: Robolectric<br/>
     Licensor: Xtreme Labs, Pivotal Labs and Google Inc.<br/>
     Website: https://robolectric.org/<br/>
     License: MIT
-<p>Component: Zxing<br/>
+</p>
+<p>
+    Component: Zxing<br/>
     Licensor: zxing<br/>
     Website: https://github.com/zxing/zxing/<br/>
     License: Apache 2.0
-<p>Component: ZXing Android Embedded<br/>
+</p>
+<p>
+    Component: ZXing Android Embedded<br/>
     Licensor: ZXing authors, Journey Mobile<br/>
     Website: https://github.com/journeyapps/zxing-android-embedded<br/>
     License: Apache 2.0
-<p>Component: Detekt<br/>
+</p>
+<p>
+    Component: Detekt<br/>
     Licensor: detekt<br/>
     Website: https://detekt.github.io/detekt/<br/>
     License: Apache 2.0
-<p>Component: gson<br/>
+</p>
+<p>
+    Component: gson<br/>
     Licensor: Google<br/>
     Website: https://github.com/google/gson<br/>
     License: Apache 2.0
+</p>
 <p>Component: okhttp<br/>
     Licensor: square<br/>
     Website: https://square.github.io/okhttp/<br/>
     License: Apache 2.0
+</p>
 <p>Component: kotlinx.coroutines<br/>
     Licensor: Kotlin<br/>
     Website: https://github.com/Kotlin/kotlinx.coroutines<br/>
     License: Apache 2.0
+</p>
 <p>Component: Ktlint Gradle<br/>
     Licensor: Jonathan Leitschuh<br/>
     Website: https://github.com/JLLeitschuh/ktlint-gradle<br/>
     License: MIT
-<p>Component: Retrofit<br/>
+</p>
+<p>
+    Component: Retrofit<br/>
     Licensor: square<br/>
     Website: https://square.github.io/retrofit/<br/>
     License: Apache 2.0
-<p>Component: Protobuf gradle plugin<br/>
+</p>
+<p>
+    Component: Protobuf gradle plugin<br/>
     Licensor: Google<br/>
     Website: https://github.com/google/protobuf-gradle-plugin<br/>
     License: BSD 3-Clause
-<p>Component: Dagger<br/>
+</p>
+<p>
+    Component: Dagger<br/>
     Licensor: Google<br/>
     Website: https://github.com/google/dagger<br/>
     License: Apache 2.0
-<p>Component: Conscrypt<br/>
+</p>
+<p>
+    Component: Conscrypt<br/>
     Licensor: Conscrypt<br/>
     Website: https://github.com/google/conscrypt<br/>
     License: Apache 2.0
-<p>Component: Lottie<br/>
+</p>
+<p>
+    Component: Lottie<br/>
     Licensor: Airbnb, Inc.<br/>
     Website: https://github.com/airbnb/lottie-android<br/>
     License: Apache 2.0
+</p>
 <hr/>
 <p>Copyright (c) 2008-2020 Zetetic LLC
     All rights reserved.</p>
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
index f364e27d43f8878067e4a2d0e028fd2048b4091f..cee7752cbc1bf088f981eabd22a6d506d28c62b2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
@@ -65,6 +65,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
     @Inject lateinit var onboardingSettings: OnboardingSettings
     @Inject lateinit var autoCheckOut: AutoCheckOut
     @Inject lateinit var traceLocationDbCleanupScheduler: TraceLocationDbCleanUpScheduler
+    @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
 
     @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree
 
@@ -83,8 +84,6 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
             compPreview.inject(this)
         }
 
-        BackgroundWorkScheduler.init(component)
-
         Timber.plant(rollingLogHistory)
 
         Timber.v("onCreate(): WorkManager setup done: $workManager")
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt
index 3c0a31697f3e0e87529481961e4c98943b3dc1ed..d61dd15258becc5339bc199e07035c57cb7abf83 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/KeyDownloadConfig.kt
@@ -16,6 +16,8 @@ interface KeyDownloadConfig {
 
     val revokedHourPackages: Collection<RevokedKeyPackage.Hour>
 
+    val revokedTraceWarningPackages: Collection<RevokedKeyPackage.TraceWarning>
+
     interface RevokedKeyPackage {
         val etag: String
         val region: LocationCode
@@ -27,6 +29,8 @@ interface KeyDownloadConfig {
         interface Hour : Day, RevokedKeyPackage {
             val hour: LocalTime
         }
+
+        interface TraceWarning : RevokedKeyPackage
     }
 
     interface Mapper : ConfigMapper<KeyDownloadConfig>
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt
index 7b73dbd495e8be74db6c1ad731ecf06d760e0c7a..699e95b6fd724340c2819eba9140ecc0c6071424 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfig.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.appconfig
 
 import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
 import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder
 
 interface PresenceTracingConfig {
     val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel
@@ -9,6 +10,7 @@ interface PresenceTracingConfig {
     val riskCalculationParameters: PresenceTracingRiskCalculationParamContainer
     val submissionParameters: PresenceTracingSubmissionParamContainer
     val plausibleDeniabilityParameters: PlausibleDeniabilityParametersContainer
+    val qrCodeDescriptors: List<PresenceTracingQRCodeDescriptorOrBuilder>
 
     interface Mapper : ConfigMapper<PresenceTracingConfig>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt
index 91e17852764dea900fcb19f5ce569f0730ef6c93..5957fcdd91063e0ddeef7020db73994aa2f6367a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/PresenceTracingConfigContainer.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.appconfig
 
 import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder
 
 data class PresenceTracingConfigContainer(
     override val qrCodeErrorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H,
@@ -10,5 +11,6 @@ data class PresenceTracingConfigContainer(
     override val submissionParameters: PresenceTracingSubmissionParamContainer =
         PresenceTracingSubmissionParamContainer(),
     override val plausibleDeniabilityParameters: PlausibleDeniabilityParametersContainer =
-        PlausibleDeniabilityParametersContainer()
+        PlausibleDeniabilityParametersContainer(),
+    override val qrCodeDescriptors: List<PresenceTracingQRCodeDescriptorOrBuilder> = emptyList()
 ) : PresenceTracingConfig
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt
index f1a5a2671e661788c9f5086b18aefd016af84bb0..f9c95b41853a9293c6aae7dca11767c15ff554df 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt
@@ -26,7 +26,8 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp
             individualDownloadTimeout = rawParameters.individualTimeout(),
             overallDownloadTimeout = rawParameters.overAllTimeout(),
             revokedDayPackages = rawParameters.mapDayEtags(),
-            revokedHourPackages = rawParameters.mapHourEtags()
+            revokedHourPackages = rawParameters.mapHourEtags(),
+            revokedTraceWarningPackages = rawParameters.mapTraceWarningEtags()
         )
     }
 
@@ -81,11 +82,27 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp
         }
     }
 
+    private fun KeyDownloadParametersAndroid?.mapTraceWarningEtags(): List<RevokedKeyPackage.TraceWarning> {
+        if (this == null) return emptyList()
+
+        return this.revokedTraceWarningPackagesList.mapNotNull {
+            if (it.etag == null) {
+                Timber.e("TraceWarningPackageMeta data had no ETAG: %s", it)
+                return@mapNotNull null
+            }
+            RevokedKeyPackage.TraceWarning(
+                etag = it.etag,
+                region = LocationCode("DE"),
+            )
+        }
+    }
+
     data class KeyDownloadConfigContainer(
         override val individualDownloadTimeout: Duration,
         override val overallDownloadTimeout: Duration,
         override val revokedDayPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Day>,
-        override val revokedHourPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Hour>
+        override val revokedHourPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.Hour>,
+        override val revokedTraceWarningPackages: Collection<KeyDownloadConfig.RevokedKeyPackage.TraceWarning>
     ) : KeyDownloadConfig
 
     companion object {
@@ -109,4 +126,9 @@ internal sealed class RevokedKeyPackage : KeyDownloadConfig.RevokedKeyPackage {
         override val day: LocalDate,
         override val hour: LocalTime
     ) : RevokedKeyPackage(), KeyDownloadConfig.RevokedKeyPackage.Hour
+
+    data class TraceWarning(
+        override val etag: String,
+        override val region: LocationCode
+    ) : RevokedKeyPackage(), KeyDownloadConfig.RevokedKeyPackage.TraceWarning
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt
index bdba0734eca097fa3d2cbbf037a13c4762aef169..739a0e6fd00d27db3cd02e7f396e076eefeaa0dd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/PresenceTracingConfigMapper.kt
@@ -8,14 +8,10 @@ import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer
 import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer
 import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer
 import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid
-import de.rki.coronawarnapp.server.protocols.internal.v2
-    .PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters
-import de.rki.coronawarnapp.server.protocols.internal.v2
-    .PresenceTracingParametersOuterClass.PresenceTracingRiskCalculationParameters
-import de.rki.coronawarnapp.server.protocols.internal.v2
-    .PresenceTracingParametersOuterClass.PresenceTracingParameters.QRCodeErrorCorrectionLevel
-import de.rki.coronawarnapp.server.protocols.internal.v2
-    .PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingRiskCalculationParameters
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingParameters.QRCodeErrorCorrectionLevel
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingPlausibleDeniabilityParameters
 
 import timber.log.Timber
 import javax.inject.Inject
@@ -95,7 +91,8 @@ class PresenceTracingConfigMapper @Inject constructor() : PresenceTracingConfig.
                 revokedTraceLocationVersions = revokedTraceLocationVersionsList.orEmpty(),
                 riskCalculationParameters = riskCalculationParameters,
                 submissionParameters = submissionParameters,
-                plausibleDeniabilityParameters = plausibleDeniabilityParameters
+                plausibleDeniabilityParameters = plausibleDeniabilityParameters,
+                qrCodeDescriptors = qrCodeDescriptorsOrBuilderList
             )
         }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt
index 3409ad4b6bea470d8213773a93070efb7081697a..bb81b52052a6ab2d2583276dd2bb9046d4b162f9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt
@@ -19,8 +19,10 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.contact.Contact
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskenf.RiskEnfItem
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskEventItem
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.risk.RiskState
-import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
@@ -29,10 +31,10 @@ import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.flow.combine
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
 import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
-import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOf
 import org.joda.time.LocalDate
@@ -44,6 +46,7 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
     contactDiaryRepository: ContactDiaryRepository,
     riskLevelStorage: RiskLevelStorage,
     timeStamper: TimeStamper,
+    checkInRepository: CheckInRepository,
     private val exporter: ContactDiaryExporter
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
@@ -57,23 +60,25 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
 
     private val riskLevelPerDateFlow = riskLevelStorage.ewDayRiskStates
     private val traceLocationCheckInRiskFlow = riskLevelStorage.traceLocationCheckInRiskStates
+    private val allCheckInsFlow = checkInRepository.allCheckIns
 
     val listItems = combine(
         flowOf(dates),
         locationVisitsFlow,
         personEncountersFlow,
         riskLevelPerDateFlow,
-        traceLocationCheckInRiskFlow
-    ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList ->
+        traceLocationCheckInRiskFlow,
+        allCheckInsFlow
+    ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList, checkInList ->
         mutableListOf<DiaryOverviewItem>().apply {
             add(OverviewSubHeaderItem)
             addAll(
-                createListItemList(
-                    dateList,
+                dateList.createListItemList(
                     locationVisists,
                     personEncounters,
                     riskLevelPerDateList,
-                    traceLocationCheckInRiskList
+                    traceLocationCheckInRiskList,
+                    checkInList
                 )
             )
         }.toList()
@@ -88,12 +93,12 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
         )
     }
 
-    private fun createListItemList(
-        dateList: List<LocalDate>,
+    private fun List<LocalDate>.createListItemList(
         visits: List<ContactDiaryLocationVisit>,
         encounters: List<ContactDiaryPersonEncounter>,
         riskLevelPerDateList: List<ExposureWindowDayRisk>,
-        traceLocationCheckInRiskList: List<TraceLocationCheckInRisk>
+        traceLocationCheckInRiskList: List<TraceLocationCheckInRisk>,
+        checkInList: List<CheckIn>
     ): List<DiaryOverviewItem> {
         Timber.v(
             "createListItemList(" +
@@ -101,14 +106,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
                 "visits=%s, " +
                 "encounters=%s, " +
                 "riskLevelPerDateList=%s, " +
-                "traceLocationCheckInRiskList=%s",
-            dateList,
+                "traceLocationCheckInRiskList=%s," +
+                "checkInList=%s",
+            this,
             visits,
             encounters,
             riskLevelPerDateList,
-            traceLocationCheckInRiskList
+            traceLocationCheckInRiskList,
+            checkInList
         )
-        return dateList.map { date ->
+        return map { date ->
 
             val visitsForDate = visits.filter { it.date == date }
             val encountersForDate = encounters.filter { it.date == date }
@@ -125,16 +132,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
                 .firstOrNull { riskLevelPerDate -> riskLevelPerDate.localDateUtc == date }
                 ?.toRisk(coreItemData.isNotEmpty())
 
-            val riskEventItem = visitsForDate
-                .map {
-                    it to traceLocationCheckInRisksForDate.find {
-                        checkInRisk ->
-                        checkInRisk.checkInId == it.checkInID
+            val riskEventItem = traceLocationCheckInRisksForDate
+                .mapNotNull {
+                    val locationVisit = visitsForDate.find { visit -> visit.checkInID == it.checkInId }
+                    val checkIn = checkInList.find { checkIn -> checkIn.id == it.checkInId }
+
+                    return@mapNotNull when (locationVisit != null && checkIn != null) {
+                        true -> RiskEventDataHolder(it, locationVisit, checkIn)
+                        else -> null
                     }
-                }
-                .toMap()
-                .filter { it.value != null }
-                .toRiskEventItem()
+                }.toRiskEventItem()
 
             DayOverviewItem(
                 date = date,
@@ -187,10 +194,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
         type = ContactItem.Type.LOCATION
     )
 
-    private fun Map<ContactDiaryLocationVisit, TraceLocationCheckInRisk?>.toRiskEventItem(): RiskEventItem? {
+    private data class RiskEventDataHolder(
+        val traceLocationCheckInRisk: TraceLocationCheckInRisk,
+        val locationVisit: ContactDiaryLocationVisit,
+        val checkIn: CheckIn
+    )
+
+    private fun List<RiskEventDataHolder>.toRiskEventItem(): RiskEventItem? {
         if (isEmpty()) return null
 
-        val isHighRisk = values.any { it?.riskState == RiskState.INCREASED_RISK }
+        val isHighRisk = any { it.traceLocationCheckInRisk.riskState == RiskState.INCREASED_RISK }
 
         val body: Int = R.string.contact_diary_trace_location_risk_body
         val drawableID: Int
@@ -207,15 +220,13 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
             }
         }
 
-        val events = mapNotNull { entry ->
-            if (entry.value == null) return null
-
-            val name = entry.key.contactDiaryLocation.locationName
+        val events = map { data ->
+            val name = data.locationVisit.contactDiaryLocation.locationName
 
             val bulletPointColor: Int
             var riskInfoAddition: Int?
 
-            when (entry.value?.riskState == RiskState.INCREASED_RISK) {
+            when (data.traceLocationCheckInRisk.riskState == RiskState.INCREASED_RISK) {
                 true -> {
                     bulletPointColor = R.color.colorBulletPointHighRisk
                     riskInfoAddition = R.string.contact_diary_trace_location_risk_high
@@ -228,8 +239,11 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
 
             if (size < 2) riskInfoAddition = null
 
+            val description = data.checkIn.description
+
             RiskEventItem.Event(
                 name = name,
+                description = description,
                 bulledPointColor = bulletPointColor,
                 riskInfoAddition = riskInfoAddition
             )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt
index 157b2cbc6f48a67dd8f1fbe6a04425069781f9e1..ed4eed0a1ccf2dbc33ed196ad76e01336025e92f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt
@@ -1,8 +1,9 @@
 package de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent
 
 import android.view.ViewGroup
+import androidx.recyclerview.widget.SortedList
+import androidx.recyclerview.widget.SortedListAdapterCallback
 import de.rki.coronawarnapp.R
-import de.rki.coronawarnapp.contactdiary.util.clearAndAddAll
 import de.rki.coronawarnapp.databinding.ContactDiaryOverviewDayListItemRiskEventListItemBinding
 import de.rki.coronawarnapp.ui.lists.BaseAdapter
 import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat
@@ -10,18 +11,38 @@ import de.rki.coronawarnapp.util.lists.BindableVH
 
 class RiskEventAdapter : BaseAdapter<RiskEventAdapter.RiskEventListItemVH>() {
 
-    private val events: MutableList<RiskEventItem.Event> = mutableListOf()
+    private val events: SortedList<RiskEventItem.Event> = SortedList(
+        RiskEventItem.Event::class.java,
+        SortedList.BatchedCallback(
+            object : SortedListAdapterCallback<RiskEventItem.Event>(
+                this
+            ) {
+                override fun compare(o1: RiskEventItem.Event, o2: RiskEventItem.Event): Int =
+                    o1.description.compareTo(o2.description)
+
+                override fun areContentsTheSame(oldItem: RiskEventItem.Event?, newItem: RiskEventItem.Event?): Boolean =
+                    oldItem == newItem
+
+                override fun areItemsTheSame(item1: RiskEventItem.Event?, item2: RiskEventItem.Event?): Boolean =
+                    item1 == item2
+            }
+        )
+    )
 
     override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): RiskEventListItemVH = RiskEventListItemVH(parent)
 
     override fun onBindBaseVH(holder: RiskEventListItemVH, position: Int, payloads: MutableList<Any>) =
         holder.bind(events[position], payloads)
 
-    override fun getItemCount(): Int = events.size
+    override fun getItemCount(): Int = events.size()
 
     fun setItems(events: List<RiskEventItem.Event>) {
-        this.events.clearAndAddAll(events)
-        notifyDataSetChanged()
+        this.events.apply {
+            beginBatchedUpdates()
+            clear()
+            addAll(events)
+            endBatchedUpdates()
+        }
     }
 
     inner class RiskEventListItemVH(parent: ViewGroup) :
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt
index faa1f95d67bca1761c3f8a3204de02da3e9b3292..ea836cc33184e784385173a4f9154ab22e4b9212 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt
@@ -17,6 +17,7 @@ data class RiskEventItem(
 
     data class Event(
         val name: String,
+        val description: String,
         @ColorRes val bulledPointColor: Int,
         @StringRes val riskInfoAddition: Int? = null
     )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5e48acc14ec9537eee531c4fbd0bac77dc982b1a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorkBuilder.kt
@@ -0,0 +1,34 @@
+package de.rki.coronawarnapp.diagnosiskeys.execution
+
+import androidx.work.BackoffPolicy
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import dagger.Reusable
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@Reusable
+class DiagnosisKeyRetrievalWorkBuilder @Inject constructor() {
+
+    /**
+     * This has no network constraints, because even if there is no internet,
+     * the worker+task will trigger diagnosis key submission to the ENF.
+     * We don't want to prevent that.
+     */
+    fun createPeriodicWorkRequest(): PeriodicWorkRequest =
+        PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalWorker>(
+            60,
+            TimeUnit.MINUTES
+        )
+            .setInitialDelay(
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .setBackoffCriteria(
+                BackoffPolicy.EXPONENTIAL,
+                BackgroundConstants.BACKOFF_INITIAL_DELAY,
+                TimeUnit.MINUTES
+            )
+            .build()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt
similarity index 50%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt
index f087a031badd867c65e3f983792b90dcc833f00d..a503798ac72814a326e6f32ca14a48c95f1533b6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/execution/DiagnosisKeyRetrievalWorker.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.worker
+package de.rki.coronawarnapp.diagnosiskeys.execution
 
 import android.content.Context
 import androidx.work.CoroutineWorker
@@ -6,6 +6,7 @@ import androidx.work.WorkerParameters
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
@@ -13,49 +14,44 @@ import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
 import timber.log.Timber
 
-/**
- * One time diagnosis key retrieval work
- * Executes the retrieve diagnosis key transaction
- *
- * @see BackgroundWorkScheduler
- */
-class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
+class DiagnosisKeyRetrievalWorker @AssistedInject constructor(
     @Assisted val context: Context,
     @Assisted workerParams: WorkerParameters,
     private val taskController: TaskController
 ) : CoroutineWorker(context, workerParams) {
 
-    override suspend fun doWork(): Result {
+    override suspend fun doWork(): Result = try {
         Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount")
 
-        var result = Result.success()
-        taskController.submitBlocking(
+        val taskState = taskController.submitBlocking(
             DefaultTaskRequest(
                 DownloadDiagnosisKeysTask::class,
                 DownloadDiagnosisKeysTask.Arguments(),
-                originTag = "DiagnosisKeyRetrievalOneTimeWorker"
+                originTag = "DiagnosisKeyRetrievalWorker"
             )
-        ).error?.also { error: Throwable ->
-            Timber.tag(TAG).w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.")
+        )
 
-            if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
-                Timber.tag(TAG).w(error, "$id: Retry attempts exceeded.")
-
-                return Result.failure()
-            } else {
-                Timber.tag(TAG).d(error, "$id: Retrying.")
-                result = Result.retry()
+        when {
+            taskState.isSuccessful -> {
+                Timber.tag(TAG).d("$id: DownloadDiagnosisKeysTask finished successfully.")
+                Result.success()
+            }
+            else -> {
+                taskState.error?.let {
+                    Timber.tag(TAG).w(it, "$id: Error during DownloadDiagnosisKeysTask.")
+                }
+                Result.retry()
             }
         }
-
-        Timber.tag(TAG).d("$id: doWork() finished with %s", result)
-        return result
+    } catch (e: Exception) {
+        e.reportProblem(TAG, "DownloadDiagnosisKeysTask failed exceptionally, will retry.")
+        Result.retry()
     }
 
     @AssistedFactory
-    interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalOneTimeWorker>
+    interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalWorker>
 
     companion object {
-        private val TAG = DiagnosisKeyRetrievalOneTimeWorker::class.java.simpleName
+        private val TAG = DiagnosisKeyRetrievalWorker::class.java.simpleName
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
index 952449960c4e2acb337506fc4fc4316edc0eb7cb..4c72ad4158a2f32c908c91a610a66bbf612978c9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/EnvironmentSetup.kt
@@ -35,7 +35,6 @@ class EnvironmentSetup @Inject constructor(
         DOWNLOAD("DOWNLOAD_CDN_URL"),
         VERIFICATION_KEYS("PUB_KEYS_SIGNATURE_VERIFICATION"),
         DATA_DONATION("DATA_DONATION_CDN_URL"),
-        QRCODE_POSTER_TEMPLATE("QRCODE_POSTER_TEMPLATE_URL"),
         LOG_UPLOAD("LOG_UPLOAD_SERVER_URL"),
         SAFETYNET_API_KEY("SAFETYNET_API_KEY"),
         CROWD_NOTIFIER_PUBLIC_KEY("CROWD_NOTIFIER_PUBLIC_KEY")
@@ -48,6 +47,7 @@ class EnvironmentSetup @Inject constructor(
         WRU("WRU"),
         WRU_XA("WRU-XA"), // (aka ACME),
         WRU_XD("WRU-XD"), // (aka Germany)
+        TESTER_MOCK("TESTER-MOCK"), // (aka Germany)
         LOCAL("LOCAL"); // Emulator/CLI tooling
 
         companion object {
@@ -118,10 +118,8 @@ class EnvironmentSetup @Inject constructor(
         get() = getEnvironmentValue(DOWNLOAD).asString
     val dataDonationCdnUrl: String
         get() = getEnvironmentValue(DATA_DONATION).asString
-    val qrCodePosterTemplateCdnUrl: String
-        get() = getEnvironmentValue(EnvKey.QRCODE_POSTER_TEMPLATE).asString
 
-    val appConfigVerificationKey: String
+    val appConfigPublicKey: String
         get() = getEnvironmentValue(VERIFICATION_KEYS).asString
 
     val useEuropeKeyPackageFiles: Boolean
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt
index 5d7a0d8a163fecdda2d4b87c66453551bc88cc68..9e987ba5e53ecf55760e01d47c1d155d467dffab 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/environment/eventregistration/qrcodeposter/QrCodePosterTemplateModule.kt
@@ -4,8 +4,8 @@ import android.content.Context
 import dagger.Module
 import dagger.Provides
 import de.rki.coronawarnapp.environment.BaseEnvironmentModule
-import de.rki.coronawarnapp.environment.EnvironmentSetup
 import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
+import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
 import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateApiV1
 import de.rki.coronawarnapp.util.di.AppContext
 import okhttp3.Cache
@@ -23,7 +23,7 @@ class QrCodePosterTemplateModule : BaseEnvironmentModule() {
     @QrCodePosterTemplate
     fun cacheDir(
         @AppContext context: Context
-    ): File = File(context.cacheDir, "qrCodePoster")
+    ): File = File(context.cacheDir, "poster")
 
     @Singleton
     @Provides
@@ -32,19 +32,11 @@ class QrCodePosterTemplateModule : BaseEnvironmentModule() {
         @QrCodePosterTemplate cacheDir: File
     ): Cache = Cache(File(cacheDir, "cache_http"), CACHE_SIZE_5MB)
 
-    @Singleton
-    @QrCodePosterTemplate
-    @Provides
-    fun provideQrCodePosterTemplateCDNServerUrl(environment: EnvironmentSetup): String {
-        val url = environment.qrCodePosterTemplateCdnUrl
-        return requireValidUrl(url)
-    }
-
     @Singleton
     @Provides
     fun api(
         @DownloadCDNHttpClient client: OkHttpClient,
-        @QrCodePosterTemplate url: String,
+        @DownloadCDNServerUrl url: String,
         @QrCodePosterTemplate cache: Cache
     ): QrCodePosterTemplateApiV1 {
         val httpClient = client.newBuilder().apply {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt
index 2a1be8176e660b0e3b8c24a2b6af2739037812b3..2704dfe269b033a3425aa37e3d44df5738ce3a0c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/EventRegistrationModule.kt
@@ -3,23 +3,20 @@ package de.rki.coronawarnapp.eventregistration
 import dagger.Binds
 import dagger.Module
 import de.rki.coronawarnapp.environment.eventregistration.qrcodeposter.QrCodePosterTemplateModule
-import de.rki.coronawarnapp.eventregistration.checkins.download.FakeTraceTimeIntervalWarningRepository
-import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository
 import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository
 import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
+import de.rki.coronawarnapp.presencetracing.warning.PresenceTracingWarningModule
 
 @Module(
     includes = [
-        QrCodePosterTemplateModule::class
+        QrCodePosterTemplateModule::class,
+        PresenceTracingWarningModule::class,
     ]
 )
 abstract class EventRegistrationModule {
 
     @Binds
-    abstract fun traceLocationRepository(defaultTraceLocationRepo: DefaultTraceLocationRepository):
-        TraceLocationRepository
-
-    @Binds
-    abstract fun traceTimeIntervalWarningRepository(repository: FakeTraceTimeIntervalWarningRepository):
-        TraceTimeIntervalWarningRepository
+    abstract fun traceLocationRepository(
+        defaultTraceLocationRepo: DefaultTraceLocationRepository
+    ): TraceLocationRepository
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt
index eae4e6abe8ff65f534022fb2da42f9ca9903fef4..1f755923ad63b8c50438a965499c63fd4c005fda 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt
@@ -1,15 +1,15 @@
 package de.rki.coronawarnapp.eventregistration.checkins
 
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocationIdHash
 import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity
 import okio.ByteString
-import okio.ByteString.Companion.encode
 import org.joda.time.Instant
 
 @Suppress("LongParameterList")
 data class CheckIn(
     val id: Long = 0L,
-    val traceLocationId: ByteString = "TODO: calculate".encode(),
-    val traceLocationIdHash: ByteString = "TODO: calculate".encode(),
+    val traceLocationId: TraceLocationId,
     val version: Int,
     val type: Int,
     val description: String,
@@ -24,13 +24,18 @@ data class CheckIn(
     val completed: Boolean,
     val createJournalEntry: Boolean
 ) {
-    // val locationGuidHash: com.google.protobuf.ByteString by lazy { copyFromUtf8(guid.toSHA256()) }
+    /**
+     *  Returns SHA-256 hash of [traceLocationId] which itself may also be SHA-256 hash.
+     *  For privacy reasons TraceTimeIntervalWarnings only offer a hash of the actual locationId.
+     *
+     *  @see [de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation]
+     */
+    val traceLocationIdHash by lazy { traceLocationId.toTraceLocationIdHash() }
 }
 
 fun CheckIn.toEntity() = TraceLocationCheckInEntity(
     id = id,
     traceLocationIdBase64 = traceLocationId.base64(),
-    traceLocationIdHashBase64 = traceLocationIdHash.base64(),
     version = version,
     type = type,
     description = description,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt
index 6f76b0b04c1c86b8894e7ec7182343b834dbb2ab..723ea4bd4761c9c5a550293d6f5bd79291590038 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformer.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.eventregistration.checkins
 
-import com.google.protobuf.ByteString
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.eventregistration.checkins.derivetime.deriveTime
 import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC
@@ -14,6 +13,7 @@ import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.toProtoByteString
 import org.joda.time.Days
 import org.joda.time.Instant
 import timber.log.Timber
@@ -88,7 +88,7 @@ class CheckInsTransformer @Inject constructor(
         }
 
         return CheckInOuterClass.CheckIn.newBuilder()
-            // .locationId = TODO: Set calculated trace location
+            .setLocationId(traceLocationId.toProtoByteString())
             .setStartIntervalNumber(checkInStart.derive10MinutesInterval().toInt())
             .setEndIntervalNumber(checkInEnd.derive10MinutesInterval().toInt())
             .setTransmissionRiskLevel(transmissionRiskLevel)
@@ -107,5 +107,3 @@ fun CheckIn.determineRiskTransmission(now: Instant, transmissionVector: Transmis
     val ageInDays = Days.daysBetween(startMidnight, nowMidnight).days
     return transmissionVector.raw.getOrElse(ageInDays) { 1 } // Default value
 }
-
-private fun okio.ByteString.toProtoByteString() = ByteString.copyFrom(toByteArray())
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt
deleted file mode 100644
index be16290c8c184f5986a714d65142612957917ab1..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningPackage.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.rki.coronawarnapp.eventregistration.checkins.download
-
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
-
-interface TraceTimeIntervalWarningPackage {
-
-    /**
-     * Hides the file reading
-     */
-    suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning>
-
-    /**
-     * Numeric identifier representing the hour since epoch, used in the Api endpoint
-     */
-    val warningPackageId: String
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt
deleted file mode 100644
index 70a8d633266b300d12a2f6ac76d52a4f7c63d2c5..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/download/TraceTimeIntervalWarningRepository.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package de.rki.coronawarnapp.eventregistration.checkins.download
-
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.asFlow
-import org.joda.time.Duration
-import org.joda.time.Instant
-import javax.inject.Inject
-
-interface TraceTimeIntervalWarningRepository {
-
-    val allWarningPackages: Flow<List<TraceTimeIntervalWarningPackage>>
-
-    fun addWarningPackages(list: List<TraceTimeIntervalWarningPackage>)
-
-    fun removeWarningPackages(list: List<TraceTimeIntervalWarningPackage>)
-}
-
-// proprietary dummy implementations
-class FakeTraceTimeIntervalWarningRepository @Inject constructor() : TraceTimeIntervalWarningRepository {
-    override val allWarningPackages: Flow<List<TraceTimeIntervalWarningPackage>>
-        get() = listOf(listOf<TraceTimeIntervalWarningPackage>(DummyCheckInPackage)).asFlow()
-
-    override fun addWarningPackages(list: List<TraceTimeIntervalWarningPackage>) {
-        // TODO("Not yet implemented")
-    }
-
-    override fun removeWarningPackages(list: List<TraceTimeIntervalWarningPackage>) {
-        // TODO("Not yet implemented")
-    }
-}
-
-object DummyCheckInPackage : TraceTimeIntervalWarningPackage {
-    override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
-        return warnings
-    }
-
-    override val warningPackageId: String
-        get() = "id"
-}
-
-val warnings = (1L..1000L).map {
-    createWarning(
-        startIntervalDateStr = "2021-03-04T10:00+01:00",
-        period = 6,
-        transmissionRiskLevel = 8
-    )
-}
-
-fun createWarning(
-    startIntervalDateStr: String,
-    period: Int,
-    transmissionRiskLevel: Int
-) = TraceWarning.TraceTimeIntervalWarning.newBuilder()
-    // .locationIdHash = TODO: set location Id hash
-    .setPeriod(period)
-    .setStartIntervalNumber((Duration(Instant.parse(startIntervalDateStr).millis).standardMinutes / 10).toInt())
-    .setTransmissionRiskLevel(transmissionRiskLevel)
-    .build()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt
deleted file mode 100644
index 935e86b319ff3fc5ca3d685371c417fe706bf64a..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeDataException.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.rki.coronawarnapp.eventregistration.checkins.qrcode
-
-class InvalidQRCodeDataException constructor(
-    message: String? = null,
-    cause: Throwable? = null
-) : QRCodeException(message, cause)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt
deleted file mode 100644
index eae77689d0aab837f5527c2682e72f55078c70ab..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeSignatureException.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.rki.coronawarnapp.eventregistration.checkins.qrcode
-
-class InvalidQRCodeSignatureException constructor(
-    message: String? = null,
-    cause: Throwable? = null
-) : QRCodeException(message, cause)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f00a0c22f14152f4909eabf4c93667c76e4a8a8a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/PosterTemplateProvider.kt
@@ -0,0 +1,67 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.pdf.PdfRenderer
+import android.os.ParcelFileDescriptor
+import de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate.QrCodePosterTemplateServer
+import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.di.AppContext
+import kotlinx.coroutines.withContext
+import timber.log.Timber
+import java.io.File
+import java.io.FileOutputStream
+import javax.inject.Inject
+import kotlin.math.roundToInt
+
+class PosterTemplateProvider @Inject constructor(
+    private val posterTemplateServer: QrCodePosterTemplateServer,
+    private val dispatcherProvider: DispatcherProvider,
+    @AppContext private val context: Context
+) {
+    @Suppress("BlockingMethodInNonBlockingContext")
+    suspend fun template(): Template = withContext(dispatcherProvider.IO) {
+        val templateData = posterTemplateServer.downloadQrCodePosterTemplate()
+        val file = File(context.cacheDir, "template.pdf")
+        FileOutputStream(file).use { it.write(templateData.template.toByteArray()) }
+
+        val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
+        val renderer = PdfRenderer(fileDescriptor)
+
+        val page = renderer.openPage(0)
+        val scale = (context.resources.displayMetrics.density / page.width * page.height).roundToInt()
+        Timber.d("scale=$scale")
+        val bitmap = Bitmap.createBitmap(
+            context.resources.displayMetrics,
+            page.width * scale,
+            page.height * scale,
+            Bitmap.Config.ARGB_8888
+        )
+
+        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
+        page.close()
+        renderer.close()
+        file.delete()
+
+        Template(
+            bitmap = bitmap,
+            width = page.width,
+            height = page.height,
+            offsetX = templateData.offsetX,
+            offsetY = templateData.offsetY,
+            qrCodeLength = templateData.qrCodeSideLength,
+            textBox = templateData.descriptionTextBox
+        )
+    }
+}
+
+data class Template(
+    val bitmap: Bitmap?,
+    val width: Int,
+    val height: Int,
+    val offsetX: Float,
+    val offsetY: Float,
+    val qrCodeLength: Int,
+    val textBox: QRCodeTextBoxAndroid
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt
index 1fb4c881a81819cd76149537923ad3956f8d942a..3df60ea22ecbc0d8683f5d5987475eb287373e7a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeException.kt
@@ -1,6 +1,21 @@
 package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
-open class QRCodeException constructor(
+sealed class QRCodeException constructor(
     message: String? = null,
     cause: Throwable? = null
 ) : Exception(message, cause)
+
+class InvalidQrCodeUriException constructor(
+    message: String? = null,
+    cause: Throwable? = null
+) : QRCodeException(message, cause)
+
+class InvalidQrCodePayloadException constructor(
+    message: String? = null,
+    cause: Throwable? = null
+) : QRCodeException(message, cause)
+
+class InvalidQrCodeDataException constructor(
+    message: String? = null,
+    cause: Throwable? = null
+) : QRCodeException(message, cause)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt
index 7d622c3425e2e986c9117edf50e179940ea1ecde..493d2bdb667c846e7189099574c2018b63fd5a57 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParser.kt
@@ -1,46 +1,86 @@
 package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
+import com.google.common.io.BaseEncoding
 import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptorOrBuilder
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor.PayloadEncoding
 import de.rki.coronawarnapp.util.decodeBase32
-import okio.ByteString
+import okio.ByteString.Companion.toByteString
 import timber.log.Timber
 import java.net.URI
 import javax.inject.Inject
 
 @Reusable
-class QRCodeUriParser @Inject constructor() {
+class QRCodeUriParser @Inject constructor(
+    private val configProvider: AppConfigProvider
+) {
 
     /**
-     * Validate that QRCode scanned uri matches the following formulas:
-     * https://e.coronawarn.app/c1/SIGNED_TRACE_LOCATION_BASE32
-     * HTTPS://E.CORONAWARN.APP/C1/SIGNED_TRACE_LOCATION_BASE32
+     * Parse [QRCodePayload] from [input]
+     *
+     * @throws [Exception] such as [QRCodeException],
+     * exceptions from [URI.create]
+     * and possible decoding exceptions
      */
-    fun getQrCodePayload(maybeUri: String): ByteString? = URI.create(maybeUri).run {
-        if (!scheme.equals(SCHEME, true)) return@run null
-        if (!authority.equals(AUTHORITY, true)) return@run null
+    @Suppress("BlockingMethodInNonBlockingContext")
+    suspend fun getQrCodePayload(input: String): QRCodePayload {
+        Timber.d("input=$input")
+        try {
+            URI.create(input) // Verify it is a valid uri
+        } catch (e: Exception) {
+            Timber.d(e, "Invalid URI")
+            throw InvalidQrCodeUriException("Invalid URI", e)
+        }
 
-        if (!path.substringBeforeLast("/").equals(PATH_PREFIX, true)) return@run null
+        val descriptor = descriptor(input)
+        val groups = descriptor.matchedGroups(input)
 
-        val rawData = path.substringAfterLast("/")
-        val paddingDiff = 8 - (rawData.length % 8)
-        val maybeBase32 = rawData + createPadding(paddingDiff)
+        val payload = groups[descriptor.encodedPayloadGroupIndex]
+        Timber.d("payload=$payload")
 
-        if (!maybeBase32.matches(BASE32_REGEX)) return@run null
+        val encoding = PayloadEncoding.forNumber(descriptor.payloadEncoding.number)
+        Timber.d("encoding=$encoding")
 
-        return@run try {
-            maybeBase32.decodeBase32()
+        val rawPayload = try {
+            when (encoding) {
+                PayloadEncoding.BASE32 -> payload.decodeBase32()
+                PayloadEncoding.BASE64 -> BaseEncoding.base64Url().decode(payload).toByteString()
+                else -> null
+            }
         } catch (e: Exception) {
-            Timber.w(e, "Data wasn't base32: %s", maybeBase32)
+            Timber.d(e, "Payload decoding failed")
             null
+        } ?: throw InvalidQrCodeDataException("Payload decoding failed")
+
+        return QRCodePayload.parseFrom(rawPayload.toByteArray())
+    }
+
+    private suspend fun descriptor(input: String): PresenceTracingQRCodeDescriptorOrBuilder {
+        val descriptors = configProvider.getAppConfig().presenceTracing.qrCodeDescriptors
+        Timber.d("descriptors=$descriptors")
+        val descriptor = descriptors.find { it.regexPattern.toRegex(RegexOption.IGNORE_CASE).matches(input) }
+        if (descriptor == null) {
+            Timber.d("Invalid URI - no matchedDescriptor")
+            throw InvalidQrCodeUriException("Invalid URI - no matchedDescriptor")
         }
+        Timber.d("descriptor=$descriptor")
+        return descriptor
     }
 
-    companion object {
-        private fun createPadding(length: Int) = (0 until length).joinToString(separator = "") { "=" }
+    private fun PresenceTracingQRCodeDescriptorOrBuilder.matchedGroups(
+        input: String
+    ): List<String> {
+        val groups = regexPattern
+            .toRegex(RegexOption.IGNORE_CASE).find(input) // Find matched result [MatchResult]
+            ?.destructured?.toList().orEmpty() // Destructured groups - excluding the zeroth group (Whole String)
+        Timber.d("groups=$groups")
 
-        private const val SCHEME = "https"
-        private const val AUTHORITY = "e.coronawarn.app"
-        private const val PATH_PREFIX = "/c1"
-        private val BASE32_REGEX = "^([A-Z2-7=]{8})+$".toRegex(RegexOption.IGNORE_CASE)
+        if (encodedPayloadGroupIndex !in groups.indices) {
+            Timber.d("Invalid payload - group index is out of bounds")
+            throw InvalidQrCodePayloadException("Invalid payload - group index is out of bounds")
+        }
+        return groups
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt
index 6dac184d63cd303b1ea1cd36d45bdaa16d01d6f6..96a244814db3cb17550f44466aa76668e58d0294 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodeGenerator.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.ui.eventregistration.organizer.details
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
 import android.content.Context
 import android.graphics.Bitmap
@@ -6,6 +6,7 @@ import android.graphics.Color
 import com.google.zxing.BarcodeFormat
 import com.google.zxing.EncodeHintType
 import com.google.zxing.MultiFormatWriter
+import com.google.zxing.WriterException
 import com.google.zxing.common.BitMatrix
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
@@ -19,22 +20,31 @@ class QrCodeGenerator @Inject constructor(
     @AppContext private val context: Context,
 ) {
 
-    suspend fun createQrCode(input: String, size: Int = 1000): Bitmap? {
-
-        val qrCodeErrorCorrectionLevel = appConfigProvider
+    /**
+     * Decodes input String into a QR Code [Bitmap]
+     * @param input [String]
+     * @param length [Int] QR Code side length
+     * @param margin [Int] QR Code side's margin
+     *
+     * @throws [Exception] it could throw [IllegalArgumentException] , [WriterException]
+     * or exception while creating the bitmap
+     */
+    suspend fun createQrCode(input: String, length: Int = 1000, margin: Int = 1): Bitmap {
+        val correctionLevel = appConfigProvider
             .getAppConfig()
             .presenceTracing
             .qrCodeErrorCorrectionLevel
-        Timber.i("QrCodeErrorCorrectionLevel: $qrCodeErrorCorrectionLevel")
+        Timber.i("correctionLevel=$correctionLevel")
+
         val hints = mapOf(
-            EncodeHintType.ERROR_CORRECTION to qrCodeErrorCorrectionLevel
+            EncodeHintType.ERROR_CORRECTION to correctionLevel,
+            EncodeHintType.MARGIN to margin
         )
-
         return MultiFormatWriter().encode(
             input,
             BarcodeFormat.QR_CODE,
-            size,
-            size,
+            length,
+            length,
             hints
         ).toBitmap()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ac7412993b6bea0ea8e9c666d013bb41dac2dbe
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QrCodePayload.kt
@@ -0,0 +1,57 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
+import de.rki.coronawarnapp.util.toOkioByteString
+import de.rki.coronawarnapp.util.toProtoByteString
+import okio.ByteString.Companion.decodeBase64
+import org.joda.time.Instant
+
+fun TraceLocation.qrCodePayload(): QRCodePayload {
+    val vendorData = CWALocationData.newBuilder()
+        .setType(type)
+        .setDefaultCheckInLengthInMinutes(defaultCheckInLengthInMinutes ?: 0)
+        .setVersion(TraceLocation.VERSION)
+        .build()
+
+    val crowdNotifierData = CrowdNotifierData.newBuilder()
+        .setCryptographicSeed(cryptographicSeed.toProtoByteString())
+        .setPublicKey(cnPublicKey.decodeBase64()!!.toProtoByteString())
+        .setVersion(TraceLocation.VERSION)
+
+    val locationData = TraceLocationOuterClass.TraceLocation.newBuilder()
+        .setDescription(description)
+        .setAddress(address)
+        .setStartTimestamp(startDate?.seconds ?: 0)
+        .setEndTimestamp(endDate?.seconds ?: 0)
+        .setVersion(TraceLocation.VERSION)
+        .build()
+
+    return QRCodePayload.newBuilder()
+        .setVendorData(vendorData.toByteString())
+        .setCrowdNotifierData(crowdNotifierData)
+        .setLocationData(locationData)
+        .setVersion(TraceLocation.VERSION)
+        .build()
+}
+
+fun QRCodePayload.traceLocation(): TraceLocation {
+    val cwaLocationData = CWALocationData.parseFrom(vendorData)
+    return TraceLocation(
+        version = version,
+        type = cwaLocationData.type,
+        defaultCheckInLengthInMinutes = cwaLocationData.defaultCheckInLengthInMinutes,
+        description = locationData.description,
+        address = locationData.address,
+        startDate = locationData.startTimestamp.instant(),
+        endDate = locationData.endTimestamp.instant(),
+        cryptographicSeed = crowdNotifierData.cryptographicSeed.toOkioByteString(),
+        cnPublicKey = crowdNotifierData.publicKey.toOkioByteString().base64()
+    )
+}
+
+private fun Long.instant(): Instant? = if (this == 0L) null else secondsToInstant()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt
index 92409f9bf5c1ad34c00b28d063b9be8a17f03edd..bccfcf4b1dcf3768f8bfab1e48bf267cc71f4e4c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocation.kt
@@ -1,15 +1,16 @@
 package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
 import android.os.Parcelable
+import com.google.common.io.BaseEncoding
 import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity
 import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parcelize
 import okio.ByteString
 import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.toByteString
 import org.joda.time.Instant
 
-const val TRACE_LOCATION_VERSION = 1
-
 @Parcelize
 data class TraceLocation(
     val id: Long = 0L,
@@ -21,14 +22,60 @@ data class TraceLocation(
     val defaultCheckInLengthInMinutes: Int?,
     val cryptographicSeed: ByteString,
     val cnPublicKey: String,
-    val version: Int = TRACE_LOCATION_VERSION,
+    val version: Int = VERSION,
 ) : Parcelable {
 
+    /**
+     * Return a url for [TraceLocation] to be used as an input for [QrCodeGenerator]
+     * URL format https://e.coronawarn.app?v=1#QR_CODE_PAYLOAD_BASE64URL
+     */
+    @IgnoredOnParcel
+    val locationUrl: String by lazy {
+        val payloadBytes = qrCodePayload().toByteArray()
+        val base64Url = BaseEncoding.base64Url().omitPadding().encode(payloadBytes)
+        AUTHORITY.plus(base64Url)
+    }
+
+    /**
+     *  Returns a byte sequence that serves as an identifier for the trace location.
+     *  The ID is the byte representation of SHA-256 hash.
+     */
+    @IgnoredOnParcel
+    val locationId: TraceLocationId by lazy {
+        val cwaDomain = CWA_GUID.toByteArray()
+        val payloadBytes = qrCodePayload().toByteArray()
+        val totalByteSequence = cwaDomain + payloadBytes
+        totalByteSequence.toByteString().sha256()
+    }
+
+    /**
+     *  Returns SHA-256 hash of [locationId] which itself may also be SHA-256 hash.
+     *  For privacy reasons TraceTimeIntervalWarnings only offer a hash of the actual locationId.
+     *
+     *  @see [de.rki.coronawarnapp.eventregistration.checkins.CheckIn]
+     */
+    @IgnoredOnParcel
+    val locationIdHash: ByteString by lazy { locationId.toTraceLocationIdHash() }
+
     fun isBeforeStartTime(now: Instant): Boolean = startDate?.isAfter(now) ?: false
 
     fun isAfterEndTime(now: Instant): Boolean = endDate?.isBefore(now) ?: false
+
+    companion object {
+        /**
+         * Trace location version. This is a static data and not calculated from [TraceLocation]
+         */
+        const val VERSION = 1
+
+        private const val AUTHORITY = "https://e.coronawarn.app?v=$VERSION#"
+        private const val CWA_GUID = "CWA-GUID"
+    }
 }
 
+typealias TraceLocationId = ByteString
+
+fun TraceLocationId.toTraceLocationIdHash() = sha256()
+
 fun List<TraceLocationEntity>.toTraceLocations() = this.map { it.toTraceLocation() }
 
 fun TraceLocationEntity.toTraceLocation() = TraceLocation(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt
index 6eb4f36452e936ca31e30161c025c074eb3a9948..b524577b5626e51801189188cb5a4db447ff6f69 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/VerifiedTraceLocation.kt
@@ -7,12 +7,6 @@ import kotlinx.parcelize.IgnoredOnParcel
 import kotlinx.parcelize.Parceler
 import kotlinx.parcelize.Parcelize
 import kotlinx.parcelize.TypeParceler
-import okio.Buffer
-import okio.ByteString
-import okio.ByteString.Companion.encode
-import okio.ByteString.Companion.toByteString
-import org.joda.time.Instant
-import java.util.concurrent.TimeUnit
 
 @Parcelize
 @TypeParceler<TraceLocationOuterClass.TraceLocation, TraceLocationParceler>()
@@ -20,42 +14,7 @@ import java.util.concurrent.TimeUnit
 data class VerifiedTraceLocation(
     private val protoQrCodePayload: TraceLocationOuterClass.QRCodePayload
 ) : Parcelable {
-
-    @IgnoredOnParcel private val vendorData by lazy {
-        TraceLocationOuterClass.CWALocationData.parseFrom(protoQrCodePayload.vendorData)
-    }
-
-    @IgnoredOnParcel val traceLocation: TraceLocation by lazy {
-
-        TraceLocation(
-            version = protoQrCodePayload.version,
-            type = vendorData.type,
-            description = protoQrCodePayload.locationData.description,
-            address = protoQrCodePayload.locationData.address,
-            startDate = protoQrCodePayload.locationData.startTimestamp.toInstant(),
-            endDate = protoQrCodePayload.locationData.endTimestamp.toInstant(),
-            defaultCheckInLengthInMinutes = vendorData.defaultCheckInLengthInMinutes,
-            cryptographicSeed = protoQrCodePayload.crowdNotifierData.cryptographicSeed.toByteArray().toByteString(),
-            cnPublicKey = protoQrCodePayload.crowdNotifierData.publicKey.toStringUtf8()
-        )
-    }
-
-    @IgnoredOnParcel private val traceLocationHeader: ByteString by lazy {
-        "CWA-GUID".encode(Charsets.UTF_8)
-    }
-
-    @IgnoredOnParcel val traceLocationID: ByteString by lazy {
-        Buffer()
-            .write(traceLocationHeader)
-            .write(protoQrCodePayload.toByteArray())
-            .readByteString()
-    }
-
-    /**
-     * Converts time in seconds into [Instant]
-     */
-    private fun Long.toInstant() =
-        if (this == 0L) null else Instant.ofEpochMilli(TimeUnit.SECONDS.toMillis(this))
+    @IgnoredOnParcel val traceLocation: TraceLocation = protoQrCodePayload.traceLocation()
 }
 
 private object TraceLocationParceler : Parceler<TraceLocationOuterClass.TraceLocation> {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateSource.kt
new file mode 100644
index 0000000000000000000000000000000000000000..eac888cca0185974c5ba86be10fccb7b96b0f742
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateSource.kt
@@ -0,0 +1,13 @@
+package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate
+
+import android.content.Context
+import dagger.Reusable
+import de.rki.coronawarnapp.util.di.AppContext
+import javax.inject.Inject
+
+@Reusable
+class DefaultQrCodePosterTemplateSource @Inject constructor(@AppContext private val context: Context) {
+
+    fun getDefaultQrCodePosterTemplate() =
+        context.assets.open("default_qr_code_poster_template_android.bin").readBytes()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt
index dccb0432de5badcba1c0a4277a4ddd161284f24d..9e24711b07252248f5d4fe9b932168efe87db3cd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServer.kt
@@ -1,11 +1,11 @@
 package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate
 
+import androidx.annotation.VisibleForTesting
 import com.google.protobuf.InvalidProtocolBufferException
 import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate
 import de.rki.coronawarnapp.util.ZipHelper.readIntoMap
 import de.rki.coronawarnapp.util.ZipHelper.unzip
 import de.rki.coronawarnapp.util.security.SignatureValidation
-import retrofit2.HttpException
 import timber.log.Timber
 import javax.inject.Inject
 import javax.inject.Singleton
@@ -13,48 +13,54 @@ import javax.inject.Singleton
 @Singleton
 class QrCodePosterTemplateServer @Inject constructor(
     private val api: QrCodePosterTemplateApiV1,
+    private val defaultTemplateSource: DefaultQrCodePosterTemplateSource,
     private val signatureValidation: SignatureValidation
 ) {
     suspend fun downloadQrCodePosterTemplate(): QrCodePosterTemplate.QRCodePosterTemplateAndroid {
 
         Timber.d("Start download of QR-Code poster template.")
+        val binaryTemplate = getTemplateFromApiOrCache()
 
-        val response = api.getQrCodePosterTemplate()
-        Timber.d("Received: %s", response)
-
-        if (!response.isSuccessful) {
-            // TODO return cached or default response
-            throw HttpException(response)
-        }
-        if (response.body() == null) {
-            throw IllegalStateException("Response is successful, but body is empty.")
+        return try {
+            QrCodePosterTemplate.QRCodePosterTemplateAndroid.parseFrom(binaryTemplate)
+        } catch (exception: InvalidProtocolBufferException) {
+            throw QrCodePosterTemplateInvalidResponseException(
+                message = "QR Code poster template could not be parsed",
+                cause = exception
+            )
         }
+    }
 
-        val fileMap = response.body()!!.byteStream().unzip().readIntoMap()
-
-        val exportBinary = fileMap[EXPORT_BINARY_FILE_NAME]
-        val exportSignature = fileMap[EXPORT_SIGNATURE_FILE_NAME]
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    suspend fun getTemplateFromApiOrCache(): ByteArray {
+        return try {
+            val response = api.getQrCodePosterTemplate()
+            if (response.body() == null) {
+                throw IllegalStateException("Response is successful, but body is empty.")
+            }
 
-        if (exportBinary == null || exportSignature == null) {
-            throw QrCodePosterTemplateInvalidResponseException(message = "Unknown files: ${fileMap.keys}")
-        }
+            val fileMap = response.body()!!.byteStream().unzip().readIntoMap()
 
-        val hasValidSignature = signatureValidation.hasValidSignature(
-            exportBinary,
-            SignatureValidation.parseTEKStyleSignature(exportSignature)
-        )
+            val exportBinary = fileMap[EXPORT_BINARY_FILE_NAME]
+            val exportSignature = fileMap[EXPORT_SIGNATURE_FILE_NAME]
 
-        if (!hasValidSignature) {
-            throw QrCodePosterTemplateInvalidResponseException(message = "Invalid Signature!")
-        }
+            if (exportBinary == null || exportSignature == null) {
+                throw QrCodePosterTemplateInvalidResponseException(message = "Unknown files: ${fileMap.keys}")
+            }
 
-        return try {
-            QrCodePosterTemplate.QRCodePosterTemplateAndroid.parseFrom(exportBinary)
-        } catch (exception: InvalidProtocolBufferException) {
-            throw QrCodePosterTemplateInvalidResponseException(
-                message = "QR Code poster template could not be parsed",
-                cause = exception
+            val hasValidSignature = signatureValidation.hasValidSignature(
+                exportBinary,
+                SignatureValidation.parseTEKStyleSignature(exportSignature)
             )
+
+            if (!hasValidSignature) {
+                throw QrCodePosterTemplateInvalidResponseException(message = "Invalid Signature!")
+            }
+
+            exportBinary
+        } catch (exception: Exception) {
+            Timber.d(exception, "Response is not successful, trying to load template from cache")
+            defaultTemplateSource.getDefaultQrCodePosterTemplate()
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt
index 2df991c639036753b347014adcd46b1fb17f337b..2616685c8a0a15690dc2aa1d472b806d31b1019e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt
@@ -11,7 +11,6 @@ import org.joda.time.Instant
 data class TraceLocationCheckInEntity(
     @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0L,
     @ColumnInfo(name = "traceLocationIdBase64") val traceLocationIdBase64: String,
-    @ColumnInfo(name = "traceLocationIdHashBase64") val traceLocationIdHashBase64: String,
     @ColumnInfo(name = "version") val version: Int,
     @ColumnInfo(name = "type") val type: Int,
     @ColumnInfo(name = "description") val description: String,
@@ -30,7 +29,6 @@ data class TraceLocationCheckInEntity(
 fun TraceLocationCheckInEntity.toCheckIn() = CheckIn(
     id = id,
     traceLocationId = traceLocationIdBase64.decodeBase64()!!,
-    traceLocationIdHash = traceLocationIdHashBase64.decodeBase64()!!,
     version = version,
     type = type,
     description = description,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt
index cce89d6506f7ccdb7fb2bc872f11f5a76aa5991e..57791940bf1a983cda707f6804b8c0d6aee22db4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt
@@ -10,11 +10,12 @@ import kotlin.random.Random
 @Singleton
 class BackgroundNoise @Inject constructor(
     private val submissionSettings: SubmissionSettings,
-    private val playbook: Playbook
+    private val playbook: Playbook,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler
 ) {
     fun scheduleDummyPattern() {
         if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0)
-            BackgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork()
+            backgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork()
     }
 
     suspend fun foregroundScheduleCheck() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt
index 0a169254e27d98d1d747e0f9c2a09d9594f3edf6..419cc7637bc798614d8c50fe7fca07e156cdaa29 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutNotification.kt
@@ -5,7 +5,7 @@ import androidx.core.app.NotificationCompat
 import dagger.Reusable
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.notification.NotificationConstants
-import de.rki.coronawarnapp.presencetracing.common.TraceLocationNotifications
+import de.rki.coronawarnapp.presencetracing.common.PresenceTracingNotifications
 import de.rki.coronawarnapp.ui.launcher.LauncherActivity
 import de.rki.coronawarnapp.util.device.ForegroundState
 import de.rki.coronawarnapp.util.di.AppContext
@@ -19,7 +19,7 @@ import javax.inject.Inject
 class CheckOutNotification @Inject constructor(
     @AppContext private val context: Context,
     private val foregroundState: ForegroundState,
-    private val notificationHelper: TraceLocationNotifications,
+    private val notificationHelper: PresenceTracingNotifications,
     private val deepLinkBuilderFactory: NavDeepLinkBuilderFactory,
 ) {
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt
similarity index 96%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt
index 349096d2d218959b8970ac24f639db9c5068489c..7fd3c1f255bdec1cec5d065dfe584bae79f4521f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotifications.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/common/PresenceTracingNotifications.kt
@@ -19,18 +19,18 @@ import timber.log.Timber
 import javax.inject.Inject
 
 /**
- * Helper to send notifications on the notification channel for trace location related events.
+ * Helper to send notifications on the notification channel for presence tracing related events.
  *
  * Also see **[de.rki.coronawarnapp.notification.GeneralNotifications]**
  */
 @Reusable
-class TraceLocationNotifications @Inject constructor(
+class PresenceTracingNotifications @Inject constructor(
     @AppContext private val context: Context,
     private val apiLevel: ApiLevel,
     private val notificationManagerCompat: NotificationManagerCompat,
 ) {
 
-    private val channelId = "${context.packageName}.notification.traceLocationChannelId"
+    private val channelId = "${context.packageName}.notification.presenceTracingChannelId"
     private var isNotificationChannelSetup = false
 
     @TargetApi(Build.VERSION_CODES.O)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt
deleted file mode 100644
index 66f82291fa3d6e0bf3148bebacf473626cfcb8ae..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcher.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-package de.rki.coronawarnapp.presencetracing.risk
-
-import androidx.annotation.VisibleForTesting
-import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
-import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
-import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage
-import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository
-import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
-import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.async
-import kotlinx.coroutines.awaitAll
-import kotlinx.coroutines.flow.firstOrNull
-import kotlinx.coroutines.withContext
-import okio.ByteString.Companion.toByteString
-import org.joda.time.Instant
-import timber.log.Timber
-import java.lang.reflect.Modifier.PRIVATE
-import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
-
-class CheckInWarningMatcher @Inject constructor(
-    private val checkInsRepository: CheckInRepository,
-    private val traceTimeIntervalWarningRepository: TraceTimeIntervalWarningRepository,
-    private val presenceTracingRiskRepository: PresenceTracingRiskRepository,
-    private val dispatcherProvider: DispatcherProvider
-) {
-    suspend fun execute(): List<CheckInWarningOverlap> {
-
-        presenceTracingRiskRepository.deleteStaleData()
-
-        val checkIns = checkInsRepository.allCheckIns.firstOrNull()
-        if (checkIns.isNullOrEmpty()) {
-            Timber.i("No check-ins available. Deleting all matches.")
-            presenceTracingRiskRepository.deleteAllMatches()
-            presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList())
-            return emptyList()
-        }
-
-        val warningPackages = traceTimeIntervalWarningRepository.allWarningPackages.firstOrNull()
-
-        if (warningPackages.isNullOrEmpty()) {
-            // nothing to be done here
-            return emptyList()
-        }
-
-        val splitCheckIns = checkIns.flatMap { it.splitByMidnightUTC() }
-
-        val matchLists = createMatchingLaunchers(
-            splitCheckIns,
-            warningPackages,
-            dispatcherProvider.IO
-        )
-            .awaitAll()
-
-        if (matchLists.contains(null)) {
-            Timber.e("Error occurred during matching. Abort calculation.")
-            presenceTracingRiskRepository.reportFailedCalculation()
-            return emptyList()
-        }
-
-        // delete stale matches from new packages and mark packages as processed
-        warningPackages.forEach {
-            presenceTracingRiskRepository.deleteMatchesOfPackage(it.warningPackageId)
-            presenceTracingRiskRepository.markPackageProcessed(it.warningPackageId)
-        }
-        val matches = matchLists.filterNotNull().flatten()
-
-        presenceTracingRiskRepository.reportSuccessfulCalculation(matches)
-
-        return matches
-    }
-}
-
-@VisibleForTesting(otherwise = PRIVATE)
-internal suspend fun createMatchingLaunchers(
-    checkIns: List<CheckIn>,
-    warningPackages: List<TraceTimeIntervalWarningPackage>,
-    coroutineContext: CoroutineContext
-): Collection<Deferred<List<CheckInWarningOverlap>?>> {
-
-    val launcher: CoroutineScope.(
-        List<CheckIn>,
-        List<TraceTimeIntervalWarningPackage>
-    ) -> Deferred<List<CheckInWarningOverlap>?> =
-        { list, packageChunk ->
-            async {
-                try {
-                    packageChunk.flatMap {
-                        findMatches(list, it)
-                    }
-                } catch (e: Throwable) {
-                    Timber.e("Failed to process packages $packageChunk")
-                    null
-                }
-            }
-        }
-
-    // at most 4 parallel processes
-    val chunkSize = (checkIns.size / 4) + 1
-
-    return warningPackages.chunked(chunkSize).map { packageChunk ->
-        withContext(context = coroutineContext) {
-            launcher(checkIns, packageChunk)
-        }
-    }
-}
-
-@VisibleForTesting(otherwise = PRIVATE)
-internal suspend fun findMatches(
-    checkIns: List<CheckIn>,
-    warningPackage: TraceTimeIntervalWarningPackage
-): List<CheckInWarningOverlap> {
-    return warningPackage
-        .extractTraceTimeIntervalWarnings()
-        .flatMap { warning ->
-            checkIns
-                .mapNotNull { checkIn ->
-                    checkIn.calculateOverlap(warning, warningPackage.warningPackageId).also { overlap ->
-                        if (overlap == null) {
-                            Timber.d("No match/overlap found for $checkIn and $warning")
-                        } else {
-                            Timber.i("Overlap found $overlap")
-                        }
-                    }
-                }
-        }
-}
-
-@VisibleForTesting(otherwise = PRIVATE)
-internal fun CheckIn.calculateOverlap(
-    warning: TraceWarning.TraceTimeIntervalWarning,
-    traceWarningPackageId: String
-): CheckInWarningOverlap? {
-
-    if (warning.locationIdHash.toByteArray().toByteString() != traceLocationIdHash) return null
-
-    val warningStartMillis = warning.startIntervalNumber.tenMinIntervalToMillis()
-    val warningEndMillis = (warning.startIntervalNumber + warning.period).tenMinIntervalToMillis()
-
-    val overlapStartMillis = kotlin.math.max(checkInStart.millis, warningStartMillis)
-    val overlapEndMillis = kotlin.math.min(checkInEnd.millis, warningEndMillis)
-    val overlapMillis = overlapEndMillis - overlapStartMillis
-
-    if (overlapMillis <= 0) return null
-
-    return CheckInWarningOverlap(
-        checkInId = id,
-        transmissionRiskLevel = warning.transmissionRiskLevel,
-        traceWarningPackageId = traceWarningPackageId,
-        startTime = Instant.ofEpochMilli(overlapStartMillis),
-        endTime = Instant.ofEpochMilli(overlapEndMillis)
-    )
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt
deleted file mode 100644
index 0d0f7add0e2639932e641501a87abccfd259fa74..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskRepository.kt
+++ /dev/null
@@ -1,224 +0,0 @@
-package de.rki.coronawarnapp.presencetracing.risk
-
-import androidx.room.ColumnInfo
-import androidx.room.Dao
-import androidx.room.Entity
-import androidx.room.ForeignKey
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy.REPLACE
-import androidx.room.PrimaryKey
-import androidx.room.Query
-import androidx.room.TypeConverter
-import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity
-import de.rki.coronawarnapp.risk.RiskState
-import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk
-import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
-import de.rki.coronawarnapp.util.TimeStamper
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-import org.joda.time.Days
-import org.joda.time.Instant
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class PresenceTracingRiskRepository @Inject constructor(
-    private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator,
-    private val databaseFactory: PresenceTracingRiskDatabase.Factory,
-    private val timeStamper: TimeStamper
-) {
-
-    private val database by lazy {
-        databaseFactory.create()
-    }
-
-    private val traceTimeIntervalMatchDao by lazy {
-        database.traceTimeIntervalMatchDao()
-    }
-
-    private val riskLevelResultDao by lazy {
-        database.presenceTracingRiskLevelResultDao()
-    }
-
-    private val allMatches = traceTimeIntervalMatchDao.allMatches().map { list ->
-        list.map {
-            it.toModel()
-        }
-    }
-
-    private val normalizedTime = allMatches.map {
-        presenceTracingRiskCalculator.calculateNormalizedTime(it)
-    }
-
-    private val fifteenDaysAgo: Instant
-        get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration())
-
-    val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> =
-        normalizedTime.map {
-            presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it)
-        }
-
-    val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> =
-        normalizedTime.map {
-            presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it)
-        }
-
-    internal suspend fun reportSuccessfulCalculation(list: List<CheckInWarningOverlap>) {
-        traceTimeIntervalMatchDao.insert(list.map { it.toEntity() })
-        val last14days = normalizedTime.first().filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) }
-        val risk = presenceTracingRiskCalculator.calculateTotalRisk(last14days)
-        add(
-            PtRiskLevelResult(
-                timeStamper.nowUTC,
-                risk
-            )
-        )
-    }
-
-    internal suspend fun deleteStaleData() {
-        traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis)
-        riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis)
-    }
-
-    internal suspend fun markPackageProcessed(warningPackageId: String) {
-        // TODO
-    }
-
-    internal suspend fun deleteMatchesOfPackage(warningPackageId: String) {
-        traceTimeIntervalMatchDao.deleteMatchesForPackage(warningPackageId)
-    }
-
-    suspend fun deleteAllMatches() {
-        traceTimeIntervalMatchDao.deleteAll()
-    }
-
-    fun latestAndLastSuccessful() = riskLevelResultDao.latestAndLastSuccessful().map { list ->
-        list.map { entity ->
-            entity.toModel()
-        }
-    }
-
-    fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list ->
-        list.map { entity ->
-            entity.toModel()
-        }
-    }
-
-    fun add(riskLevelResult: PtRiskLevelResult) {
-        riskLevelResultDao.insert(riskLevelResult.toEntity())
-    }
-
-    fun reportFailedCalculation() {
-        add(
-            PtRiskLevelResult(
-                timeStamper.nowUTC,
-                RiskState.CALCULATION_FAILED
-            )
-        )
-    }
-}
-
-@Dao
-interface TraceTimeIntervalMatchDao {
-
-    @Query("SELECT * FROM TraceTimeIntervalMatchEntity")
-    fun allMatches(): Flow<List<TraceTimeIntervalMatchEntity>>
-
-    @Query("DELETE FROM TraceTimeIntervalMatchEntity")
-    suspend fun deleteAll()
-
-    @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE endTimeMillis < :endTimeMillis")
-    suspend fun deleteOlderThan(endTimeMillis: Long)
-
-    @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE traceWarningPackageId = :warningPackageId")
-    suspend fun deleteMatchesForPackage(warningPackageId: String)
-
-    @Insert
-    suspend fun insert(entities: List<TraceTimeIntervalMatchEntity>)
-}
-
-@Entity
-data class TraceTimeIntervalMatchEntity(
-    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long? = null,
-    @ForeignKey(
-        entity = TraceLocationCheckInEntity::class,
-        parentColumns = ["id"],
-        childColumns = ["checkInId"],
-        onDelete = ForeignKey.CASCADE
-    )
-    @ColumnInfo(name = "checkInId") val checkInId: Long,
-    @ColumnInfo(name = "traceWarningPackageId") val traceWarningPackageId: String,
-    @ColumnInfo(name = "transmissionRiskLevel") val transmissionRiskLevel: Int,
-    @ColumnInfo(name = "startTimeMillis") val startTimeMillis: Long,
-    @ColumnInfo(name = "endTimeMillis") val endTimeMillis: Long
-)
-
-private fun CheckInWarningOverlap.toEntity() = TraceTimeIntervalMatchEntity(
-    checkInId = checkInId,
-    traceWarningPackageId = traceWarningPackageId,
-    transmissionRiskLevel = transmissionRiskLevel,
-    startTimeMillis = startTime.millis,
-    endTimeMillis = endTime.millis
-)
-
-private fun TraceTimeIntervalMatchEntity.toModel() = CheckInWarningOverlap(
-    checkInId = checkInId,
-    traceWarningPackageId = traceWarningPackageId,
-    transmissionRiskLevel = transmissionRiskLevel,
-    startTime = Instant.ofEpochMilli(startTimeMillis),
-    endTime = Instant.ofEpochMilli(endTimeMillis)
-)
-
-@Suppress("MaxLineLength")
-@Dao
-interface PresenceTracingRiskLevelResultDao {
-    @Query("SELECT * FROM (SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT 1) UNION ALL SELECT * FROM (SELECT * FROM PresenceTracingRiskLevelResultEntity where riskStateCode is not 0 ORDER BY calculatedAtMillis DESC LIMIT 1)")
-    fun latestAndLastSuccessful(): Flow<List<PresenceTracingRiskLevelResultEntity>>
-
-    @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT :limit")
-    fun latestEntries(limit: Int): Flow<List<PresenceTracingRiskLevelResultEntity>>
-
-    @Insert(onConflict = REPLACE)
-    fun insert(entity: PresenceTracingRiskLevelResultEntity)
-
-    @Query("DELETE FROM PresenceTracingRiskLevelResultEntity WHERE calculatedAtMillis < :calculatedAtMillis")
-    suspend fun deleteOlderThan(calculatedAtMillis: Long)
-}
-
-@Entity
-data class PresenceTracingRiskLevelResultEntity(
-    @PrimaryKey @ColumnInfo(name = "calculatedAtMillis") val calculatedAtMillis: Long,
-    @ColumnInfo(name = "riskStateCode")val riskState: RiskState
-)
-
-private fun PresenceTracingRiskLevelResultEntity.toModel() = PtRiskLevelResult(
-    calculatedAt = Instant.ofEpochMilli((calculatedAtMillis)),
-    riskState = riskState
-)
-
-private fun PtRiskLevelResult.toEntity() = PresenceTracingRiskLevelResultEntity(
-    calculatedAtMillis = calculatedAt.millis,
-    riskState = riskState
-)
-
-class RiskStateConverter {
-    @TypeConverter
-    fun toRiskStateCode(value: Int?): RiskState? = value?.toRiskState()
-
-    @TypeConverter
-    fun fromRiskStateCode(code: RiskState?): Int? = code?.toCode()
-
-    private fun RiskState.toCode() = when (this) {
-        RiskState.CALCULATION_FAILED -> 0
-        RiskState.LOW_RISK -> 1
-        RiskState.INCREASED_RISK -> 2
-    }
-
-    private fun Int.toRiskState() = when (this) {
-        0 -> RiskState.CALCULATION_FAILED
-        1 -> RiskState.LOW_RISK
-        2 -> RiskState.INCREASED_RISK
-        else -> null
-    }
-}
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 1fa338aab48560329f4776711fde3e6439c59272..62b25b76061904b9d6a11001752fdce79f8f3dda 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
@@ -1,5 +1,6 @@
 package de.rki.coronawarnapp.presencetracing.risk
 
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
 import de.rki.coronawarnapp.risk.RiskState
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -7,6 +8,7 @@ import org.joda.time.LocalDate
 data class PtRiskLevelResult(
     val calculatedAt: Instant,
     val riskState: RiskState,
+    // only available for the last calculation if successful, otherwise null
     val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null
 ) {
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt
similarity index 62%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt
index a43306755b43665da40c4d218969c7025d7fa16a..a9d2d3d016e37984cf5174100bc2d3d60d995801 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TraceLocationCheckInRisk.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/TraceLocationCheckInRisk.kt
@@ -1,5 +1,6 @@
-package de.rki.coronawarnapp.risk
+package de.rki.coronawarnapp.presencetracing.risk
 
+import de.rki.coronawarnapp.risk.RiskState
 import org.joda.time.LocalDate
 
 interface TraceLocationCheckInRisk {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8c318df97711ea7ca4108ee90bcce03a155b7540
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcher.kt
@@ -0,0 +1,143 @@
+package de.rki.coronawarnapp.presencetracing.risk.calculation
+
+import androidx.annotation.VisibleForTesting
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.toOkioByteString
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.withContext
+import org.joda.time.Instant
+import timber.log.Timber
+import java.lang.reflect.Modifier.PRIVATE
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+
+class CheckInWarningMatcher @Inject constructor(
+    private val dispatcherProvider: DispatcherProvider
+) {
+    suspend fun process(
+        checkIns: List<CheckIn>,
+        warningPackages: List<TraceWarningPackage>
+    ): Result {
+        val splitCheckIns = checkIns.flatMap { it.splitByMidnightUTC() }
+
+        val matchLists: List<List<MatchesPerPackage>?> = runMatchingLaunchers(
+            splitCheckIns,
+            warningPackages,
+            dispatcherProvider.IO
+        )
+
+        val successful = if (matchLists.contains(null)) {
+            Timber.e("Calculation partially failed.")
+            false
+        } else {
+            Timber.d("Matching was successful.")
+            true
+        }
+
+        return Result(
+            successful = successful,
+            processedPackages = matchLists.filterNotNull().flatten()
+        )
+    }
+
+    data class Result(
+        val successful: Boolean,
+        val processedPackages: Collection<MatchesPerPackage> = emptyList(),
+    )
+
+    @VisibleForTesting(otherwise = PRIVATE)
+    internal suspend fun runMatchingLaunchers(
+        checkIns: List<CheckIn>,
+        warningPackages: List<TraceWarningPackage>,
+        coroutineContext: CoroutineContext
+    ): List<List<MatchesPerPackage>?> {
+
+        val launcher: CoroutineScope.(
+            List<CheckIn>,
+            List<TraceWarningPackage>
+        ) -> Deferred<List<MatchesPerPackage>?> = { list, packageChunk ->
+            async {
+                try {
+                    packageChunk.map {
+                        val overlaps = findMatches(list, it)
+                        Timber.d("%d overlaps for %s", overlaps.size, it.packageId)
+                        MatchesPerPackage(warningPackage = it, overlaps = overlaps)
+                    }
+                } catch (e: Throwable) {
+                    Timber.e(e, "Failed to process packages $packageChunk")
+                    null
+                }
+            }
+        }
+
+        // at most 4 parallel processes
+        val chunkSize = (checkIns.size / 4) + 1
+
+        return warningPackages.chunked(chunkSize).map { packageChunk ->
+            withContext(context = coroutineContext) {
+                launcher(checkIns, packageChunk)
+            }
+        }.awaitAll()
+    }
+
+    data class MatchesPerPackage(
+        val warningPackage: TraceWarningPackage,
+        val overlaps: List<CheckInWarningOverlap>,
+    )
+}
+
+@VisibleForTesting(otherwise = PRIVATE)
+internal suspend fun findMatches(
+    checkIns: List<CheckIn>,
+    warningPackage: TraceWarningPackage
+): List<CheckInWarningOverlap> {
+    return warningPackage
+        .extractWarnings()
+        .flatMap { warning ->
+            checkIns
+                .mapNotNull { checkIn ->
+                    checkIn.calculateOverlap(warning, warningPackage.packageId).also { overlap ->
+                        if (overlap == null) {
+                            Timber.v("No match found for $checkIn and $warning")
+                        } else {
+                            Timber.w("Overlap was found $overlap")
+                        }
+                    }
+                }
+        }
+}
+
+@VisibleForTesting(otherwise = PRIVATE)
+internal fun CheckIn.calculateOverlap(
+    warning: TraceWarning.TraceTimeIntervalWarning,
+    traceWarningPackageId: String
+): CheckInWarningOverlap? {
+    if (warning.locationIdHash.toOkioByteString() != traceLocationIdHash) return null
+
+    val warningStartMillis = warning.startIntervalNumber.tenMinIntervalToMillis()
+    val warningEndMillis = (warning.startIntervalNumber + warning.period).tenMinIntervalToMillis()
+
+    val overlapStartMillis = kotlin.math.max(checkInStart.millis, warningStartMillis)
+    val overlapEndMillis = kotlin.math.min(checkInEnd.millis, warningEndMillis)
+    val overlapMillis = overlapEndMillis - overlapStartMillis
+
+    if (overlapMillis <= 0) {
+        Timber.i("No overlap (%dms) with match %s (%s)", overlapMillis, description, traceLocationIdHash)
+        return null
+    }
+
+    return CheckInWarningOverlap(
+        checkInId = id,
+        transmissionRiskLevel = warning.transmissionRiskLevel,
+        traceWarningPackageId = traceWarningPackageId,
+        startTime = Instant.ofEpochMilli(overlapStartMillis),
+        endTime = Instant.ofEpochMilli(overlapEndMillis)
+    )
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt
similarity index 73%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt
index 1d5c986675ec528039fef44acda4d9ef162a67f6..4e529779dc78875644dda7905f5d250866b606f0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingConversions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingConversions.kt
@@ -1,12 +1,11 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
 import de.rki.coronawarnapp.risk.RiskState
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
+import java.util.concurrent.TimeUnit
 
 // converts number of 10min intervals into milliseconds
-internal fun Int.tenMinIntervalToMillis() = this * MILLIS_IN_MIN
-
-private const val MILLIS_IN_MIN = 600L * 1000L
+internal fun Int.tenMinIntervalToMillis() = this * TimeUnit.MINUTES.toMillis(10L)
 
 fun RiskLevel.mapToRiskState(): RiskState {
     return when (this) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt
similarity index 97%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt
index 1c2bd002f2aa3bdb6499247ce107f69fa68151b4..568273ff125bcc1716ec8bc64d9ead1a201bc339 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
 import de.rki.coronawarnapp.risk.RiskState
 import javax.inject.Inject
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt
similarity index 97%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt
index 6558491cac825c86b3d8a09b44fd17349e46ebf1..1cebd0aec5de5430225dbe41568f31b4e326b5a4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt
similarity index 90%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt
index 91591e74a3c4a833682c6482dab82a7fa8c15412..4fc9b892d8004c7ceb6fdbcb08ce64a728ce3c5f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskModel.kt
@@ -1,7 +1,7 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
+import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.risk.RiskState
-import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import org.joda.time.DateTimeConstants
 import org.joda.time.Duration
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e0d9556af8f02e0ee7577eeebdcbee4a9c0f4a5b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt
@@ -0,0 +1,158 @@
+package de.rki.coronawarnapp.presencetracing.risk.execution
+
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.bugreporting.reportProblem
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskCancellationException
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.common.DefaultProgress
+import de.rki.coronawarnapp.util.TimeStamper
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.firstOrNull
+import org.joda.time.Duration
+import org.joda.time.Instant
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Provider
+
+class PresenceTracingWarningTask @Inject constructor(
+    private val timeStamper: TimeStamper,
+    private val syncTool: TraceWarningPackageSyncTool,
+    private val checkInWarningMatcher: CheckInWarningMatcher,
+    private val presenceTracingRiskRepository: PresenceTracingRiskRepository,
+    private val traceWarningRepository: TraceWarningRepository,
+    private val checkInsRepository: CheckInRepository,
+) : Task<DefaultProgress, PresenceTracingWarningTask.Result> {
+
+    private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>()
+    override val progress: Flow<DefaultProgress> = internalProgress.asFlow()
+
+    private var isCanceled = false
+
+    override suspend fun run(arguments: Task.Arguments): Result = try {
+        Timber.d("Running with arguments=%s", arguments)
+
+        try {
+            doWork()
+        } catch (e: Exception) {
+            // We need to reported a failed calculation to update the risk card state
+            presenceTracingRiskRepository.reportCalculation(successful = false)
+            throw e
+        }
+    } catch (error: Exception) {
+        Timber.tag(TAG).e(error)
+        error.report(ExceptionCategory.EXPOSURENOTIFICATION)
+        throw error
+    } finally {
+        Timber.i("Finished (isCanceled=$isCanceled).")
+        internalProgress.close()
+    }
+
+    private suspend fun doWork(): Result {
+        val nowUTC = timeStamper.nowUTC
+
+        Timber.tag(TAG).d("Running package sync.")
+        syncTool.syncPackages()
+
+        checkCancel()
+
+        presenceTracingRiskRepository.deleteStaleData()
+
+        val checkIns = checkInsRepository.allCheckIns.firstOrNull() ?: emptyList()
+        Timber.tag(TAG).d("There are %d check-ins to match against.", checkIns.size)
+
+        if (checkIns.isEmpty()) {
+            Timber.tag(TAG).i("No check-ins available. Deleting all matches.")
+            presenceTracingRiskRepository.deleteAllMatches()
+
+            presenceTracingRiskRepository.reportCalculation(successful = true)
+
+            return Result(calculatedAt = nowUTC)
+        }
+
+        val unprocessedPackages = traceWarningRepository.unprocessedWarningPackages.firstOrNull() ?: emptyList()
+        Timber.tag(TAG).d("There are %d unprocessed warning packages.", unprocessedPackages.size)
+
+        if (unprocessedPackages.isEmpty()) {
+            Timber.tag(TAG).i("No new warning packages available.")
+
+            presenceTracingRiskRepository.reportCalculation(successful = true)
+
+            return Result(calculatedAt = nowUTC)
+        }
+
+        Timber.tag(TAG).d("Running check-in matcher.")
+        val matcherResult = checkInWarningMatcher.process(
+            checkIns = checkIns,
+            warningPackages = unprocessedPackages,
+        )
+        Timber.tag(TAG).i("Check-in matcher result: %s", matcherResult)
+
+        val overlaps = matcherResult.processedPackages.flatMap { it.overlaps }
+        val overlapsDistinct = overlaps.distinct()
+        if (overlaps.size != overlapsDistinct.size) {
+            IllegalArgumentException("Matched overlaps are not distinct").also {
+                it.reportProblem(TAG, "CheckInWarningMatcher results are not distinct.")
+            }
+        }
+
+        // Partial processing: if calculation was not successful, but some packages were processed, we still save them
+        presenceTracingRiskRepository.reportCalculation(
+            successful = matcherResult.successful,
+            overlaps = overlapsDistinct,
+        )
+
+        // markPackagesProcessed only after reportCalculation, if there is an exception, then we can process again.
+        traceWarningRepository.markPackagesProcessed(
+            matcherResult.processedPackages.map { it.warningPackage.packageId }
+        )
+
+        return Result(calculatedAt = nowUTC)
+    }
+
+    private fun checkCancel() {
+        if (isCanceled) throw TaskCancellationException()
+    }
+
+    override suspend fun cancel() {
+        Timber.w("cancel() called.")
+        isCanceled = true
+    }
+
+    data class Result(
+        val calculatedAt: Instant
+    ) : Task.Result
+
+    data class Config(
+        override val executionTimeout: Duration = Duration.standardMinutes(9),
+        override val collisionBehavior: TaskFactory.Config.CollisionBehavior =
+            TaskFactory.Config.CollisionBehavior.SKIP_IF_SIBLING_RUNNING
+    ) : TaskFactory.Config
+
+    class Factory @Inject constructor(
+        private val taskByDagger: Provider<PresenceTracingWarningTask>,
+        private val appConfigProvider: AppConfigProvider
+    ) : TaskFactory<DefaultProgress, Task.Result> {
+
+        override suspend fun createConfig(): TaskFactory.Config = Config(
+            executionTimeout = appConfigProvider.getAppConfig().overallDownloadTimeout
+        )
+
+        override val taskProvider: () -> Task<DefaultProgress, Task.Result> = {
+            taskByDagger.get()
+        }
+    }
+
+    companion object {
+        private const val TAG = "TracingWarningTask"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1479650a56f13e0cdba642c58a176baadb74e009
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkBuilder.kt
@@ -0,0 +1,37 @@
+package de.rki.coronawarnapp.presencetracing.risk.execution
+
+import androidx.work.BackoffPolicy
+import androidx.work.Constraints
+import androidx.work.NetworkType
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import dagger.Reusable
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@Reusable
+class PresenceTracingWarningWorkBuilder @Inject constructor() {
+
+    fun createPeriodicWorkRequest(): PeriodicWorkRequest =
+        PeriodicWorkRequestBuilder<PresenceTracingWarningWorker>(
+            60,
+            TimeUnit.MINUTES
+        )
+            .setInitialDelay(
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .setBackoffCriteria(
+                BackoffPolicy.EXPONENTIAL,
+                BackgroundConstants.BACKOFF_INITIAL_DELAY,
+                TimeUnit.MINUTES
+            )
+            .setConstraints(buildConstraints())
+            .build()
+
+    private fun buildConstraints() =
+        Constraints.Builder()
+            .setRequiredNetworkType(NetworkType.CONNECTED)
+            .build()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt
new file mode 100644
index 0000000000000000000000000000000000000000..300cab25a65daca21a1fbcc90ec7e4e6177e0faa
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorker.kt
@@ -0,0 +1,52 @@
+package de.rki.coronawarnapp.presencetracing.risk.execution
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.bugreporting.reportProblem
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
+import timber.log.Timber
+
+class PresenceTracingWarningWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters,
+    private val taskController: TaskController
+) : CoroutineWorker(context, workerParams) {
+
+    override suspend fun doWork(): Result = try {
+        Timber.tag(TAG).v("$id: doWork() started. Run attempt: $runAttemptCount")
+
+        val taskState = taskController.submitBlocking(
+            DefaultTaskRequest(PresenceTracingWarningTask::class, originTag = TAG)
+        )
+
+        when {
+            taskState.isSuccessful -> {
+                Timber.tag(TAG).d("$id: PresenceTracingWarningTask finished successfully.")
+                Result.success()
+            }
+            else -> {
+                taskState.error?.let {
+                    Timber.tag(TAG).w(it, "$id: Error during PresenceTracingWarningTask.")
+                }
+                Result.retry()
+            }
+        }
+    } catch (e: Exception) {
+        e.reportProblem(TAG, "PresenceTracingWarningTask failed exceptionally, will retry.")
+        Result.retry()
+    }
+
+    @AssistedFactory
+    interface Factory : InjectedWorkerFactory<PresenceTracingWarningWorker>
+
+    companion object {
+        private val TAG = PresenceTracingWarningWorker::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt
similarity index 85%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt
index 2d39bd5eaa6ab3ca9b186aacca859ae0b747a7e8..b80b28fbfb8fb9e687d1f9d608e6a64075e90652 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskDatabase.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskDatabase.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.storage
 
 import android.content.Context
 import androidx.room.Database
@@ -27,6 +27,8 @@ abstract class PresenceTracingRiskDatabase : RoomDatabase() {
             .databaseBuilder(context, PresenceTracingRiskDatabase::class.java, DATABASE_NAME)
             .build()
     }
-}
 
-private const val DATABASE_NAME = "PresenceTracingRisk_db"
+    companion object {
+        private const val DATABASE_NAME = "PresenceTracingRisk_db"
+    }
+}
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
new file mode 100644
index 0000000000000000000000000000000000000000..daf5c60bcd39437e7d54a410654397d5855bbbc9
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt
@@ -0,0 +1,283 @@
+package de.rki.coronawarnapp.presencetracing.risk.storage
+
+import androidx.room.ColumnInfo
+import androidx.room.Dao
+import androidx.room.Entity
+import androidx.room.ForeignKey
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy.REPLACE
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.TypeConverter
+import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity
+import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
+import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
+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 kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import org.joda.time.Days
+import org.joda.time.Instant
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class PresenceTracingRiskRepository @Inject constructor(
+    private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator,
+    private val databaseFactory: PresenceTracingRiskDatabase.Factory,
+    private val timeStamper: TimeStamper,
+) {
+
+    private val database by lazy {
+        databaseFactory.create()
+    }
+
+    private val traceTimeIntervalMatchDao by lazy {
+        database.traceTimeIntervalMatchDao()
+    }
+
+    private val riskLevelResultDao by lazy {
+        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()
+            }
+        }
+
+    private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.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)
+        }
+
+    val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> =
+        normalizedTimeOfLast14DaysPlusToday.map {
+            presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it)
+        }
+
+    /**
+     * We delete warning packages after processing, we need to store the latest matches independent of success state
+     * For a future update we should look into partial processing.
+     */
+    internal suspend fun reportCalculation(
+        successful: Boolean,
+        overlaps: List<CheckInWarningOverlap> = emptyList()
+    ) {
+        Timber.v("reportCalculation(successful=%b, overlaps=%s)", successful, overlaps)
+
+        val nowUTC = timeStamper.nowUTC
+
+        // delete stale matches from new packages, old matches are superseeded
+        overlaps.map { it.traceWarningPackageId }.forEach {
+            traceTimeIntervalMatchDao.deleteMatchesForPackage(it)
+        }
+
+        if (overlaps.isNotEmpty()) {
+            traceTimeIntervalMatchDao.insert(overlaps.map { it.toTraceTimeIntervalMatchEntity() })
+        }
+
+        val result = if (successful) {
+            val last14daysPlusToday = normalizedTimeOfLast14DaysPlusToday.first()
+            val risk = presenceTracingRiskCalculator.calculateTotalRisk(last14daysPlusToday)
+            PtRiskLevelResult(nowUTC, risk)
+        } else {
+            PtRiskLevelResult(nowUTC, RiskState.CALCULATION_FAILED)
+        }
+        addResult(result)
+    }
+
+    internal suspend fun deleteStaleData() {
+        Timber.d("deleteStaleData()")
+        traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis)
+        riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis)
+    }
+
+    suspend fun deleteAllMatches() {
+        Timber.d("deleteAllMatches()")
+        traceTimeIntervalMatchDao.deleteAll()
+    }
+
+    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.toCheckInWarningOverlap(presenceTracingDayRisk.first())
+                } else {
+                    entity.toCheckInWarningOverlap(null)
+                }
+            }
+    }
+
+    fun allEntries() = riskLevelResultDao.allEntries().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.toCheckInWarningOverlap(presenceTracingDayRisk.first())
+                } else {
+                    entity.toCheckInWarningOverlap(null)
+                }
+            }
+    }
+
+    private fun addResult(result: PtRiskLevelResult) {
+        Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.")
+        riskLevelResultDao.insert(result.toTraceTimeIntervalMatchEntity())
+    }
+
+    suspend fun clearAllTables() {
+        traceTimeIntervalMatchDao.deleteAll()
+        riskLevelResultDao.deleteAll()
+    }
+}
+
+/*
+* Stores matches from the last successful execution
+* */
+@Dao
+interface TraceTimeIntervalMatchDao {
+
+    @Query("SELECT * FROM TraceTimeIntervalMatchEntity")
+    fun allMatches(): Flow<List<TraceTimeIntervalMatchEntity>>
+
+    @Query("DELETE FROM TraceTimeIntervalMatchEntity")
+    suspend fun deleteAll()
+
+    @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE endTimeMillis < :endTimeMillis")
+    suspend fun deleteOlderThan(endTimeMillis: Long)
+
+    @Query("DELETE FROM TraceTimeIntervalMatchEntity WHERE traceWarningPackageId = :warningPackageId")
+    suspend fun deleteMatchesForPackage(warningPackageId: String)
+
+    @Insert
+    suspend fun insert(entities: List<TraceTimeIntervalMatchEntity>)
+}
+
+@Entity
+data class TraceTimeIntervalMatchEntity(
+    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long? = null,
+    @ForeignKey(
+        entity = TraceLocationCheckInEntity::class,
+        parentColumns = ["id"],
+        childColumns = ["checkInId"],
+        onDelete = ForeignKey.CASCADE
+    )
+    @ColumnInfo(name = "checkInId") val checkInId: Long,
+    @ColumnInfo(name = "traceWarningPackageId") val traceWarningPackageId: String,
+    @ColumnInfo(name = "transmissionRiskLevel") val transmissionRiskLevel: Int,
+    @ColumnInfo(name = "startTimeMillis") val startTimeMillis: Long,
+    @ColumnInfo(name = "endTimeMillis") val endTimeMillis: Long
+)
+
+internal fun CheckInWarningOverlap.toTraceTimeIntervalMatchEntity() = TraceTimeIntervalMatchEntity(
+    checkInId = checkInId,
+    traceWarningPackageId = traceWarningPackageId,
+    transmissionRiskLevel = transmissionRiskLevel,
+    startTimeMillis = startTime.millis,
+    endTimeMillis = endTime.millis
+)
+
+internal fun TraceTimeIntervalMatchEntity.toCheckInWarningOverlap() = CheckInWarningOverlap(
+    checkInId = checkInId,
+    traceWarningPackageId = traceWarningPackageId,
+    transmissionRiskLevel = transmissionRiskLevel,
+    startTime = Instant.ofEpochMilli(startTimeMillis),
+    endTime = Instant.ofEpochMilli(endTimeMillis)
+)
+
+@Suppress("MaxLineLength")
+@Dao
+interface PresenceTracingRiskLevelResultDao {
+
+    @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity ORDER BY calculatedAtMillis DESC LIMIT :limit")
+    fun latestEntries(limit: Int): Flow<List<PresenceTracingRiskLevelResultEntity>>
+
+    @Query("SELECT * FROM PresenceTracingRiskLevelResultEntity")
+    fun allEntries(): Flow<List<PresenceTracingRiskLevelResultEntity>>
+
+    @Insert(onConflict = REPLACE)
+    fun insert(entity: PresenceTracingRiskLevelResultEntity)
+
+    @Query("DELETE FROM PresenceTracingRiskLevelResultEntity WHERE calculatedAtMillis < :calculatedAtMillis")
+    suspend fun deleteOlderThan(calculatedAtMillis: Long)
+
+    @Query("DELETE FROM PresenceTracingRiskLevelResultEntity")
+    suspend fun deleteAll()
+}
+
+@Entity
+data class PresenceTracingRiskLevelResultEntity(
+    @PrimaryKey @ColumnInfo(name = "calculatedAtMillis") val calculatedAtMillis: Long,
+    @ColumnInfo(name = "riskStateCode") val riskState: RiskState
+)
+
+private fun PresenceTracingRiskLevelResultEntity.toCheckInWarningOverlap(
+    presenceTracingDayRisk: List<PresenceTracingDayRisk>?
+) = PtRiskLevelResult(
+    calculatedAt = Instant.ofEpochMilli((calculatedAtMillis)),
+    riskState = riskState,
+    presenceTracingDayRisk = presenceTracingDayRisk
+)
+
+private fun PtRiskLevelResult.toTraceTimeIntervalMatchEntity() = PresenceTracingRiskLevelResultEntity(
+    calculatedAtMillis = calculatedAt.millis,
+    riskState = riskState
+)
+
+class RiskStateConverter {
+    @TypeConverter
+    fun toRiskStateCode(value: Int?): RiskState? = value?.toRiskState()
+
+    @TypeConverter
+    fun fromRiskStateCode(code: RiskState?): Int? = code?.toCode()
+
+    private fun RiskState.toCode() = when (this) {
+        RiskState.CALCULATION_FAILED -> CALCULATION_FAILED
+        RiskState.LOW_RISK -> LOW_RISK
+        RiskState.INCREASED_RISK -> INCREASED_RISK
+    }
+
+    private fun Int.toRiskState() = when (this) {
+        CALCULATION_FAILED -> RiskState.CALCULATION_FAILED
+        LOW_RISK -> RiskState.LOW_RISK
+        INCREASED_RISK -> RiskState.INCREASED_RISK
+        else -> null
+    }
+
+    companion object {
+        private const val CALCULATION_FAILED = 0
+        private const val LOW_RISK = 1
+        private const val INCREASED_RISK = 2
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..49ced6787c86ec1438eb887d7b021ebe7e1f6448
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/PresenceTracingWarningModule.kt
@@ -0,0 +1,43 @@
+package de.rki.coronawarnapp.presencetracing.warning
+
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient
+import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask
+import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningApiV1
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.TaskTypeKey
+import okhttp3.OkHttpClient
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Singleton
+
+@Module
+class PresenceTracingWarningModule {
+
+    @Provides
+    @IntoMap
+    @TaskTypeKey(PresenceTracingWarningTask::class)
+    fun taskFactory(
+        factory: PresenceTracingWarningTask.Factory
+    ): TaskFactory<out Task.Progress, out Task.Result> = factory
+
+    @Singleton
+    @Provides
+    fun api(
+        @DownloadCDNHttpClient client: OkHttpClient,
+        @DownloadCDNServerUrl url: String,
+        gsonConverterFactory: GsonConverterFactory,
+    ): TraceWarningApiV1 {
+
+        return Retrofit.Builder()
+            .client(client)
+            .baseUrl(url)
+            .addConverterFactory(gsonConverterFactory)
+            .build()
+            .create(TraceWarningApiV1::class.java)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt
new file mode 100644
index 0000000000000000000000000000000000000000..581df13ed6c0afec399e579fc8a42a67da801d47
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloader.kt
@@ -0,0 +1,164 @@
+package de.rki.coronawarnapp.presencetracing.warning.download
+
+import dagger.Reusable
+import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningServer
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackageMetadata
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository
+import de.rki.coronawarnapp.util.HourInterval
+import de.rki.coronawarnapp.util.ZipHelper.readIntoMap
+import de.rki.coronawarnapp.util.ZipHelper.unzip
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.security.SignatureValidation
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import org.joda.time.Duration
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class TraceWarningPackageDownloader @Inject constructor(
+    private val repository: TraceWarningRepository,
+    private val dispatcherProvider: DispatcherProvider,
+    private val server: TraceWarningServer,
+    private val signatureValidation: SignatureValidation,
+) {
+
+    data class DownloadResult(
+        val successful: Boolean,
+        val newPackages: Collection<TraceWarningPackageMetadata>
+    ) {
+        override fun toString(): String {
+            return "DownloadResult(successful=$successful, newPackages.size=${newPackages.size})"
+        }
+    }
+
+    suspend fun launchDownloads(
+        location: LocationCode,
+        hourIntervals: List<HourInterval>,
+        downloadTimeout: Duration
+    ): DownloadResult {
+        val launcher: CoroutineScope.(HourInterval) -> Deferred<TraceWarningPackageMetadata?> = { hourInterval ->
+            async {
+                val metadata = repository.createMetadata(location, hourInterval)
+                withTimeout(downloadTimeout.millis) {
+                    downloadPackageForMetaData(metadata)
+                }
+            }
+        }
+
+        Timber.tag(TAG).d("Launching %d downloads.", hourIntervals.size)
+
+        val launchedDownloads: Collection<Deferred<TraceWarningPackageMetadata?>> =
+            hourIntervals.map { warningPackageId ->
+                withContext(context = dispatcherProvider.IO) {
+                    launcher(warningPackageId)
+                }
+            }
+
+        val successfulDownloads = launchedDownloads.awaitAll()
+            .filterNotNull()
+            .also {
+                Timber.tag(TAG).v("Downloaded keyfile: %s", it.joinToString("\n"))
+            }
+        Timber.tag(TAG).i("Download success: ${successfulDownloads.size}/${launchedDownloads.size}")
+
+        return DownloadResult(
+            successful = launchedDownloads.size == successfulDownloads.size,
+            newPackages = successfulDownloads
+        )
+    }
+
+    private suspend fun downloadPackageForMetaData(
+        metaData: TraceWarningPackageMetadata
+    ): TraceWarningPackageMetadata? = try {
+        val downloadInfo = server.downloadPackage(
+            location = metaData.location,
+            hourInterval = metaData.hourInterval
+        )
+
+        if (!downloadInfo.isEmptyPkg) {
+            val fileMap = downloadInfo.readBody().unzip().readIntoMap()
+            val rawProtoBuf = getValidatedBinary(metaData, fileMap)
+            writeProtoBufToFile(metaData, rawProtoBuf)
+        } else {
+            Timber.tag(TAG).v("Empty package for %s", metaData)
+        }
+
+        Timber.tag(TAG).v("Download finished: %s -> %s", metaData, downloadInfo)
+
+        val eTag = requireNotNull(downloadInfo.etag) { "Server provided no ETAG!" }
+
+        repository.markDownloadComplete(metaData, eTag, downloadInfo.isEmptyPkg)
+    } catch (e: Exception) {
+        Timber.tag(TAG).e(e, "Download failed: %s", metaData)
+        null
+    }
+
+    private fun writeProtoBufToFile(
+        metaData: TraceWarningPackageMetadata,
+        rawProtoBuf: ByteArray,
+    ) {
+        if (rawProtoBuf.isEmpty()) {
+            Timber.tag(TAG).d("rawProtoBuf was empty for  %s", metaData.packageId)
+            return
+        }
+
+        val saveTo = repository.getPathForMetaData(metaData)
+        if (saveTo.exists()) {
+            Timber.tag(TAG).w("File existed, overwriting: %s", saveTo)
+            if (saveTo.delete()) {
+                Timber.tag(TAG).e("%s exists, but can't be deleted.", saveTo)
+            }
+        }
+        try {
+            saveTo.parentFile?.let {
+                if (!it.exists() && it.mkdir()) {
+                    Timber.w("Had to create parent dir: %s", it)
+                }
+            }
+            saveTo.writeBytes(rawProtoBuf)
+        } catch (e: Exception) {
+            Timber.tag(TAG).e(e, "Failed to write %s to %s", metaData, saveTo)
+            saveTo.delete()
+            throw e
+        }
+        Timber.tag(TAG).v("%d bytes written to %s.", rawProtoBuf.size, saveTo)
+    }
+
+    private fun getValidatedBinary(
+        metaData: TraceWarningPackageMetadata,
+        fileMap: Map<String, ByteArray>
+    ): ByteArray {
+        val signature = fileMap[EXPORT_SIGNATURE_NAME] ?: throw TraceWarningPackageValidationException(
+            message = "Signature was null for ${metaData.packageId}(${metaData.eTag})."
+        )
+
+        val binary = fileMap[EXPORT_BINARY_NAME] ?: throw TraceWarningPackageValidationException(
+            message = "Binary was null for ${metaData.packageId}(${metaData.eTag})."
+        )
+
+        val hasValidSignature = signatureValidation.hasValidSignature(
+            binary,
+            SignatureValidation.parseTEKStyleSignature(signature)
+        )
+
+        if (!hasValidSignature) {
+            throw TraceWarningPackageValidationException(
+                message = "Signature didn't match for ${metaData.packageId}(${metaData.eTag})."
+            )
+        }
+
+        return binary
+    }
+
+    companion object {
+        private const val TAG = "TraceWarningDownloader"
+        private const val EXPORT_BINARY_NAME = "export.bin"
+        private const val EXPORT_SIGNATURE_NAME = "export.sig"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ea2d87f227017e3399c5c4b12468448f47914ec4
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageSyncTool.kt
@@ -0,0 +1,178 @@
+package de.rki.coronawarnapp.presencetracing.warning.download
+
+import androidx.annotation.VisibleForTesting
+import dagger.Reusable
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.KeyDownloadConfig
+import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningApiV1
+import de.rki.coronawarnapp.presencetracing.warning.download.server.TraceWarningServer
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackageMetadata
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository
+import de.rki.coronawarnapp.storage.DeviceStorage
+import de.rki.coronawarnapp.util.HourInterval
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval
+import de.rki.coronawarnapp.util.debug.measureTime
+import kotlinx.coroutines.flow.first
+import timber.log.Timber
+import javax.inject.Inject
+import kotlin.math.max
+
+@Reusable
+class TraceWarningPackageSyncTool @Inject constructor(
+    private val deviceStorage: DeviceStorage,
+    private val server: TraceWarningServer,
+    private val repository: TraceWarningRepository,
+    private val configProvider: AppConfigProvider,
+    private val checkInRepository: CheckInRepository,
+    private val downloader: TraceWarningPackageDownloader
+) {
+
+    suspend fun syncPackages(): SyncResult {
+        repository.cleanMetadata()
+        return measureTime(
+            { Timber.tag(TAG).d("syncPackagesForLocation(DE), took %dms", it) },
+            { syncPackagesForLocation(LocationCode("DE")) }
+        )
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal suspend fun syncPackagesForLocation(location: LocationCode): SyncResult {
+        Timber.tag(TAG).d("syncTraceWarningPackages(location=%s)", location)
+
+        val oldestCheckIn = checkInRepository.allCheckIns.first().minByOrNull { it.checkInStart }.also {
+            Timber.tag(TAG).d("Our oldest check-in is %s", it)
+        }
+
+        if (oldestCheckIn == null) {
+            Timber.tag(TAG).w("There were no checkins, cleaning up package metadata, aborting early.")
+            val metaDataForLocation = repository.getMetaDataForLocation(location)
+            repository.delete(metaDataForLocation)
+            return SyncResult(successful = true)
+        }
+
+        val downloadConfig: KeyDownloadConfig = configProvider.getAppConfig()
+
+        cleanUpRevokedPackages(downloadConfig)
+
+        val intervalDiscovery: TraceWarningApiV1.DiscoveryResult = try {
+            server.getAvailableIds(location)
+        } catch (e: Exception) {
+            Timber.tag(TAG).w(e, "Failed to discover available IDs.")
+            return SyncResult(successful = false)
+        }
+
+        val firstRelevantInterval: HourInterval = max(
+            oldestCheckIn.checkInStart.deriveHourInterval(),
+            intervalDiscovery.oldest
+        )
+
+        cleanUpIrrelevantPackages(location, firstRelevantInterval)
+
+        if (firstRelevantInterval > intervalDiscovery.latest) {
+            Timber.tag(TAG).d("Known server IDs are older then ours newest, aborting early.")
+            return SyncResult(successful = true)
+        }
+
+        val missingHourIntervals = determineIntervalsToDownload(
+            location = location,
+            firstRelevant = oldestCheckIn.checkInStart.deriveHourInterval(),
+            lastRelevant = intervalDiscovery.latest
+        )
+
+        if (missingHourIntervals.isEmpty()) {
+            Timber.tag(TAG).d("There are no missing intervals for %s", location)
+            return SyncResult(successful = true)
+        }
+
+        requireStorageSpaceFor(missingHourIntervals.size)
+
+        val downloadResult = downloader.launchDownloads(
+            location = location,
+            hourIntervals = missingHourIntervals,
+            downloadTimeout = downloadConfig.individualDownloadTimeout
+        )
+        Timber.tag(TAG).i("Download result: %s", downloadResult)
+
+        return SyncResult(
+            successful = downloadResult.successful,
+            newPackages = downloadResult.newPackages,
+        )
+    }
+
+    /**
+     * Returns true if any of our cached keys were revoked
+     */
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal suspend fun cleanUpRevokedPackages(
+        config: KeyDownloadConfig
+    ): List<TraceWarningPackageMetadata> {
+        val revokedKeyPackages = config.revokedTraceWarningPackages
+
+        if (revokedKeyPackages.isEmpty()) {
+            Timber.tag(TAG).d("No revoked key packages to delete.")
+            return emptyList()
+        }
+
+        val badEtags = revokedKeyPackages.map { it.etag }
+        val toDelete = repository.allMetaData.first().filter { badEtags.contains(it.eTag) }
+        Timber.tag(TAG).d("Revoked key packages matched %s", toDelete)
+
+        repository.delete(toDelete)
+
+        return toDelete.also {
+            Timber.tag(TAG).d("Cleaned up TraceWarning ids: %s", it)
+        }
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal suspend fun cleanUpIrrelevantPackages(
+        location: LocationCode,
+        oldestRelevantInterval: HourInterval
+    ): List<TraceWarningPackageMetadata> {
+        val downloaded = repository.getMetaDataForLocation(location)
+        val toDelete = downloaded.filter { it.hourInterval < oldestRelevantInterval }
+        Timber.tag(TAG).d("Removing irrelevant ids older than %d: %s", oldestRelevantInterval, toDelete)
+
+        repository.delete(toDelete)
+
+        return toDelete.also {
+            Timber.tag(TAG).d("Removed irrelevant packages: %s", it)
+        }
+    }
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal suspend fun determineIntervalsToDownload(
+        location: LocationCode,
+        firstRelevant: HourInterval,
+        lastRelevant: HourInterval
+    ): List<HourInterval> {
+        val metadatas = repository.getMetaDataForLocation(location)
+
+        return (firstRelevant..lastRelevant).filter { interval ->
+            // If there is no metadata, it's unknown, so we want to download it
+            metadatas.none { it.hourInterval == interval }
+        }
+    }
+
+    private suspend fun requireStorageSpaceFor(size: Int): DeviceStorage.CheckResult {
+        val requiredBytes: Long = APPROX_FILE_SIZE * size
+        Timber.tag(TAG).d("%dB are required for %d files", requiredBytes, size)
+        return deviceStorage.requireSpacePrivateStorage(requiredBytes).also {
+            Timber.tag(TAG).d("Storage check result: %s", it)
+        }
+    }
+
+    data class SyncResult(
+        val successful: Boolean,
+        val newPackages: Collection<TraceWarningPackageMetadata> = emptyList()
+    )
+
+    companion object {
+        private const val TAG = "TraceWarningSyncTool"
+
+        // TODO check size
+        private const val APPROX_FILE_SIZE = 22 * 1024L // ~22KB
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b05d7ac349fe3d0885f709c4e4bcc6b2fffee6e5
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageValidationException.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.presencetracing.warning.download
+
+import de.rki.coronawarnapp.exception.reporting.ErrorCodes
+import de.rki.coronawarnapp.util.security.InvalidSignatureException
+
+class TraceWarningPackageValidationException(message: String) : InvalidSignatureException(
+    code = ErrorCodes.APPLICATION_CONFIGURATION_INVALID.code,
+    message = message
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt
new file mode 100644
index 0000000000000000000000000000000000000000..0c99daef7570bf8beb5dc8b3015b7280804449ae
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningApiV1.kt
@@ -0,0 +1,31 @@
+package de.rki.coronawarnapp.presencetracing.warning.download.server
+
+import androidx.annotation.Keep
+import com.google.gson.annotations.SerializedName
+import de.rki.coronawarnapp.util.HourInterval
+import okhttp3.ResponseBody
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Path
+import retrofit2.http.Streaming
+
+interface TraceWarningApiV1 {
+
+    @Keep
+    data class DiscoveryResult(
+        @SerializedName("oldest") val oldest: HourInterval,
+        @SerializedName("latest") val latest: HourInterval
+    )
+
+    @GET("/version/v1/twp/country/{region}/hour")
+    suspend fun getWarningPackageIds(
+        @Path("region") region: String
+    ): DiscoveryResult
+
+    @Streaming
+    @GET("/version/v1/twp/country/{region}/hour/{timeId}")
+    suspend fun downloadKeyFileForHour(
+        @Path("region") region: String,
+        @Path("timeId") timeId: Long
+    ): Response<ResponseBody>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt
new file mode 100644
index 0000000000000000000000000000000000000000..02e5d44480f76ebbcd58e5a1621bbd2783b1ce25
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningPackageDownload.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.presencetracing.warning.download.server
+
+import okhttp3.ResponseBody
+import retrofit2.Response
+import java.io.InputStream
+
+data class TraceWarningPackageDownload(val response: Response<ResponseBody>) {
+
+    private val headers = response.headers()
+
+    val etag by lazy { headers.values("ETag").singleOrNull() }
+
+    val isEmptyPkg by lazy { headers.values("cwa-empty-pkg").singleOrNull() == "1" }
+
+    fun readBody(): InputStream = requireNotNull(response.body()) { "Response body was null" }.byteStream()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3cc3c8bb40e34e4ac7a2856a093d0ab2a3e71e94
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/download/server/TraceWarningServer.kt
@@ -0,0 +1,54 @@
+package de.rki.coronawarnapp.presencetracing.warning.download.server
+
+import dagger.Lazy
+import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.util.HourInterval
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import retrofit2.HttpException
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TraceWarningServer @Inject constructor(
+    private val traceWarningApi: Lazy<TraceWarningApiV1>
+) {
+
+    private val warningApi: TraceWarningApiV1
+        get() = traceWarningApi.get()
+
+    suspend fun getAvailableIds(
+        location: LocationCode
+    ): TraceWarningApiV1.DiscoveryResult = withContext(Dispatchers.IO) {
+        warningApi.getWarningPackageIds(location.identifier).also {
+            Timber.d("getAvailableIds(location=%s): %s", location, it)
+        }
+    }
+
+    suspend fun downloadPackage(
+        location: LocationCode,
+        hourInterval: HourInterval
+    ): TraceWarningPackageDownload = withContext(Dispatchers.IO) {
+        Timber.tag(TAG).v("downloadPackage(location=%s, hourInterval=%s)", location, hourInterval)
+
+        val response = warningApi.downloadKeyFileForHour(
+            location.identifier,
+            hourInterval
+        )
+
+        val downloadInfo = TraceWarningPackageDownload(response)
+
+        if (response.isSuccessful) {
+            Timber.tag(TAG).v("TraceTimeWarning download available: %s", downloadInfo)
+
+            return@withContext downloadInfo
+        } else {
+            throw HttpException(response)
+        }
+    }
+
+    companion object {
+        private val TAG = TraceWarningServer::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt
new file mode 100644
index 0000000000000000000000000000000000000000..91e655f43ba78c1cafbf4ec571e56a09fe7b33b6
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackage.kt
@@ -0,0 +1,14 @@
+package de.rki.coronawarnapp.presencetracing.warning.storage
+
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
+
+interface TraceWarningPackage {
+
+    val packageId: WarningPackageId
+
+    /**
+     * May throw an exception if there is an issue with the protobuf
+     */
+    suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e16436f59241d064d0225cd15b8a2717932a8f77
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageContainer.kt
@@ -0,0 +1,23 @@
+package de.rki.coronawarnapp.presencetracing.warning.storage
+
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
+import java.io.File
+
+data class TraceWarningPackageContainer(
+    override val packageId: WarningPackageId,
+    private val packagePath: File,
+) : TraceWarningPackage {
+
+    private val warningPackage by lazy<TraceWarning.TraceWarningPackage> {
+        if (packagePath.exists()) {
+            TraceWarning.TraceWarningPackage.parseFrom(packagePath.readBytes())
+        } else {
+            TraceWarning.TraceWarningPackage.getDefaultInstance()
+        }
+    }
+
+    override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+        return warningPackage.timeIntervalWarningsList
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5939360ce43edaf791ae2eaeedb59cd78982bce5
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageDatabase.kt
@@ -0,0 +1,62 @@
+package de.rki.coronawarnapp.presencetracing.warning.storage
+
+import android.content.Context
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.room.Update
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.util.database.CommonConverters
+import de.rki.coronawarnapp.util.di.AppContext
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@Dao
+interface TraceWarningPackageDao {
+
+    @Query("SELECT * FROM TraceWarningPackageMetadata")
+    fun getAllMetaData(): Flow<List<TraceWarningPackageMetadata>>
+
+    @Query("SELECT * FROM TraceWarningPackageMetadata WHERE location = :location")
+    suspend fun getAllMetaDataForLocation(location: String): List<TraceWarningPackageMetadata>
+
+    @Query("SELECT * FROM TraceWarningPackageMetadata WHERE id = :packageId")
+    suspend fun get(packageId: WarningPackageId): TraceWarningPackageMetadata?
+
+    @Insert(onConflict = OnConflictStrategy.IGNORE)
+    suspend fun insert(entity: TraceWarningPackageMetadata)
+
+    @Update(entity = TraceWarningPackageMetadata::class)
+    suspend fun updateMetaData(update: TraceWarningPackageMetadata.UpdateDownload)
+
+    @Update(entity = TraceWarningPackageMetadata::class)
+    suspend fun updateMetaData(update: TraceWarningPackageMetadata.UpdateProcessed)
+
+    @Query("DELETE FROM TraceWarningPackageMetadata WHERE id in (:packageIds)")
+    suspend fun deleteByIds(packageIds: List<WarningPackageId>)
+
+    @Query("DELETE FROM TraceWarningPackageMetadata")
+    suspend fun clear()
+}
+
+@Database(
+    entities = [TraceWarningPackageMetadata::class],
+    version = 1,
+    exportSchema = true
+)
+@TypeConverters(CommonConverters::class)
+abstract class TraceWarningDatabase : RoomDatabase() {
+
+    abstract fun traceWarningPackageDao(): TraceWarningPackageDao
+
+    class Factory @Inject constructor(@AppContext private val context: Context) {
+        fun create() = Room
+            .databaseBuilder(context, TraceWarningDatabase::class.java, "TraceWarning_db")
+            .build()
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt
new file mode 100644
index 0000000000000000000000000000000000000000..54725874a1c3c8931c8f9f9196786d861076456b
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningPackageMetadata.kt
@@ -0,0 +1,58 @@
+package de.rki.coronawarnapp.presencetracing.warning.storage
+
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.util.HourInterval
+import org.joda.time.Instant
+
+@Entity(tableName = "TraceWarningPackageMetadata")
+data class TraceWarningPackageMetadata(
+    @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId,
+    @ColumnInfo(name = "createdAt") val createdAt: Instant,
+    @ColumnInfo(name = "location") val location: LocationCode, // i.e. "DE"
+    @ColumnInfo(name = "hourInterval") val hourInterval: HourInterval,
+    @ColumnInfo(name = "eTag") val eTag: String? = null,
+    @ColumnInfo(name = "downloaded") val isDownloaded: Boolean = false,
+    @ColumnInfo(name = "emptyPkg") val isEmptyPkg: Boolean = false,
+    @ColumnInfo(name = "processed") val isProcessed: Boolean = false
+) {
+
+    constructor(
+        location: LocationCode,
+        hourInterval: HourInterval,
+        createdAt: Instant
+    ) : this(
+        packageId = calcluateId(location, hourInterval),
+        location = location,
+        hourInterval = hourInterval,
+        createdAt = createdAt,
+    )
+
+    @Transient
+    val fileName: String = "$packageId.bin"
+
+    companion object {
+        fun calcluateId(
+            location: LocationCode,
+            hourInterval: HourInterval
+        ): WarningPackageId = "${location.identifier}_$hourInterval"
+    }
+
+    @Entity
+    data class UpdateDownload(
+        @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId,
+        @ColumnInfo(name = "eTag") val eTag: String?,
+        @ColumnInfo(name = "downloaded") val isDownloaded: Boolean,
+        @ColumnInfo(name = "emptyPkg") val isEmptyPkg: Boolean,
+        @ColumnInfo(name = "processed") val isProcessed: Boolean,
+    )
+
+    @Entity
+    data class UpdateProcessed(
+        @PrimaryKey @ColumnInfo(name = "id") val packageId: WarningPackageId,
+        @ColumnInfo(name = "processed") val isProcessed: Boolean,
+    )
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4af922212d36bfd5c4ae94950d2156abec57b3ca
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/TraceWarningRepository.kt
@@ -0,0 +1,182 @@
+package de.rki.coronawarnapp.presencetracing.warning.storage
+
+import android.content.Context
+import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.util.HourInterval
+import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.di.AppContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import timber.log.Timber
+import java.io.File
+import java.io.IOException
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class TraceWarningRepository @Inject constructor(
+    @AppContext private val context: Context,
+    private val factory: TraceWarningDatabase.Factory,
+    private val timeStamper: TimeStamper
+) {
+    private val database by lazy { factory.create() }
+    private val dao: TraceWarningPackageDao by lazy { database.traceWarningPackageDao() }
+
+    private val storageDir by lazy {
+        File(context.cacheDir, "trace_warning_packages").apply {
+            if (!exists()) {
+                if (mkdirs()) {
+                    Timber.tag(TAG).d("Trace warning package directory created: %s", this)
+                } else {
+                    throw IOException("Trace warning package directory creation failed: $this")
+                }
+            }
+        }
+    }
+
+    val unprocessedWarningPackages: Flow<List<TraceWarningPackage>> = dao.getAllMetaData()
+        .map { metadatas ->
+            Timber.tag(TAG).v("Known packages: ${metadatas.size}")
+            metadatas.filter { !it.isProcessed }
+        }
+        .map { unprocessed ->
+            Timber.tag(TAG).v("Unprocessed packages: ${unprocessed.size}")
+            unprocessed.filter { !it.isEmptyPkg }
+        }
+        .map { metaDatas ->
+            Timber.tag(TAG).v("There are ${metaDatas.size} unprocessed non-empty warning packages.")
+            metaDatas.map { metaData ->
+                TraceWarningPackageContainer(
+                    packageId = metaData.packageId,
+                    packagePath = getPathForMetaData(metaData)
+                )
+            }
+        }
+
+    fun getPathForMetaData(metaData: TraceWarningPackageMetadata): File {
+        return File(storageDir, metaData.fileName)
+    }
+
+    val allMetaData = dao.getAllMetaData()
+
+    suspend fun createMetadata(location: LocationCode, hourInterval: HourInterval): TraceWarningPackageMetadata {
+        val metadata = TraceWarningPackageMetadata(
+            location = location,
+            hourInterval = hourInterval,
+            createdAt = timeStamper.nowUTC
+        )
+        dao.insert(metadata)
+        Timber.tag(TAG).d("Inserted new Metadata: %s", metadata)
+        return metadata
+    }
+
+    suspend fun getMetaDataForLocation(location: LocationCode): List<TraceWarningPackageMetadata> {
+        return dao.getAllMetaDataForLocation(location.identifier)
+    }
+
+    suspend fun markDownloadComplete(
+        metadata: TraceWarningPackageMetadata,
+        eTag: String,
+        isEmptyPkg: Boolean
+    ): TraceWarningPackageMetadata {
+        Timber.tag(TAG).d("markDownloadComplete(metaData=%s, eTag=%s)", metadata, eTag)
+        val update = TraceWarningPackageMetadata.UpdateDownload(
+            packageId = metadata.packageId,
+            eTag = eTag,
+            isDownloaded = true,
+            isProcessed = false,
+            isEmptyPkg = isEmptyPkg,
+        )
+        Timber.tag(TAG).d("Metadata marked as complete: %s", update)
+        dao.updateMetaData(update)
+        return metadata.copy(
+            eTag = eTag,
+            isDownloaded = true,
+            isProcessed = false,
+            isEmptyPkg = isEmptyPkg,
+        )
+    }
+
+    suspend fun markPackagesProcessed(packageIds: List<WarningPackageId>) {
+        Timber.tag(TAG).v("markPackagesProcessed(packageIds=%s)", packageIds)
+
+        packageIds.forEach { packageId ->
+            Timber.tag(TAG).d("markPackageProcessed(packageId=%s)", packageId)
+            val update = TraceWarningPackageMetadata.UpdateProcessed(
+                packageId = packageId,
+                isProcessed = true,
+            )
+            dao.updateMetaData(update)
+
+            dao.get(packageId)?.also {
+                val file = getPathForMetaData(it)
+                if (file.delete()) {
+                    Timber.tag(TAG).v("Deleted processed file: %s", file)
+                }
+            }
+        }
+    }
+
+    suspend fun delete(metadata: List<TraceWarningPackageMetadata>) {
+        Timber.tag(TAG).d("delete(metaData=%s)", metadata.map { it.packageId })
+        dao.deleteByIds(metadata.map { it.packageId })
+        metadata.map { getPathForMetaData(it) }.forEach {
+            if (it.exists()) {
+                if (it.delete()) {
+                    Timber.tag(TAG).d("Delete TraceWarningPackage file.")
+                } else {
+                    Timber.tag(TAG).w("Failed to delete TraceWarningPackage file: %s", it)
+                }
+            }
+        }
+    }
+
+    suspend fun clear() {
+        Timber.tag(TAG).d("clear()")
+        dao.clear()
+
+        if (!storageDir.deleteRecursively()) {
+            Timber.tag(TAG).e("Failed to delete all TraceWarningPackage files.")
+        }
+    }
+
+    suspend fun cleanMetadata() {
+        Timber.tag(TAG).d("cleanMetadata()")
+        val allMetadata = allMetaData.first()
+
+        // Lost files, system deleted cache?
+        run {
+            val shouldHaveFile = allMetadata.filter { it.isDownloaded && !it.isProcessed && !it.isEmptyPkg }
+            val toDelete = shouldHaveFile.filter { !getPathForMetaData(it).exists() }
+            if (toDelete.isNotEmpty()) {
+                Timber.tag(TAG).w("%d Metadata items lost their file", toDelete.size)
+            }
+            delete(toDelete)
+        }
+
+        // Shouldn't have a file, but has one? Gremlins?
+        run {
+            val shouldNotHaveFile = allMetadata.filter { it.isDownloaded && (it.isProcessed || it.isEmptyPkg) }
+            val toDelete = shouldNotHaveFile.filter { getPathForMetaData(it).exists() }
+            if (toDelete.isNotEmpty()) {
+                Timber.tag(TAG).w("%d Metadata items have unexpected files", toDelete.size)
+            }
+            delete(toDelete)
+        }
+
+        // File without owner?
+        storageDir.listFiles()?.forEach { file ->
+            val orphan = allMetadata.none { getPathForMetaData(it) == file }
+
+            if (orphan && file.delete()) {
+                Timber.tag(TAG).w("Deleted orphaned file: %s", file)
+            }
+        }
+    }
+
+    companion object {
+        private val TAG = TraceWarningRepository::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt
new file mode 100644
index 0000000000000000000000000000000000000000..af612ce8bad3c3eb934fa92b3bc9b264108ce1ac
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/warning/storage/WarningPackageId.kt
@@ -0,0 +1,3 @@
+package de.rki.coronawarnapp.presencetracing.warning
+
+typealias WarningPackageId = String
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
new file mode 100644
index 0000000000000000000000000000000000000000..b4e6305fb069fdd90d9136baa9a2549a2250023a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt
@@ -0,0 +1,64 @@
+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.util.TimeAndDateExtensions.toLocalDateUtc
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+
+data class CombinedEwPtDayRisk(
+    val localDate: LocalDate,
+    val riskState: RiskState
+)
+
+data class CombinedEwPtRiskLevelResult(
+    val ptRiskLevelResult: PtRiskLevelResult,
+    val ewRiskLevelResult: EwRiskLevelResult
+) {
+
+    val riskState: RiskState by lazy {
+        combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState)
+    }
+
+    val wasSuccessfullyCalculated: Boolean by lazy {
+        riskState != RiskState.CALCULATION_FAILED
+    }
+
+    val calculatedAt: Instant by lazy {
+        max(ewRiskLevelResult.calculatedAt, ptRiskLevelResult.calculatedAt)
+    }
+
+    val daysWithEncounters: Int by lazy {
+        when (riskState) {
+            RiskState.INCREASED_RISK -> {
+                (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0) +
+                    ptRiskLevelResult.numberOfDaysWithHighRisk
+            }
+            RiskState.LOW_RISK -> {
+                (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0) +
+                    ptRiskLevelResult.numberOfDaysWithLowRisk
+            }
+            else -> 0
+        }
+    }
+
+    val lastRiskEncounterAt: LocalDate? by lazy {
+        when (riskState) {
+            RiskState.INCREASED_RISK -> max(
+                ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithHighRisk?.toLocalDateUtc(),
+                ptRiskLevelResult.mostRecentDateWithHighRisk
+            )
+            RiskState.LOW_RISK -> max(
+                ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithLowRisk?.toLocalDateUtc(),
+                ptRiskLevelResult.mostRecentDateWithLowRisk
+            )
+            else -> null
+        }
+    }
+}
+
+data class LastCombinedRiskResults(
+    val lastCalculated: CombinedEwPtRiskLevelResult,
+    val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
index d09e184b9cd11199166917a2337a23bf200aaf48..123f45edceef714e0fefff56dff06bb8dc7f6f73 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt
@@ -8,7 +8,6 @@ import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettin
 import de.rki.coronawarnapp.datadonation.survey.Surveys
 import de.rki.coronawarnapp.notification.GeneralNotifications
 import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
-import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.storage.TracingSettings
 import de.rki.coronawarnapp.submission.SubmissionSettings
@@ -49,7 +48,7 @@ class RiskLevelChangeDetector @Inject constructor(
             }
             .filter { it.size == 2 }
             .onEach {
-                Timber.v("Checking for risklevel change.")
+                Timber.v("Checking for ew risklevel change.")
                 checkEwRiskForStateChanges(it)
             }
             .catch { Timber.e(it, "App config change checks failed.") }
@@ -61,7 +60,7 @@ class RiskLevelChangeDetector @Inject constructor(
             }
             .filter { it.size == 2 }
             .onEach {
-                Timber.v("Checking for risklevel change.")
+                Timber.v("Checking for combined risklevel change.")
                 checkCombinedRiskForStateChanges(it)
             }
             .catch { Timber.e(it, "App config change checks failed.") }
@@ -69,7 +68,6 @@ class RiskLevelChangeDetector @Inject constructor(
     }
 
     private suspend fun checkCombinedRiskForStateChanges(results: List<CombinedEwPtRiskLevelResult>) {
-        // TODO refactor
         val oldResult = results.first()
         val newResult = results.last()
 
@@ -164,7 +162,6 @@ class RiskLevelChangeDetector @Inject constructor(
             } else {
                 Timber.d("App is in foreground, not sending notifications")
             }
-
             Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskState")
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt
index c47ff8c40e84d723cf7d1585d83c48174aa81c66..1d0ea0988e7b013f220062d5ffd009ccf30f65c7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResultExtensions.kt
@@ -1,9 +1,7 @@
 package de.rki.coronawarnapp.risk
 
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
-import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
-import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult
 import org.joda.time.Instant
 
 fun List<EwRiskLevelResult>.tryLatestEwResultsWithDefaults(): DisplayableEwRiskResults {
@@ -24,41 +22,8 @@ data class DisplayableEwRiskResults(
     val lastSuccessfullyCalculated: EwRiskLevelResult
 )
 
-fun List<CombinedEwPtRiskLevelResult>.tryLatestResultsWithDefaults(): DisplayableRiskResults {
-    val latestCalculation = this.maxByOrNull { it.calculatedAt }
-        ?: initialLowLevelEwRiskLevelResult
-
-    val lastSuccessfullyCalculated = this.filter { it.wasSuccessfullyCalculated }
-        .maxByOrNull { it.calculatedAt } ?: undeterminedEwRiskLevelResult
-
-    return DisplayableRiskResults(
-        lastCalculated = latestCalculation,
-        lastSuccessfullyCalculated = lastSuccessfullyCalculated
-    )
-}
-
-data class DisplayableRiskResults(
-    val lastCalculated: CombinedEwPtRiskLevelResult,
-    val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult
-)
-
-private val undeterminedEwRiskLevelResult = CombinedEwPtRiskLevelResult(
-    PtRiskLevelResult(
-        calculatedAt = Instant.EPOCH,
-        riskState = RiskState.CALCULATION_FAILED
-    ),
-    EwUndeterminedRiskLevelResult
-)
-
-private val initialLowLevelEwRiskLevelResult = CombinedEwPtRiskLevelResult(
-    PtRiskLevelResult(
-        calculatedAt = Instant.now(),
-        riskState = RiskState.LOW_RISK
-    ),
-    EwInitialLowRiskLevelResult
-)
-
 private object EwInitialLowRiskLevelResult : EwRiskLevelResult {
+    // TODO this causes flaky tests as this is set in memory, once.
     override val calculatedAt: Instant = Instant.now()
     override val riskState: RiskState = RiskState.LOW_RISK
     override val failureReason: EwRiskLevelResult.FailureReason? = null
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ba05ce2a89622ce6cb12e207ae02095270fd799a
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/execution/RiskWorkScheduler.kt
@@ -0,0 +1,109 @@
+package de.rki.coronawarnapp.risk.execution
+
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkInfo
+import androidx.work.WorkManager
+import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
+import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorkBuilder
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorkBuilder
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.TaskState
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
+import de.rki.coronawarnapp.util.coroutine.AppScope
+import de.rki.coronawarnapp.util.coroutine.await
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class RiskWorkScheduler @Inject constructor(
+    @AppScope private val appScope: CoroutineScope,
+    private val workManager: WorkManager,
+    private val taskController: TaskController,
+    private val presenceWorkBuilder: PresenceTracingWarningWorkBuilder,
+    private val diagnosisWorkBuilder: DiagnosisKeyRetrievalWorkBuilder,
+) {
+
+    suspend fun runRiskTasksNow(): List<TaskState> {
+        val diagnosisKeysState = appScope.async {
+            Timber.tag(TAG).d("Running DownloadDiagnosisKeysTask")
+            val result = taskController.submitBlocking(
+                DefaultTaskRequest(
+                    DownloadDiagnosisKeysTask::class,
+                    DownloadDiagnosisKeysTask.Arguments(),
+                    originTag = "RiskWorkScheduler-runRiskTasksNow"
+                )
+            )
+            Timber.tag(TAG).d("DownloadDiagnosisKeysTask finished with %s", result)
+            result
+        }
+        val presenceWarningState = appScope.async {
+            Timber.tag(TAG).d("Running PresenceTracingWarningTask")
+            val result = taskController.submitBlocking(
+                DefaultTaskRequest(
+                    PresenceTracingWarningTask::class,
+                    originTag = "RiskWorkScheduler-runRiskTasksNow"
+                )
+            )
+            Timber.tag(TAG).d("PresenceTracingWarningTask finished with %s", result)
+            result
+        }
+        return listOf(diagnosisKeysState, presenceWarningState).awaitAll()
+    }
+
+    suspend fun isScheduled(): Boolean {
+        val diagnosisWorkerInfos = appScope.async {
+            workManager.getWorkInfosForUniqueWork(WORKER_ID_PRESENCE_TRACING).await()
+        }
+        val warningWorkerInfos = appScope.async {
+            workManager.getWorkInfosForUniqueWork(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD).await()
+        }
+        return listOf(diagnosisWorkerInfos, warningWorkerInfos).awaitAll().all { perWorkerInfos ->
+            perWorkerInfos.any { it.isScheduled }
+        }
+    }
+
+    fun setPeriodicRiskCalculation(enabled: Boolean) {
+        Timber.tag(TAG).i("setPeriodicRiskCalculation(enabled=$enabled)")
+
+        if (enabled) {
+            val diagnosisRequest = diagnosisWorkBuilder.createPeriodicWorkRequest()
+            queueWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD, diagnosisRequest)
+
+            val warningRequest = presenceWorkBuilder.createPeriodicWorkRequest()
+            queueWorker(WORKER_ID_PRESENCE_TRACING, warningRequest)
+        } else {
+            cancelWorker(WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD)
+            cancelWorker(WORKER_ID_PRESENCE_TRACING)
+        }
+    }
+
+    private fun queueWorker(workerId: String, request: PeriodicWorkRequest) {
+        Timber.tag(TAG).d("queueWorker(workerId=%s, request=%s)", workerId, request)
+        workManager.enqueueUniquePeriodicWork(
+            workerId,
+            ExistingPeriodicWorkPolicy.KEEP,
+            request,
+        )
+    }
+
+    private fun cancelWorker(workerId: String) {
+        Timber.tag(TAG).d("cancelWorker(workerId=$workerId")
+        workManager.cancelUniqueWork(workerId)
+    }
+
+    private val WorkInfo.isScheduled: Boolean
+        get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED
+
+    companion object {
+        private const val WORKER_ID_DIAGNOSIS_KEY_DOWNLOAD = "DiagnosisKeyRetrievalWorker"
+        private const val WORKER_ID_PRESENCE_TRACING = "PresenceTracingWarningWorker"
+        private const val TAG = "RiskWorkScheduler"
+    }
+}
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 a0844cd0b8f1415b491cdf1fd3dfc16133335a3e..d5d0d71f03319705140b0a456969d7e9c2289b4f 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,14 +1,19 @@
 package de.rki.coronawarnapp.risk.storage
 
 import androidx.annotation.VisibleForTesting
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository
+import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
-import de.rki.coronawarnapp.presencetracing.risk.mapToRiskState
+import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
+import de.rki.coronawarnapp.presencetracing.risk.calculation.mapToRiskState
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk
+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.TraceLocationCheckInRisk
+import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
@@ -30,7 +35,7 @@ import de.rki.coronawarnapp.util.flow.combine as flowCombine
 
 abstract class BaseRiskLevelStorage constructor(
     private val riskResultDatabaseFactory: RiskResultDatabase.Factory,
-    presenceTracingRiskRepository: PresenceTracingRiskRepository,
+    private val presenceTracingRiskRepository: PresenceTracingRiskRepository,
     scope: CoroutineScope
 ) : RiskLevelStorage {
 
@@ -185,44 +190,20 @@ abstract class BaseRiskLevelStorage constructor(
         }
         .shareLatest(tag = TAG, scope = scope)
 
-    private val latestAndLastSuccessfulPtRiskLevelResult: Flow<List<PtRiskLevelResult>> =
-        presenceTracingRiskRepository
-            .latestAndLastSuccessful()
-            .shareLatest(tag = TAG, scope = scope)
-
-    // TODO maybe refactor
-    override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<List<CombinedEwPtRiskLevelResult>>
+    // used for risk state in tracing state/details
+    override val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults>
         get() = combine(
-            latestAndLastSuccessfulEwRiskLevelResult,
-            latestAndLastSuccessfulPtRiskLevelResult
+            allEwRiskLevelResults,
+            presenceTracingRiskRepository.allEntries()
         ) { ewRiskLevelResults, ptRiskLevelResults ->
-            val latestEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt }
-            val latestPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt }
-            val combinedList = mutableListOf<CombinedEwPtRiskLevelResult>()
-            if (latestEwResult != null && latestPtResult != null) {
-                combinedList.add(
-                    CombinedEwPtRiskLevelResult(
-                        ewRiskLevelResult = latestEwResult,
-                        ptRiskLevelResult = latestPtResult
-                    )
-                )
-            }
-            val lastSuccessfulEwResult = ewRiskLevelResults
-                .filter { it.wasSuccessfullyCalculated }.maxByOrNull { it.calculatedAt }
-            val lastSuccessfulPtResult = ptRiskLevelResults
-                .filter { it.wasSuccessfullyCalculated }.maxByOrNull { it.calculatedAt }
-            if (lastSuccessfulEwResult != null && lastSuccessfulPtResult != null) {
-                combinedList.add(
-                    CombinedEwPtRiskLevelResult(
-                        ewRiskLevelResult = lastSuccessfulEwResult,
-                        // current ptDayRiskStates belong to the last successful calculation - ugly
-                        ptRiskLevelResult = lastSuccessfulPtResult.copy(
-                            presenceTracingDayRisk = ptDayRiskStates.first()
-                        )
-                    )
-                )
-            }
-            combinedList
+
+            val combinedResults = combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults)
+                .sortedByDescending { it.calculatedAt }
+
+            LastCombinedRiskResults(
+                lastCalculated = combinedResults.firstOrNull() ?: currentCombinedLowRisk,
+                lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: initialCombined
+            )
         }
 
     private val latestPtRiskLevelResults: Flow<List<PtRiskLevelResult>> =
@@ -230,33 +211,15 @@ abstract class BaseRiskLevelStorage constructor(
             .latestEntries(2)
             .shareLatest(tag = TAG, scope = scope)
 
+    // used for risk level change detector to trigger notification
     override val latestCombinedEwPtRiskLevelResults: Flow<List<CombinedEwPtRiskLevelResult>>
         get() = combine(
             latestEwRiskLevelResults,
             latestPtRiskLevelResults
         ) { ewRiskLevelResults, ptRiskLevelResults ->
-            val latestEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt }
-            val latestPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt }
-            val olderEwResult = ewRiskLevelResults.maxByOrNull { it.calculatedAt }
-            val olderPtResult = ptRiskLevelResults.maxByOrNull { it.calculatedAt }
-            val combinedList = mutableListOf<CombinedEwPtRiskLevelResult>()
-            if (latestEwResult != null && latestPtResult != null) {
-                combinedList.add(
-                    CombinedEwPtRiskLevelResult(
-                        ewRiskLevelResult = latestEwResult,
-                        ptRiskLevelResult = latestPtResult
-                    )
-                )
-            }
-            if (olderEwResult != null && olderPtResult != null) {
-                combinedList.add(
-                    CombinedEwPtRiskLevelResult(
-                        ewRiskLevelResult = olderEwResult,
-                        ptRiskLevelResult = olderPtResult
-                    )
-                )
-            }
-            combinedList
+            combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults)
+                .sortedByDescending { it.calculatedAt }
+                .take(2)
         }
 
     internal abstract suspend fun storeExposureWindows(storedResultId: String, resultEw: EwRiskLevelResult)
@@ -266,6 +229,7 @@ abstract class BaseRiskLevelStorage constructor(
     override suspend fun clear() {
         Timber.w("clear() - Clearing stored risklevel/exposure-detection results.")
         database.clearAllTables()
+        presenceTracingRiskRepository.clearAllTables()
     }
 
     companion object {
@@ -284,7 +248,7 @@ internal fun combineRisk(
         val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date }
         CombinedEwPtDayRisk(
             date,
-            max(
+            combine(
                 ptRisk?.riskState,
                 ewRisk?.riskLevel?.mapToRiskState()
             )
@@ -292,7 +256,7 @@ internal fun combineRisk(
     }
 }
 
-internal fun max(left: RiskState?, right: RiskState?): RiskState {
+internal fun combine(left: RiskState?, right: RiskState?): RiskState {
     return if (left == RiskState.INCREASED_RISK || right == RiskState.INCREASED_RISK) RiskState.INCREASED_RISK
     else if (left == RiskState.LOW_RISK || right == RiskState.LOW_RISK) RiskState.LOW_RISK
     else RiskState.CALCULATION_FAILED
@@ -308,3 +272,66 @@ internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? {
     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/RiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt
index 46d4366ef381eaf6cd2d4d992692f80ffebec0d6..a72b458ba00fde938c47d01db704dc33001864d5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt
@@ -1,15 +1,13 @@
 package de.rki.coronawarnapp.risk.storage
 
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk
-import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
+import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
+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.TraceLocationCheckInRisk
+import de.rki.coronawarnapp.risk.LastCombinedRiskResults
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
-import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import kotlinx.coroutines.flow.Flow
-import org.joda.time.Instant
-import org.joda.time.LocalDate
 
 interface RiskLevelStorage {
 
@@ -51,7 +49,7 @@ interface RiskLevelStorage {
      * Can be 0-2 entries.
      * Newest item first.
      */
-    val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<List<CombinedEwPtRiskLevelResult>>
+    val latestAndLastSuccessfulCombinedEwPtRiskLevelResult: Flow<LastCombinedRiskResults>
 
     /** EXPOSURE WINDOW RISK RESULT
      * Risk level per date/day
@@ -82,48 +80,3 @@ interface RiskLevelStorage {
 
     suspend fun clear()
 }
-
-data class CombinedEwPtDayRisk(
-    val localDate: LocalDate,
-    val riskState: RiskState
-)
-
-data class CombinedEwPtRiskLevelResult(
-    val ptRiskLevelResult: PtRiskLevelResult,
-    val ewRiskLevelResult: EwRiskLevelResult
-) {
-
-    val riskState: RiskState = max(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState)
-
-    val wasSuccessfullyCalculated: Boolean
-        get() = ewRiskLevelResult.ewAggregatedRiskResult != null &&
-            ptRiskLevelResult.riskState != RiskState.CALCULATION_FAILED
-
-    val calculatedAt: Instant = max(ewRiskLevelResult.calculatedAt, ptRiskLevelResult.calculatedAt)
-
-    val daysWithEncounters: Int
-        get() = when (riskState) {
-            RiskState.INCREASED_RISK -> {
-                (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0) +
-                    ptRiskLevelResult.numberOfDaysWithHighRisk
-            }
-            RiskState.LOW_RISK -> {
-                (ewRiskLevelResult.ewAggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0) +
-                    ptRiskLevelResult.numberOfDaysWithLowRisk
-            }
-            else -> 0
-        }
-
-    val lastRiskEncounterAt: LocalDate?
-        get() = if (riskState == RiskState.INCREASED_RISK) {
-            max(
-                ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithHighRisk?.toLocalDateUtc(),
-                ptRiskLevelResult.mostRecentDateWithHighRisk
-            )
-        } else {
-            max(
-                ewRiskLevelResult.ewAggregatedRiskResult?.mostRecentDateWithLowRisk?.toLocalDateUtc(),
-                ptRiskLevelResult.mostRecentDateWithLowRisk
-            )
-        }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt
index 016ce630f2d69af7780e42975c20d1a6403692cc..29a64f31616a0e3b6f3e0ee5ca98b6d8b5007a8f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt
@@ -40,7 +40,8 @@ class SubmissionRepository @Inject constructor(
     private val backgroundNoise: BackgroundNoise,
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector,
     private val tracingSettings: TracingSettings,
-    private val testResultDataCollector: TestResultDataCollector
+    private val testResultDataCollector: TestResultDataCollector,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler,
 ) {
     private val testResultReceivedDateFlowInternal =
         MutableStateFlow((submissionSettings.initialTestResultReceivedAt ?: timeStamper.nowUTC).toDate())
@@ -192,7 +193,7 @@ class SubmissionRepository @Inject constructor(
             submissionSettings.initialTestResultReceivedAt = currentTime
             testResultReceivedDateFlowInternal.value = currentTime.toDate()
             if (testResult == TestResult.PENDING) {
-                BackgroundWorkScheduler.startWorkScheduler()
+                backgroundWorkScheduler.startWorkScheduler()
             }
         } else {
             testResultReceivedDateFlowInternal.value = initialTestResultReceivedTimestamp.toDate()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt
index 7265d2bb6e59b24541b4034d7e8b3029d269ef52..a860a2edb3b0e5692ea03bcb94157c091882638f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt
@@ -41,7 +41,8 @@ class SubmissionTask @Inject constructor(
     private val testResultAvailableNotificationService: TestResultAvailableNotificationService,
     private val checkInsRepository: CheckInRepository,
     private val checkInsTransformer: CheckInsTransformer,
-    private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
+    private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler,
 ) : Task<DefaultProgress, SubmissionTask.Result> {
 
     private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>()
@@ -176,9 +177,9 @@ class SubmissionTask @Inject constructor(
 
     private fun setSubmissionFinished() {
         Timber.tag(TAG).d("setSubmissionFinished()")
-        BackgroundWorkScheduler.stopWorkScheduler()
+        backgroundWorkScheduler.stopWorkScheduler()
         submissionSettings.isSubmissionSuccessful = true
-        BackgroundWorkScheduler.startWorkScheduler()
+        backgroundWorkScheduler.startWorkScheduler()
 
         shareTestResultNotificationService.cancelSharePositiveTestResultNotification()
         testResultAvailableNotificationService.cancelTestResultAvailableNotification()
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 c3d9018fc884b95d644643eeb8f0f713e0fcc0c8..5aa675aae2f4b9182fce26433476fb2427f97a6c 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
@@ -8,7 +8,6 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTra
 import de.rki.coronawarnapp.nearby.modules.detectiontracker.latestSubmission
 import de.rki.coronawarnapp.risk.RiskState
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
-import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults
 import de.rki.coronawarnapp.storage.TracingRepository
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus
 import de.rki.coronawarnapp.tracing.TracingProgress
@@ -51,15 +50,13 @@ class TracingStateProvider @AssistedInject constructor(
         latestSubmission,
         isBackgroundJobEnabled ->
 
-        val (
-            latestCalc,
-            latestSuccessfulCalc
-        ) = riskLevelResults.tryLatestResultsWithDefaults()
+        val latestCalc = riskLevelResults.lastCalculated
+        val lastSuccessfullyCalc = riskLevelResults.lastSuccessfullyCalculated
 
         return@combine when {
             tracingStatus == GeneralTracingStatus.Status.TRACING_INACTIVE -> TracingDisabled(
                 isInDetailsMode = isDetailsMode,
-                riskState = latestSuccessfulCalc.riskState,
+                riskState = lastSuccessfullyCalc.riskState,
                 lastExposureDetectionTime = latestSubmission?.startedAt
             )
             tracingProgress != TracingProgress.Idle -> TracingInProgress(
@@ -86,7 +83,7 @@ class TracingStateProvider @AssistedInject constructor(
             )
             else -> TracingFailed(
                 isInDetailsMode = isDetailsMode,
-                riskState = latestSuccessfulCalc.riskState,
+                riskState = lastSuccessfullyCalc.riskState,
                 lastExposureDetectionTime = latestSubmission?.startedAt
             )
         }
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 eba70d84b142b805a168c7253e5c67b18fd0779e..dd52cdbc939d76e6c95e680b4982fe33ebd93500 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
@@ -9,7 +9,6 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys.ConsentResult.AlreadyGiv
 import de.rki.coronawarnapp.datadonation.survey.Surveys.ConsentResult.Needed
 import de.rki.coronawarnapp.risk.RiskState
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
-import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults
 import de.rki.coronawarnapp.storage.TracingRepository
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus
 import de.rki.coronawarnapp.tracing.states.IncreasedRisk
@@ -81,7 +80,7 @@ class TracingDetailsFragmentViewModel @AssistedInject constructor(
         riskLevelResults,
         isBackgroundJobEnabled ->
 
-        val (latestCalc, _) = riskLevelResults.tryLatestResultsWithDefaults()
+        val latestCalc = riskLevelResults.lastCalculated
 
         val isRestartButtonEnabled = !isBackgroundJobEnabled || latestCalc.riskState == RiskState.CALCULATION_FAILED
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt
index f58268644a91e315a23dce10bd13513da1be2568..f47238e681de2d5ec7077d10bf0ba2e1d60aa649 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProvider.kt
@@ -5,7 +5,6 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys
 import de.rki.coronawarnapp.installTime.InstallTimeProvider
 import de.rki.coronawarnapp.risk.RiskState
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
-import de.rki.coronawarnapp.risk.tryLatestResultsWithDefaults
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status
 import de.rki.coronawarnapp.tracing.ui.details.items.DetailsItem
@@ -43,7 +42,7 @@ class TracingDetailsItemProvider @Inject constructor(
         riskLevelResults,
         availableSurveys ->
 
-        val (latestCalc, _) = riskLevelResults.tryLatestResultsWithDefaults()
+        val latestCalc = riskLevelResults.lastCalculated
 
         mutableListOf<DetailsItem>().apply {
             if (status != Status.TRACING_INACTIVE &&
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt
index 3628d6addafda4ec484a6d988aa7a0142a4d3ab8..574698d978b25369e7610465708645e40beb0f21 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/items/riskdetails/DetailsLowRiskBox.kt
@@ -50,6 +50,7 @@ class DetailsLowRiskBox(
     ) : RiskDetailsStateItem {
 
         fun getRiskDetailsRiskLevelBody(c: Context): String {
+            // TODO consider pt encounters?
             return c.getString(
                 if (matchedKeyCount > 0) R.string.risk_details_information_body_low_risk_with_encounter
                 else R.string.risk_details_information_body_low_risk
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt
index 76af2958e4c24c74ad19e65dc40976470bb64161..571ae77c03533982ee966b7edbd1709094b7a9fe 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/settings/SettingsTracingFragmentViewModel.kt
@@ -34,7 +34,8 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
     tracingStatus: GeneralTracingStatus,
     installTimeProvider: InstallTimeProvider,
     private val backgroundStatus: BackgroundModeStatus,
-    tracingPermissionHelperFactory: TracingPermissionHelper.Factory
+    tracingPermissionHelperFactory: TracingPermissionHelper.Factory,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val loggingPeriod: LiveData<PeriodLoggedBox.Item> =
@@ -75,7 +76,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
                             if (!backgroundStatus.isIgnoringBatteryOptimizations.first()) {
                                 events.postValue(Event.ManualCheckingDialog)
                             }
-                            BackgroundWorkScheduler.startWorkScheduler()
+                            backgroundWorkScheduler.startWorkScheduler()
                         }
                         isTracingSwitchChecked.postValue(isTracingEnabled)
                     }
@@ -109,7 +110,7 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor(
                 launch {
                     if (InternalExposureNotificationClient.asyncIsEnabled()) {
                         InternalExposureNotificationClient.asyncStop()
-                        BackgroundWorkScheduler.stopWorkScheduler()
+                        backgroundWorkScheduler.stopWorkScheduler()
                     }
                 }
             }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3ae8e15318e8231b5d52e460ce4227db18d75817
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/color/Color.kt
@@ -0,0 +1,17 @@
+package de.rki.coronawarnapp.ui.color
+
+import android.graphics.Color
+import androidx.annotation.ColorInt
+import timber.log.Timber
+
+/**
+ * Parse color from String - default color is a fallback
+ * @param defaultColor [Int] color
+ */
+fun String.parseColor(@ColorInt defaultColor: Int = Color.BLACK): Int =
+    try {
+        Color.parseColor(this)
+    } catch (e: Exception) {
+        Timber.d(e, "Parsing color failed")
+        defaultColor
+    }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt
index 8840ff0adfeffac890939bad167637d87be50db1..e6f0123e69c4734517875010ea148e6006d78206 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt
@@ -6,10 +6,10 @@ import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsFragm
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.CheckInsModule
 import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInFragment
 import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInModule
-import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment
-import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingModule
 import de.rki.coronawarnapp.ui.eventregistration.attendee.edit.EditCheckInFragment
 import de.rki.coronawarnapp.ui.eventregistration.attendee.edit.EditCheckInModule
+import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment
+import de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingModule
 import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeFragment
 import de.rki.coronawarnapp.ui.eventregistration.attendee.scan.ScanCheckInQrCodeModule
 import de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment
@@ -18,6 +18,8 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationC
 import de.rki.coronawarnapp.ui.eventregistration.organizer.create.TraceLocationCreateFragmentModule
 import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragment
 import de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragmentModule
+import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment
+import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragmentModule
 import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment
 import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragmentModule
 
@@ -50,4 +52,7 @@ internal abstract class EventRegistrationUIModule {
 
     @ContributesAndroidInjector(modules = [TraceLocationsFragmentModule::class])
     abstract fun traceLocationsFragment(): TraceLocationsFragment
+
+    @ContributesAndroidInjector(modules = [QrCodePosterFragmentModule::class])
+    abstract fun qrCodePosterFragment(): QrCodePosterFragment
 }
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 eaa1800739549ee6f5c44a1a7331dc3e6d91a371..b39c06703c9b30c4dd27fd5f0a4802e59f545375 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
@@ -12,7 +12,6 @@ import androidx.appcompat.widget.Toolbar
 import androidx.core.net.toUri
 import androidx.core.view.isGone
 import androidx.fragment.app.Fragment
-import androidx.navigation.NavOptions
 import androidx.navigation.fragment.FragmentNavigatorExtras
 import androidx.navigation.fragment.findNavController
 import androidx.navigation.fragment.navArgs
@@ -24,7 +23,6 @@ import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsFragmentBin
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem
-import de.rki.coronawarnapp.util.CWADebug
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.list.isSwipeable
 import de.rki.coronawarnapp.util.list.onSwipeItem
@@ -38,6 +36,8 @@ 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
 
 class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_fragment), AutoInject {
@@ -149,18 +149,6 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
                     FragmentNavigatorExtras(this to transitionName)
                 )
             }
-            // TODO Remove once feature is done
-            if (CWADebug.isDeviceForTestersBuild) {
-                setOnLongClickListener {
-                    findNavController().navigate(
-                        createCheckInUri(DEBUG_CHECKINS.random()),
-                        NavOptions.Builder().apply {
-                            setLaunchSingleTop(true)
-                        }.build()
-                    )
-                    true
-                }
-            }
         }
     }
 
@@ -224,15 +212,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag
     }
 
     companion object {
-        fun createCheckInUri(rootUri: String): Uri = "coronawarnapp://check-ins/$rootUri".toUri()
-
-        @Suppress("MaxLineLength")
-        private val DEBUG_CHECKINS = listOf(
-            "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDFMNSTKMJYGY3S2NJYHA4S2NBRG5QS2YLGMM3C2ZDDHFRTSNRSGZTGIYZWCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEBFRIDICXSP4QTNMBRDF7EOJ3EIJD6AWT24YDOWWXQI22KCUD7R7WARBAC7ONBRPJDB2KK6QKZLF4RE3PXU7PMON4IOZVIHCYPJGBZ27FF5S4",
-            "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDEMVRDGZTGMU2C2MZUGQ2C2NBWGZQS2YLCHEYC2NJQHBRDCMBRMVTDIZBTCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEAJRWAYJARF3V4AS5OVBODPLPX2V3IJFMFU4O2CAKRH6HGHHWCDMJYCEBH7BO2IU2EEGRKEXBZT2DAOFIMXES5ETUT45QIWDCX64APY7C2ME",
-            "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDBMNQWIMLFHA3S2NZQGVTC2NDDGY3C2ODGGBTC2ZBWGQYDCZJUMRTDEN3FCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEAKJM3RPYMM2VVCE2GLVK6OKY36F64FRNSI6DWYV7WW6MGESFCDNNQCEA44UHS2GEWHJYHTIJ3AJYM6BC3HEIYHY2HRMPIP7ZF62YBAUKOIY",
-            "HTTPS://E.CORONAWARN.APP/C1/BJHAUJBQGZRDOMJXHEYC2NBRG44C2NBWGZRC2YRQGA4S2MJTGRRDQOJZMU4DMMRQCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEB365TX5SEWICC3JUOAZCQX5YUK2LZZA7RGRTNBXTSEBXTD2766CAARBADXEYUJHQSE7QRQOIPEMSSPLCVC5D4I3FOBDRX64NASE47XKKK5EY",
-            "HTTPS://E.CORONAWARN.APP/C1/BJHAUJDGGE4WKMTEMQZC2OJRMUYC2NBQGNTC2OJZMZRC2MTEG4ZWGMJTGA3GEOBTCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEEANT4HDNB7V5DWCKKUV22YQ7NYOBCTOZ2QUFBOUDZS6V5J2VRVLVSICEBU2YHAEBPQSLWTR75VFC6OEFIS22V6KU4NRDYZHTIBMHS4FDADG6",
-        )
+        fun createCheckInUri(rootUri: String): 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()
+        }
     }
 }
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 dbfafa23aac8f2e6410cc218b13eceb6d12170c5..e78ae163bec10c1d32b585fc1482ed5ea204597f 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
@@ -8,13 +8,11 @@ import dagger.assisted.AssistedFactory
 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.InvalidQRCodeDataException
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 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
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CheckInsItem
@@ -138,15 +136,7 @@ class CheckInsViewModel @AssistedInject constructor(
     private fun verifyUri(uri: String) = launch {
         try {
             Timber.i("uri: $uri")
-            val qrCodePayloadRaw = qrCodeUriParser.getQrCodePayload(uri)?.toByteArray()
-                ?: throw IllegalArgumentException("Invalid uri: $uri")
-
-            val qrCodePayload = try {
-                TraceLocationOuterClass.QRCodePayload.parseFrom(qrCodePayloadRaw)
-            } catch (e: Exception) {
-                throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.")
-            }
-
+            val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri)
             val verifiedTraceLocation = VerifiedTraceLocation(qrCodePayload)
             events.postValue(CheckInEvent.ConfirmCheckIn(verifiedTraceLocation))
         } catch (e: Exception) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt
index bd50ffcbe58afd1376da965d07776a3d8d8d0864..840682e97c68268d05e0cfc5ce38376f7c1a3eba 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/confirm/ConfirmCheckInViewModel.kt
@@ -88,23 +88,25 @@ class ConfirmCheckInViewModel @AssistedInject constructor(
         ),
         completed: Boolean = false,
         createJournalEntry: Boolean = true
-    ): CheckIn = CheckIn(
-        traceLocationId = verifiedTraceLocation.traceLocationID,
-        traceLocationIdHash = verifiedTraceLocation.traceLocationID.sha256(),
-        version = traceLocation.version,
-        type = traceLocation.type.number,
-        description = traceLocation.description,
-        address = traceLocation.address,
-        traceLocationStart = traceLocation.startDate,
-        traceLocationEnd = traceLocation.endDate,
-        defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes,
-        cryptographicSeed = traceLocation.cryptographicSeed,
-        cnPublicKey = traceLocation.cnPublicKey,
-        checkInStart = checkInStart,
-        checkInEnd = checkInEnd,
-        completed = completed,
-        createJournalEntry = createJournalEntry,
-    )
+    ): CheckIn {
+        val traceLocation = verifiedTraceLocation.traceLocation
+        return CheckIn(
+            traceLocationId = traceLocation.locationId,
+            version = traceLocation.version,
+            type = traceLocation.type.number,
+            description = traceLocation.description,
+            address = traceLocation.address,
+            traceLocationStart = traceLocation.startDate,
+            traceLocationEnd = traceLocation.endDate,
+            defaultCheckInLengthInMinutes = traceLocation.defaultCheckInLengthInMinutes,
+            cryptographicSeed = traceLocation.cryptographicSeed,
+            cnPublicKey = traceLocation.cnPublicKey,
+            checkInStart = checkInStart,
+            checkInEnd = checkInEnd,
+            completed = completed,
+            createJournalEntry = createJournalEntry,
+        )
+    }
 
     @AssistedFactory
     interface Factory : CWAViewModelFactory<ConfirmCheckInViewModel> {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt
index a300e882482f4a88c8bbdb9c2b5ea7fe924dbd68..bd2763bc00c30b84cdc58a650948906d7fa478dd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailFragment.kt
@@ -11,10 +11,12 @@ import androidx.fragment.app.Fragment
 import androidx.navigation.fragment.navArgs
 import com.google.android.material.appbar.AppBarLayout
 import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener
+import com.google.android.material.transition.MaterialContainerTransform
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.TraceLocationOrganizerQrCodeDetailFragmentBinding
 import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat
 import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.ui.doNavigate
 import de.rki.coronawarnapp.util.ui.observe2
 import de.rki.coronawarnapp.util.ui.popBackStack
 import de.rki.coronawarnapp.util.ui.viewBindingLazy
@@ -29,7 +31,7 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_
 
     private val navArgs by navArgs<QrCodeDetailFragmentArgs>()
 
-    private val vm: QrCodeDetailViewModel by cwaViewModelsAssisted(
+    private val viewModel: QrCodeDetailViewModel by cwaViewModelsAssisted(
         factoryProducer = { viewModelFactory },
         constructorCall = { factory, _ ->
             factory as QrCodeDetailViewModel.Factory
@@ -39,6 +41,13 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_
 
     private val binding: TraceLocationOrganizerQrCodeDetailFragmentBinding by viewBindingLazy()
 
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        sharedElementEnterTransition = MaterialContainerTransform()
+        sharedElementReturnTransition = MaterialContainerTransform()
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
@@ -59,11 +68,16 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_
             toolbar.apply {
                 navigationIcon = context.getDrawableCompat(R.drawable.ic_close_white)
                 navigationContentDescription = getString(R.string.accessibility_close)
-                setNavigationOnClickListener { vm.onBackButtonPress() }
+                setNavigationOnClickListener { viewModel.onBackButtonPress() }
+            }
+
+            qrCodePrintButton.setOnClickListener {
+                viewModel.onPrintQrCode()
             }
         }
 
-        vm.qrCodeBitmap.observe2(this) {
+        viewModel.qrCodeBitmap.observe2(this) {
+            binding.progressBar.hide()
             binding.qrCodeImage.apply {
                 val resourceId = RoundedBitmapDrawableFactory.create(resources, it)
                 resourceId.cornerRadius = it.width * 0.1f
@@ -71,19 +85,20 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_
             }
         }
 
-        vm.routeToScreen.observe2(this) {
+        viewModel.routeToScreen.observe2(this) {
             when (it) {
-                QrCodeDetailNavigationEvents.NavigateBack -> {
-                    popBackStack()
-                }
-                QrCodeDetailNavigationEvents.NavigateToPrintFragment -> { /* TODO */
-                }
+                QrCodeDetailNavigationEvents.NavigateBack -> popBackStack()
+
                 QrCodeDetailNavigationEvents.NavigateToDuplicateFragment -> { /* TODO */
                 }
+
+                is QrCodeDetailNavigationEvents.NavigateToQrCodePosterFragment -> doNavigate(
+                    QrCodeDetailFragmentDirections.actionQrCodeDetailFragmentToQrCodePosterFragment(it.locationId)
+                )
             }
         }
 
-        vm.uiState.observe2(this) { uiState ->
+        viewModel.uiState.observe2(this) { uiState ->
             with(binding) {
                 title.text = uiState.description
                 subtitle.text = uiState.address
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt
index b91b99b27a5868544414d5093dc60311e97a2244..68027a1fd55345d94dd670307491137c2f9b8a04 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailNavigationEvents.kt
@@ -2,6 +2,6 @@ package de.rki.coronawarnapp.ui.eventregistration.organizer.details
 
 sealed class QrCodeDetailNavigationEvents {
     object NavigateBack : QrCodeDetailNavigationEvents()
-    object NavigateToPrintFragment : QrCodeDetailNavigationEvents()
+    data class NavigateToQrCodePosterFragment(val locationId: Long) : QrCodeDetailNavigationEvents()
     object NavigateToDuplicateFragment : QrCodeDetailNavigationEvents()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt
index d9a58e3871a4c6ce7db771476f2ca273b5683b00..739078b1e3d5e2716236e7fca68f3bf355c1d141 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/details/QrCodeDetailViewModel.kt
@@ -1,12 +1,17 @@
 package de.rki.coronawarnapp.ui.eventregistration.organizer.details
 
 import android.graphics.Bitmap
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.asLiveData
 import dagger.assisted.Assisted
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
 import de.rki.coronawarnapp.eventregistration.storage.repo.DefaultTraceLocationRepository
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -18,7 +23,7 @@ import org.joda.time.Instant
 import timber.log.Timber
 
 class QrCodeDetailViewModel @AssistedInject constructor(
-    @Assisted private val traceLocationId: Long?,
+    @Assisted private val traceLocationId: Long,
     private val dispatcher: DispatcherProvider,
     private val qrCodeGenerator: QrCodeGenerator,
     private val traceLocationRepository: DefaultTraceLocationRepository
@@ -29,11 +34,12 @@ class QrCodeDetailViewModel @AssistedInject constructor(
     private val subtitleFlow = MutableStateFlow<String?>(null)
     private val startTimeFlow = MutableStateFlow<Instant?>(null)
     private val endTimeFlow = MutableStateFlow<Instant?>(null)
+    private val bitmapLiveData = MutableLiveData<Bitmap>()
 
     init {
 
         launch {
-            val traceLocation = traceLocationRepository.traceLocationForId(traceLocationId ?: 0L)
+            val traceLocation = traceLocationRepository.traceLocationForId(traceLocationId)
 
             if (titleFlow.value == null) {
                 titleFlow.value = traceLocation.description
@@ -53,11 +59,7 @@ class QrCodeDetailViewModel @AssistedInject constructor(
 
             traceLocationFlow.value = traceLocation
 
-            createQrCode(
-                "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUD" +
-                    "BOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFF" +
-                    "BU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
-            )
+            createQrCode(traceLocation)
         }
     }
 
@@ -84,20 +86,20 @@ class QrCodeDetailViewModel @AssistedInject constructor(
         val endDateTime: Instant? get() = endInstant
     }
 
-    val qrCodeBitmap = SingleLiveEvent<Bitmap>()
-    val errorMessage = SingleLiveEvent<String>()
+    val qrCodeBitmap: LiveData<Bitmap> = bitmapLiveData
     val routeToScreen: SingleLiveEvent<QrCodeDetailNavigationEvents> = SingleLiveEvent()
 
     /**
      * Creates a QR Code [Bitmap] ,result is delivered by [qrCodeBitmap]
      */
-    private fun createQrCode(input: String) = launch(context = dispatcher.IO) {
-
+    private fun createQrCode(traceLocation: TraceLocation) = launch(context = dispatcher.IO) {
         try {
-            qrCodeBitmap.postValue(qrCodeGenerator.createQrCode(input))
+            val input = traceLocation.locationUrl
+            Timber.d("input=$input")
+            bitmapLiveData.postValue(qrCodeGenerator.createQrCode(input))
         } catch (e: Exception) {
             Timber.d(e, "Qr code creation failed")
-            errorMessage.postValue(e.localizedMessage ?: "QR code creation failed")
+            e.report(ExceptionCategory.INTERNAL)
         }
     }
 
@@ -105,10 +107,16 @@ class QrCodeDetailViewModel @AssistedInject constructor(
         routeToScreen.postValue(QrCodeDetailNavigationEvents.NavigateBack)
     }
 
+    fun onPrintQrCode() {
+        routeToScreen.postValue(
+            QrCodeDetailNavigationEvents.NavigateToQrCodePosterFragment(traceLocationId)
+        )
+    }
+
     @AssistedFactory
     interface Factory : CWAViewModelFactory<QrCodeDetailViewModel> {
         fun create(
-            traceLocationId: Long?
+            traceLocationId: Long
         ): QrCodeDetailViewModel
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt
index d66aa6361215365a3402c5b72f4052ed329db2ca..5909c982933269feaeeb47a6fa3d1f3aebc0e2f8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationEvent.kt
@@ -10,5 +10,7 @@ sealed class TraceLocationEvent {
 
     data class ConfirmSwipeItem(val traceLocation: TraceLocation, val position: Int) : TraceLocationEvent()
 
-    data class StartQrCodeDetailFragment(val id: Long) : TraceLocationEvent()
+    data class StartQrCodeDetailFragment(val id: Long, val position: Int) : TraceLocationEvent()
+
+    data class StartQrCodePosterFragment(val traceLocation: TraceLocation) : TraceLocationEvent()
 }
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 f9edf14cac9155606064e68f1d6e5564209f8ba3..1847e8f7b3912ec7198198b0986705753f2e1952 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
@@ -14,6 +14,7 @@ import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.TraceLocationOrganizerTraceLocationsListFragmentBinding
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
 import de.rki.coronawarnapp.ui.eventregistration.organizer.category.adapter.category.traceLocationCategories
+import de.rki.coronawarnapp.ui.eventregistration.organizer.details.QrCodeDetailFragmentArgs
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.di.AutoInject
 import de.rki.coronawarnapp.util.list.isSwipeable
@@ -60,7 +61,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_
             }
         }
 
-        binding.toolbar.setOnClickListener {
+        binding.toolbar.setNavigationOnClickListener {
             popBackStack()
         }
 
@@ -81,22 +82,36 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_
                     showDeleteSingleDialog(it.traceLocation, it.position)
                 }
                 is TraceLocationEvent.StartQrCodeDetailFragment -> {
-                    doNavigate(
-                        TraceLocationsFragmentDirections.actionTraceLocationOrganizerListFragmentToQrCodeDetailFragment(
-                            traceLocationId = it.id,
-                        )
+
+                    val navigatorExtras = binding.recyclerView.findViewHolderForAdapterPosition(it.position)?.itemView
+                        ?.run {
+                            // Set it on the fly to avoid confusion of recycler's items
+                            this.transitionName = "trace_location_container_transition"
+                            FragmentNavigatorExtras(this to this.transitionName)
+                        }
+
+                    findNavController().navigate(
+                        R.id.action_traceLocationsFragment_to_qrCodeDetailFragment,
+                        QrCodeDetailFragmentArgs(traceLocationId = it.id).toBundle(),
+                        null,
+                        navigatorExtras
                     )
                 }
                 is TraceLocationEvent.DuplicateItem -> {
                     openCreateEventFragment(it.traceLocation)
                 }
+                is TraceLocationEvent.StartQrCodePosterFragment -> doNavigate(
+                    TraceLocationsFragmentDirections.actionTraceLocationsFragmentToQrCodePosterFragment(
+                        it.traceLocation.id
+                    )
+                )
             }
         }
 
         binding.qrCodeFab.apply {
             setOnClickListener {
                 findNavController().navigate(
-                    R.id.action_traceLocationOrganizerListFragment_to_traceLocationOrganizerCategoriesFragment,
+                    R.id.action_traceLocationsFragment_to_traceLocationCategoryFragment,
                     null,
                     null,
                     FragmentNavigatorExtras(this to transitionName)
@@ -116,7 +131,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_
             when (it.itemId) {
                 R.id.menu_information -> {
                     findNavController().navigate(
-                        R.id.action_traceLocationOrganizerListFragment_to_traceLocationOrganizerQRInfoFragment
+                        R.id.action_traceLocationOrganizerListFragment_to_traceLocationInfoFragment
                     )
                     true
                 }
@@ -149,11 +164,10 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_
             Timber.e("Category not found, traceLocation = $traceLocation")
         } else {
             findNavController().navigate(
-                TraceLocationsFragmentDirections
-                    .actionTraceLocationOrganizerListFragmentToTraceLocationCreateFragment(
-                        category,
-                        traceLocation
-                    )
+                TraceLocationsFragmentDirections.actionTraceLocationsFragmentToTraceLocationCreateFragment(
+                    category,
+                    traceLocation
+                )
             )
         }
     }
@@ -166,18 +180,10 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_
                 viewModel.deleteSingleTraceLocation(traceLocation)
             }
             setNegativeButton(R.string.trace_location_organiser_list_delete_all_popup_negative_button) { _, _ ->
-                position?.let {
-                    traceLocationsAdapter.notifyItemChanged(
-                        position
-                    )
-                }
+                position?.let { traceLocationsAdapter.notifyItemChanged(position) }
             }
             setOnCancelListener {
-                position?.let {
-                    traceLocationsAdapter.notifyItemChanged(
-                        position
-                    )
-                }
+                position?.let { traceLocationsAdapter.notifyItemChanged(position) }
             }
         }.show()
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt
index 8c57430618bbc290e76ab55e63b36da8264db358..d4167ccd56634c989b0e015f00cffb71b9aeebaa 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsViewModel.kt
@@ -35,14 +35,18 @@ class TraceLocationsViewModel @AssistedInject constructor(
                     traceLocation = traceLocation,
                     onCheckIn = { /* TODO */ },
                     onDuplicate = { events.postValue(TraceLocationEvent.DuplicateItem(it)) },
-                    onShowPrint = { /* TODO */ },
-                    onClearItem = { events.postValue(TraceLocationEvent.ConfirmDeleteItem(it)) },
-                    onSwipeItem = { traceLocation, position ->
+                    onShowPrint = { events.postValue(TraceLocationEvent.StartQrCodePosterFragment(it)) },
+                    onDeleteItem = { events.postValue(TraceLocationEvent.ConfirmDeleteItem(it)) },
+                    onSwipeItem = { location, position ->
                         events.postValue(
-                            TraceLocationEvent.ConfirmSwipeItem(traceLocation, position)
+                            TraceLocationEvent.ConfirmSwipeItem(location, position)
+                        )
+                    },
+                    onCardClicked = { traceLocation, position ->
+                        events.postValue(
+                            TraceLocationEvent.StartQrCodeDetailFragment(traceLocation.id, position)
                         )
                     },
-                    onCardClicked = { events.postValue(TraceLocationEvent.StartQrCodeDetailFragment(it.id)) },
                 )
             }
         }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt
index 8cc28cf70dd8585d2c5f46e3e5df50f371388a26..da8cca4747fd735671ea02fdfed288116addb87a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/items/TraceLocationVH.kt
@@ -57,14 +57,13 @@ class TraceLocationVH(parent: ViewGroup) :
             when (it.itemId) {
                 R.id.menu_duplicate -> item.onDuplicate(item.traceLocation).let { true }
                 R.id.menu_show_print -> item.onShowPrint(item.traceLocation).let { true }
-                R.id.menu_clear -> item.onClearItem(item.traceLocation).let { true }
+                R.id.menu_clear -> item.onDeleteItem(item.traceLocation).let { true }
                 else -> false
             }
         }
 
         checkinAction.setOnClickListener { item.onCheckIn(item.traceLocation) }
-
-        itemView.setOnClickListener { item.onCardClicked(item.traceLocation) }
+        itemView.setOnClickListener { item.onCardClicked(item.traceLocation, adapterPosition) }
     }
 
     data class Item(
@@ -72,9 +71,9 @@ class TraceLocationVH(parent: ViewGroup) :
         val onCheckIn: (TraceLocation) -> Unit,
         val onDuplicate: (TraceLocation) -> Unit,
         val onShowPrint: (TraceLocation) -> Unit,
-        val onClearItem: (TraceLocation) -> Unit,
+        val onDeleteItem: (TraceLocation) -> Unit,
         val onSwipeItem: (TraceLocation, Int) -> Unit,
-        val onCardClicked: (TraceLocation) -> Unit
+        val onCardClicked: (TraceLocation, Int) -> Unit
     ) : TraceLocationItem, SwipeConsumer {
         override val stableId: Long = traceLocation.id.hashCode().toLong()
         override fun onSwipe(position: Int, direction: Int) = onSwipeItem(traceLocation, position)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b5840444875465bb97ae4112c546b037c8686b89
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragment.kt
@@ -0,0 +1,156 @@
+package de.rki.coronawarnapp.ui.eventregistration.organizer.poster
+
+import android.os.Bundle
+import android.print.PrintAttributes
+import android.print.PrintManager
+import android.util.TypedValue
+import android.view.View
+import android.view.ViewTreeObserver.OnGlobalLayoutListener
+import android.widget.Toast
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.getSystemService
+import androidx.core.widget.TextViewCompat
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.navArgs
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.QrCodePosterFragmentBinding
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid
+import de.rki.coronawarnapp.ui.color.parseColor
+import de.rki.coronawarnapp.ui.print.PrintingAdapter
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.files.FileSharing
+import de.rki.coronawarnapp.util.ui.popBackStack
+import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
+import timber.log.Timber
+import java.io.File
+import javax.inject.Inject
+
+class QrCodePosterFragment : Fragment(R.layout.qr_code_poster_fragment), AutoInject {
+
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+
+    private val args by navArgs<QrCodePosterFragmentArgs>()
+    private val viewModel: QrCodePosterViewModel by cwaViewModelsAssisted(
+        factoryProducer = { viewModelFactory },
+        constructorCall = { factory, _ ->
+            factory as QrCodePosterViewModel.Factory
+            factory.create(args.traceLocationId)
+        }
+    )
+
+    private val binding: QrCodePosterFragmentBinding by viewBindingLazy()
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        with(binding) {
+            toolbar.setNavigationOnClickListener { popBackStack() }
+            viewModel.poster.observe(viewLifecycleOwner) { poster ->
+                bindPoster(poster)
+                // Avoid creating blank PDF
+                if (poster.hasImages()) onPosterDrawn()
+            }
+        }
+
+        viewModel.sharingIntent.observe(viewLifecycleOwner) {
+            onShareIntent(it)
+        }
+    }
+
+    private fun QrCodePosterFragmentBinding.bindPoster(poster: Poster) {
+        Timber.d("poster=$poster")
+        progressBar.hide()
+
+        val template = poster.template ?: return // Exit early
+        Timber.d("template=$template")
+
+        // Adjust poster image dimensions ratio to have a proper printing preview
+        val posterLayoutParam = posterImage.layoutParams as ConstraintLayout.LayoutParams
+        val dimensionRatio = template.run { "$width:$height" } // W:H
+        Timber.d("dimensionRatio=$dimensionRatio")
+        posterLayoutParam.dimensionRatio = dimensionRatio
+
+        // Display images
+        qrCodeImage.setImageBitmap(poster.qrCode)
+        posterImage.setImageBitmap(template.bitmap)
+
+        // Position QR Code image based on data provided by server
+        topGuideline.setGuidelinePercent(template.offsetY)
+        startGuideline.setGuidelinePercent(template.offsetX)
+        endGuideline.setGuidelinePercent(1 - template.offsetX)
+
+        // Bind text info
+        bindTextBox(poster.infoText, poster.template.textBox)
+    }
+
+    private fun onPosterDrawn() = with(binding.qrCodePoster) {
+        viewTreeObserver.addOnGlobalLayoutListener(
+            object : OnGlobalLayoutListener {
+                override fun onGlobalLayout() {
+                    viewModel.createPDF(binding.qrCodePoster)
+                    viewTreeObserver.removeOnGlobalLayoutListener(this)
+                }
+            }
+        )
+    }
+
+    private fun QrCodePosterFragmentBinding.bindTextBox(
+        infoText: String,
+        textBox: QRCodeTextBoxAndroid
+    ) = with(infoTextView) {
+        text = infoText
+        val minFontSize = textBox.fontSize - 6
+        val maxFontSize = textBox.fontSize
+        TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
+            infoTextView,
+            minFontSize,
+            maxFontSize,
+            1,
+            TypedValue.COMPLEX_UNIT_SP
+        )
+        setTextSize(TypedValue.COMPLEX_UNIT_SP, maxFontSize.toFloat())
+        setTextColor(textBox.fontColor.parseColor())
+        textEndGuideline.setGuidelinePercent(1 - textBox.offsetX)
+        textStartGuideline.setGuidelinePercent(textBox.offsetX)
+        textTopGuideline.setGuidelinePercent(textBox.offsetY)
+        // TODO setTypeface()
+    }
+
+    private fun onShareIntent(fileIntent: FileSharing.FileIntentProvider) {
+        binding.toolbar.setOnMenuItemClickListener {
+            when (it.itemId) {
+                R.id.action_print -> printFile(fileIntent.file).run { true }
+                R.id.action_share -> startActivity(fileIntent.intent(requireActivity())).run { true }
+                else -> false
+            }
+        }
+    }
+
+    private fun printFile(file: File) {
+        val printingManger = context?.getSystemService<PrintManager>()
+        Timber.i("PrintingManager=$printingManger")
+        if (printingManger == null) {
+            Toast.makeText(requireContext(), R.string.errors_generic_headline, Toast.LENGTH_LONG).show()
+            return
+        }
+
+        try {
+            val job = printingManger.print(
+                getString(R.string.app_name),
+                PrintingAdapter(file),
+                PrintAttributes.Builder()
+                    .setMediaSize(PrintAttributes.MediaSize.ISO_A3)
+                    .build()
+            )
+
+            Timber.d("JobState=%s", job.info.state)
+        } catch (e: Exception) {
+            Timber.d(e, "Printing job failed")
+            e.report(ExceptionCategory.INTERNAL)
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt
similarity index 56%
rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt
index 33fa4c48d9b99cc6df1ef0c3801310a0dccda239..a64e78afba4fc2e0735838b97974b6c494306f97 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/qrcode/QrCodeCreationTestFragmentModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterFragmentModule.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.test.eventregistration.ui.qrcode
+package de.rki.coronawarnapp.ui.eventregistration.organizer.poster
 
 import dagger.Binds
 import dagger.Module
@@ -8,11 +8,11 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
 
 @Module
-abstract class QrCodeCreationTestFragmentModule {
+abstract class QrCodePosterFragmentModule {
     @Binds
     @IntoMap
-    @CWAViewModelKey(QrCodeCreationTestViewModel::class)
-    abstract fun qrCodeCreation(
-        factory: QrCodeCreationTestViewModel.Factory
+    @CWAViewModelKey(QrCodePosterViewModel::class)
+    abstract fun qrCodePosterFragment(
+        factory: QrCodePosterViewModel.Factory
     ): CWAViewModelFactory<out CWAViewModel>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3a5a36a36e0e9ad166cd5b192aecaf9117572998
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModel.kt
@@ -0,0 +1,119 @@
+package de.rki.coronawarnapp.ui.eventregistration.organizer.poster
+
+import android.graphics.Bitmap
+import android.graphics.pdf.PdfDocument
+import android.view.View
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.PosterTemplateProvider
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.Template
+import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.files.FileSharing
+import de.rki.coronawarnapp.util.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import timber.log.Timber
+import java.io.File
+import java.io.FileOutputStream
+import java.lang.ref.WeakReference
+
+class QrCodePosterViewModel @AssistedInject constructor(
+    @Assisted private val traceLocationId: Long,
+    private val dispatcher: DispatcherProvider,
+    private val qrCodeGenerator: QrCodeGenerator,
+    private val posterTemplateProvider: PosterTemplateProvider,
+    private val traceLocationRepository: TraceLocationRepository,
+    private val fileSharing: FileSharing
+) : CWAViewModel(dispatcher) {
+
+    private val posterLiveData = MutableLiveData<Poster>()
+    val poster: LiveData<Poster> = posterLiveData
+    val sharingIntent = SingleLiveEvent<FileSharing.FileIntentProvider>()
+
+    init {
+        generatePoster()
+    }
+
+    /**
+     * Create a new PDF file and result is delivered by [sharingIntent]
+     * as a sharing [FileSharing.ShareIntentProvider]
+     */
+    @Suppress("BlockingMethodInNonBlockingContext")
+    fun createPDF(view: View) = launch(context = dispatcher.IO) {
+        try {
+            val weakViewRef = WeakReference(view) // Accessing view in background thread
+            val directory = File(view.context.cacheDir, "poster").apply { if (!exists()) mkdirs() }
+            val file = File(directory, "cwa-qr-code.pdf")
+
+            val weakView = weakViewRef.get() ?: return@launch // View is not existing anymore
+            val pageInfo = PdfDocument.PageInfo.Builder(weakView.width, weakView.height, 1).create()
+
+            PdfDocument().apply {
+                startPage(pageInfo).apply {
+                    weakView.draw(canvas)
+                    finishPage(this)
+                }
+
+                FileOutputStream(file).use {
+                    writeTo(it)
+                    close()
+                }
+            }
+
+            sharingIntent.postValue(fileSharing.getFileIntentProvider(file, traceLocation().description))
+        } catch (e: Exception) {
+            Timber.d(e, "Creating pdf failed")
+            e.report(ExceptionCategory.INTERNAL)
+        }
+    }
+
+    private fun generatePoster() = launch(context = dispatcher.IO) {
+        try {
+            val traceLocation = traceLocation()
+            val template = posterTemplateProvider.template()
+            Timber.d("template=$template")
+            val qrCode = qrCodeGenerator.createQrCode(
+                input = traceLocation.locationUrl,
+                length = template.qrCodeLength,
+                margin = 0
+            )
+
+            val textInfo = buildString {
+                append(traceLocation.description)
+                appendLine()
+                append(traceLocation.address)
+            }
+            posterLiveData.postValue(
+                Poster(qrCode, template, textInfo)
+            )
+        } catch (e: Exception) {
+            Timber.d(e, "Generating poster failed")
+            posterLiveData.postValue(Poster())
+            e.report(ExceptionCategory.INTERNAL)
+        }
+    }
+
+    private suspend fun traceLocation() = traceLocationRepository.traceLocationForId(traceLocationId)
+
+    @AssistedFactory
+    interface Factory : CWAViewModelFactory<QrCodePosterViewModel> {
+        fun create(
+            traceLocationId: Long
+        ): QrCodePosterViewModel
+    }
+}
+
+data class Poster(
+    val qrCode: Bitmap? = null,
+    val template: Template? = null,
+    val infoText: String = ""
+) {
+    fun hasImages(): Boolean = qrCode != null && template?.bitmap != null
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
index 8722443459668785f042e1fa85b15038a8aae2b2..798abdfcc35bce36c4b9e698e84572ffdb6191d3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
@@ -80,6 +80,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
     @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler
     @Inject lateinit var dataDonationAnalyticsScheduler: DataDonationAnalyticsScheduler
     @Inject lateinit var submissionSettings: SubmissionSettings
+    @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
 
     override fun onCreate(savedInstanceState: Bundle?) {
         AppInjector.setup(this)
@@ -117,6 +118,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
                 if (count > 0) {
                     val badge = getOrCreateBadge(targetId)
                     badge.number = count
+                    badge.badgeTextColor = getColor(android.R.color.white)
                 } else {
                     removeBadge(targetId)
                 }
@@ -189,7 +191,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
      */
     override fun onResume() {
         super.onResume()
-        scheduleWork()
+        backgroundWorkScheduler.startWorkScheduler()
         vm.doBackgroundNoiseCheck()
         contactDiaryWorkScheduler.schedulePeriodic()
         dataDonationAnalyticsScheduler.schedulePeriodic()
@@ -270,9 +272,4 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
     fun goBack() {
         onBackPressed()
     }
-
-    /**
-     * Scheduling for a download of keys every hour.
-     */
-    private fun scheduleWork() = BackgroundWorkScheduler.startWorkScheduler()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt
index a51ff449508c687a9819f04bab261ed78f4082e9..f2596fefb4c970957ce572717bbec87fc3df2cb2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt
@@ -35,7 +35,7 @@ import javax.inject.Inject
 class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
 
     @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
-    private val vm: HomeFragmentViewModel by cwaViewModels(
+    private val viewModel: HomeFragmentViewModel by cwaViewModels(
         ownerProducer = { requireActivity().viewModelStore },
         factoryProducer = { viewModelFactory }
     )
@@ -53,7 +53,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
 
         homeMenu.setupMenu(binding.toolbar)
 
-        vm.tracingHeaderState.observe2(this) {
+        viewModel.tracingHeaderState.observe2(this) {
             binding.tracingHeader = it
         }
 
@@ -64,11 +64,11 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
             adapter = homeAdapter
         }
 
-        vm.homeItems.observe2(this) {
+        viewModel.homeItems.observe2(this) {
             homeAdapter.update(it)
         }
 
-        vm.routeToScreen.observe2(this) {
+        viewModel.routeToScreen.observe2(this) {
             doNavigate(it)
         }
 
@@ -76,28 +76,25 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
             doNavigate(HomeFragmentDirections.actionMainFragmentToSettingsTracingFragment())
         }
 
-        vm.openFAQUrlEvent.observe2(this) {
+        viewModel.openFAQUrlEvent.observe2(this) {
             ExternalActionHelper.openUrl(this@HomeFragment, getString(R.string.main_about_link))
         }
 
-        vm.openTraceLocationOrganizerFlow.observe2(this) {
-            vm.wasQRInfoWasAcknowledged()
-            val nestedGraph = findNavController().graph.findNode(R.id.trace_location_organizer_nav_graph) as NavGraph
-
-            if (vm.wasQRInfoWasAcknowledged()) {
-                nestedGraph.startDestination = R.id.traceLocationOrganizerListFragment
-            } else {
-                nestedGraph.startDestination = R.id.traceLocationOrganizerQRInfoFragment
+        viewModel.openTraceLocationOrganizerFlow.observe2(this) {
+            if (viewModel.wasQRInfoWasAcknowledged()) {
+                val nestedGraph =
+                    findNavController().graph.findNode(R.id.trace_location_organizer_nav_graph) as NavGraph
+                nestedGraph.startDestination = R.id.traceLocationsFragment
             }
             doNavigate(HomeFragmentDirections.actionMainFragmentToTraceLocationOrganizerNavGraph())
         }
 
-        vm.popupEvents.observe2(this) { event ->
+        viewModel.popupEvents.observe2(this) { event ->
             when (event) {
                 HomeFragmentEvents.ShowErrorResetDialog -> {
                     RecoveryByResetDialogFactory(this).showDialog(
                         detailsLink = R.string.errors_generic_text_catastrophic_error_encryption_failure,
-                        onPositive = { vm.errorResetDialogDismissed() }
+                        onPositive = { viewModel.errorResetDialogDismissed() }
                     )
                 }
                 HomeFragmentEvents.ShowDeleteTestDialog -> showRemoveTestDialog()
@@ -109,29 +106,29 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
                 }
                 HomeFragmentEvents.ShowTracingExplanation -> {
                     tracingExplanationDialog.show {
-                        vm.tracingExplanationWasShown()
+                        viewModel.tracingExplanationWasShown()
                     }
                 }
             }
         }
 
-        vm.showPopUps()
+        viewModel.showPopUps()
 
-        vm.showLoweredRiskLevelDialog.observe2(this) {
+        viewModel.showLoweredRiskLevelDialog.observe2(this) {
             if (it) showRiskLevelLoweredDialog()
         }
-        vm.showIncorrectDeviceTimeDialog.observe2(this) { showDialog ->
+        viewModel.showIncorrectDeviceTimeDialog.observe2(this) { showDialog ->
             if (!showDialog) return@observe2
-            deviceTimeIncorrectDialog.show { vm.userHasAcknowledgedIncorrectDeviceTime() }
+            deviceTimeIncorrectDialog.show { viewModel.userHasAcknowledgedIncorrectDeviceTime() }
         }
 
-        vm.observeTestResultToSchedulePositiveTestResultReminder()
+        viewModel.observeTestResultToSchedulePositiveTestResultReminder()
     }
 
     override fun onResume() {
         super.onResume()
-        vm.refreshRequiredData()
-        vm.restoreAppShortcuts()
+        viewModel.refreshRequiredData()
+        viewModel.restoreAppShortcuts()
         binding.container.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
     }
 
@@ -143,7 +140,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
             R.string.submission_test_result_dialog_remove_test_button_positive,
             R.string.submission_test_result_dialog_remove_test_button_negative,
             positiveButtonFunction = {
-                vm.deregisterWarningAccepted()
+                viewModel.deregisterWarningAccepted()
             }
         )
         DialogHelper.showDialog(removeTestDialog).apply {
@@ -160,7 +157,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
             R.string.dialog_reactivate_risk_calculation_button_positive,
             R.string.dialog_reactivate_risk_calculation_button_negative,
             positiveButtonFunction = {
-                vm.reenableRiskCalculation()
+                viewModel.reenableRiskCalculation()
             }
         )
         DialogHelper.showDialog(removeTestDialog).apply {
@@ -177,7 +174,7 @@ class HomeFragment : Fragment(R.layout.home_fragment_layout), AutoInject {
             positiveButton = R.string.risk_lowered_dialog_button_confirm,
             negativeButton = null,
             cancelable = false,
-            positiveButtonFunction = { vm.userHasAcknowledgedTheLoweredRiskLevel() }
+            positiveButtonFunction = { viewModel.userHasAcknowledgedTheLoweredRiskLevel() }
         )
 
         DialogHelper.showDialog(riskLevelLoweredDialog).apply {
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt
similarity index 97%
rename from Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt
index 2d20c87a1d4b6f1d63abc4e59f62c813740ff26a..622afae9fb3bf8b300896511ebd45baee8c847f1 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/eventregistration/ui/PrintingAdapter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/print/PrintingAdapter.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.test.eventregistration.ui
+package de.rki.coronawarnapp.ui.print
 
 import android.os.Bundle
 import android.os.CancellationSignal
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt
index e61dfcc8a84099ece7ece5ed6709fa90abd34c4f..14459385664ce333363ac8b4bf4a86769f0f920b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt
@@ -19,7 +19,8 @@ class SettingsResetViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
     private val dataReset: DataReset,
     private val shareTestResultNotificationService: ShareTestResultNotificationService,
-    private val shortcutsHelper: AppShortcutsHelper
+    private val shortcutsHelper: AppShortcutsHelper,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler,
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val clickEvent: SingleLiveEvent<SettingsEvents> = SingleLiveEvent()
@@ -39,7 +40,7 @@ class SettingsResetViewModel @AssistedInject constructor(
                 // only stop tracing if it is currently enabled
                 if (isTracingEnabled) {
                     InternalExposureNotificationClient.asyncStop()
-                    BackgroundWorkScheduler.stopWorkScheduler()
+                    backgroundWorkScheduler.stopWorkScheduler()
                 }
             } catch (apiException: ApiException) {
                 apiException.report(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c0a185307df427ba46134ef4d13afadf0c7c3816
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ProtoBuf.kt
@@ -0,0 +1,7 @@
+package de.rki.coronawarnapp.util
+
+import com.google.protobuf.ByteString
+import okio.ByteString.Companion.toByteString
+
+fun okio.ByteString.toProtoByteString(): ByteString = ByteString.copyFrom(toByteArray())
+fun ByteString.toOkioByteString() = toByteArray().toByteString()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt
index 30c03c4a5c0a85f8d25f4758df640727cbed5197..f7f9f38cfecdbeb0fe260e791236ceb6d3cb8e9b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt
@@ -74,6 +74,11 @@ object TimeAndDateExtensions {
     fun Instant.derive10MinutesInterval(): Long =
         seconds / TimeUnit.MINUTES.toSeconds(10) // 10 min in seconds
 
+    /**
+     * Derive a UNIX timestamp (in seconds) and returns the corresponding 10-minute interval
+     */
+    fun Instant.deriveHourInterval(): HourInterval = millis / 3600000
+
     /**
      * Converts milliseconds to human readable format hh:mm:ss
      *
@@ -110,3 +115,5 @@ object TimeAndDateExtensions {
 
     fun Instant.toUserTimeZone() = this.toDateTime(DateTimeZone.forTimeZone(TimeZone.getDefault()))
 }
+
+typealias HourInterval = Long
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
index 6ead4ba58ab9d791885e0f583891dcad02328f09..442df0780ea00459160a7c9471bfec09a44293eb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt
@@ -5,11 +5,9 @@ import android.net.wifi.WifiManager
 import android.os.PowerManager
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
-import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
+import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
 import de.rki.coronawarnapp.storage.OnboardingSettings
 import de.rki.coronawarnapp.task.TaskController
-import de.rki.coronawarnapp.task.common.DefaultTaskRequest
-import de.rki.coronawarnapp.task.submitBlocking
 import de.rki.coronawarnapp.util.device.BackgroundModeStatus
 import de.rki.coronawarnapp.util.di.AppContext
 import de.rki.coronawarnapp.util.di.ProcessLifecycle
@@ -28,7 +26,9 @@ class WatchdogService @Inject constructor(
     private val taskController: TaskController,
     private val backgroundModeStatus: BackgroundModeStatus,
     @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner,
-    private val onboardingSettings: OnboardingSettings
+    private val onboardingSettings: OnboardingSettings,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler,
+    private val riskWorkScheduler: RiskWorkScheduler,
 ) {
 
     private val powerManager by lazy {
@@ -55,18 +55,8 @@ class WatchdogService @Inject constructor(
 
             Timber.tag(TAG).d("Automatic mode is on, check if we have downloaded keys already today")
 
-            val state = taskController.submitBlocking(
-                DefaultTaskRequest(
-                    DownloadDiagnosisKeysTask::class,
-                    DownloadDiagnosisKeysTask.Arguments(),
-                    originTag = "WatchdogService"
-                )
-            )
-            if (state.isFailed) {
-                Timber.tag(TAG).e(state.error, "RetrieveDiagnosisKeysTransaction failed")
-                // retry the key retrieval in case of an error with a scheduled work
-                BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
-            }
+            val results = riskWorkScheduler.runRiskTasksNow()
+            Timber.tag(TAG).d("runRiskTasksNow() results: %s", results)
 
             if (wifiLock.isHeld) wifiLock.release()
             if (wakeLock.isHeld) wakeLock.release()
@@ -75,7 +65,7 @@ class WatchdogService @Inject constructor(
         // if the user is onboarded we will schedule period background jobs
         // in case the app was force stopped and woken up again by the Google WakeUpService
         if (onboardingSettings.isOnboarded) {
-            BackgroundWorkScheduler.startWorkScheduler()
+            backgroundWorkScheduler.startWorkScheduler()
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt
new file mode 100644
index 0000000000000000000000000000000000000000..553f18903372bc8c15238c88cb9ca688eaae28c1
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/coroutine/ListenableFuture.kt
@@ -0,0 +1,57 @@
+package de.rki.coronawarnapp.util.coroutine
+
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.MoreExecutors
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.concurrent.CancellationException
+import java.util.concurrent.ExecutionException
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/**
+ * Awaits for the completion of the [ListenableFuture] without blocking a thread.
+ *
+ * @return R The result from the [ListenableFuture]
+ *
+ */
+@Suppress("BlockingMethodInNonBlockingContext")
+suspend inline fun <R> ListenableFuture<R>.await(): R {
+    // Fast path
+    if (isDone) {
+        try {
+            return get()
+        } catch (e: ExecutionException) {
+            throw e.cause ?: e
+        }
+    }
+    return suspendCancellableCoroutine { cancellableContinuation ->
+        val action = Runnable {
+            try {
+                cancellableContinuation.resume(get())
+            } catch (throwable: Throwable) {
+                val cause = throwable.cause ?: throwable
+                when (throwable) {
+                    is CancellationException -> cancellableContinuation.cancel(cause)
+                    else -> cancellableContinuation.resumeWithException(cause)
+                }
+            }
+        }
+        addListener(action, MoreExecutors.directExecutor())
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt
index 8605f6a5a6f8a31b2d8261979b797112c7054eba..377c7491c7a41b32f2d8990c86451b58d844b215 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterStatistics.kt
@@ -8,31 +8,32 @@ import de.rki.coronawarnapp.statistics.InfectionStats
 import de.rki.coronawarnapp.statistics.KeySubmissionsStats
 import de.rki.coronawarnapp.statistics.SevenDayRValue
 import de.rki.coronawarnapp.statistics.StatsItem
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
 import org.joda.time.LocalDate
 import org.joda.time.format.DateTimeFormat
 
 fun StatsItem.getPrimaryLabel(context: Context): String {
     val today = LocalDate()
     val yesterday = today.minusDays(1)
-    val day = LocalDate(updatedAt)
+    val updatedAtDate = LocalDate(updatedAt.toUserTimeZone())
     val dateTimeFormatter = DateTimeFormat.mediumDate().withLocale(context.getLocale())
 
     return when (this) {
         is InfectionStats,
-        is KeySubmissionsStats -> when (day) {
+        is KeySubmissionsStats -> when (updatedAtDate) {
             today -> context.getString(R.string.statistics_primary_value_today)
             yesterday -> context.getString(R.string.statistics_primary_value_yesterday)
-            else -> dateTimeFormatter.print(day)
+            else -> dateTimeFormatter.print(updatedAtDate)
         }
-        is IncidenceStats -> when (day) {
+        is IncidenceStats -> when (updatedAtDate) {
             today -> context.getString(R.string.statistics_primary_value_until_today)
             yesterday -> context.getString(R.string.statistics_primary_value_until_yesterday)
-            else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(day))
+            else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(updatedAtDate))
         }
-        is SevenDayRValue -> when (day) {
+        is SevenDayRValue -> when (updatedAtDate) {
             today -> context.getString(R.string.statistics_primary_value_current)
             yesterday -> context.getString(R.string.statistics_primary_value_yesterday)
-            else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(day))
+            else -> context.getString(R.string.statistics_primary_value_until, dateTimeFormatter.print(updatedAtDate))
         }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt
index a3221d6d27e661752b4161e9d2dc81855f36fb32..3167ba6655cc6c7bc2230250b9f14508b07667b8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt
@@ -26,7 +26,7 @@ class SignatureValidation @Inject constructor(
 
     // Public keys within this server environment
     private val publicKeys by lazy {
-        environmentSetup.appConfigVerificationKey.split(KEY_DELIMITER)
+        environmentSetup.appConfigPublicKey.split(KEY_DELIMITER)
             .mapNotNull { pubKeyBase64 -> Base64.decode(pubKeyBase64, Base64.DEFAULT) }
             .map { pubKeyBinary -> keyFactory.generatePublic(X509EncodedKeySpec(pubKeyBinary)) }
             .onEach { Timber.tag(TAG).v("ENV PubKey: %s", it) }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt
index 2088d2fbed238c49289934158195088b78a2a889..de0c40995b889326071304923fbf63169564967a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactory.kt
@@ -27,19 +27,29 @@ class CWAWorkerFactory @Inject constructor(
     ): ListenableWorker? {
         Timber.v("Checking in known worker factories for %s", workerClassName)
         val ourWorkerFactories = factories.entries.find {
-            Class.forName(workerClassName).isAssignableFrom(it.key)
+            try {
+                Class.forName(workerClassName).isAssignableFrom(it.key)
+            } catch (e: ClassNotFoundException) {
+                Timber.e(e, "Failed to create worker class $workerClassName")
+                false
+            }
         }?.value
 
-        return if (ourWorkerFactories != null) {
+        if (ourWorkerFactories != null) {
             Timber.v("It's one of ours, creating worker for %s with %s", workerClassName, workerParameters)
-            ourWorkerFactories.get().create(appContext, workerParameters).also {
+            return ourWorkerFactories.get().create(appContext, workerParameters).also {
                 Timber.i("Our worker was created: %s", it)
             }
-        } else {
+        }
+
+        return try {
             Timber.w("Unknown worker class, trying direct instantiation on %s", workerClassName)
             workerClassName.toNewWorkerInstance(appContext, workerParameters).also {
                 Timber.i("Unknown worker was created: %s", it)
             }
+        } catch (e: ClassNotFoundException) {
+            Timber.w(e, "Failed to create unknown worker class: %s", workerClassName)
+            null
         }
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
index 3cc59eabfb2c1f84bd892cc528138e4fc15149f9..f93c59b45ba5d0cbd6c0501bb84be8e5c6ccb16d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
@@ -8,14 +8,14 @@ import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker
 import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker
+import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker
 import de.rki.coronawarnapp.eventregistration.storage.retention.TraceLocationDbCleanUpPeriodicWorker
 import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOutWorker
+import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorker
 import de.rki.coronawarnapp.submission.auto.SubmissionWorker
 import de.rki.coronawarnapp.worker.BackgroundNoiseOneTimeWorker
 import de.rki.coronawarnapp.worker.BackgroundNoisePeriodicWorker
-import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker
-import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalPeriodicWorker
 import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker
 
 @Module
@@ -44,16 +44,9 @@ abstract class WorkerBinder {
 
     @Binds
     @IntoMap
-    @WorkerKey(DiagnosisKeyRetrievalOneTimeWorker::class)
+    @WorkerKey(DiagnosisKeyRetrievalWorker::class)
     abstract fun diagnosisKeyRetrievalOneTime(
-        factory: DiagnosisKeyRetrievalOneTimeWorker.Factory
-    ): InjectedWorkerFactory<out ListenableWorker>
-
-    @Binds
-    @IntoMap
-    @WorkerKey(DiagnosisKeyRetrievalPeriodicWorker::class)
-    abstract fun diagnosisKeyRetrievalPeriodic(
-        factory: DiagnosisKeyRetrievalPeriodicWorker.Factory
+        factory: DiagnosisKeyRetrievalWorker.Factory
     ): InjectedWorkerFactory<out ListenableWorker>
 
     @Binds
@@ -111,4 +104,11 @@ abstract class WorkerBinder {
     abstract fun traceLocationCleanUpWorker(
         factory: TraceLocationDbCleanUpPeriodicWorker.Factory
     ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(PresenceTracingWarningWorker::class)
+    abstract fun traceWarningWorker(
+        factory: PresenceTracingWarningWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
index 75b991b690e2f25d6c7d474257765028129c0c80..7e7340ebb38c7a76df1ccdbfdc4059cadf3b6057 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt
@@ -9,16 +9,6 @@ import java.util.concurrent.TimeUnit
  */
 object BackgroundConstants {
 
-    /**
-     * Tag for diagnosis key retrieval one time work
-     */
-    const val DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG = "DIAGNOSIS_KEY_ONE_TIME_WORKER"
-
-    /**
-     * Tag for diagnosis key retrieval periodic work
-     */
-    const val DIAGNOSIS_KEY_PERIODIC_WORKER_TAG = "DIAGNOSIS_KEY_PERIODIC_WORKER"
-
     /**
      * Tag for background polling tp check test result periodic work
      */
@@ -34,16 +24,6 @@ object BackgroundConstants {
      */
     const val BACKGROUND_NOISE_ONE_TIME_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER"
 
-    /**
-     * Unique name for diagnosis key retrieval one time work
-     */
-    const val DIAGNOSIS_KEY_ONE_TIME_WORK_NAME = "DiagnosisKeyBackgroundOneTimeWork"
-
-    /**
-     * Unique name for diagnosis key retrieval periodic work
-     */
-    const val DIAGNOSIS_KEY_PERIODIC_WORK_NAME = "DiagnosisKeyBackgroundPeriodicWork"
-
     /**
      * Unique name for diagnosis test result retrieval periodic work
      */
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt
index 31922abf4120e6341508e056d82b84386aec82ab..2168b02875a0b162f33c781faa418839b1dd000f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt
@@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop
 import org.joda.time.Duration
 import org.joda.time.Instant
 import timber.log.Timber
@@ -23,7 +22,8 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor(
     @Assisted val context: Context,
     @Assisted workerParams: WorkerParameters,
     private val submissionSettings: SubmissionSettings,
-    private val timeStamper: TimeStamper
+    private val timeStamper: TimeStamper,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler,
 ) : CoroutineWorker(context, workerParams) {
 
     /**
@@ -45,7 +45,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor(
                 return result
             }
 
-            BackgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork()
+            backgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork()
         } catch (e: Exception) {
             result = if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
                 Result.failure()
@@ -58,7 +58,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor(
     }
 
     private fun stopWorker() {
-        BackgroundWorkScheduler.WorkType.BACKGROUND_NOISE_PERIODIC_WORK.stop()
+        backgroundWorkScheduler.stopBackgroundNoisePeriodicWork()
         Timber.tag(TAG).d("$id: worker stopped")
     }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt
index b109e8ba261979dc70e6760555a0a1c772bcf301..2c03e9544d7eb89824da44430afdf47ad39f0eb8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt
@@ -5,131 +5,84 @@ import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.PeriodicWorkRequestBuilder
 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.WorkTag
 import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
 
-/**
- * Build diagnosis key periodic work request
- * Set "kind delay" for accessibility reason.
- * Backoff criteria set to Linear type.
- *
- * The launchInterval is 60 minutes as we want to check every hour, for new hour packages on the CDN.
- *
- * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER
- * @see BackgroundConstants.KIND_DELAY
- * @see BackgroundConstants.BACKOFF_INITIAL_DELAY
- * @see BackoffPolicy.LINEAR
- */
-fun buildDiagnosisKeyRetrievalPeriodicWork() =
-    PeriodicWorkRequestBuilder<DiagnosisKeyRetrievalPeriodicWorker>(60, TimeUnit.MINUTES)
-        .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag)
-        .setInitialDelay(
-            BackgroundConstants.KIND_DELAY,
-            TimeUnit.MINUTES
-        )
-        .setBackoffCriteria(
-            BackoffPolicy.EXPONENTIAL,
-            BackgroundConstants.BACKOFF_INITIAL_DELAY,
-            TimeUnit.MINUTES
-        )
-        .build()
+@Singleton
+class BackgroundWorkBuilder @Inject constructor() {
 
-/**
- * Build diagnosis key one time work request
- * Set random initial delay for security reason.
- * Backoff criteria set to Linear type.
- *
- * @return OneTimeWorkRequest
- *
- * @see WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER
- * @see buildDiagnosisKeyRetrievalOneTimeWork
- * @see BackgroundConstants.BACKOFF_INITIAL_DELAY
- * @see BackoffPolicy.LINEAR
- */
-fun buildDiagnosisKeyRetrievalOneTimeWork() =
-    OneTimeWorkRequestBuilder<DiagnosisKeyRetrievalOneTimeWorker>()
-        .addTag(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER.tag)
-        .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
-        .setInitialDelay(
-            BackgroundConstants.KIND_DELAY,
-            TimeUnit.MINUTES
-        )
-        .setBackoffCriteria(
-            BackoffPolicy.EXPONENTIAL,
-            BackgroundConstants.BACKOFF_INITIAL_DELAY,
+    /**
+     * Build diagnosis Test Result periodic work request
+     * Set "kind delay" for accessibility reason.
+     *
+     * @return PeriodicWorkRequest
+     *
+     * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER
+     * @see BackgroundConstants.KIND_DELAY
+     */
+    fun buildDiagnosisTestResultRetrievalPeriodicWork() =
+        PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>(
+            BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(),
             TimeUnit.MINUTES
         )
-        .build()
+            .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag)
+            .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
+            .setInitialDelay(
+                BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY,
+                TimeUnit.SECONDS
+            ).setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .build()
 
-/**
- * Build diagnosis Test Result periodic work request
- * Set "kind delay" for accessibility reason.
- *
- * @return PeriodicWorkRequest
- *
- * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER
- * @see BackgroundConstants.KIND_DELAY
- */
-fun buildDiagnosisTestResultRetrievalPeriodicWork() =
-    PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>(
-        BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(),
-        TimeUnit.MINUTES
-    )
-        .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag)
-        .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
-        .setInitialDelay(
-            BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY,
-            TimeUnit.SECONDS
-        ).setBackoffCriteria(
-            BackoffPolicy.LINEAR,
-            BackgroundConstants.KIND_DELAY,
-            TimeUnit.MINUTES
-        )
-        .build()
+    /**
+     * Build background noise one time work request
+     * Set BackgroundNoiseOneTimeWorkDelay for timing randomness.
+     *
+     * @return PeriodicWorkRequest
+     *
+     * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER
+     * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay
+     */
+    fun buildBackgroundNoiseOneTimeWork() =
+        OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>()
+            .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag)
+            .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
+            .setInitialDelay(
+                BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(),
+                TimeUnit.HOURS
+            ).setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .build()
 
-/**
- * Build background noise one time work request
- * Set BackgroundNoiseOneTimeWorkDelay for timing randomness.
- *
- * @return PeriodicWorkRequest
- *
- * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER
- * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay
- */
-fun buildBackgroundNoiseOneTimeWork() =
-    OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>()
-        .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag)
-        .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork())
-        .setInitialDelay(
-            BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(),
+    /**
+     * Build background noise periodic work request
+     * Set "kind delay" for accessibility reason.
+     *
+     * @return PeriodicWorkRequest
+     *
+     * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
+     * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER
+     * @see BackgroundConstants.KIND_DELAY
+     */
+    fun buildBackgroundNoisePeriodicWork() =
+        PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>(
+            BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION,
             TimeUnit.HOURS
-        ).setBackoffCriteria(
-            BackoffPolicy.LINEAR,
-            BackgroundConstants.KIND_DELAY,
-            TimeUnit.MINUTES
-        )
-        .build()
-
-/**
- * Build background noise periodic work request
- * Set "kind delay" for accessibility reason.
- *
- * @return PeriodicWorkRequest
- *
- * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION
- * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER
- * @see BackgroundConstants.KIND_DELAY
- */
-fun buildBackgroundNoisePeriodicWork() =
-    PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>(
-        BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION,
-        TimeUnit.HOURS
-    )
-        .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag)
-        .setInitialDelay(
-            BackgroundConstants.KIND_DELAY,
-            TimeUnit.SECONDS
-        ).setBackoffCriteria(
-            BackoffPolicy.LINEAR,
-            BackgroundConstants.KIND_DELAY,
-            TimeUnit.MINUTES
         )
-        .build()
+            .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag)
+            .setInitialDelay(
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.SECONDS
+            ).setBackoffCriteria(
+                BackoffPolicy.LINEAR,
+                BackgroundConstants.KIND_DELAY,
+                TimeUnit.MINUTES
+            )
+            .build()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
index aaf6317d9be63bf12856a97c46cc9665512e65b7..4f83979c39c974861e80c04df100c1439be3b50e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt
@@ -6,9 +6,13 @@ import androidx.work.Operation
 import androidx.work.WorkInfo
 import androidx.work.WorkManager
 import de.rki.coronawarnapp.CoronaWarnApplication
-import de.rki.coronawarnapp.util.di.ApplicationComponent
+import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
+import de.rki.coronawarnapp.storage.TracingSettings
+import de.rki.coronawarnapp.submission.SubmissionSettings
 import timber.log.Timber
 import java.util.concurrent.ExecutionException
+import javax.inject.Inject
+import javax.inject.Singleton
 
 /**
  * Singleton class for background work handling
@@ -17,26 +21,24 @@ import java.util.concurrent.ExecutionException
  * @see BackgroundConstants
  * @see BackgroundWorkHelper
  */
-object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
-
-    fun init(component: ApplicationComponent) {
-        component.inject(this)
-    }
+@Singleton
+class BackgroundWorkScheduler @Inject constructor(
+    private val backgroundWorkBuilder: BackgroundWorkBuilder,
+    private val submissionSettings: SubmissionSettings,
+    private val tracingSettings: TracingSettings,
+    private val riskWorkScheduler: RiskWorkScheduler
+) {
 
     /**
      * Enum class for work tags
      *
      * @param tag the tag of the worker
      *
-     * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG
-     * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG
      * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG
      * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG
      * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG
      */
     enum class WorkTag(val tag: String) {
-        DIAGNOSIS_KEY_RETRIEVAL_ONE_TIME_WORKER(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORKER_TAG),
-        DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORKER_TAG),
         DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG),
         BACKGROUND_NOISE_ONE_TIME_WORKER(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG),
         BACKGROUND_NOISE_PERIODIC_WORKER(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG)
@@ -47,15 +49,11 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
      *
      * @param uniqueName the unique name of specified work
      *
-     * @see BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME
-     * @see BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME
      * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME
      * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME
      * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME
      */
     enum class WorkType(val uniqueName: String) {
-        DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK(BackgroundConstants.DIAGNOSIS_KEY_ONE_TIME_WORK_NAME),
-        DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK(BackgroundConstants.DIAGNOSIS_KEY_PERIODIC_WORK_NAME),
         DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME),
         BACKGROUND_NOISE_PERIODIC_WORK(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME),
         BACKGROUND_NOISE_ONE_TIME_WORK(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME)
@@ -75,17 +73,9 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
      * @see isWorkActive
      */
     fun startWorkScheduler() {
-        val notificationBody = StringBuilder()
-        notificationBody.append("Jobs starting: ")
-        val isPeriodicWorkActive = isWorkActive(WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag)
-        logWorkActiveStatus(
-            WorkTag.DIAGNOSIS_KEY_RETRIEVAL_PERIODIC_WORKER.tag,
-            isPeriodicWorkActive
-        )
-        if (!isPeriodicWorkActive) {
-            WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start()
-            notificationBody.append("[DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK] ")
-        }
+        Timber.d("startWorkScheduler()")
+        riskWorkScheduler.setPeriodicRiskCalculation(enabled = true)
+
         if (!submissionSettings.isSubmissionSuccessful) {
             if (!isWorkActive(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) &&
                 submissionSettings.registrationToken.value != null &&
@@ -93,10 +83,22 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
             ) {
                 WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start()
                 tracingSettings.initialPollingForTestResultTimeStamp = System.currentTimeMillis()
-                notificationBody.append("[DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER]")
+                Timber.d("Starting DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER")
             }
         }
-        Timber.d("Background Job Starting: %s", notificationBody)
+    }
+
+    /**
+     * Stop work scheduler
+     * Stops all background work by tag.
+     */
+    fun stopWorkScheduler() {
+        WorkTag.values().map { workTag: WorkTag ->
+            workManager.cancelAllWorkByTag(workTag.tag)
+                .also { it.logOperationCancelByTag(workTag) }
+        }
+        riskWorkScheduler.setPeriodicRiskCalculation(enabled = false)
+        Timber.d("All Background Jobs Stopped")
     }
 
     /**
@@ -139,34 +141,12 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
         return result
     }
 
-    /**
-     * Stop work scheduler
-     * Stops all background work by tag.
-     */
-    fun stopWorkScheduler() {
-        WorkTag.values().map { workTag: WorkTag ->
-            workManager.cancelAllWorkByTag(workTag.tag)
-                .also { it.logOperationCancelByTag(workTag) }
-        }
-        Timber.d("All Background Jobs Stopped")
+    fun scheduleDiagnosisTestResultPeriodicWork() {
+        WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start()
     }
 
-    /**
-     * Schedule diagnosis key periodic time work
-     *
-     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK
-     */
-    fun scheduleDiagnosisKeyPeriodicWork() {
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start()
-    }
-
-    /**
-     * Schedule diagnosis key one time work
-     *
-     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK
-     */
-    fun scheduleDiagnosisKeyOneTimeWork() {
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start()
+    fun stopDiagnosisTestResultPeriodicWork() {
+        WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop()
     }
 
     /**
@@ -178,6 +158,10 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
         WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start()
     }
 
+    fun stopBackgroundNoisePeriodicWork() {
+        WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start()
+    }
+
     /**
      * Schedule background noise one time work
      *
@@ -195,41 +179,11 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
      * @see WorkType
      */
     private fun WorkType.start(): Operation = when (this) {
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK -> enqueueDiagnosisKeyBackgroundPeriodicWork()
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK -> enqueueDiagnosisKeyBackgroundOneTimeWork()
         WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER -> enqueueDiagnosisTestResultBackgroundPeriodicWork()
         WorkType.BACKGROUND_NOISE_PERIODIC_WORK -> enqueueBackgroundNoisePeriodicWork()
         WorkType.BACKGROUND_NOISE_ONE_TIME_WORK -> enqueueBackgroundNoiseOneTimeWork()
     }
 
-    /**
-     * Enqueue diagnosis key periodic work and log it
-     * Replace with new if older work exists.
-     *
-     * @return Operation
-     *
-     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK
-     */
-    private fun enqueueDiagnosisKeyBackgroundPeriodicWork() = workManager.enqueueUniquePeriodicWork(
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.uniqueName,
-        ExistingPeriodicWorkPolicy.REPLACE,
-        buildDiagnosisKeyRetrievalPeriodicWork()
-    ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK) }
-
-    /**
-     * Enqueue diagnosis key one time work and log it
-     * Replace with new if older work exists.
-     *
-     * @return Operation
-     *
-     * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK
-     */
-    private fun enqueueDiagnosisKeyBackgroundOneTimeWork() = workManager.enqueueUniqueWork(
-        WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.uniqueName,
-        ExistingWorkPolicy.REPLACE,
-        buildDiagnosisKeyRetrievalOneTimeWork()
-    ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK) }
-
     /**
      * Enqueue diagnosis Test Result periodic
      * Show a Notification when new Test Results are in.
@@ -243,7 +197,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
         workManager.enqueueUniquePeriodicWork(
             WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.uniqueName,
             ExistingPeriodicWorkPolicy.REPLACE,
-            buildDiagnosisTestResultRetrievalPeriodicWork()
+            backgroundWorkBuilder.buildDiagnosisTestResultRetrievalPeriodicWork()
         ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER) }
 
     /**
@@ -258,7 +212,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
         workManager.enqueueUniquePeriodicWork(
             WorkType.BACKGROUND_NOISE_PERIODIC_WORK.uniqueName,
             ExistingPeriodicWorkPolicy.REPLACE,
-            buildBackgroundNoisePeriodicWork()
+            backgroundWorkBuilder.buildBackgroundNoisePeriodicWork()
         ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_PERIODIC_WORK) }
 
     /**
@@ -273,7 +227,7 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
         workManager.enqueueUniqueWork(
             WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.uniqueName,
             ExistingWorkPolicy.REPLACE,
-            buildBackgroundNoiseOneTimeWork()
+            backgroundWorkBuilder.buildBackgroundNoiseOneTimeWork()
         ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_ONE_TIME_WORK) }
 
     /**
@@ -293,11 +247,4 @@ object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() {
             { Timber.d("All work with tag ${workTag.tag} canceled.") },
             { it.run() }
         ).also { Timber.d("Canceling all work with tag ${workTag.tag}") }
-
-    /**
-     * Log work active status
-     */
-    private fun logWorkActiveStatus(tag: String, active: Boolean) {
-        Timber.d("Work type $tag is active: $active")
-    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt
deleted file mode 100644
index 9085a1645570aa546c4f8294904096753b46b17c..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import de.rki.coronawarnapp.storage.TracingSettings
-import de.rki.coronawarnapp.submission.SubmissionSettings
-import javax.inject.Inject
-
-@Suppress("UnnecessaryAbstractClass")
-abstract class BackgroundWorkSchedulerBase {
-    @Inject internal lateinit var submissionSettings: SubmissionSettings
-    @Inject internal lateinit var tracingSettings: TracingSettings
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
deleted file mode 100644
index 5a59a5b2f2d0952471b30f81ad39e3ade4ef2b1d..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import android.content.Context
-import androidx.work.CoroutineWorker
-import androidx.work.WorkerParameters
-import dagger.assisted.Assisted
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
-import timber.log.Timber
-
-/**
- * Periodic diagnosis key retrieval work
- * Executes the scheduling of one time diagnosis key retrieval work
- *
- * @see BackgroundWorkScheduler
- * @see DiagnosisKeyRetrievalOneTimeWorker
- */
-class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor(
-    @Assisted val context: Context,
-    @Assisted workerParams: WorkerParameters
-) : CoroutineWorker(context, workerParams) {
-
-    /**
-     * @see BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
-     * @see BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
-     */
-    override suspend fun doWork(): Result {
-        Timber.tag(TAG).d("$id: doWork() started. Run attempt: $runAttemptCount")
-
-        var result = Result.success()
-        try {
-            BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()
-        } catch (e: Exception) {
-            Timber.tag(TAG).w(
-                e,
-                "$id: Error during BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork()."
-            )
-
-            if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
-                Timber.tag(TAG).w(e, "$id: Retry attempts exceeded.")
-
-                return Result.failure()
-            } else {
-                Timber.tag(TAG).d(e, "$id: Retrying.")
-                result = Result.retry()
-            }
-        }
-
-        Timber.tag(TAG).d("$id: doWork() finished with %s", result)
-        return result
-    }
-
-    @AssistedFactory
-    interface Factory : InjectedWorkerFactory<DiagnosisKeyRetrievalPeriodicWorker>
-
-    companion object {
-        private val TAG = DiagnosisKeyRetrievalPeriodicWorker::class.java.simpleName
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
index cd80fd7100a57719ab97efd62e2f622383e56aa5..348777db1e7947374017e72c7a262459deeee0a5 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt
@@ -17,7 +17,6 @@ import de.rki.coronawarnapp.util.TimeAndDateExtensions
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.formatter.TestResult
 import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop
 import timber.log.Timber
 
 /**
@@ -34,6 +33,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
     private val submissionService: SubmissionService,
     private val timeStamper: TimeStamper,
     private val tracingSettings: TracingSettings,
+    private val backgroundWorkScheduler: BackgroundWorkScheduler,
 ) : CoroutineWorker(context, workerParams) {
 
     override suspend fun doWork(): Result {
@@ -42,7 +42,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
         if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
             Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling")
 
-            BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork()
+            backgroundWorkScheduler.scheduleDiagnosisTestResultPeriodicWork()
             Timber.tag(TAG).d("$id Rescheduled background worker")
 
             return Result.failure()
@@ -117,7 +117,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor(
 
     private fun stopWorker() {
         tracingSettings.initialPollingForTestResultTimeStamp = 0L
-        BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop()
+        backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork()
         Timber.tag(TAG).d("$id: Background worker stopped")
     }
 
diff --git a/Corona-Warn-App/src/main/res/drawable/ic_print.xml b/Corona-Warn-App/src/main/res/drawable/ic_print.xml
new file mode 100644
index 0000000000000000000000000000000000000000..90691b5cc2a20beb3c772e4d5e0e3142582f0634
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/drawable/ic_print.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="20dp"
+    android:height="18dp"
+    android:viewportWidth="20"
+    android:viewportHeight="18">
+    <path
+        android:pathData="M17,5H3C1.34,5 0,6.34 0,8V14H4V18H16V14H20V8C20,6.34 18.66,5 17,5ZM14,16H6V11H14V16ZM17,9C16.45,9 16,8.55 16,8C16,7.45 16.45,7 17,7C17.55,7 18,7.45 18,8C18,8.55 17.55,9 17,9ZM16,0H4V4H16V0Z"
+        android:fillColor="#757575" />
+</vector>
diff --git a/Corona-Warn-App/src/main/res/drawable/ic_share.xml b/Corona-Warn-App/src/main/res/drawable/ic_share.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2ed6672d49ee19a438ce2f1744fd7a4d54ccbc63
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/drawable/ic_share.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="18dp"
+    android:height="20dp"
+    android:viewportWidth="18"
+    android:viewportHeight="20">
+    <path
+        android:pathData="M15,14.08C14.24,14.08 13.56,14.38 13.04,14.85L5.91,10.7C5.96,10.47 6,10.24 6,10C6,9.76 5.96,9.53 5.91,9.3L12.96,5.19C13.5,5.69 14.21,6 15,6C16.66,6 18,4.66 18,3C18,1.34 16.66,0 15,0C13.34,0 12,1.34 12,3C12,3.24 12.04,3.47 12.09,3.7L5.04,7.81C4.5,7.31 3.79,7 3,7C1.34,7 0,8.34 0,10C0,11.66 1.34,13 3,13C3.79,13 4.5,12.69 5.04,12.19L12.16,16.35C12.11,16.56 12.08,16.78 12.08,17C12.08,18.61 13.39,19.92 15,19.92C16.61,19.92 17.92,18.61 17.92,17C17.92,15.39 16.61,14.08 15,14.08Z"
+        android:fillColor="#757575" />
+</vector>
diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_stay.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_stay.xml
deleted file mode 100644
index ddd2f57d763a62e290f50999165a7e5cfbb227fa..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/res/drawable/trace_location_stay.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="18dp"
-    android:height="21dp"
-    android:viewportWidth="18"
-    android:viewportHeight="21">
-  <path
-      android:pathData="M13,11C10.24,11 8,13.24 8,16C8,18.76 10.24,21 13,21C15.76,21 18,18.76 18,16C18,13.24 15.76,11 13,11ZM14.65,18.35L12.5,16.2V13H13.5V15.79L15.35,17.64L14.65,18.35ZM14,2H10.82C10.4,0.84 9.3,0 8,0C6.7,0 5.6,0.84 5.18,2H2C0.9,2 0,2.9 0,4V19C0,20.1 0.9,21 2,21H8.11C7.52,20.43 7.04,19.75 6.69,19H2V4H4V7H12V4H14V9.08C14.71,9.18 15.38,9.39 16,9.68V4C16,2.9 15.1,2 14,2ZM8,4C7.45,4 7,3.55 7,3C7,2.45 7.45,2 8,2C8.55,2 9,2.45 9,3C9,3.55 8.55,4 8,4Z"
-      android:fillColor="#007FAD"/>
-</vector>
diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient.xml
new file mode 100644
index 0000000000000000000000000000000000000000..2b5115ceca76d8b517354f8a1a65e00c4d2d8d4c
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="135"
+        android:centerColor="#6C648C"
+        android:endColor="#3C8CBB"
+        android:startColor="#A93F45" />
+</shape>
diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml
index 2b5115ceca76d8b517354f8a1a65e00c4d2d8d4c..3f18b2b7a68fee245d4e02e9d2453257a8d19292 100644
--- a/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml
+++ b/Corona-Warn-App/src/main/res/drawable/trace_location_view_cardhighlight_gradient_all_corners.xml
@@ -1,6 +1,12 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
+    <corners
+        android:radius="0dp"
+        android:bottomLeftRadius="@dimen/radius_card"
+        android:bottomRightRadius="@dimen/radius_card"
+        android:topLeftRadius="@dimen/radius_card"
+        android:topRightRadius="@dimen/radius_card" />
     <gradient
         android:angle="135"
         android:centerColor="#6C648C"
diff --git a/Corona-Warn-App/src/main/res/drawable/trace_location_warning.xml b/Corona-Warn-App/src/main/res/drawable/trace_location_warning.xml
deleted file mode 100644
index 82e171873cd2a0887136c8c6043c7c823e0e155d..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/main/res/drawable/trace_location_warning.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="32dp"
-    android:height="32dp"
-    android:viewportWidth="32"
-    android:viewportHeight="32">
-  <path
-      android:pathData="M16,16m-16,0a16,16 0,1 1,32 0a16,16 0,1 1,-32 0"
-      android:fillColor="#F5F5F5"/>
-  <path
-      android:pathData="M22.4593,15.7042C22.4593,19.3782 19.4813,22.3562 15.8073,22.3562C12.1333,22.3562 9.1553,19.3782 9.1553,15.7042C9.1553,12.0302 12.1333,9.0522 15.8073,9.0522C19.4813,9.0522 22.4593,12.0302 22.4593,15.7042Z"
-      android:strokeWidth="1.5"
-      android:fillColor="#00000000"
-      android:fillType="evenOdd"
-      android:strokeColor="#007FAD"/>
-  <group>
-    <clip-path
-        android:pathData="M5,9.0332H8.9876V23.3061H5V9.0332Z"
-        android:fillType="evenOdd"/>
-    <path
-        android:pathData="M8.2726,23.3062C1.6166,16.4932 7.1646,9.1062 7.2216,9.0332L8.0146,9.6422C7.8046,9.9152 2.9456,16.4222 8.9876,22.6072L8.2726,23.3062Z"
-        android:fillColor="#007FAD"
-        android:fillType="evenOdd"/>
-  </group>
-  <path
-      android:pathData="M23.3117,23.273C29.9677,16.46 24.4197,9.073 24.3627,9L23.5697,9.609C23.7797,9.882 28.6387,16.389 22.5967,22.574L23.3117,23.273Z"
-      android:fillColor="#007FAD"
-      android:fillType="evenOdd"/>
-</vector>
diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml
index ad69901e4fea98bc046bc90a73e74cc59e165abf..ea59fb9e0b29f95ecfe8689bef0ecfc0824d7edb 100644
--- a/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_fragment.xml
@@ -16,7 +16,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent"
-            android:paddingBottom="@dimen/spacing_huge"
+            android:paddingBottom="@dimen/spacing_fab_padding"
             android:clipToPadding="false"
             tools:listitem="@layout/contact_diary_location_list_item" />
 
diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml
index 7553db974231d2afcde0fe9a93e58ac0b1b09177..37cf73437b095a12485cb8d39825bbbf51492352 100644
--- a/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_fragment.xml
@@ -16,7 +16,7 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toTopOf="parent"
-            android:paddingBottom="@dimen/spacing_huge"
+            android:paddingBottom="@dimen/spacing_fab_padding"
             android:clipToPadding="false"
             tools:listitem="@layout/contact_diary_person_list_item" />
 
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml b/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml
index 09b1a08305859ef5c3bd2a69132f8d7cf56911e2..b179c9fcb9fb183b95db937bf6908e7f289ee40b 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_confirm_check_in.xml
@@ -36,7 +36,7 @@
                     android:layout_width="match_parent"
                     android:layout_height="270dp"
                     app:layout_collapseMode="parallax"
-                    app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient_all_corners" />
+                    app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient" />
 
                 <LinearLayout
                     android:layout_width="match_parent"
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml b/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml
index 15edd5621dd232084da2e2a6bb9302796dea86d0..89f49d4ca8894f916057eb240bc49c8f5f93f857 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_edit_check_in.xml
@@ -37,7 +37,7 @@
                     android:layout_width="match_parent"
                     android:layout_height="270dp"
                     app:layout_collapseMode="parallax"
-                    app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient_all_corners" />
+                    app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient" />
 
                 <LinearLayout
                     android:layout_width="match_parent"
diff --git a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml
index d34935e17da592457de742da36b484d4aa7fdfa7..8100354a0dc61451dc5d49bc5adf28237a694085 100644
--- a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml
+++ b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml
@@ -77,12 +77,13 @@
 
             <ImageView
                 android:id="@+id/check_in_warning_image"
-                android:layout_width="32dp"
-                android:layout_height="32dp"
-                android:layout_marginTop="24dp"
-                android:src="@drawable/trace_location_warning"
-                android:layout_marginStart="24dp"
-                app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_subtitle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/spacing_normal"
+                android:layout_marginTop="@dimen/spacing_large"
+                android:importantForAccessibility="no"
+                android:src="@drawable/ic_qr_tracing_static"
+                app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_subtitle"
                 app:layout_constraintStart_toStartOf="parent" />
 
             <TextView
@@ -98,12 +99,13 @@
 
             <ImageView
                 android:id="@+id/check_in_stay_image"
-                android:layout_width="32dp"
-                android:layout_height="32dp"
-                android:layout_marginTop="24dp"
-                android:src="@drawable/trace_location_stay"
-                android:layout_marginStart="24dp"
-                app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_warning"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="@dimen/spacing_normal"
+                android:layout_marginTop="@dimen/spacing_medium"
+                android:importantForAccessibility="no"
+                android:src="@drawable/ic_qr_time"
+                app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_warning"
                 app:layout_constraintStart_toStartOf="parent" />
 
             <TextView
@@ -148,7 +150,7 @@
                     android:layout_marginTop="4dp"
                     app:layout_constraintStart_toStartOf="parent"
                     app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_card_title" />
+                    app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_card_title" />
 
                 <ImageView
                     android:id="@+id/check_in_onboarding_bulletpoint1"
@@ -156,7 +158,7 @@
                     android:layout_height="8dp"
                     android:src="@drawable/bullet_point"
                     android:layout_marginTop="24dp"
-                    app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_card_subtitle"
+                    app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_card_subtitle"
                     app:layout_constraintStart_toStartOf="parent" />
 
                 <TextView
@@ -192,7 +194,7 @@
                     android:layout_height="8dp"
                     android:src="@drawable/bullet_point"
                     android:layout_marginTop="22dp"
-                    app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_body3"
+                    app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_body3"
                     app:layout_constraintStart_toStartOf="parent" />
 
                 <TextView
@@ -215,7 +217,7 @@
                     android:layout_height="8dp"
                     android:src="@drawable/bullet_point"
                     android:layout_marginTop="22dp"
-                    app:layout_constraintTop_toBottomOf="@+id/check_in_onboarding_body4"
+                    app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_body4"
                     app:layout_constraintStart_toStartOf="parent" />
 
                 <TextView
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
new file mode 100644
index 0000000000000000000000000000000000000000..f2b6d1d139a858278c8c7abcbd497482a39e5a76
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml
@@ -0,0 +1,122 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@android:color/white"
+    tools:context=".ui.eventregistration.organizer.poster.QrCodePosterFragment">
+
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/toolbar"
+        style="@style/CWAToolbar.BackArrow"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:elevation="2dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:menu="@menu/menu_trace_location_qr_code_poster"
+        app:title="@string/trace_location_organiser_poster_title" />
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/qr_code_poster"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/toolbar">
+
+        <ImageView
+            android:id="@+id/poster_image"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:adjustViewBounds="true"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:layout_constraintDimensionRatio="595:841" />
+
+        <ImageView
+            android:id="@+id/qr_code_image"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:scaleType="fitXY"
+            app:layout_constraintDimensionRatio="1:1"
+            app:layout_constraintEnd_toEndOf="@id/end_guideline"
+            app:layout_constraintStart_toStartOf="@id/start_guideline"
+            app:layout_constraintTop_toTopOf="@id/top_guideline"
+            tools:src="@drawable/ic_qrcode"
+            tools:tint="@android:color/black" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/start_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.16" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/end_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.84" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/top_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_percent="0.095" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/text_start_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.132" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/text_end_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            app:layout_constraintGuide_percent="0.87" />
+
+        <androidx.constraintlayout.widget.Guideline
+            android:id="@+id/text_top_guideline"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintGuide_percent="0.61" />
+
+        <TextView
+            android:id="@+id/info_text_view"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:maxLines="2"
+            app:layout_constraintEnd_toEndOf="@id/text_end_guideline"
+            app:layout_constraintStart_toStartOf="@id/text_start_guideline"
+            app:layout_constraintTop_toTopOf="@id/text_top_guideline"
+            tools:ignore="SmallSp"
+            tools:text="Vereinsaktivität: Jahrestreffen der deutschen SAP Anwendergruppe\nHauptstr 3, 69115 Heidelberg"
+            tools:textColor="#000000"
+            tools:textSize="10sp" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+    <com.google.android.material.progressindicator.LinearProgressIndicator
+        android:id="@+id/progress_bar"
+        android:layout_width="150dp"
+        android:layout_height="wrap_content"
+        android:indeterminate="true"
+        app:hideAnimationBehavior="inward"
+        app:indicatorColor="@color/colorAccent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml
index cb2c9478f11425e75d037c5b97d1204f6b54cf8b..0d86c8e3555782de8c1716a1d36f5ea1d37e271a 100644
--- a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml
@@ -20,6 +20,8 @@
         android:id="@+id/check_ins_list"
         android:layout_width="0dp"
         android:layout_height="0dp"
+        android:clipToPadding="false"
+        android:paddingBottom="@dimen/spacing_fab_padding"
         android:visibility="gone"
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         app:layout_constraintBottom_toBottomOf="parent"
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml
index 08c5fcb7ba98c27c26a51e42e6e74ee487c8bf86..3a19e3c3b4f64b8066c9f51219504968a1929175 100644
--- a/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_create_fragment.xml
@@ -9,24 +9,16 @@
 
     <com.google.android.material.appbar.MaterialToolbar
         android:id="@+id/toolbar"
-        android:layout_width="match_parent"
+        style="@style/CWAToolbar.BackArrow"
+        android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:elevation="2dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent"
-        app:navigationIcon="@drawable/ic_back"
         app:title="@string/tracelocation_organizer_category_title"
         tools:subtitle="@string/tracelocation_organizer_category_craft_title" />
 
-    <View
-        android:id="@+id/toolbar_divider"
-        android:layout_width="0dp"
-        android:layout_height="@dimen/card_divider"
-        android:background="@color/colorHairline"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@+id/toolbar" />
-
     <ScrollView
         android:layout_width="0dp"
         android:layout_height="0dp"
@@ -35,7 +27,7 @@
         app:layout_constraintBottom_toTopOf="@id/button_submit"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/toolbar_divider">
+        app:layout_constraintTop_toBottomOf="@id/toolbar">
 
         <LinearLayout
             android:layout_width="match_parent"
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml
index 3d67480093e2f75751b8a0ee35c49f4ac29332ba..58782363908fa6bf30e610630d4ad2c63b2b6fcc 100644
--- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml
@@ -4,9 +4,10 @@
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/content_container"
     android:layout_width="match_parent"
-    android:contentDescription="@string/trace_location_event_detail_title_accessibility"
+    android:layout_height="match_parent"
     android:background="@drawable/trace_location_gradient_background"
-    android:layout_height="match_parent">
+    android:contentDescription="@string/trace_location_event_detail_title_accessibility"
+    android:transitionName="trace_location_container_transition">
 
     <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:id="@+id/coordinator_layout"
@@ -36,7 +37,7 @@
                     android:id="@+id/expandedImage"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
-                    android:src="@drawable/trace_location_view_cardhighlight_gradient_all_corners"
+                    android:src="@drawable/trace_location_view_cardhighlight_gradient"
                     app:layout_collapseMode="parallax" />
 
                 <LinearLayout
@@ -63,8 +64,8 @@
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
                         android:layout_gravity="center"
-                        android:gravity="center"
                         android:layout_marginBottom="8dp"
+                        android:gravity="center"
                         android:textColor="@android:color/white"
                         android:textSize="18sp"
                         android:textStyle="normal"
@@ -118,31 +119,44 @@
                     android:id="@+id/qrCodeImage"
                     android:layout_width="0dp"
                     android:layout_height="0dp"
-                    android:contentDescription="@string/trace_location_event_detail_qr_code_accessibility"
                     android:layout_marginHorizontal="24dp"
                     android:layout_marginVertical="12dp"
                     android:layout_marginTop="16dp"
                     android:background="@drawable/trace_location_qr_code_background"
+                    android:contentDescription="@string/trace_location_event_detail_qr_code_accessibility"
                     app:layout_constraintDimensionRatio="H,1:1"
                     app:layout_constraintLeft_toLeftOf="parent"
                     app:layout_constraintRight_toRightOf="parent"
                     app:layout_constraintTop_toTopOf="parent"
-                    tools:src="@drawable/ic_illustrations_qr_code_scan_info" />
+                    tools:src="@drawable/ic_qrcode"
+                    tools:tint="@android:color/black" />
+
+                <com.google.android.material.progressindicator.LinearProgressIndicator
+                    android:id="@+id/progress_bar"
+                    android:layout_width="150dp"
+                    android:layout_height="wrap_content"
+                    android:indeterminate="true"
+                    app:hideAnimationBehavior="inward"
+                    app:indicatorColor="@color/colorAccent"
+                    app:layout_constraintBottom_toBottomOf="@id/qrCodeImage"
+                    app:layout_constraintEnd_toEndOf="@id/qrCodeImage"
+                    app:layout_constraintStart_toStartOf="@id/qrCodeImage"
+                    app:layout_constraintTop_toTopOf="@id/qrCodeImage" />
 
                 <TextView
                     android:id="@+id/eventDate"
                     style="@style/headline6Sixteen"
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
-                    app:layout_constraintTop_toBottomOf="@id/qrCodeImage"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintVertical_chainStyle="packed"
-                    android:layout_marginTop="12dp"
                     android:layout_marginHorizontal="24dp"
-                    app:layout_constraintEnd_toEndOf="parent"
+                    android:layout_marginTop="12dp"
                     android:gravity="center"
                     android:textSize="16sp"
                     android:textStyle="normal"
+                    app:layout_constraintEnd_toEndOf="parent"
+                    app:layout_constraintStart_toStartOf="parent"
+                    app:layout_constraintTop_toBottomOf="@id/qrCodeImage"
+                    app:layout_constraintVertical_chainStyle="packed"
                     tools:text="21.01.2021, 18:00 - 21:00 Uhr">
 
                 </TextView>
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml
index 1ba929717c19f4e0d71a7ff950615d2d35717bd1..2c3516cab07bf0aa2e3e00d29b07c8fdc6ab8556 100644
--- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_list_fragment.xml
@@ -13,7 +13,6 @@
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:background="@drawable/contact_diary_background"
-        android:elevation="@dimen/elevation_weak"
         android:focusable="true"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
@@ -24,10 +23,10 @@
 
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/recycler_view"
-        android:clipToPadding="false"
         android:layout_width="0dp"
         android:layout_height="0dp"
-        android:paddingBottom="@dimen/spacing_huge"
+        android:clipToPadding="false"
+        android:paddingBottom="@dimen/spacing_fab_padding"
         android:visibility="gone"
         app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         app:layout_constraintBottom_toBottomOf="parent"
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml b/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml
index 3d36130cbf9dcd3f3fcc5a25eeb1cc2abbf20c5b..cd37450cf9ba6b4a96d717d4efff30f5775ee0b2 100644
--- a/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_privacy_card.xml
@@ -41,7 +41,8 @@
 
     <TextView
         android:id="@+id/first_bulletpoint_title"
-        style="@style/subtitleBoldSixteen"
+        style="@style/subtitle"
+        android:textStyle="bold"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginStart="@dimen/spacing_normal"
@@ -71,7 +72,8 @@
 
     <TextView
         android:id="@+id/second_bulletpoint_title"
-        style="@style/subtitleBoldSixteen"
+        style="@style/subtitle"
+        android:textStyle="bold"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
         android:layout_marginTop="@dimen/spacing_normal"
diff --git a/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml b/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml
new file mode 100644
index 0000000000000000000000000000000000000000..0158331ca362e23687471d693014ffbcb708d5f9
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/menu/menu_trace_location_qr_code_poster.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/action_print"
+        android:icon="@drawable/ic_print"
+        android:title="@string/trace_location_organiser_poster_print"
+        app:iconTint="@color/colorTextPrimary1"
+        app:showAsAction="always" />
+    <item
+        android:id="@+id/action_share"
+        android:icon="@drawable/ic_share"
+        android:title="@string/trace_location_organiser_poster_share"
+        app:iconTint="@color/colorTextPrimary1"
+        app:showAsAction="ifRoom" />
+</menu>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
index 5389cde771a0e757cfd1cdea5fac05d58d8aff7d..6a025e8397fc03d2239e57a6cfd02a906f346b41 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -153,7 +153,7 @@
         tools:layout="@layout/fragment_settings_reset">
         <action
             android:id="@+id/action_settingsResetFragment_to_mainFragment"
-            app:destination="@id/trace_location_organizer_nav_graph" />
+            app:destination="@id/mainFragment" />
     </fragment>
 
     <!-- Information -->
@@ -267,7 +267,7 @@
         tools:layout="@layout/fragment_submission_no_consent_positive_other_warning">
         <action
             android:id="@+id/action_submissionResultPositiveOtherWarningNoConsentFragment_to_mainFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
         <action
@@ -371,7 +371,7 @@
         tools:layout="@layout/fragment_submission_result_ready">
         <action
             android:id="@+id/action_submissionResultReadyFragment_to_mainFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
         <action
@@ -395,7 +395,7 @@
             app:destination="@id/submissionSymptomCalendarFragment" />
         <action
             android:id="@+id/action_submissionSymptomIntroductionFragment_to_mainFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
     </fragment>
@@ -414,7 +414,7 @@
             app:destination="@id/submissionResultPositiveOtherWarningNoConsentFragment" />
         <action
             android:id="@+id/action_submissionSymptomCalendarFragment_to_mainFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
     </fragment>
@@ -437,7 +437,7 @@
         android:label="SubmissionTestResultConsentFragment">
         <action
             android:id="@+id/action_submissionTestResultConsentGivenFragment_to_homeFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
         <action
@@ -450,7 +450,7 @@
         android:label="SubmissionTestResultNoConsentFragment">
         <action
             android:id="@+id/action_submissionTestResultNoConsentFragment_to_homeFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
         <action
@@ -477,7 +477,7 @@
         tools:layout="@layout/fragment_submission_test_result_available">
         <action
             android:id="@+id/action_submissionTestResultAvailableFragment_to_mainFragment"
-            app:destination="@id/trace_location_organizer_nav_graph"
+            app:destination="@id/mainFragment"
             app:popUpTo="@id/nav_graph"
             app:popUpToInclusive="true" />
         <action
diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml
index a80ae6d058433de49909ad8ee20c52612f24e3d3..aa52e6cb304971cded06b59023927f8fe76811eb 100644
--- a/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/trace_location_organizer_nav_graph.xml
@@ -3,10 +3,10 @@
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:id="@+id/trace_location_organizer_nav_graph"
-    app:startDestination="@id/traceLocationOrganizerQRInfoFragment">
+    app:startDestination="@id/traceLocationInfoFragment">
 
     <fragment
-        android:id="@+id/traceLocationOrganizerQRInfoFragment"
+        android:id="@+id/traceLocationInfoFragment"
         android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment"
         android:label="TraceLocationQRInfoFragment"
         tools:layout="@layout/trace_location_organizer_qr_code_info_fragment">
@@ -14,13 +14,13 @@
 
         <action
             android:id="@+id/action_traceLocationOrganizerQRInfoFragment_to_traceLocationOrganizerListFragment"
-            app:destination="@id/traceLocationOrganizerListFragment"
-            app:popUpTo="@id/traceLocationOrganizerQRInfoFragment"
+            app:destination="@id/traceLocationsFragment"
+            app:popUpTo="@id/traceLocationInfoFragment"
             app:popUpToInclusive="true" />
     </fragment>
 
     <fragment
-        android:id="@+id/traceLocationOrganizerCategoriesFragment"
+        android:id="@+id/traceLocationCategoryFragment"
         android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.category.TraceLocationCategoryFragment"
         android:label="TraceLocationCategoryFragment"
         tools:layout="@layout/trace_location_organizer_category_fragment">
@@ -43,27 +43,33 @@
             app:nullable="true" />
         <action
             android:id="@+id/action_traceLocationCreateFragment_to_traceLocationOrganizerListFragment"
-            app:destination="@id/traceLocationOrganizerListFragment"
+            app:destination="@id/traceLocationsFragment"
             app:popUpTo="@id/mainFragment"
             app:popUpToInclusive="false" />
     </fragment>
     <fragment
-        android:id="@+id/traceLocationOrganizerListFragment"
+        android:id="@+id/traceLocationsFragment"
         android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.list.TraceLocationsFragment"
-        android:label="TraceLocationCategoryFragment"
+        android:label="TraceLocationsFragment"
         tools:layout="@layout/trace_location_organizer_trace_locations_list_fragment">
         <action
-            android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationOrganizerCategoriesFragment"
-            app:destination="@id/traceLocationOrganizerCategoriesFragment" />
+            android:id="@+id/action_traceLocationsFragment_to_traceLocationCategoryFragment"
+            app:destination="@id/traceLocationCategoryFragment" />
         <action
             android:id="@+id/action_traceLocationOrganizerListFragment_to_qrCodeDetailFragment"
             app:destination="@id/qrCodeDetailFragment" />
         <action
-            android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationCreateFragment"
+            android:id="@+id/action_traceLocationsFragment_to_qrCodeDetailFragment"
+            app:destination="@id/qrCodeDetailFragment" />
+        <action
+            android:id="@+id/action_traceLocationsFragment_to_qrCodePosterFragment"
+            app:destination="@id/qrCodePosterFragment" />
+        <action
+            android:id="@+id/action_traceLocationsFragment_to_traceLocationCreateFragment"
             app:destination="@id/traceLocationCreateFragment" />
         <action
-            android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationOrganizerQRInfoFragment"
-            app:destination="@id/traceLocationOrganizerQRInfoFragment" />
+            android:id="@+id/action_traceLocationOrganizerListFragment_to_traceLocationInfoFragment"
+            app:destination="@id/traceLocationInfoFragment" />
     </fragment>
     <fragment
         android:id="@+id/qrCodeDetailFragment"
@@ -73,6 +79,19 @@
         <argument
             android:name="traceLocationId"
             app:argType="long" />
+        <action
+            android:id="@+id/action_qrCodeDetailFragment_to_qrCodePosterFragment"
+            app:destination="@id/qrCodePosterFragment" />
+    </fragment>
+
+    <fragment
+        android:id="@+id/qrCodePosterFragment"
+        android:name="de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragment"
+        android:label="qr_code_poster_fragment"
+        tools:layout="@layout/qr_code_poster_fragment">
+        <argument
+            android:name="traceLocationId"
+            app:argType="long" />
     </fragment>
 
 </navigation>
\ No newline at end of file
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 588d5bff5cb0d7be5af478ea91bd2350d9b7596d..2c21432140c2dd68198ee608f07da3079797bd9e 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
@@ -151,9 +151,9 @@
     <!-- XTXT: Text for qr icon of qr info screen -->
     <string name="trace_location_qr_info_qr_code_text">"Stellen Sie den QR-Code Ihren Gästen entweder über Ihr Smartphone oder in ausgedruckter Form zur Verfügung."</string>
     <!-- XTXT: Text for time sheet icon of qr info screen -->
-    <string name="trace_location_qr_info_time_text">"Wenn Sie dauerhaft einen QR-Code verwenden, sollten Sie diesen einmal täglich neu erstellen außerhalb der Öffnungszeiten."</string>
+    <string name="trace_location_qr_info_time_text">"Wenn Sie dauerhaft einen QR-Code verwenden, sollten Sie diesen einmal täglich außerhalb der Öffnungszeiten neu erstellen."</string>
     <!-- XTXT: Text for I understand button -->
-    <string name="acknowledge_button">"Enverstanden"</string>
+    <string name="acknowledge_button">"Weiter"</string>
 
     <!-- XHED: Title of the notification channel for event registration related notifications. -->
     <string name="tracelocation_notification_channel_title">"Check-ins"</string>
@@ -178,7 +178,7 @@
     <string name="trace_location_organiser_list_no_codes_subtitle">"Hier werden alle QR-Codes angezeigt, die Sie für ein Ort oder Event erstellt haben. Sie können die QR-Codes löschen, wenn diese nicht mehr verwendet werden sollen."</string>
 
     <!-- XBUT: Event organiser qr codes list: menu: information button  -->
-    <string name="trace_location_organizer_list_menu_information_btn">"Information"</string>
+    <string name="trace_location_organizer_list_menu_information_btn">"Informationen"</string>
     <!-- XBUT: Event organiser qr codes list: menu: remove all button  -->
     <string name="trace_location_organizer_list_menu_remove_all_btn">"Alle entfernen"</string>
 
diff --git a/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml b/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml
index 1762d8bec5dd3f1da51cfd486526cabf898ac3b8..b1493e50e3f95cd0b0fa920142b2f9b4a9e49baa 100644
--- a/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml
@@ -22,7 +22,7 @@
 
     <!-- XTXT: Text bodies for the release info screen bullet points -->
     <string-array name="new_release_body">
-        <item>Die CWA ermöglicht nun einen Check-in für Events und Orte. Personen, die Events veranstalten oder ein Geschäft haben, können über die App einen QR-Code erstellen. Durch Scannen des QR-Codes können sich Gäste bei Ankunft einchecken, um so ihre Anwesenheit zu registrieren. Auf Wunsch legt die App außerdem einen entsprechenden Tagebuch-Eintrag an. Wird eine eingecheckte Person später positiv auf das Coronavirus getestet, werden alle Personen automatisch gewarnt, die zur selben Zeit eingescheckt waren.</item>
+        <item>Die CWA ermöglicht nun einen Check-in für Events und Orte. Personen, die Events veranstalten oder ein Geschäft haben, können über die App einen QR-Code erstellen. Durch Scannen des QR-Codes können sich Gäste bei Ankunft einchecken, um so ihre Anwesenheit zu registrieren. Auf Wunsch legt die App außerdem einen entsprechenden Tagebuch-Eintrag an. Wird eine eingecheckte Person später positiv auf das Coronavirus getestet, können andere Personen gewarnt werden, die zur selben Zeit eingescheckt waren.</item>
     </string-array>
 
     <!-- XTXT: Text labels that will be converted to Links -->
diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml
index 676b78bd3a9fe4abafd7a39160a23b4f48c237cb..5e76c5740c29d83e7f88e404d4fd7c668277a559 100644
--- a/Corona-Warn-App/src/main/res/values-de/strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/strings.xml
@@ -308,7 +308,7 @@
     <!-- XTXT: risk details - infection period logged information body, under 14 days -->
     <string name="risk_details_information_body_period_logged_assessment_under_14_days">"Die Corona-Warn-App ist seit %s Tagen installiert. Das Infektionsrisiko wird für Zeiträume berechnet, in denen die Risiko-Ermittlung aktiv ist. Wenn Sie andere Personen getroffen haben und die Risiko-Ermittlung aktiv war, wird Ihr Infektions-Risiko berechnet."</string>
     <!-- XTXT: risk details - infection period logged information body, over 14 days -->
-    <string name="risk_details_information_body_period_logged_assessment_over_14_days">"Wenn die Risiko-Ermittlung zu Zeiten in denen sie andere Personen getroffen haben aktiv war, kann die Berechnung des Infektionsrisikos für diesen Zeitraum erfolgen."</string>
+    <string name="risk_details_information_body_period_logged_assessment_over_14_days">"Wenn die Risiko-Ermittlung zu Zeiten, in denen Sie andere Personen getroffen haben, aktiv war, kann die Berechnung des Infektionsrisikos für diesen Zeitraum erfolgen."</string>
     <!-- XHED: risk details - infection period logged information body, below behaviors -->	    <!-- XTXT: risk details - infection period logged information body, under 14 days -->
     <string name="risk_details_information_body_period_logged_assessment">"Ältere Tage werden automatisch gelöscht, da sie aus Sicht des Infektionsschutzes nicht mehr relevant sind."</string>
     <!-- XTXT: risk details - infection period days logged/14 -->
@@ -1968,4 +1968,10 @@
     <string name="trace_location_attendee_camera_card_button">Einstellungen öffnen</string>
     <!--  YMSG: Trace location onboarding image description-->
     <string name="trace_location_onboarding_content_description">Drei Personen an einem Stehtisch, zwei von ihnen schauen auf ihr Smartphone.</string>
+    <!-- XMIT: Trace location poster print -->
+    <string name="trace_location_organiser_poster_print">"Drucken"</string>
+    <!-- XMIT: Trace location poster share -->
+    <string name="trace_location_organiser_poster_share">"Teilen"</string>
+    <!-- XHED: Trace location poster title -->
+    <string name="trace_location_organiser_poster_title">"Druckversion"</string>
 </resources>
diff --git a/Corona-Warn-App/src/main/res/values/dimens.xml b/Corona-Warn-App/src/main/res/values/dimens.xml
index 79e9dae773d6b9c36433d66e9ca90ca824b522d7..627ff2bc15f763c7b66a6cf4fe450a048b09694c 100644
--- a/Corona-Warn-App/src/main/res/values/dimens.xml
+++ b/Corona-Warn-App/src/main/res/values/dimens.xml
@@ -156,4 +156,8 @@
 
     <!-- Swipe to delete icons left or right margin from the edge-->
     <dimen name="swipe_icon_margin">33dp</dimen>
+
+    <!-- Padding for the collapsed floating action button -->
+    <dimen name="spacing_fab_padding">72dp</dimen>
+
 </resources>
diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml
index e369ddf69e91d8cb7be66023fd85174623dc6c3f..ced53c0830fe16fd8fffe0e7348008df28fb0198 100644
--- a/Corona-Warn-App/src/main/res/values/strings.xml
+++ b/Corona-Warn-App/src/main/res/values/strings.xml
@@ -1977,4 +1977,10 @@
     <string name="trace_location_attendee_camera_card_button">Einstellungen öffnen</string>
     <!--  YMSG: Trace location onboarding image description-->
     <string name="trace_location_onboarding_content_description">Drei Personen an einem Stehtisch, zwei von ihnen schauen auf ihr Smartphone.</string>
+    <!-- XMIT: Trace location poster print -->
+    <string name="trace_location_organiser_poster_print">"Print"</string>
+    <!-- XMIT: Trace location poster share -->
+    <string name="trace_location_organiser_poster_share">"Share"</string>
+    <!-- XHED: Trace location poster title -->
+    <string name="trace_location_organiser_poster_title">"Print version"</string>
 </resources>
diff --git a/Corona-Warn-App/src/main/res/xml/provider_paths.xml b/Corona-Warn-App/src/main/res/xml/provider_paths.xml
index 104e4132425efe495666ece2264a7b4e24aa5dc4..823eb81820e952965917ed99e4ae20099ae4dcca 100644
--- a/Corona-Warn-App/src/main/res/xml/provider_paths.xml
+++ b/Corona-Warn-App/src/main/res/xml/provider_paths.xml
@@ -6,7 +6,8 @@
     <cache-path
         name="shared_logs"
         path="debuglog/shared/" />
-    <files-path
-        name="Events"
-        path="events/" />
+
+    <cache-path
+        name="Poster"
+        path="poster/" />
 </paths>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigTest.kt
similarity index 93%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigTest.kt
index 67679862b65b1b2560d1b91ba942016cbc58e787..4760d3f098dad3186517c9cba5de6c80db3fa6d7 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigTest.kt
@@ -20,7 +20,7 @@ import testhelpers.EmptyApplication
 
 @Config(sdk = [Build.VERSION_CODES.P], application = EmptyApplication::class)
 @RunWith(RobolectricTestRunner::class)
-class DefaultAppConfigSanityCheck : BaseTest() {
+class DefaultAppConfigTest : BaseTest() {
 
     private val configName = "default_app_config_android.bin"
     private val checkSumName = "default_app_config_android.sha256"
@@ -37,7 +37,7 @@ class DefaultAppConfigSanityCheck : BaseTest() {
     fun `current default matches checksum`() {
         val config = context.assets.open(configName).readBytes()
         val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8)
-        sha256 shouldBe "3d108b3fee7d1b4c227087c82bb804048de8d0542c3f2b26cf507a918201124d"
+        sha256 shouldBe "f10bbfb50eae9f304114bded52972832346279776e9b43f9fdf1a39557497119"
         config.toSHA256() shouldBe sha256
     }
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt
index bd06b47d299e49aeb0b611eac9e98b24534209ce..7c0754dd1a75fa657fe35e47ee9156b1cc95ef31 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt
@@ -15,8 +15,10 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskE
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem
 import de.rki.coronawarnapp.contactdiary.util.ContactDiaryData
 import de.rki.coronawarnapp.contactdiary.util.mockStringsForContactDiaryExporterTests
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.risk.RiskState
-import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
@@ -29,6 +31,7 @@ import io.mockk.MockKAnnotations
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
+import io.mockk.mockk
 import io.mockk.runs
 import io.mockk.verify
 import kotlinx.coroutines.flow.flowOf
@@ -51,9 +54,10 @@ open class ContactDiaryOverviewViewModelTest {
     @MockK lateinit var riskLevelStorage: RiskLevelStorage
     @MockK lateinit var timeStamper: TimeStamper
     @MockK lateinit var context: Context
+    @MockK lateinit var checkInRepository: CheckInRepository
 
     private val testDispatcherProvider = TestDispatcherProvider()
-    private val date = LocalDate.now()
+    private val date = LocalDate.parse("2021-04-07")
     private val dateMillis = date.toDateTimeAtStartOfDay(DateTimeZone.UTC).millis
 
     @BeforeEach
@@ -65,9 +69,10 @@ open class ContactDiaryOverviewViewModelTest {
         every { contactDiaryRepository.personEncounters } returns flowOf(emptyList())
         every { riskLevelStorage.ewDayRiskStates } returns flowOf(emptyList())
         every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(emptyList())
+        every { checkInRepository.allCheckIns } returns flowOf(emptyList())
 
         mockStringsForContactDiaryExporterTests(context)
-        every { timeStamper.nowUTC } returns Instant.now()
+        every { timeStamper.nowUTC } returns Instant.ofEpochMilli(dateMillis)
     }
 
     private val person = DefaultContactDiaryPerson(123, "Romeo")
@@ -113,6 +118,16 @@ open class ContactDiaryOverviewViewModelTest {
         override val riskState: RiskState = RiskState.INCREASED_RISK
     }
 
+    private val checkInLow = mockk<CheckIn>().apply {
+        every { id } returns traceLocationCheckInRiskLow.checkInId
+        every { description } returns "I can make orange rhyme with banana... Bornana"
+    }
+
+    private val checkInHigh = mockk<CheckIn>().apply {
+        every { id } returns traceLocationCheckInRiskHigh.checkInId
+        every { description } returns "I'm the bad guy cause I caused the high risk"
+    }
+
     private val aggregatedRiskPerDateResultLowRisk = ExposureWindowDayRisk(
         dateMillisSinceEpoch = dateMillis,
         riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW,
@@ -139,8 +154,9 @@ open class ContactDiaryOverviewViewModelTest {
         dispatcherProvider = testDispatcherProvider,
         contactDiaryRepository = contactDiaryRepository,
         riskLevelStorage = riskLevelStorage,
-        timeStamper,
-        ContactDiaryExporter(
+        timeStamper = timeStamper,
+        checkInRepository = checkInRepository,
+        exporter = ContactDiaryExporter(
             context,
             timeStamper,
             testDispatcherProvider
@@ -345,6 +361,7 @@ open class ContactDiaryOverviewViewModelTest {
         every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultLowRisk))
         every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh))
         every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit))
+        every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh))
 
         var item = createInstance().listItems.getOrAwaitValue().first {
             it is DayOverviewItem && it.date == date
@@ -360,9 +377,14 @@ open class ContactDiaryOverviewViewModelTest {
             riskEventItem!!.validate(highRisk = true)
         }
 
-        every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultHighRiskDueToLowRiskEncounter))
+        every { riskLevelStorage.ewDayRiskStates } returns flowOf(
+            listOf(
+                aggregatedRiskPerDateResultHighRiskDueToLowRiskEncounter
+            )
+        )
         every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow))
         every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit))
+        every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow))
 
         item = createInstance().listItems.getOrAwaitValue().first {
             it is DayOverviewItem && it.date == date
@@ -392,6 +414,7 @@ open class ContactDiaryOverviewViewModelTest {
     fun `low risk event by attending event with low risk`() {
         every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit))
         every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow))
+        every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow))
 
         val item = createInstance().listItems.getOrAwaitValue().first {
             it is DayOverviewItem && it.date == date
@@ -404,6 +427,7 @@ open class ContactDiaryOverviewViewModelTest {
                 name shouldBe locationEventLowRisk.locationName
                 riskInfoAddition shouldBe null
                 bulledPointColor shouldBe R.color.colorBulletPointLowRisk
+                description shouldBe checkInLow.description
             }
         }
     }
@@ -412,6 +436,7 @@ open class ContactDiaryOverviewViewModelTest {
     fun `high risk event by attending event with high risk`() {
         every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit))
         every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh))
+        every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh))
 
         val item = createInstance().listItems.getOrAwaitValue().first {
             it is DayOverviewItem && it.date == date
@@ -424,6 +449,7 @@ open class ContactDiaryOverviewViewModelTest {
                 name shouldBe locationEventHighRisk.locationName
                 riskInfoAddition shouldBe null
                 bulledPointColor shouldBe R.color.colorBulletPointHighRisk
+                description shouldBe checkInHigh.description
             }
         }
     }
@@ -443,6 +469,8 @@ open class ContactDiaryOverviewViewModelTest {
             )
         )
 
+        every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh, checkInLow))
+
         val item = createInstance().listItems.getOrAwaitValue().first {
             it is DayOverviewItem && it.date == date
         } as DayOverviewItem
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
index fa335d5e8129fd17e4575868872077471183314a..cfbee9a6393b9464a3c8b85d14e5dd9158516988 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/environment/EnvironmentSetupTest.kt
@@ -63,11 +63,10 @@ class EnvironmentSetupTest : BaseTest() {
                 downloadCdnUrl shouldBe "https://download-${env.rawKey}"
                 submissionCdnUrl shouldBe "https://submission-${env.rawKey}"
                 verificationCdnUrl shouldBe "https://verification-${env.rawKey}"
-                appConfigVerificationKey shouldBe "12345678-${env.rawKey}"
+                appConfigPublicKey shouldBe "12345678-${env.rawKey}"
                 safetyNetApiKey shouldBe "placeholder-${env.rawKey}"
                 dataDonationCdnUrl shouldBe "https://datadonation-${env.rawKey}"
                 logUploadServerUrl shouldBe "https://logupload-${env.rawKey}"
-                qrCodePosterTemplateCdnUrl shouldBe "https://qrcodepostertemplate-${env.rawKey}"
                 crowdNotifierPublicKey shouldBe "123_abc-${env.rawKey}"
             }
         }
@@ -115,8 +114,9 @@ class EnvironmentSetupTest : BaseTest() {
         EnvironmentSetup.Type.WRU.rawKey shouldBe "WRU"
         EnvironmentSetup.Type.WRU_XA.rawKey shouldBe "WRU-XA"
         EnvironmentSetup.Type.WRU_XD.rawKey shouldBe "WRU-XD"
+        EnvironmentSetup.Type.TESTER_MOCK.rawKey shouldBe "TESTER-MOCK"
         EnvironmentSetup.Type.LOCAL.rawKey shouldBe "LOCAL"
-        EnvironmentSetup.Type.values().size shouldBe 7
+        EnvironmentSetup.Type.values().size shouldBe 8
 
         EnvironmentSetup.EnvKey.USE_EUR_KEY_PKGS.rawKey shouldBe "USE_EUR_KEY_PKGS"
         EnvironmentSetup.EnvKey.SUBMISSION.rawKey shouldBe "SUBMISSION_CDN_URL"
@@ -126,9 +126,8 @@ class EnvironmentSetupTest : BaseTest() {
         EnvironmentSetup.EnvKey.DATA_DONATION.rawKey shouldBe "DATA_DONATION_CDN_URL"
         EnvironmentSetup.EnvKey.LOG_UPLOAD.rawKey shouldBe "LOG_UPLOAD_SERVER_URL"
         EnvironmentSetup.EnvKey.SAFETYNET_API_KEY.rawKey shouldBe "SAFETYNET_API_KEY"
-        EnvironmentSetup.EnvKey.QRCODE_POSTER_TEMPLATE.rawKey shouldBe "QRCODE_POSTER_TEMPLATE_URL"
         EnvironmentSetup.EnvKey.CROWD_NOTIFIER_PUBLIC_KEY.rawKey shouldBe "CROWD_NOTIFIER_PUBLIC_KEY"
-        EnvironmentSetup.EnvKey.values().size shouldBe 10
+        EnvironmentSetup.EnvKey.values().size shouldBe 9
     }
 
     companion object {
@@ -137,6 +136,7 @@ class EnvironmentSetupTest : BaseTest() {
             EnvironmentSetup.Type.PRODUCTION,
             EnvironmentSetup.Type.WRU_XD,
             EnvironmentSetup.Type.WRU_XA,
+            EnvironmentSetup.Type.TESTER_MOCK,
             EnvironmentSetup.Type.LOCAL
         )
         private const val GOOD_JSON =
@@ -149,7 +149,6 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-PROD",
                     "DATA_DONATION_CDN_URL": "https://datadonation-PROD",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-PROD",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-PROD",
                     "SAFETYNET_API_KEY": "placeholder-PROD",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-PROD",
                     "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-PROD"                    
@@ -161,7 +160,6 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-DEV",
                     "DATA_DONATION_CDN_URL": "https://datadonation-DEV",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-DEV",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-DEV",
                     "SAFETYNET_API_KEY": "placeholder-DEV",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-DEV",
                     "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-DEV"
@@ -173,7 +171,6 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-INT",
                     "DATA_DONATION_CDN_URL": "https://datadonation-INT",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-INT",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-INT",
                     "SAFETYNET_API_KEY": "placeholder-INT",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-INT",
                     "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-INT"
@@ -185,7 +182,6 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-WRU",
                     "DATA_DONATION_CDN_URL": "https://datadonation-WRU",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU",
                     "SAFETYNET_API_KEY": "placeholder-WRU",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU",
                     "CREATE_TRACELOCATION_URL": "https://tracelocation-WRU",
@@ -198,7 +194,6 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-WRU-XD",
                     "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XD",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XD",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU-XD",
                     "SAFETYNET_API_KEY": "placeholder-WRU-XD",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XD",
                     "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XD"
@@ -210,11 +205,21 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-WRU-XA",
                     "DATA_DONATION_CDN_URL": "https://datadonation-WRU-XA",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-WRU-XA",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-WRU-XA",
                     "SAFETYNET_API_KEY": "placeholder-WRU-XA",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-WRU-XA",
                     "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-WRU-XA"
                 },
+                "TESTER-MOCK": {
+                    "USE_EUR_KEY_PKGS" : true,
+                    "SUBMISSION_CDN_URL": "https://submission-TESTER-MOCK",
+                    "DOWNLOAD_CDN_URL": "https://download-TESTER-MOCK",
+                    "VERIFICATION_CDN_URL": "https://verification-TESTER-MOCK",
+                    "DATA_DONATION_CDN_URL": "https://datadonation-TESTER-MOCK",
+                    "LOG_UPLOAD_SERVER_URL": "https://logupload-TESTER-MOCK",
+                    "SAFETYNET_API_KEY": "placeholder-TESTER-MOCK",
+                    "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-TESTER-MOCK",
+                    "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-TESTER-MOCK"
+                },
                 "LOCAL": {
                     "USE_EUR_KEY_PKGS" : true,
                     "SUBMISSION_CDN_URL": "https://submission-LOCAL",
@@ -222,7 +227,6 @@ class EnvironmentSetupTest : BaseTest() {
                     "VERIFICATION_CDN_URL": "https://verification-LOCAL",
                     "DATA_DONATION_CDN_URL": "https://datadonation-LOCAL",
                     "LOG_UPLOAD_SERVER_URL": "https://logupload-LOCAL",
-                    "QRCODE_POSTER_TEMPLATE_URL": "https://qrcodepostertemplate-LOCAL",
                     "SAFETYNET_API_KEY": "placeholder-LOCAL",
                     "PUB_KEYS_SIGNATURE_VERIFICATION": "12345678-LOCAL",
                     "CROWD_NOTIFIER_PUBLIC_KEY": "123_abc-LOCAL"
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt
index 054e7b9473651d136d6f07c03b5a4c5f5f244652..aee24dd7643b2551114c25038649cc427269aad3 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/attendee/confirm/ConfirmCheckInViewModelTest.kt
@@ -3,13 +3,18 @@ package de.rki.coronawarnapp.eventregistration.attendee.confirm
 import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
 import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInNavigation
 import de.rki.coronawarnapp.ui.eventregistration.attendee.confirm.ConfirmCheckInViewModel
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
 import de.rki.coronawarnapp.util.TimeStamper
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
+import io.mockk.coEvery
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
+import okio.ByteString.Companion.decodeBase64
+import org.joda.time.Instant
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.api.extension.ExtendWith
@@ -20,19 +25,32 @@ import testhelpers.extensions.getOrAwaitValue
 @ExtendWith(InstantExecutorExtension::class)
 class ConfirmCheckInViewModelTest : BaseTest() {
 
-    @MockK lateinit var traceLocation: TraceLocation
     @MockK lateinit var verifiedTraceLocation: VerifiedTraceLocation
     @MockK lateinit var checkInRepository: CheckInRepository
     @MockK lateinit var timeStamper: TimeStamper
 
     private lateinit var viewModel: ConfirmCheckInViewModel
 
+    private val traceLocation = TraceLocation(
+        id = 1,
+        type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
+        description = "My Birthday Party",
+        address = "at my place",
+        startDate = 2687955L.secondsToInstant(),
+        endDate = 2687991L.secondsToInstant(),
+        defaultCheckInLengthInMinutes = null,
+        cryptographicSeed = "CRYPTOGRAPHIC_SEED".decodeBase64()!!,
+        cnPublicKey = "PUB_KEY",
+        version = TraceLocation.VERSION
+    )
+
     @BeforeEach
     fun setUp() {
         MockKAnnotations.init(this)
 
+        coEvery { checkInRepository.addCheckIn(any()) } returns 1L
         every { verifiedTraceLocation.traceLocation } returns traceLocation
-        every { traceLocation.defaultCheckInLengthInMinutes } returns 10
+        every { timeStamper.nowUTC } returns Instant.parse("2021-03-04T10:30:00Z")
 
         viewModel = ConfirmCheckInViewModel(
             verifiedTraceLocation = verifiedTraceLocation,
@@ -49,8 +67,7 @@ class ConfirmCheckInViewModelTest : BaseTest() {
 
     @Test
     fun onConfirmEvent() {
-        // TODO
-//        viewModel.onConfirmTraceLocation()
-//        viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation
+        viewModel.onConfirmTraceLocation()
+        viewModel.events.getOrAwaitValue() shouldBe ConfirmCheckInNavigation.ConfirmNavigation
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt
index e869c2869ed6fc16b1bc2b9a7e91b94f41f9e1dc..0298f03aec66f49a628fb48937b3f1106e802387 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt
@@ -48,7 +48,6 @@ class CheckInRepositoryTest : BaseTest() {
             val checkIn = CheckIn(
                 id = 1L,
                 traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
-                traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
                 version = 1,
                 type = 2,
                 description = "brothers birthday",
@@ -77,7 +76,6 @@ class CheckInRepositoryTest : BaseTest() {
                 CheckIn(
                     id = 0L,
                     traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
-                    traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
                     version = 1,
                     type = 2,
                     description = "brothers birthday",
@@ -98,7 +96,6 @@ class CheckInRepositoryTest : BaseTest() {
                     TraceLocationCheckInEntity(
                         id = 0L,
                         traceLocationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(),
-                        traceLocationIdHashBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(),
                         version = 1,
                         type = 2,
                         description = "brothers birthday",
@@ -143,7 +140,6 @@ class CheckInRepositoryTest : BaseTest() {
             TraceLocationCheckInEntity(
                 id = 1L,
                 traceLocationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(),
-                traceLocationIdHashBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64(),
                 version = 1,
                 type = 2,
                 description = "sisters birthday",
@@ -164,7 +160,6 @@ class CheckInRepositoryTest : BaseTest() {
                 CheckIn(
                     id = 1L,
                     traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
-                    traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
                     version = 1,
                     type = 2,
                     description = "sisters birthday",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt
index bafd28dd1ac1725c8978e0c714238c97e6f0b1af..6439da3b9157a29a38ea87c89127bfdd2f6496f3 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInTransmissionRiskLevelTest.kt
@@ -12,7 +12,6 @@ class CheckInTransmissionRiskLevelTest : BaseTest() {
     private val checkIn = CheckIn(
         id = 1L,
         traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
-        traceLocationIdHash = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode(),
         version = 1,
         type = 2,
         description = "restaurant_1",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt
index 14d50535d1815d99ca6dc88917933c06885a6b97..1f188c3ec580271c013d8d0e963e9533accecc8e 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInsTransformerTest.kt
@@ -1,12 +1,10 @@
 package de.rki.coronawarnapp.eventregistration.checkins
 
-import com.google.protobuf.ByteString
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.appconfig.ConfigData
 import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer
 import de.rki.coronawarnapp.appconfig.PresenceTracingRiskCalculationParamContainer
 import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
 import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear
 import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.DurationFilter
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.Range
@@ -16,6 +14,7 @@ import de.rki.coronawarnapp.submission.task.TransmissionRiskVectorDeterminator
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import de.rki.coronawarnapp.util.TimeStamper
+import de.rki.coronawarnapp.util.toOkioByteString
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.coEvery
@@ -42,7 +41,6 @@ class CheckInsTransformerTest : BaseTest() {
     private val checkIn1 = CheckIn(
         id = 1L,
         traceLocationId = "traceLocationId1".encode(),
-        traceLocationIdHash = "traceLocationIdHash1".encode(),
         version = 1,
         type = 1,
         description = "restaurant_1",
@@ -67,7 +65,6 @@ class CheckInsTransformerTest : BaseTest() {
     private val checkIn2 = CheckIn(
         id = 2L,
         traceLocationId = "traceLocationId2".encode(),
-        traceLocationIdHash = "traceLocationIdHash2".encode(),
         version = 1,
         type = 2,
         description = "restaurant_2",
@@ -87,7 +84,6 @@ class CheckInsTransformerTest : BaseTest() {
     private val checkIn3 = CheckIn(
         id = 3L,
         traceLocationId = "traceLocationId3".encode(),
-        traceLocationIdHash = "traceLocationIdHash3".encode(),
         version = 1,
         type = 3,
         description = "restaurant_3",
@@ -201,175 +197,63 @@ class CheckInsTransformerTest : BaseTest() {
             // Check In 1 is excluded from submission due to time deriving
             // Check In 2 mapping and transformation
             get(0).apply {
-                /*
-                    id = 2L,                  // Not mapped - client specific
-                    guid = "trace_location_2",
-                    guidHash = EMPTY,         // Not mapped - client specific
-                    version = 1,
-                    type = 2,
-                    description = "restaurant_2",
-                    address = "address_2",
-                    traceLocationStart = null,
-                    traceLocationEnd = null,
-                    defaultCheckInLengthInMinutes = null,
-                    traceLocationBytes = EMPTY,
-                    signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!,
-                    checkInStart = Instant.parse("2021-03-04T10:20:00Z"),
-                    checkInEnd = Instant.parse("2021-03-04T10:30:00Z"),
-                    completed = false,         // Not mapped - client specific
-                    createJournalEntry = false // Not mapped - client specific
-                 */
-
+                locationId.toOkioByteString() shouldBe checkIn2.traceLocationId
                 // New derived start time
                 startIntervalNumber shouldBe Instant.parse("2021-03-04T10:20:00Z").seconds / TEN_MINUTES_IN_SECONDS
                 // New derived end time
                 endIntervalNumber shouldBe Instant.parse("2021-03-04T10:40:00Z").seconds / TEN_MINUTES_IN_SECONDS
-                /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray())
-                parseLocation(signedLocation.location).apply {
-                    guid shouldBe "trace_location_2"
-                    version shouldBe 1
-                    type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER
-                    description shouldBe "restaurant_2"
-                    address shouldBe "address_2"
-                    startTimestamp shouldBe 0
-                    endTimestamp shouldBe 0
-                    defaultCheckInLengthInMinutes shouldBe 0
-                    transmissionRiskLevel shouldBe 1
-                }*/
             }
 
             // Check-In 3 mappings and transformation
-            /*
-                id = 3L,                   // Not mapped - client specific
-                guid = "trace_location_3",
-                guidHash = EMPTY,          // Not mapped - client specific
-                version = 1,
-                type = 3,
-                description = "restaurant_3",
-                address = "address_3",
-                traceLocationStart = Instant.parse("2021-03-04T09:00:00Z"),
-                traceLocationEnd = Instant.parse("2021-03-10T11:00:00Z")
-                defaultCheckInLengthInMinutes = 10,
-                traceLocationBytes = EMPTY,
-                signature = "c2lnbmF0dXJlMQ==".decodeBase64()!!,
-                checkInStart = Instant.parse("2021-03-04T09:30:00Z"),
-                checkInEnd = Instant.parse("2021-03-10T09:45:00Z"),
-                completed = false,         // Not mapped - client specific
-                createJournalEntry = false // Not mapped - client specific
-             */
-
             // Splitted CheckIn 1
             get(1).apply {
+                locationId.toOkioByteString() shouldBe checkIn3.traceLocationId
                 // Start time from original check-in
                 startIntervalNumber shouldBe Instant.parse("2021-03-04T09:30:00Z").seconds / TEN_MINUTES_IN_SECONDS
                 // End time for splitted check-in 1
                 endIntervalNumber shouldBe Instant.parse("2021-03-05T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
-                /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray())
-                parseLocation(signedLocation.location).apply {
-                    guid shouldBe "trace_location_3"
-                    version shouldBe 1
-                    type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL
-                    description shouldBe "restaurant_3"
-                    address shouldBe "address_3"
-                    startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds
-                    endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds
-                    defaultCheckInLengthInMinutes shouldBe 10
-                    transmissionRiskLevel shouldBe 1
-                }*/
             }
 
             // Splitted CheckIn 2
             get(2).apply {
+                locationId.toOkioByteString() shouldBe checkIn3.traceLocationId
+
                 // Start time for splitted check-in 2
                 startIntervalNumber shouldBe Instant.parse("2021-03-05T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
                 // End time for splitted check-in 2
                 endIntervalNumber shouldBe Instant.parse("2021-03-06T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
-                /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray())
-                parseLocation(signedLocation.location).apply {
-                    guid shouldBe "trace_location_3"
-                    version shouldBe 1
-                    type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL
-                    description shouldBe "restaurant_3"
-                    address shouldBe "address_3"
-                    startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds
-                    endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds
-                    defaultCheckInLengthInMinutes shouldBe 10
-                    transmissionRiskLevel shouldBe 1
-                }*/
             }
 
             // Splitted CheckIn 3
             get(3).apply {
+                locationId.toOkioByteString() shouldBe checkIn3.traceLocationId
                 // Start time from splitted check-in 3
                 startIntervalNumber shouldBe Instant.parse("2021-03-06T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
                 // End time for splitted check-in 3
                 endIntervalNumber shouldBe Instant.parse("2021-03-07T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
-                /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray())
-                parseLocation(signedLocation.location).apply {
-                    guid shouldBe "trace_location_3"
-                    version shouldBe 1
-                    type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL
-                    description shouldBe "restaurant_3"
-                    address shouldBe "address_3"
-                    startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds
-                    endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds
-                    defaultCheckInLengthInMinutes shouldBe 10
-                    transmissionRiskLevel shouldBe 2
-                }*/
             }
 
             // Splitted CheckIn 4
             get(4).apply {
+                locationId.toOkioByteString() shouldBe checkIn3.traceLocationId
                 // Start time from splitted check-in 4
                 startIntervalNumber shouldBe Instant.parse("2021-03-07T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
                 // End time for splitted check-in 4
                 endIntervalNumber shouldBe Instant.parse("2021-03-08T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
-                /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray())
-                parseLocation(signedLocation.location).apply {
-                    guid shouldBe "trace_location_3"
-                    version shouldBe 1
-                    type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL
-                    description shouldBe "restaurant_3"
-                    address shouldBe "address_3"
-                    startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds
-                    endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds
-                    defaultCheckInLengthInMinutes shouldBe 10
-                    transmissionRiskLevel shouldBe 4
-                }*/
             }
 
             // Splitted CheckIn 5
             get(5).apply {
+                locationId.toOkioByteString() shouldBe checkIn3.traceLocationId
                 // Start time from splitted check-in 5
                 startIntervalNumber shouldBe Instant.parse("2021-03-10T00:00:00Z").seconds / TEN_MINUTES_IN_SECONDS
                 // End time for splitted check-in 5
                 endIntervalNumber shouldBe Instant.parse("2021-03-10T10:20:00Z").seconds / TEN_MINUTES_IN_SECONDS
-                /*signedLocation.signature shouldBe ByteString.copyFrom("signature1".toByteArray())
-                parseLocation(signedLocation.location).apply {
-                    guid shouldBe "trace_location_3"
-                    version shouldBe 1
-                    type shouldBe TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_RETAIL
-                    description shouldBe "restaurant_3"
-                    address shouldBe "address_3"
-                    startTimestamp shouldBe Instant.parse("2021-03-04T09:00:00Z").seconds
-                    endTimestamp shouldBe Instant.parse("2021-03-10T11:00:00Z").seconds
-                    defaultCheckInLengthInMinutes shouldBe 10
-                    transmissionRiskLevel shouldBe 8
-                }*/
             }
         }
     }
 
-    private fun parseLocation(bytes: ByteString): TraceLocationOuterClass.TraceLocation =
-        TraceLocationOuterClass.TraceLocation.parseFrom(bytes)
-
     companion object {
         private val TEN_MINUTES_IN_SECONDS = TimeUnit.MINUTES.toSeconds(10)
-
-        // Base64 Strings of trace locations
-        private const val TRACE_LOCATION_2 =
-            "ChB0cmFjZV9sb2NhdGlvbl8yEAEYAiIMcmVzdGF1cmFudF8yKglhZGRyZXNzXzI="
-        private const val TRACE_LOCATION_3 =
-            "ChB0cmFjZV9sb2NhdGlvbl8zEAEYAyIMcmVzdGF1cmFudF8zKglhZGRyZXNzXzMwkMOCggY4sM2iggZACg=="
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..036343e6dcf6c681a7f35f89809b365b11da401e
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base32UrlProvider.kt
@@ -0,0 +1,81 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData
+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 Base32UrlProvider : ArgumentsProvider {
+    override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
+        return Stream.of(
+            Arguments.of(
+                "https://e.coronawarn.app?v=1#BAARELAIAEJBCTLZEBBGS4TUNBSGC6JAKBQXE5DZDIFWC5BANV4SA4DMMFRWKKGTQ6SA" +
+                    "CMHXQ6SACGTFBAAREWZQLEYBGBQHFKDERTR5AIAQMCBKQZEM4PIDAEDQGQQAARZ3BRFS24KCCFZSSN7E4YBSPXT7QU4DO" +
+                    "UKYNRUDLSSPJTFOZWCHHV5DFTRKISOOU5Y3ENLQ2WS2HYW5YAPUE3C6XBJRSAEF6352AOK6DICDCMRTGQRAICABCABA",
+                QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocation.newBuilder()
+                            .setDescription("My Birthday Party")
+                            .setAddress("at my place")
+                            .setStartTimestamp(2687955)
+                            .setEndTimestamp(2687991)
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build(),
+                CWALocationData.newBuilder()
+                    .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER)
+                    .setVersion(1)
+                    .build()
+            ),
+            Arguments.of(
+                "https://e.coronawarn.app?v=1#BAAREIAIAEJA2SLDMVRXEZLBNUQFG2DPOANA2TLBNFXCAU3UOJSWK5BAGENGKCABCJNTA" +
+                    "WJQCMDAOKUGJDHD2AQBAYECVBSIZY6QGAIHANBAABDTWDCLFVYUEELTFE36JZQDE7PH7BJYG5IVQ3DIGXFE6TGK5TMEOPL" +
+                    "2GLHCURE45J3RWI2XBVNFUPRN3QA7IJWF5OCTDEAIL5X3UA4V4GQEGEZDGNBCAYEACEABDAFA",
+                QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocation.newBuilder()
+                            .setDescription("Icecream Shop")
+                            .setAddress("Main Street 1")
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build(),
+                CWALocationData.newBuilder()
+                    .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER)
+                    .setVersion(1)
+                    .setDefaultCheckInLengthInMinutes(10)
+                    .build()
+            )
+        )
+    }
+
+    companion object {
+        const val CRYPTOGRAPHIC_SEED = "MTIzNA=="
+        const val PUB_KEY =
+            "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2d92ae6bc967dc070b483590991c231b79295626
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/Base64UrlProvider.kt
@@ -0,0 +1,82 @@
+package de.rki.coronawarnapp.eventregistration.checkins.qrcode
+
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.TraceLocation
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CrowdNotifierData
+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 Base64UrlProvider : ArgumentsProvider {
+    override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
+        return Stream.of(
+            Arguments.of(
+                "https://e.coronawarn.app?v=1#CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmoIARJgO" +
+                    "MTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-vox" +
+                    "Q1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgQIARAC",
+                QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocation.newBuilder()
+                            .setDescription("My Birthday Party")
+                            .setAddress("at my place")
+                            .setStartTimestamp(2687955)
+                            .setEndTimestamp(2687991)
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build(),
+                CWALocationData.newBuilder()
+                    .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER)
+                    .setVersion(1)
+                    .build()
+            ),
+            Arguments.of(
+                "https://e.coronawarn.app?v=1#CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmoIARJgOMTa6eYSiaDv8lW1" +
+                    "3xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ" +
+                    "1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgYIARABGAo",
+                QRCodePayload.newBuilder()
+                    .setVersion(1)
+                    .setCrowdNotifierData(
+                        CrowdNotifierData.newBuilder()
+                            .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString())
+                            .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString())
+                            .setVersion(1)
+                    )
+                    .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString())
+                    .setLocationData(
+                        TraceLocation.newBuilder()
+                            .setDescription("Icecream Shop")
+                            .setAddress("Main Street 1")
+                            .setVersion(1)
+                            .build()
+                    )
+                    .build(),
+                CWALocationData.newBuilder()
+                    .setType(TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER)
+                    .setVersion(1)
+                    .setDefaultCheckInLengthInMinutes(10)
+                    .build()
+            )
+        )
+    }
+
+    companion object {
+        const val CRYPTOGRAPHIC_SEED = "MTIzNA=="
+        const val PUB_KEY =
+            "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" +
+                "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD"
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt
deleted file mode 100644
index 5a767d467dcc7696076c8e28082bf2f79dee8074..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/DefaultQRCodeVerifierTest2.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package de.rki.coronawarnapp.eventregistration.checkins.qrcode
-
-import testhelpers.BaseTest
-
-class DefaultQRCodeVerifierTest2 : BaseTest() {
-
-    /* Disabled, because new protobuf doesn't include signed traceLocation, we should write tests for the parsing of
-    QrCodePayload protobuf ...
-
-    @Test
-    fun `protobuf decoding 1`() {
-        val signedTraceLocation =
-            TraceLocationOuterClass.SignedTraceLocation.parseFrom(
-                "BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVGRSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAFAAAESGGBCAEIDFJJ7KHRO3ZZ2SFMJSBXSUY2ZZKGOIZS27L2D6VPKTA57M6RZY3MBCARR7LXAA2BY3IGNTHNFFAJSMIXF6PP4TEB3I2C3D7P32QUZHVVER"
-                    .decodeBase32().toByteArray()
-            )
-
-        signedTraceLocation.apply {
-            TraceLocationOuterClass.TraceLocation.parseFrom(location).apply {
-                guid shouldBe "3055331c-2306-43f3-9742-6d8fab54e848"
-                version shouldBe 1
-                typeValue shouldBe 2
-                description shouldBe "My Birthday Party"
-                address shouldBe "at my place"
-                startTimestamp shouldBe 2687955
-                endTimestamp shouldBe 2687991
-                defaultCheckInLengthInMinutes shouldBe 0
-            }
-            signature.toByteArray().toByteString()
-                .base64() shouldBe "MEQCIGVKfqPF2851IrEyDeVMazlRnIzLX16H6r1TB37PRzjbAiBGP13ADQcbQZsztKUCZMRcvnv5Mgdo0LY/v3qFMnrUkQ=="
-        }
-
-        signedTraceLocation.location.toByteArray().toByteString()
-            .base64() shouldBe "CiQzMDU1MzMxYy0yMzA2LTQzZjMtOTc0Mi02ZDhmYWI1NGU4NDgQARgCIhFNeSBCaXJ0aGRheSBQYXJ0eSoLYXQgbXkgcGxhY2Uw04ekATj3h6QBQAA="
-    }
-
-    @Test
-    fun `protobuf decoding 2`() {
-        val signedTraceLocation = TraceLocationOuterClass.SignedTraceLocation.parseFrom(
-            "BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEGUYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDDARACEA2ZCTGOF2HH2RQU7ODZMCSUTUBBNQYM6AR4NG6FFLC6ISXWEOI5UARADO44YYH3U53ZYL6IYM5DWALXUESAJNWRGRL5KLNLS5BM54SHDDCA"
-                .decodeBase32().toByteArray()
-        )
-
-        signedTraceLocation.apply {
-            TraceLocationOuterClass.TraceLocation.parseFrom(location).apply {
-                guid shouldBe "fca84b37-61c0-4a7c-b2f8-825cadd506cf"
-                version shouldBe 1
-                typeValue shouldBe 1
-                description shouldBe "Icecream Shop"
-                address shouldBe "Main Street 1"
-                startTimestamp shouldBe 0
-                endTimestamp shouldBe 0
-                defaultCheckInLengthInMinutes shouldBe 10
-            }
-            signature.toByteArray().toByteString()
-                .base64() shouldBe "MEQCIDWRTM4ujn1GFPuHlgpUnQIWwwzwI8abxSrF5Er2I5HaAiAbucxg+6d3nC/Iwzo7AXehJAS20TRX1S2rl0LO8kcYxA=="
-        }
-
-        signedTraceLocation.location.toByteArray().toByteString()
-            .base64() shouldBe "CiRmY2E4NGIzNy02MWMwLTRhN2MtYjJmOC04MjVjYWRkNTA2Y2YQARgBIg1JY2VjcmVhbSBTaG9wKg1NYWluIFN0cmVldCAxMAA4AEAK"
-    }
-
-     */
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt
index 71a1c8722df6ebbcae83bb335bebdd1176f56d30..c01a15b2b532b8da63ff40271b1c22e9ed49910f 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/QRCodeUriParserTest.kt
@@ -1,24 +1,91 @@
 package de.rki.coronawarnapp.eventregistration.checkins.qrcode
 
+import de.rki.coronawarnapp.appconfig.AppConfigProvider
+import de.rki.coronawarnapp.appconfig.ConfigData
+import de.rki.coronawarnapp.appconfig.PresenceTracingConfigContainer
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.CWALocationData
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass.QRCodePayload
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor
+import de.rki.coronawarnapp.server.protocols.internal.v2.PresenceTracingParametersOuterClass.PresenceTracingQRCodeDescriptor.PayloadEncoding
+import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
-import io.kotest.matchers.shouldNotBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.ArgumentsSource
 import testhelpers.BaseTest
 
+@Suppress("BlockingMethodInNonBlockingContext")
 class QRCodeUriParserTest : BaseTest() {
 
-    fun createInstance() = QRCodeUriParser()
+    @MockK lateinit var configProvider: AppConfigProvider
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        coEvery { configProvider.getAppConfig() } returns mockk<ConfigData>().apply {
+            every { presenceTracing } returns PresenceTracingConfigContainer(
+                qrCodeDescriptors = listOf(
+                    PresenceTracingQRCodeDescriptor.newBuilder()
+                        .setVersionGroupIndex(0)
+                        .setEncodedPayloadGroupIndex(1)
+                        .setPayloadEncoding(PayloadEncoding.BASE64)
+                        .setRegexPattern("https://e\\.coronawarn\\.app\\?v=(\\d+)\\#(.+)")
+                        .build()
+                )
+            )
+        }
+    }
+
+    fun createInstance() = QRCodeUriParser(configProvider)
 
     @ParameterizedTest
-    @ArgumentsSource(ValidUrlProvider::class)
-    fun `Valid URLs`(input: String) {
-        createInstance().getQrCodePayload(input) shouldNotBe null
+    @ArgumentsSource(Base64UrlProvider::class)
+    fun `Base64 Valid URLs`(
+        input: String,
+        expectedPayload: QRCodePayload,
+        expectedVendorData: CWALocationData
+    ) = runBlockingTest {
+        val qrCodePayload = createInstance().getQrCodePayload(input)
+        qrCodePayload shouldBe expectedPayload
+        CWALocationData.parseFrom(qrCodePayload.vendorData) shouldBe expectedVendorData
+    }
+
+    @ParameterizedTest
+    @ArgumentsSource(Base32UrlProvider::class)
+    fun `Base32 Valid URLs`(
+        input: String,
+        expectedPayload: QRCodePayload,
+        expectedVendorData: CWALocationData
+    ) = runBlockingTest {
+        coEvery { configProvider.getAppConfig() } returns mockk<ConfigData>().apply {
+            every { presenceTracing } returns PresenceTracingConfigContainer(
+                qrCodeDescriptors = listOf(
+                    PresenceTracingQRCodeDescriptor.newBuilder()
+                        .setVersionGroupIndex(0)
+                        .setEncodedPayloadGroupIndex(1)
+                        .setPayloadEncoding(PayloadEncoding.BASE32)
+                        .setRegexPattern("https://e\\.coronawarn\\.app\\?v=(\\d+)\\#(.+)")
+                        .build()
+                )
+            )
+        }
+
+        val qrCodePayload = createInstance().getQrCodePayload(input)
+        qrCodePayload shouldBe expectedPayload
+        CWALocationData.parseFrom(qrCodePayload.vendorData) shouldBe expectedVendorData
     }
 
     @ParameterizedTest
     @ArgumentsSource(InvalidUrlProvider::class)
-    fun `Invalid URLs`(input: String) {
-        createInstance().getQrCodePayload(input) shouldBe null
+    fun `Invalid URLs`(input: String) = runBlockingTest {
+        shouldThrow<InvalidQrCodeUriException> {
+            createInstance().getQrCodePayload(input)
+        }
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt
deleted file mode 100644
index f7e0bf42fae6a0c6d362ad86c1ffdb14bf97fc9f..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidUrlProvider.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package de.rki.coronawarnapp.eventregistration.checkins.qrcode
-
-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 ValidUrlProvider : ArgumentsProvider {
-    override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> {
-        return Stream.of(
-            Arguments.of(
-                "HTTPS://E.CORONAWARN.APP/C1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" +
-                    "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" +
-                    "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
-            ),
-            Arguments.of(
-                "https://e.coronawarn.app/c1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBO" +
-                    "J2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2" +
-                    "SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="
-            ),
-            Arguments.of(
-                "https://e.coronawarn.app/c1/BJLAUJBTGA2TKMZTGFRS2MRTGA3C2NBTMYZS2OJXGQZC2NTEHBTGCYRVG" +
-                    "RSTQNBYCAARQARCCFGXSICCNFZHI2DEMF4SAUDBOJ2HSKQLMF2CA3LZEBYGYYLDMUYNHB5EAE4PPB5EAF" +
-                    "AAAESIGBDAEIIARVENF6QT6XZATJ5GSDHL77BCAGR6QKDEUJRP2RDCTKTS7QECWMFAEIIA47MT2EA7MQK" +
-                    "GNQU2XCY3Y2ZOZXCILDPC65PBUO4JJHT5LQQWDQSA"
-            ),
-            Arguments.of(
-                "https://e.coronawarn.app/c1/BJHAUJDGMNQTQNDCGM3S2NRRMMYC2NDBG5RS2YRSMY4C2OBSGVRWCZDEG" +
-                    "UYDMY3GCAARQAJCBVEWGZLDOJSWC3JAKNUG64BKBVGWC2LOEBJXI4TFMV2CAMJQAA4AAQAKCJDTARICEB" +
-                    "NEPPKKTAAIH5BSV45EPOINHOASARJLYYSHNTUUHLNGVYUZXZEBWARBACD53WYEGYXYQS3STOFLSOVM3XX" +
-                    "D5A5HKMFQR7WYYARKKVOFGYGHO"
-            ),
-            Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ===="),
-            Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ"),
-            Arguments.of("https://e.coronawarn.app/c1/JBSWY3DPEBLW64TMMQQQ========"),
-        )
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt
index dc037d1caee0f5ac21a14b0f269ead33e909e035..8c76d64b9b633b1c8fcde91572d7ce43c9f1a47f 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/split/CheckInSplitterTest.kt
@@ -16,7 +16,6 @@ class CheckInSplitterTest : BaseTest() {
     private val defaultCheckIn = CheckIn(
         id = 1L,
         traceLocationId = "traceLocationId1".encode(),
-        traceLocationIdHash = "traceLocationIdHash1".encode(),
         version = 1,
         type = 1,
         description = "Restaurant",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..683a645639868de09b9607ee724e21801bfac02c
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/QrCodePayloadTest.kt
@@ -0,0 +1,67 @@
+package de.rki.coronawarnapp.eventregistration.events
+
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.qrCodePayload
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
+import io.kotest.matchers.shouldBe
+import okio.ByteString.Companion.decodeBase64
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class QrCodePayloadTest : BaseTest() {
+
+    @Test
+    fun `Trace location to QrCodePayload 1`() {
+        val traceLocation = TraceLocation(
+            id = 1,
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
+            description = "My Birthday Party",
+            address = "at my place",
+            startDate = 2687955L.secondsToInstant(),
+            endDate = 2687991L.secondsToInstant(),
+            defaultCheckInLengthInMinutes = null,
+            cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!,
+            cnPublicKey = PUB_KEY,
+            version = TraceLocation.VERSION
+        )
+
+        traceLocation.qrCodePayload() shouldBe
+            TraceLocationOuterClass.QRCodePayload.parseFrom(PAYLOAD_1.decodeBase64()!!.toByteArray())
+    }
+
+    @Test
+    fun `Trace location to QrCodePayload 2`() {
+        val traceLocation = TraceLocation(
+            id = 2,
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER,
+            description = "Icecream Shop",
+            address = "Main Street 1",
+            startDate = null,
+            endDate = null,
+            defaultCheckInLengthInMinutes = 10,
+            cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!,
+            cnPublicKey = PUB_KEY,
+            version = TraceLocation.VERSION
+        )
+
+        traceLocation.qrCodePayload() shouldBe
+            TraceLocationOuterClass.QRCodePayload.parseFrom(PAYLOAD_2.decodeBase64()!!.toByteArray())
+    }
+
+    companion object {
+        private const val PAYLOAD_1 =
+            "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKo" +
+                "ZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxe" +
+                "uFMZAIX2+6A5XhoEMTIzNCIECAEQAg=="
+
+        private const val PAYLOAD_2 =
+            "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIR" +
+                "cyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK"
+
+        private const val CRYPTOGRAPHIC_SEED = "MTIzNA=="
+        private const val PUB_KEY =
+            "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0z" +
+                "K7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f92345e30e279cf4183212ddf5f99bbf93a72007
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationIdTest.kt
@@ -0,0 +1,245 @@
+package de.rki.coronawarnapp.eventregistration.events
+
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationId
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.qrCodePayload
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.toTraceLocationIdHash
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.traceLocation
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
+import io.kotest.matchers.shouldBe
+import okio.ByteString
+import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.decodeHex
+import okio.ByteString.Companion.toByteString
+import org.joda.time.Instant
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+@Suppress("MaxLineLength")
+class TraceLocationIdTest : BaseTest() {
+    @Test
+    fun `locationId from qrCodePayloadBase64 - 1`() {
+        val qrCodePayloadBase64 =
+            "CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIECAEQAg=="
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        )
+        qrCodePayload.traceLocation().apply {
+            locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0="
+        }
+    }
+
+    @Test
+    fun `locationId from qrCodePayloadBase64 - 2`() {
+        val qrCodePayloadBase64 =
+            "CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmUIARJbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5XhoEMTIzNCIGCAEQARgK"
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        )
+        qrCodePayload.traceLocation().apply {
+            locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ="
+        }
+    }
+
+    @Test
+    fun `locationId from traceLocation - 1`() {
+        val traceLocation = TraceLocation(
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
+            description = "My Birthday Party",
+            address = "at my place",
+            startDate = 2687955L.secondsToInstant(),
+            endDate = 2687991L.secondsToInstant(),
+            defaultCheckInLengthInMinutes = null,
+            cryptographicSeed = "MTIzNA==".decodeBase64()!!,
+            cnPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==",
+            version = TraceLocation.VERSION
+        )
+        traceLocation.locationId.base64() shouldBe "jNcJTCajd9Sen6Tbexl2Yb7O3J7ps47b6k4+QMT4xS0="
+    }
+
+    @Test
+    fun `locationId from traceLocation - 2`() {
+        val traceLocation = TraceLocation(
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER,
+            description = "Icecream Shop",
+            address = "Main Street 1",
+            startDate = null,
+            endDate = null,
+            defaultCheckInLengthInMinutes = 10,
+            cryptographicSeed = "MTIzNA==".decodeBase64()!!,
+            cnPublicKey = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==",
+            version = TraceLocation.VERSION
+        )
+
+        traceLocation.locationId.base64() shouldBe "GMuCjqNmOdYyrFhyvFNTVEeLaZh+uShgUoY0LYJo4YQ="
+    }
+
+    /**
+     * Match server calculation
+     * https://github.com/corona-warn-app/cwa-server/blob/5ce7d27a74fbf4f2ed560772f97ac17e2189ad33/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/TraceTimeIntervalWarningServiceTest.java#L141
+     */
+    @Test
+    fun `test trace location hash generation`() {
+        val locationId = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d"
+        val locationIdByte: ByteString = locationId.decodeHex()
+        val hashedLocationId: ByteString = locationIdByte.sha256()
+
+        val expectedLocationIdHash = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230"
+
+        hashedLocationId.hex() shouldBe expectedLocationIdHash
+        hashedLocationId shouldBe expectedLocationIdHash.decodeHex()
+    }
+
+    @Test
+    fun `test trace location hash generation - 2`() {
+        val id: TraceLocationId = "1b02111da7c0799df6ad67deb7b397bdfb07e63da0fdea30fae335762826e34f".decodeHex()
+        id.toTraceLocationIdHash().hex() shouldBe "394db434a2e9c2ca9f32eed266d30bc037b4314cb3d53249fada68de45450cbb"
+    }
+
+    @Test
+    fun `test trace location hash generation - 3`() {
+        val id: TraceLocationId = "14c7a20ed81ebabdc32c8521382c56b851af15ccd8d13c86cd91a0620e78d664".decodeHex()
+        id.toTraceLocationIdHash().hex() shouldBe "852475fa271a29c67ad85578bb86ff4922dabf9f2b081353e1b5cdf99442889d"
+    }
+
+    @Test
+    fun `turn location ID into location ID hash`() {
+        val traceLocationIdHex = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d"
+        val expectedLocationIdHashHex = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230"
+
+        val traceLocationId: TraceLocationId = traceLocationIdHex.decodeHex()
+        traceLocationId.toTraceLocationIdHash() shouldBe expectedLocationIdHashHex.decodeHex()
+    }
+
+    @Test
+    fun `mock server test data - 1`() {
+        val qrCodePayloadBase64 =
+            "CAESKAgBEhRBcHBsZSBDb21wdXRlciwgSW5jLhoOMTk1OCBGb2hlIFJvYWQadggBEmA4xNrp5hKJoO_yVbXfF1gS8Yc5nURhOIVLG3nUcSg8IPsI2e8JSIhg-FrHUymQ3RR80KUKb1lZjLQkfTUINUP16r6-jFDURwUlCQQi6NXCgI0rQw0a4MrVrKMbF4NzhQMaENXiVYke5XY0HddkDmj-3HYiBwgBEAQYqQE"
+        val expectedLocationIdHex = "fc925439f45417a14403b25c95fdc6d8711653f8aa08c0d0967bd30a6348c7fc"
+        val expectedLocationIdBase64 = "/JJUOfRUF6FEA7Jclf3G2HEWU/iqCMDQlnvTCmNIx/w="
+
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        )
+
+        // Payload -> locationId (direct from raw)
+        val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes
+        totalByteSequence.toByteString().sha256().base64() shouldBe expectedLocationIdBase64
+
+        // TraceLocation -> locationId (manual reconstruction)
+        val traceLocation = TraceLocation(
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_FOOD_SERVICE,
+            description = "Apple Computer, Inc.",
+            address = "1958 Fohe Road",
+            startDate = null,
+            endDate = null,
+            defaultCheckInLengthInMinutes = 169,
+            cryptographicSeed = "1eJViR7ldjQd12QOaP7cdg==".decodeBase64()!!,
+            cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD",
+            version = TraceLocation.VERSION
+        ).apply {
+            locationId.base64() shouldBe expectedLocationIdBase64
+            locationId.hex() shouldBe expectedLocationIdHex
+        }
+
+        // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId
+        qrCodePayload.traceLocation().apply {
+            description shouldBe "Apple Computer, Inc."
+            locationId.base64() shouldBe expectedLocationIdBase64
+            locationId.hex() shouldBe expectedLocationIdHex
+        }
+
+        // ByteArrays actually match, we construct the data the same way.
+        qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray()
+    }
+
+    @Test
+    fun `mock server test data - 2`() {
+        val qrCodePayloadBase64 =
+            "CAESKQgBEhRXYXN0ZSBNYW5hZ2VtZW50IEluYxoPMzc5IE96Zm9jIE1hbm9yGnYIARJgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGhCUpoAjtgZJPU4suuQZ6dVUIgcIARAIGJ4E"
+        val expectedLocationIdHex = "2c4e7a3be61004ed952cc189e85039e01be65f3d82439c8c7fe0f23b12ffa523"
+        val expectedLocationIdBase64 = "LE56O+YQBO2VLMGJ6FA54BvmXz2CQ5yMf+DyOxL/pSM="
+
+        // Payload -> locationId (direct from raw)
+        val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes
+        totalByteSequence.toByteString().sha256().base64() shouldBe expectedLocationIdBase64
+
+        // TraceLocation -> locationId (manual reconstruction)
+        val traceLocation = TraceLocation(
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_PUBLIC_BUILDING,
+            description = "Waste Management Inc",
+            address = "379 Ozfoc Manor",
+            startDate = null,
+            endDate = null,
+            defaultCheckInLengthInMinutes = 542,
+            cryptographicSeed = "lKaAI7YGST1OLLrkGenVVA==".decodeBase64()!!,
+            cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD",
+            version = TraceLocation.VERSION
+        ).apply {
+            locationId.base64() shouldBe expectedLocationIdBase64
+            locationId.hex() shouldBe expectedLocationIdHex
+        }
+
+        // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        )
+
+        qrCodePayload.traceLocation().apply {
+            description shouldBe "Waste Management Inc"
+            locationId.base64() shouldBe expectedLocationIdBase64
+            locationId.hex() shouldBe expectedLocationIdHex
+        }
+
+        // ByteArrays actually match, we construct the data the same way.
+        qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray()
+    }
+
+    @Test
+    fun `mock server test data - 3`() {
+        val qrCodePayloadBase64 =
+            "CAESMggBEg9NZXRhbHMgVVNBIEluYy4aETk2NiBEaXZ1ZCBIZWlnaHRzKLD6qYMGMNDZtoMGGnYIARJgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGhD_mjsnT4b74d8M1P4cUflpIgcIARAKGOkJ"
+        val expectedLocationIdHex = "7051e04206ef9caf3a3165e82d0fec4cfe7ade770a2e01d9c0e456add760934d"
+        val expectedLocationIdBase64 = "cFHgQgbvnK86MWXoLQ/sTP563ncKLgHZwORWrddgk00="
+
+        // Payload -> locationId (direct from raw)
+        val payloadBytes = qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        val totalByteSequence = "CWA-GUID".toByteArray() + payloadBytes
+        totalByteSequence.toByteString().sha256().apply {
+            base64() shouldBe expectedLocationIdBase64
+        }
+
+        // TraceLocation -> locationId (manual reconstruction)
+        val traceLocation = TraceLocation(
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_CLUB_ACTIVITY,
+            description = "Metals USA Inc.",
+            address = "966 Divud Heights",
+            startDate = Instant.ofEpochSecond(1617591600),
+            endDate = Instant.ofEpochSecond(1617800400),
+            defaultCheckInLengthInMinutes = 1257,
+            cryptographicSeed = "/5o7J0+G++HfDNT+HFH5aQ==".decodeBase64()!!,
+            cnPublicKey = "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD",
+            version = TraceLocation.VERSION
+        ).apply {
+            locationId.base64() shouldBe expectedLocationIdBase64
+            locationId.hex() shouldBe expectedLocationIdHex
+        }
+
+        // Full circle: Payload -> tracelocation -> qrpayload -> bytearray -> locationId
+        val qrCodePayload = TraceLocationOuterClass.QRCodePayload.parseFrom(
+            qrCodePayloadBase64.decodeBase64()!!.toByteArray()
+        )
+
+        qrCodePayload.traceLocation().apply {
+            description shouldBe "Metals USA Inc."
+            locationId.base64() shouldBe expectedLocationIdBase64
+            locationId.hex() shouldBe expectedLocationIdHex
+        }
+
+        // ByteArrays actually match, we construct the data the same way.
+        qrCodePayload.toByteArray() shouldBe traceLocation.qrCodePayload().toByteArray()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..aee6965e78df1ed7e771ec7b6bb041c3fbaa6ec6
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/TraceLocationUrlTest.kt
@@ -0,0 +1,62 @@
+package de.rki.coronawarnapp.eventregistration.events
+
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
+import io.kotest.matchers.shouldBe
+import kotlinx.coroutines.test.runBlockingTest
+import okio.ByteString.Companion.decodeBase64
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class TraceLocationUrlTest : BaseTest() {
+
+    @Test
+    fun `locationUrl 1`() = runBlockingTest {
+        val traceLocation = TraceLocation(
+            id = 1,
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_TEMPORARY_OTHER,
+            description = "My Birthday Party",
+            address = "at my place",
+            startDate = 2687955L.secondsToInstant(),
+            endDate = 2687991L.secondsToInstant(),
+            defaultCheckInLengthInMinutes = null,
+            cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!,
+            cnPublicKey = PUB_KEY,
+            version = TraceLocation.VERSION
+        )
+
+        traceLocation.locationUrl shouldBe
+            "https://e.coronawarn.app?v=1#CAESLAgBEhFNeSBCaXJ0aGRheSBQYXJ0eRoLYXQgbXkgcGxhY2Uo04ekATD3h6QBGmoIAR" +
+            "JgOMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJ" +
+            "QkEIujVwoCNK0MNGuDK1ayjGxeDc4UDGgQxMjM0IgQIARAC"
+    }
+
+    @Test
+    fun `locationUrl 2`() = runBlockingTest {
+        val traceLocation = TraceLocation(
+            id = 2,
+            type = TraceLocationOuterClass.TraceLocationType.LOCATION_TYPE_PERMANENT_OTHER,
+            description = "Icecream Shop",
+            address = "Main Street 1",
+            startDate = null,
+            endDate = null,
+            defaultCheckInLengthInMinutes = 10,
+            cryptographicSeed = CRYPTOGRAPHIC_SEED.decodeBase64()!!,
+            cnPublicKey = PUB_KEY,
+            version = TraceLocation.VERSION
+        )
+
+        traceLocation.locationUrl shouldBe
+            "https://e.coronawarn.app?v=1#CAESIAgBEg1JY2VjcmVhbSBTaG9wGg1NYWluIFN0cmVldCAxGmoIARJgOMTa6eYSiaDv8l" +
+            "W13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9eq-voxQ1EcFJQkEIujVwoCNK0MNG" +
+            "uDK1ayjGxeDc4UDGgQxMjM0IgYIARABGAo"
+    }
+
+    companion object {
+        private const val CRYPTOGRAPHIC_SEED = "MTIzNA=="
+        private const val PUB_KEY =
+            "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" +
+                "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD"
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bac5aeb8f0611dc7c43bab07185fb0d492ea00df
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/DefaultQrCodePosterTemplateTest.kt
@@ -0,0 +1,39 @@
+package de.rki.coronawarnapp.eventregistration.events.server.qrcodepostertemplate
+
+import android.content.Context
+import android.os.Build
+import androidx.test.core.app.ApplicationProvider
+import de.rki.coronawarnapp.util.HashExtensions.toSHA256
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import testhelpers.BaseTest
+import testhelpers.EmptyApplication
+
+@Config(sdk = [Build.VERSION_CODES.P], application = EmptyApplication::class)
+@RunWith(RobolectricTestRunner::class)
+class DefaultQrCodePosterTemplateTest : BaseTest() {
+
+    private val templateName = "default_qr_code_poster_template_android.bin"
+    private val checkSumName = "default_qr_code_poster_template_android.sha256"
+
+    @Before
+    fun setup() {
+        MockKAnnotations.init(this)
+    }
+
+    val context: Context
+        get() = ApplicationProvider.getApplicationContext()
+
+    @Test
+    fun `current default matches checksum`() {
+        val template = context.assets.open(templateName).readBytes()
+        val sha256 = context.assets.open(checkSumName).readBytes().toString(Charsets.UTF_8)
+        sha256 shouldBe "1e972018100828aa63bc2559713cffa22d8db62f3ce56b3edbe72ad8cb7adb16"
+        template.toSHA256() shouldBe sha256
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt
index 6f2a1b4b719753cf830a3f455dcfb938cc6a9b3d..15083e52e544f6bc9586f5963ca5f7ba1071eb78 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/events/server/qrcodepostertemplate/QrCodePosterTemplateServerTest.kt
@@ -20,6 +20,7 @@ internal class QrCodePosterTemplateServerTest : BaseTest() {
 
     @MockK lateinit var api: QrCodePosterTemplateApiV1
     @MockK lateinit var signatureValidation: SignatureValidation
+    @MockK lateinit var defaultTemplateSource: DefaultQrCodePosterTemplateSource
 
     /**
      * Info: [QrCodePosterTemplateApiV1Test] is testing if the ETag is set correctly
@@ -30,11 +31,13 @@ internal class QrCodePosterTemplateServerTest : BaseTest() {
         MockKAnnotations.init(this)
 
         every { signatureValidation.hasValidSignature(any(), any()) } returns true
+        every { defaultTemplateSource.getDefaultQrCodePosterTemplate() } returns "CACHE".toByteArray()
     }
 
     private fun createInstance() = QrCodePosterTemplateServer(
         api = api,
-        signatureValidation = signatureValidation
+        signatureValidation = signatureValidation,
+        defaultTemplateSource = defaultTemplateSource
     )
 
     @Test
@@ -45,12 +48,12 @@ internal class QrCodePosterTemplateServerTest : BaseTest() {
 
         createInstance().downloadQrCodePosterTemplate().apply {
             template.toStringUtf8().substring(0, 22) shouldBe "<vector xmlns:android="
-            offsetX shouldBe 10
-            offsetY shouldBe 10
+            offsetX shouldBe 10.0f
+            offsetY shouldBe 10.0f
             qrCodeSideLength shouldBe 100
             with(descriptionTextBox) {
-                offsetX shouldBe 10
-                offsetY shouldBe 50
+                offsetX shouldBe 0.0f
+                offsetY shouldBe 0.0f
                 width shouldBe 100
                 height shouldBe 20
                 fontSize shouldBe 10
@@ -62,16 +65,14 @@ internal class QrCodePosterTemplateServerTest : BaseTest() {
     }
 
     @Test
-    fun `should throw exception if signature is invalid`() = runBlockingTest {
+    fun `should fallback to cached or default template if signature is invalid`() = runBlockingTest {
         every { signatureValidation.hasValidSignature(any(), any()) } returns false
 
         coEvery {
             api.getQrCodePosterTemplate()
         } returns Response.success(POSTER_BUNDLE.toResponseBody())
 
-        shouldThrow<QrCodePosterTemplateInvalidResponseException> {
-            createInstance().downloadQrCodePosterTemplate()
-        }
+        createInstance().getTemplateFromApiOrCache() shouldBe "CACHE".toByteArray()
     }
 
     @Test
@@ -86,13 +87,12 @@ internal class QrCodePosterTemplateServerTest : BaseTest() {
     }
 
     @Test
-    fun `should return default poster template when response is not successful`() = runBlockingTest {
-        // TODO
-    }
+    fun `should fallback to cached or default template when response is not successful`() = runBlockingTest {
+        coEvery {
+            api.getQrCodePosterTemplate()
+        } returns Response.error(404, "ERROR".toResponseBody())
 
-    @Test
-    fun `should return latest cached template when response is not successful`() = runBlockingTest {
-        // TODO
+        createInstance().getTemplateFromApiOrCache() shouldBe "CACHE".toByteArray()
     }
 
     companion object {
@@ -127,6 +127,7 @@ internal class QrCodePosterTemplateServerTest : BaseTest() {
                 "</vector>\n"""))
             .build()*/
 
+        // TODO update this bundle to send PDF file not XML
         private val POSTER_BUNDLE = (
             "504b03040a000000080014867d52008c85fefb000000ab0100000a0000006578706f72742e62" +
                 "696e7d90cf4bc33014c7071e949c0415bc0825bbc8685f9676edbad216440fbb78f61c9a6a8ae912da9089ff80ffb66957700e" +
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt
index 0dc18d47738d237a87375ba55922621dbe29814a..c7661962153098c69e209b30e0ed0f02563f3cb5 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/storage/retention/CheckInCleanerTest.kt
@@ -71,7 +71,6 @@ internal class CheckInCleanerTest : BaseTest() {
 
     private fun createCheckIn(checkOutDate: Instant) = CheckIn(
         traceLocationId = "traceLocationId1".encode(),
-        traceLocationIdHash = "traceLocationIdHash1".encode(),
         version = 1,
         type = 1,
         description = "",
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 e420e844c1b1e06c1880db9355d784c32ebcad02..04fbb3a38e611423435fd717cc6e31d76f76e096 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
@@ -25,7 +25,6 @@ class CheckOutHandlerTest : BaseTest() {
     private val testCheckIn = CheckIn(
         id = 42L,
         traceLocationId = "traceLocationId1".encode(),
-        traceLocationIdHash = "traceLocationIdHash1".encode(),
         version = 1,
         type = 1,
         description = "Restaurant",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt
index 41c1f47c9691f7f5051f9d7c976e79754d490eda..4958a8cb294eb4916e23de2f9ca1fe5d385ccd3f 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/auto/AutoCheckOutTest.kt
@@ -38,7 +38,6 @@ class AutoCheckOutTest : BaseTest() {
     private val baseCheckin = CheckIn(
         id = 0L,
         traceLocationId = "traceLocationId1".encode(),
-        traceLocationIdHash = "traceLocationIdHash1".encode(),
         version = 1,
         type = 2,
         description = "brothers birthday",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt
index 92c141796ec390d5d61f83b87ebec7c51474608f..f87f49945884faad710d647b00742aa3b4dfcd6c 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/common/TraceLocationNotificationsTest.kt
@@ -37,7 +37,7 @@ class TraceLocationNotificationsTest : BaseTest() {
         }
     }
 
-    fun createInstance() = TraceLocationNotifications(
+    fun createInstance() = PresenceTracingNotifications(
         context = context,
         apiLevel = apiLevel,
         notificationManagerCompat = notificationManager
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt
deleted file mode 100644
index 1a3dcc0488041e444d3c299a2ac98abf1084791f..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/CheckInWarningMatcherTest.kt
+++ /dev/null
@@ -1,255 +0,0 @@
-package de.rki.coronawarnapp.presencetracing.risk
-
-import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
-import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage
-import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningRepository
-import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
-import de.rki.coronawarnapp.util.debug.measureTime
-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.flowOf
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import testhelpers.BaseTest
-import testhelpers.TestDispatcherProvider
-import timber.log.Timber
-
-class CheckInWarningMatcherTest : BaseTest() {
-
-    @MockK lateinit var checkInsRepository: CheckInRepository
-    @MockK lateinit var traceTimeIntervalWarningRepository: TraceTimeIntervalWarningRepository
-    @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository
-
-    @BeforeEach
-    fun setup() {
-        MockKAnnotations.init(this)
-        coEvery { presenceTracingRiskRepository.reportSuccessfulCalculation(any()) } just Runs
-        coEvery { presenceTracingRiskRepository.deleteAllMatches() } just Runs
-        coEvery { presenceTracingRiskRepository.deleteStaleData() } just Runs
-        // TODO tests
-        coEvery { presenceTracingRiskRepository.deleteMatchesOfPackage(any()) } just Runs
-        coEvery { presenceTracingRiskRepository.markPackageProcessed(any()) } just Runs
-    }
-
-    @Test
-    fun `reports new matches`() {
-        val checkIn1 = createCheckIn(
-            id = 2L,
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
-            startDateStr = "2021-03-04T10:15+01:00",
-            endDateStr = "2021-03-04T10:17+01:00"
-        )
-        val checkIn2 = createCheckIn(
-            id = 3L,
-            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
-            startDateStr = "2021-03-04T09:15+01:00",
-            endDateStr = "2021-03-04T10:12+01:00"
-        )
-
-        val warning1 = createWarning(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
-            startIntervalDateStr = "2021-03-04T10:00+01:00",
-            period = 6,
-            transmissionRiskLevel = 8
-        )
-
-        val warning2 = createWarning(
-            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
-            startIntervalDateStr = "2021-03-04T10:00+01:00",
-            period = 6,
-            transmissionRiskLevel = 8
-        )
-
-        every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2))
-
-        val warningPackage = object : TraceTimeIntervalWarningPackage {
-            override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
-                return listOf(warning1, warning2)
-            }
-
-            override val warningPackageId: String
-                get() = "id"
-        }
-
-        every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage))
-
-        runBlockingTest {
-            createInstance().execute()
-            coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(any()) }
-            coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() }
-        }
-    }
-
-    @Test
-    fun `report empty list if no matches found`() {
-        val checkIn1 = createCheckIn(
-            id = 2L,
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
-            startDateStr = "2021-03-04T10:15+01:00",
-            endDateStr = "2021-03-04T10:17+01:00"
-        )
-        val checkIn2 = createCheckIn(
-            id = 3L,
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
-            startDateStr = "2021-03-04T09:15+01:00",
-            endDateStr = "2021-03-04T10:12+01:00"
-        )
-
-        val warning1 = createWarning(
-            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
-            startIntervalDateStr = "2021-03-04T10:00+01:00",
-            period = 6,
-            transmissionRiskLevel = 8
-        )
-
-        val warning2 = createWarning(
-            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
-            startIntervalDateStr = "2021-03-04T10:00+01:00",
-            period = 6,
-            transmissionRiskLevel = 8
-        )
-
-        every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2))
-
-        val warningPackage = object : TraceTimeIntervalWarningPackage {
-            override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
-                return listOf(warning1, warning2)
-            }
-
-            override val warningPackageId: String
-                get() = "id"
-        }
-
-        every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage))
-
-        runBlockingTest {
-            createInstance().execute()
-            coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) }
-            coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() }
-        }
-    }
-
-    @Test
-    fun `report empty list if package is empty`() {
-        val checkIn1 = createCheckIn(
-            id = 2L,
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
-            startDateStr = "2021-03-04T10:15+01:00",
-            endDateStr = "2021-03-04T10:17+01:00"
-        )
-        val checkIn2 = createCheckIn(
-            id = 3L,
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
-            startDateStr = "2021-03-04T09:15+01:00",
-            endDateStr = "2021-03-04T10:12+01:00"
-        )
-
-        every { checkInsRepository.allCheckIns } returns flowOf(listOf(checkIn1, checkIn2))
-
-        val warningPackage = object : TraceTimeIntervalWarningPackage {
-            override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
-                return listOf()
-            }
-
-            override val warningPackageId: String
-                get() = "id"
-        }
-
-        every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage))
-
-        runBlockingTest {
-            createInstance().execute()
-            coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) }
-            coVerify(exactly = 0) { presenceTracingRiskRepository.deleteAllMatches() }
-        }
-    }
-
-    @Test
-    fun `deletes all matches if no check-ins`() {
-
-        val warning1 = createWarning(
-            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
-            startIntervalDateStr = "2021-03-04T10:00+01:00",
-            period = 6,
-            transmissionRiskLevel = 8
-        )
-
-        val warning2 = createWarning(
-            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
-            startIntervalDateStr = "2021-03-04T10:00+01:00",
-            period = 6,
-            transmissionRiskLevel = 8
-        )
-
-        every { checkInsRepository.allCheckIns } returns flowOf(listOf())
-
-        val warningPackage = object : TraceTimeIntervalWarningPackage {
-            override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
-                return listOf(warning1, warning2)
-            }
-
-            override val warningPackageId: String
-                get() = "id"
-        }
-
-        every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage))
-
-        runBlockingTest {
-            createInstance().execute()
-            coVerify(exactly = 1) { presenceTracingRiskRepository.reportSuccessfulCalculation(emptyList()) }
-            coVerify(exactly = 1) { presenceTracingRiskRepository.deleteAllMatches() }
-        }
-    }
-
-    @Test
-    fun `test mass data`() {
-        val checkIns = (1L..100L).map {
-            createCheckIn(
-                id = it,
-                traceLocationId = it.toString(),
-                startDateStr = "2021-03-04T09:50+01:00",
-                endDateStr = "2021-03-04T10:05:15+01:00"
-            )
-        }
-        val warnings = (1L..1000L).map {
-            createWarning(
-                traceLocationId = it.toString(),
-                startIntervalDateStr = "2021-03-04T10:00+01:00",
-                period = 6,
-                transmissionRiskLevel = 8
-            )
-        }
-
-        val warningPackage = object : TraceTimeIntervalWarningPackage {
-            override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
-                return warnings
-            }
-
-            override val warningPackageId: String
-                get() = "id"
-        }
-
-        every { checkInsRepository.allCheckIns } returns flowOf(checkIns)
-        every { traceTimeIntervalWarningRepository.allWarningPackages } returns flowOf(listOf(warningPackage))
-
-        runBlockingTest {
-            measureTime(
-                { Timber.d("Time to compare 200 checkIns with 1000 warnings: $it millis") },
-                { createInstance().execute() }
-            )
-        }
-    }
-
-    private fun createInstance() = CheckInWarningMatcher(
-        checkInsRepository,
-        traceTimeIntervalWarningRepository,
-        presenceTracingRiskRepository,
-        TestDispatcherProvider()
-    )
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b8beb8b1106ba1b21dc47c0596ea523728adc571
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/CheckInWarningMatcherTest.kt
@@ -0,0 +1,255 @@
+package de.rki.coronawarnapp.presencetracing.risk.calculation
+
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import testhelpers.TestDispatcherProvider
+
+class CheckInWarningMatcherTest : BaseTest() {
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+    }
+
+    private fun createInstance() = CheckInWarningMatcher(
+        TestDispatcherProvider()
+    )
+
+    @Test
+    fun `reports new matches`() {
+        val checkIn1 = createCheckIn(
+            id = 2L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T10:15+01:00",
+            endDateStr = "2021-03-04T10:17+01:00"
+        )
+        val checkIn2 = createCheckIn(
+            id = 3L,
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startDateStr = "2021-03-04T09:15+01:00",
+            endDateStr = "2021-03-04T10:12+01:00"
+        )
+
+        val warning1 = createWarning(
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+
+        val warning2 = createWarning(
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+
+        val warningPackage = object : TraceWarningPackage {
+            override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+                return listOf(warning1, warning2)
+            }
+
+            override val packageId: WarningPackageId
+                get() = "id"
+        }
+
+        runBlockingTest {
+            val result = createInstance().process(
+                checkIns = listOf(checkIn1, checkIn2),
+                warningPackages = listOf(warningPackage)
+            )
+            result.apply {
+                successful shouldBe true
+                processedPackages.single().warningPackage shouldBe warningPackage
+                processedPackages.single().apply {
+                    overlaps.size shouldBe 2
+                    overlaps.any { it.checkInId == 2L } shouldBe true
+                    overlaps.any { it.checkInId == 3L } shouldBe true
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `report empty list if no matches found`() {
+        val checkIn1 = createCheckIn(
+            id = 2L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T10:15+01:00",
+            endDateStr = "2021-03-04T10:17+01:00"
+        )
+        val checkIn2 = createCheckIn(
+            id = 3L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T09:15+01:00",
+            endDateStr = "2021-03-04T10:12+01:00"
+        )
+
+        val warning1 = createWarning(
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+
+        val warning2 = createWarning(
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+
+        val warningPackage = object : TraceWarningPackage {
+            override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+                return listOf(warning1, warning2)
+            }
+
+            override val packageId: WarningPackageId
+                get() = "id"
+        }
+
+        runBlockingTest {
+            val result = createInstance().process(
+                checkIns = listOf(checkIn1, checkIn2),
+                warningPackages = listOf(warningPackage),
+            )
+
+            result.apply {
+                successful shouldBe true
+                processedPackages.single().warningPackage shouldBe warningPackage
+                processedPackages.single().overlaps.size shouldBe 0
+            }
+        }
+    }
+
+    @Test
+    fun `report empty list if package is empty`() {
+        val checkIn1 = createCheckIn(
+            id = 2L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T10:15+01:00",
+            endDateStr = "2021-03-04T10:17+01:00"
+        )
+        val checkIn2 = createCheckIn(
+            id = 3L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T09:15+01:00",
+            endDateStr = "2021-03-04T10:12+01:00"
+        )
+
+        val warningPackage = object : TraceWarningPackage {
+            override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+                return listOf()
+            }
+
+            override val packageId: WarningPackageId
+                get() = "id"
+        }
+
+        runBlockingTest {
+            val result = createInstance().process(
+                warningPackages = listOf(warningPackage),
+                checkIns = listOf(checkIn1, checkIn2),
+            )
+
+            result.apply {
+                successful shouldBe true
+                processedPackages.single().warningPackage shouldBe warningPackage
+                processedPackages.single().overlaps.size shouldBe 0
+            }
+        }
+    }
+
+    @Test
+    fun `report failure if matching throws exception`() {
+        val checkIn1 = createCheckIn(
+            id = 2L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T10:15+01:00",
+            endDateStr = "2021-03-04T10:17+01:00"
+        )
+        val checkIn2 = createCheckIn(
+            id = 3L,
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startDateStr = "2021-03-04T09:15+01:00",
+            endDateStr = "2021-03-04T10:12+01:00"
+        )
+
+        val warningPackage = object : TraceWarningPackage {
+            override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+                throw Exception()
+            }
+
+            override val packageId: String
+                get() = "id"
+        }
+
+        runBlockingTest {
+            val result = createInstance().process(
+                checkIns = listOf(checkIn1, checkIn2),
+                warningPackages = listOf(warningPackage),
+            )
+
+            result.apply {
+                successful shouldBe false
+                processedPackages shouldBe emptyList()
+            }
+        }
+    }
+
+    @Test
+    fun `partial processing is possible on exceptions`() {
+        val checkIn1 = createCheckIn(
+            id = 2L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T10:15+01:00",
+            endDateStr = "2021-03-04T10:17+01:00"
+        )
+        val checkIn2 = createCheckIn(
+            id = 3L,
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startDateStr = "2021-03-04T09:15+01:00",
+            endDateStr = "2021-03-04T10:12+01:00"
+        )
+
+        val warningPackage1 = object : TraceWarningPackage {
+            override suspend fun extractWarnings() = throw Exception()
+
+            override val packageId: WarningPackageId = "id1"
+        }
+        val warningPackage2 = object : TraceWarningPackage {
+            override suspend fun extractWarnings() = listOf(
+                createWarning(
+                    traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+                    startIntervalDateStr = "2021-03-04T10:00+01:00",
+                    period = 6,
+                    transmissionRiskLevel = 8
+                )
+            )
+
+            override val packageId: WarningPackageId = "id2"
+        }
+
+        runBlockingTest {
+            val result = createInstance().process(
+                checkIns = listOf(checkIn1, checkIn2),
+                warningPackages = listOf(warningPackage1, warningPackage2),
+            )
+
+            result.apply {
+                successful shouldBe false
+                processedPackages.single().apply {
+                    warningPackage shouldBe warningPackage2
+                    overlaps.single().checkInId shouldBe checkIn2.id
+                }
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt
similarity index 79%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt
index cc9d5e58028909f8a18c7185d3cf704340f61f87..3ddd887cdcf12de4c9a628bf24d0498350e62840 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/FindMatchesTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/FindMatchesTest.kt
@@ -1,6 +1,7 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
-import de.rki.coronawarnapp.eventregistration.checkins.download.TraceTimeIntervalWarningPackage
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage
 import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
 import io.kotest.matchers.shouldBe
 import kotlinx.coroutines.test.runBlockingTest
@@ -36,11 +37,12 @@ class FindMatchesTest {
             period = 6,
             transmissionRiskLevel = 8
         )
-        val warningPackage = object : TraceTimeIntervalWarningPackage {
-            override suspend fun extractTraceTimeIntervalWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+        val warningPackage = object : TraceWarningPackage {
+            override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
                 return listOf(warning1, warning2)
             }
-            override val warningPackageId: String
+
+            override val packageId: WarningPackageId
                 get() = "id"
         }
         runBlockingTest {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt
similarity index 72%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt
index fa1af775d15e0d2b44a4281f642e9a3d828f738a..67cdb5a057bde725542c8cd8712ade2d4aeb3ba0 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/OverlapTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/OverlapTest.kt
@@ -1,24 +1,51 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
-import com.google.protobuf.ByteString.copyFromUtf8
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
 import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
-import de.rki.coronawarnapp.util.HashExtensions.toSHA256
+import de.rki.coronawarnapp.util.toOkioByteString
+import de.rki.coronawarnapp.util.toProtoByteString
 import io.kotest.matchers.shouldBe
+import okio.ByteString.Companion.decodeHex
 import okio.ByteString.Companion.encode
 import org.joda.time.Duration
 import org.joda.time.Instant
-import org.junit.Test
+import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 
 class OverlapTest : BaseTest() {
 
-    val id = "id"
+    private val id = "id"
+
+    private val locationId = "afa27b44d43b02a9fea41d13cedc2e4016cfcf87c5dbf990e593669aa8ce286d"
+    private val locationIdHash = "0f37dac11d1b8118ea0b44303400faa5e3b876da9d758058b5ff7dc2e5da8230"
+
+    @Test
+    fun `test helper method createCheckIn`() {
+        val checkIn = createCheckIn(
+            traceLocationId = locationId,
+            startDateStr = "2021-03-04T09:30+01:00",
+            endDateStr = "2021-03-04T09:45+01:00"
+        )
+
+        checkIn.traceLocationId shouldBe locationId.decodeHex()
+        checkIn.traceLocationIdHash shouldBe locationIdHash.decodeHex()
+    }
+
+    @Test
+    fun `test helper method createWarning`() {
+        val warning = createWarning(
+            traceLocationId = locationId,
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+        warning.locationIdHash.toOkioByteString().hex() shouldBe locationIdHash
+    }
 
     @Test
     fun `returns null if guids do not match`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T09:30+01:00",
             endDateStr = "2021-03-04T09:45+01:00"
         ).calculateOverlap(
@@ -35,12 +62,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns null if check-in precedes warning`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T09:30+01:00",
             endDateStr = "2021-03-04T09:45+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -52,12 +79,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns null if check-in is preceded by warning`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T11:15+01:00",
             endDateStr = "2021-03-04T11:20+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -69,12 +96,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns null if check-in meets warning at the start`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T09:30+01:00",
             endDateStr = "2021-03-04T10:00+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -86,12 +113,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns null if check-in meets warning at the end`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T11:00+01:00",
             endDateStr = "2021-03-04T11:10+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -103,12 +130,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap if check-in overlaps warning at the start`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T09:30+01:00",
             endDateStr = "2021-03-04T10:12+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -120,12 +147,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap if check-in overlaps warning at the end`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T10:45+01:00",
             endDateStr = "2021-03-04T11:12+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -137,12 +164,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap if check-in starts warning`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T10:00+01:00",
             endDateStr = "2021-03-04T10:13+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -154,12 +181,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap if check-in during warning`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T10:15+01:00",
             endDateStr = "2021-03-04T10:17+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -171,12 +198,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap if check-in finishes warning`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T10:30+01:00",
             endDateStr = "2021-03-04T11:00+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -188,12 +215,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap if check-in equals warning`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T10:00+01:00",
             endDateStr = "2021-03-04T11:00+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -205,12 +232,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap after rounding (up)`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T09:50+01:00",
             endDateStr = "2021-03-04T10:05:45+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -222,12 +249,12 @@ class OverlapTest : BaseTest() {
     @Test
     fun `returns overlap after rounding (down)`() {
         createCheckIn(
-            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            traceLocationId = locationId,
             startDateStr = "2021-03-04T09:50+01:00",
             endDateStr = "2021-03-04T10:05:15+01:00"
         ).calculateOverlap(
             createWarning(
-                traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+                traceLocationId = locationId,
                 startIntervalDateStr = "2021-03-04T10:00+01:00",
                 period = 6,
                 transmissionRiskLevel = 8
@@ -244,8 +271,7 @@ fun createCheckIn(
     endDateStr: String
 ) = CheckIn(
     id = id,
-    traceLocationId = traceLocationId.toSHA256().encode(),
-    traceLocationIdHash = traceLocationId.toSHA256().encode(),
+    traceLocationId = traceLocationId.decodeHex(),
     version = 1,
     type = 2,
     description = "My birthday party",
@@ -266,8 +292,8 @@ fun createWarning(
     startIntervalDateStr: String,
     period: Int,
     transmissionRiskLevel: Int
-) = TraceWarning.TraceTimeIntervalWarning.newBuilder()
-    .setLocationIdHash(copyFromUtf8(traceLocationId.toSHA256()))
+): TraceWarning.TraceTimeIntervalWarning = TraceWarning.TraceTimeIntervalWarning.newBuilder()
+    .setLocationIdHash(traceLocationId.decodeHex().sha256().toProtoByteString())
     .setPeriod(period)
     .setStartIntervalNumber((Duration(Instant.parse(startIntervalDateStr).millis).standardMinutes / 10).toInt())
     .setTransmissionRiskLevel(transmissionRiskLevel)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt
similarity index 98%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt
index 17c8b74c36daa1ad362f75f499299ef4b758650e..f59cd16228473db4a9530fa15857c09c758b048a 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskCalculatorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
 import de.rki.coronawarnapp.risk.RiskState
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt
similarity index 98%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt
index e5334348dd56ede52dde06a5ab8106d777ac3d98..dd07536e331c18350214ed1ed23f8f70ca1898bf 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/PresenceTracingRiskMapperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt
@@ -1,4 +1,4 @@
-package de.rki.coronawarnapp.presencetracing.risk
+package de.rki.coronawarnapp.presencetracing.risk.calculation
 
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.appconfig.ConfigData
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e44656741f40654af7de030c9812598a591047f4
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt
@@ -0,0 +1,223 @@
+package de.rki.coronawarnapp.presencetracing.risk.execution
+
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher
+import de.rki.coronawarnapp.presencetracing.risk.calculation.createCheckIn
+import de.rki.coronawarnapp.presencetracing.risk.calculation.createWarning
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.warning.WarningPackageId
+import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningPackage
+import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceWarning
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.comparables.shouldBeLessThan
+import io.kotest.matchers.shouldNotBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.coVerifySequence
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.mockk
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Duration
+import org.joda.time.Instant
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import java.io.IOException
+
+class PresenceTracingWarningTaskTest : BaseTest() {
+
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var syncTool: TraceWarningPackageSyncTool
+    @MockK lateinit var checkInWarningMatcher: CheckInWarningMatcher
+    @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository
+    @MockK lateinit var traceWarningRepository: TraceWarningRepository
+    @MockK lateinit var checkInsRepository: CheckInRepository
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        every { timeStamper.nowUTC } returns Instant.ofEpochMilli(9000)
+        coEvery { syncTool.syncPackages() } returns mockk()
+        coEvery { checkInWarningMatcher.process(any(), any()) } answers {
+            CheckInWarningMatcher.Result(
+                successful = true,
+                processedPackages = listOf(
+                    CheckInWarningMatcher.MatchesPerPackage(
+                        warningPackage = WARNING_PKG,
+                        overlaps = listOf(mockk())
+                    )
+                )
+            )
+        }
+
+        traceWarningRepository.apply {
+            coEvery { unprocessedWarningPackages } returns flowOf(listOf(WARNING_PKG))
+            coEvery { markPackagesProcessed(any()) } just Runs
+        }
+
+        coEvery { checkInsRepository.allCheckIns } returns flowOf(listOf(CHECKIN_1, CHECKIN_2))
+
+        presenceTracingRiskRepository.apply {
+            coEvery { deleteAllMatches() } just Runs
+            coEvery { deleteStaleData() } just Runs
+            coEvery { reportCalculation(any(), any()) } just Runs
+        }
+    }
+
+    private fun createInstance() = PresenceTracingWarningTask(
+        timeStamper = timeStamper,
+        syncTool = syncTool,
+        checkInWarningMatcher = checkInWarningMatcher,
+        presenceTracingRiskRepository = presenceTracingRiskRepository,
+        traceWarningRepository = traceWarningRepository,
+        checkInsRepository = checkInsRepository,
+    )
+
+    @Test
+    fun `happy path, match result is reported successfully`() = runBlockingTest {
+        createInstance().run(mockk()) shouldNotBe null
+
+        coVerifySequence {
+            syncTool.syncPackages()
+            presenceTracingRiskRepository.deleteStaleData()
+            checkInsRepository.allCheckIns
+            traceWarningRepository.unprocessedWarningPackages
+
+            checkInWarningMatcher.process(any(), any())
+
+            presenceTracingRiskRepository.reportCalculation(
+                successful = true,
+                overlaps = any()
+            )
+            traceWarningRepository.markPackagesProcessed(listOf(WARNING_PKG.packageId))
+        }
+    }
+
+    @Test
+    fun `overall task errors lead to a reported failed calculation`() = runBlockingTest {
+        coEvery { syncTool.syncPackages() } throws IOException("Unexpected")
+
+        shouldThrow<IOException> {
+            createInstance().run(mockk())
+        }
+
+        coVerify {
+            presenceTracingRiskRepository.reportCalculation(
+                successful = false,
+                overlaps = emptyList()
+            )
+        }
+    }
+
+    @Test
+    fun `there are no check-ins to match against`() = runBlockingTest {
+        coEvery { checkInsRepository.allCheckIns } returns flowOf(emptyList())
+
+        createInstance().run(mockk()) shouldNotBe null
+
+        coVerifySequence {
+            syncTool.syncPackages()
+            presenceTracingRiskRepository.deleteStaleData()
+            checkInsRepository.allCheckIns
+
+            presenceTracingRiskRepository.deleteAllMatches()
+            presenceTracingRiskRepository.reportCalculation(successful = true)
+        }
+    }
+
+    @Test
+    fun `there are no warning packages to process`() = runBlockingTest {
+        coEvery { traceWarningRepository.unprocessedWarningPackages } returns flowOf(emptyList())
+
+        createInstance().run(mockk()) shouldNotBe null
+
+        coVerifySequence {
+            syncTool.syncPackages()
+            presenceTracingRiskRepository.deleteStaleData()
+            checkInsRepository.allCheckIns
+            traceWarningRepository.unprocessedWarningPackages
+
+            presenceTracingRiskRepository.reportCalculation(successful = true)
+        }
+    }
+
+    @Test
+    fun `report failure if matching throws exception`() = runBlockingTest {
+        coEvery { checkInWarningMatcher.process(any(), any()) } throws IllegalArgumentException()
+        shouldThrow<IllegalArgumentException> {
+            createInstance().run(mockk()) shouldNotBe null
+        }
+
+        coVerifySequence {
+            syncTool.syncPackages()
+            presenceTracingRiskRepository.deleteStaleData()
+            checkInsRepository.allCheckIns
+            traceWarningRepository.unprocessedWarningPackages
+
+            checkInWarningMatcher.process(any(), any())
+
+            presenceTracingRiskRepository.reportCalculation(
+                successful = false,
+                overlaps = any()
+            )
+        }
+
+        coVerify(exactly = 0) {
+            traceWarningRepository.markPackagesProcessed(any())
+        }
+    }
+
+    @Test
+    fun `task timeout is constrained to less than 9min`() {
+        // Worker execution time
+        val maxDuration = Duration.standardMinutes(9).plus(1)
+        PresenceTracingWarningTask.Config().executionTimeout shouldBeLessThan maxDuration
+    }
+
+    companion object {
+        val CHECKIN_1 = createCheckIn(
+            id = 2L,
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startDateStr = "2021-03-04T10:15+01:00",
+            endDateStr = "2021-03-04T10:17+01:00"
+        )
+        val CHECKIN_2 = createCheckIn(
+            id = 3L,
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startDateStr = "2021-03-04T09:15+01:00",
+            endDateStr = "2021-03-04T10:12+01:00"
+        )
+
+        val WARNING_1 = createWarning(
+            traceLocationId = "fe84394e73838590cc7707aba0350c130f6d0fb6f0f2535f9735f481dee61871",
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+
+        val WARNING_2 = createWarning(
+            traceLocationId = "69eb427e1a48133970486244487e31b3f1c5bde47415db9b52cc5a2ece1e0060",
+            startIntervalDateStr = "2021-03-04T10:00+01:00",
+            period = 6,
+            transmissionRiskLevel = 8
+        )
+
+        val WARNING_PKG = object : TraceWarningPackage {
+            override suspend fun extractWarnings(): List<TraceWarning.TraceTimeIntervalWarning> {
+                return listOf(WARNING_1, WARNING_2)
+            }
+
+            override val packageId: WarningPackageId
+                get() = "id"
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dee16970f1d0b8e9b4ff4621650b6167b1e062ed
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningWorkerTest.kt
@@ -0,0 +1,95 @@
+package de.rki.coronawarnapp.presencetracing.risk.execution
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerParameters
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.TaskRequest
+import de.rki.coronawarnapp.task.TaskState
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.mockkStatic
+import io.mockk.slot
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class PresenceTracingWarningWorkerTest : BaseTest() {
+
+    @RelaxedMockK lateinit var workerParams: WorkerParameters
+    @MockK lateinit var context: Context
+    @MockK lateinit var taskController: TaskController
+    @MockK(relaxed = true) lateinit var taskResult: TaskState
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        mockkStatic("de.rki.coronawarnapp.task.TaskControllerExtensionsKt")
+
+        coEvery { taskController.submitBlocking(any()) } returns taskResult
+
+        taskResult.apply {
+            every { isSuccessful } returns true
+            every { error } returns null
+        }
+    }
+
+    private fun createWorker() = PresenceTracingWarningWorker(
+        context = context,
+        workerParams = workerParams,
+        taskController = taskController
+    )
+
+    @Test
+    fun `worker runs task`() = runBlockingTest {
+        val slot = slot<TaskRequest>()
+        coEvery { taskController.submitBlocking(capture(slot)) } returns taskResult
+
+        val worker = createWorker()
+
+        worker.doWork() shouldBe ListenableWorker.Result.success()
+
+        slot.captured shouldBe DefaultTaskRequest(
+            id = slot.captured.id,
+            arguments = slot.captured.arguments,
+            type = PresenceTracingWarningTask::class,
+            originTag = "PresenceTracingWarningWorker",
+        )
+    }
+
+    @Test
+    fun `task errors lead to retry`() = runBlockingTest {
+        every { taskResult.isSuccessful } returns false
+        every { taskResult.error } returns Exception()
+
+        val worker = createWorker()
+
+        worker.doWork() shouldBe ListenableWorker.Result.retry()
+
+        coVerify {
+            taskController.submitBlocking(any())
+        }
+    }
+
+    @Test
+    fun `taskcontroller errors lead to retry`() = runBlockingTest {
+        coEvery { taskController.submitBlocking(any()) } throws Exception()
+
+        val worker = createWorker()
+
+        worker.doWork() shouldBe ListenableWorker.Result.retry()
+
+        coVerify {
+            taskController.submitBlocking(any())
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8bd44b79f7ebd3b74f884746953427d4c110e934
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/warning/download/TraceWarningPackageDownloaderTest.kt
@@ -0,0 +1,12 @@
+package de.rki.coronawarnapp.presencetracing.warning.download
+
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class TraceWarningPackageDownloaderTest : BaseTest() {
+
+    @Test
+    fun `errors during writeProtoBufToFile cause download to be marked as failed`() {
+        // TODO
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt
index f1b801552258d43dfbaa690f7beebcca753efdba..18acf7c6352c7cb688064d5e52989cf29d984c2c 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/EwRiskLevelResultExtensionsTest.kt
@@ -2,9 +2,9 @@ package de.rki.coronawarnapp.risk
 
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
-import io.kotest.matchers.longs.shouldBeInRange
 import io.kotest.matchers.shouldBe
 import io.mockk.mockk
+import org.joda.time.Duration
 import org.joda.time.Instant
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
@@ -31,11 +31,12 @@ class EwRiskLevelResultExtensionsTest : BaseTest() {
         emptyResults.tryLatestEwResultsWithDefaults().apply {
             lastCalculated.apply {
                 riskState shouldBe RiskState.LOW_RISK
-                val now = Instant.now().millis
-                calculatedAt.millis shouldBeInRange ((now - 60 * 1000L)..now + 60 * 1000L)
-            }
-            lastSuccessfullyCalculated.apply {
-                riskState shouldBe RiskState.CALCULATION_FAILED
+
+                calculatedAt.isAfter(Instant.EPOCH) shouldBe true
+                calculatedAt.isBefore(Instant.now().plus(Duration.standardHours(1))) shouldBe true
+                lastSuccessfullyCalculated.apply {
+                    riskState shouldBe RiskState.CALCULATION_FAILED
+                }
             }
         }
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
index d391e5cc2a9f7d54390d3eb777452ba6b2de1459..225b325d98625085679e70b2392d65944b53f471 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt
@@ -1,11 +1,14 @@
 package de.rki.coronawarnapp.risk
 
+import android.app.Notification
 import android.content.Context
+import androidx.core.app.NotificationCompat
 import androidx.core.app.NotificationManagerCompat
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings
 import de.rki.coronawarnapp.datadonation.survey.Surveys
 import de.rki.coronawarnapp.notification.GeneralNotifications
+import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
 import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED
 import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK
 import de.rki.coronawarnapp.risk.RiskState.LOW_RISK
@@ -15,6 +18,7 @@ import de.rki.coronawarnapp.storage.TracingSettings
 import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.device.ForegroundState
+import de.rki.coronawarnapp.util.notifications.setContentTextExpandable
 import io.kotest.matchers.shouldBe
 import io.mockk.Called
 import io.mockk.MockKAnnotations
@@ -47,18 +51,24 @@ class RiskLevelChangeDetectorTest : BaseTest() {
     @MockK lateinit var tracingSettings: TracingSettings
     @MockK lateinit var testResultDonorSettings: TestResultDonorSettings
 
+    @MockK lateinit var builder: NotificationCompat.Builder
+    @MockK lateinit var notification: Notification
+
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
 
         every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockFlowPreference(false)
         every { submissionSettings.isSubmissionSuccessful } returns false
-        every { foregroundState.isInForeground } returns flowOf(true)
+        every { foregroundState.isInForeground } returns flowOf(false)
         every { notificationManagerCompat.areNotificationsEnabled() } returns true
 
         every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs
         every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns null
 
+        every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp = any() } just Runs
+        every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp } returns null
+
         every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp = any() } just Runs
         every { riskLevelSettings.lastChangeToHighRiskLevelTimestamp } returns null
 
@@ -66,10 +76,17 @@ class RiskLevelChangeDetectorTest : BaseTest() {
 
         every { testResultDonorSettings.riskLevelTurnedRedTime } returns mockFlowPreference(null)
         every { testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null)
-        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns flowOf(listOf())
+
+        every { builder.build() } returns notification
+        every { builder.setContentTitle(any()) } returns builder
+        every { builder.setContentTextExpandable(any()) } returns builder
+        every { builder.setContentText(any()) } returns builder
+        every { builder.setStyle(any()) } returns builder
+        every { notificationHelper.newBaseBuilder() } returns builder
+        every { context.getString(any()) } returns ""
     }
 
-    private fun createRiskLevel(
+    private fun createEwRiskLevel(
         riskState: RiskState,
         calculatedAt: Instant = Instant.EPOCH,
         ewAggregatedRiskResult: EwAggregatedRiskResult? = null
@@ -83,6 +100,23 @@ class RiskLevelChangeDetectorTest : BaseTest() {
         override val daysWithEncounters: Int = 0
     }
 
+    private fun createPtRiskLevel(
+        riskState: RiskState,
+        calculatedAt: Instant = Instant.EPOCH
+    ): PtRiskLevelResult = PtRiskLevelResult(
+        calculatedAt = calculatedAt,
+        riskState = riskState
+    )
+
+    private fun createCombinedRiskLevel(
+        riskState: RiskState,
+        calculatedAt: Instant = Instant.EPOCH,
+        ewAggregatedRiskResult: EwAggregatedRiskResult? = null
+    ): CombinedEwPtRiskLevelResult = CombinedEwPtRiskLevelResult(
+        ewRiskLevelResult = createEwRiskLevel(riskState, calculatedAt, ewAggregatedRiskResult),
+        ptRiskLevelResult = createPtRiskLevel(riskState, calculatedAt)
+    )
+
     private fun createInstance(scope: CoroutineScope) = RiskLevelChangeDetector(
         context = context,
         appScope = scope,
@@ -99,7 +133,9 @@ class RiskLevelChangeDetectorTest : BaseTest() {
 
     @Test
     fun `nothing happens if there is only one result yet`() {
-        every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(listOf(createRiskLevel(LOW_RISK)))
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(listOf(createCombinedRiskLevel(LOW_RISK)))
+        every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(listOf(createEwRiskLevel(LOW_RISK)))
 
         runBlockingTest {
             val instance = createInstance(scope = this)
@@ -110,19 +146,28 @@ class RiskLevelChangeDetectorTest : BaseTest() {
             coVerifySequence {
                 notificationManagerCompat wasNot Called
                 surveys wasNot Called
+                testResultDonorSettings wasNot Called
             }
         }
     }
 
     @Test
-    fun `no risklevel change, nothing should happen`() {
+    fun `no risk level change, nothing should happen`() {
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(LOW_RISK),
-                createRiskLevel(LOW_RISK)
+                createEwRiskLevel(LOW_RISK),
+                createEwRiskLevel(LOW_RISK)
             )
         )
 
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(
+                listOf(
+                    createCombinedRiskLevel(LOW_RISK),
+                    createCombinedRiskLevel(LOW_RISK)
+                )
+            )
+
         runBlockingTest {
             val instance = createInstance(scope = this)
             instance.launch()
@@ -136,17 +181,53 @@ class RiskLevelChangeDetectorTest : BaseTest() {
         }
     }
 
-    // TODO test if risk level change for combined risk triggers notification
+    @Test
+    fun `combined risk state change from HIGH to LOW triggers notification`() {
+        every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
+            listOf(
+                createEwRiskLevel(LOW_RISK)
+            )
+        )
+
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(
+                listOf(
+                    createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                    createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH)
+                )
+            )
+
+        runBlockingTest {
+            val instance = createInstance(scope = this)
+            instance.launch()
+
+            advanceUntilIdle()
+
+            coVerifySequence {
+                submissionSettings.isSubmissionSuccessful
+                foregroundState.isInForeground
+                notificationHelper.newBaseBuilder()
+                notificationHelper.sendNotification(any(), any())
+            }
+        }
+    }
 
     @Test
-    fun `risklevel went from HIGH to LOW`() {
+    fun `combined risk state change from LOW to HIGH triggers notification`() {
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)),
-                createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH)
+                createEwRiskLevel(LOW_RISK)
             )
         )
 
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(
+                listOf(
+                    createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                    createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                )
+            )
+
         runBlockingTest {
             val instance = createInstance(scope = this)
             instance.launch()
@@ -154,21 +235,46 @@ class RiskLevelChangeDetectorTest : BaseTest() {
             advanceUntilIdle()
 
             coVerifySequence {
-                surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER)
+                submissionSettings.isSubmissionSuccessful
+                foregroundState.isInForeground
+                notificationHelper.newBaseBuilder()
+                notificationHelper.sendNotification(any(), any())
             }
         }
     }
 
-    // TODO test if risk level change for combined risk triggers notification
+    @Test
+    fun `risk level went from HIGH to LOW resets survey`() {
+        every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
+            listOf(
+                createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH)
+            )
+        )
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(listOf(createCombinedRiskLevel(LOW_RISK)))
+        runBlockingTest {
+            val instance = createInstance(scope = this)
+            instance.launch()
+
+            advanceUntilIdle()
+
+            coVerifySequence {
+                surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER)
+            }
+        }
+    }
 
     @Test
-    fun `risklevel went from LOW to HIGH`() {
+    fun `risk level went from LOW to HIGH`() {
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
-                createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
             )
         )
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(listOf(createCombinedRiskLevel(LOW_RISK)))
 
         runBlockingTest {
             val instance = createInstance(scope = this)
@@ -183,13 +289,15 @@ class RiskLevelChangeDetectorTest : BaseTest() {
     }
 
     @Test
-    fun `risklevel went from LOW to HIGH but it is has already been processed`() {
+    fun `risk level went from LOW to HIGH but it is has already been processed`() {
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
-                createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                createEwRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
             )
         )
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(listOf(createCombinedRiskLevel(LOW_RISK)))
         every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.EPOCH.plus(1)
 
         runBlockingTest {
@@ -205,23 +313,57 @@ class RiskLevelChangeDetectorTest : BaseTest() {
         }
     }
 
+    @Test
+    fun `combined risk level went from LOW to HIGH but it is has already been processed`() {
+        every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
+            listOf(
+                createEwRiskLevel(LOW_RISK)
+            )
+        )
+
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(
+                listOf(
+                    createCombinedRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)),
+                    createCombinedRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                )
+            )
+
+        every { riskLevelSettings.lastChangeCheckedRiskLevelCombinedTimestamp } returns Instant.EPOCH.plus(1)
+
+        runBlockingTest {
+            val instance = createInstance(scope = this)
+            instance.launch()
+
+            advanceUntilIdle()
+
+            coVerifySequence {
+                notificationManagerCompat wasNot Called
+                surveys wasNot Called
+            }
+        }
+    }
+
     @Test
     fun `riskLevelTurnedRedTime is only set once`() {
         testResultDonorSettings.riskLevelTurnedRedTime.update { Instant.EPOCH.plus(1) }
 
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(
+                createEwRiskLevel(
                     INCREASED_RISK,
                     calculatedAt = Instant.EPOCH.plus(2),
                     ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply {
                         every { isIncreasedRisk() } returns true
                     }
                 ),
-                createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
             )
         )
 
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(listOf(createCombinedRiskLevel(LOW_RISK)))
+
         runBlockingTest {
             val instance = createInstance(scope = this)
             instance.launch()
@@ -245,7 +387,7 @@ class RiskLevelChangeDetectorTest : BaseTest() {
     fun `mostRecentDateWithHighOrLowRiskLevel is updated every time`() {
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(
+                createEwRiskLevel(
                     INCREASED_RISK,
                     calculatedAt = Instant.EPOCH.plus(1),
                     ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply {
@@ -253,9 +395,11 @@ class RiskLevelChangeDetectorTest : BaseTest() {
                         every { isIncreasedRisk() } returns true
                     }
                 ),
-                createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
             )
         )
+        every { riskLevelStorage.latestCombinedEwPtRiskLevelResults } returns
+            flowOf(listOf(createCombinedRiskLevel(LOW_RISK)))
 
         runBlockingTest {
             val instance = createInstance(scope = this)
@@ -267,7 +411,7 @@ class RiskLevelChangeDetectorTest : BaseTest() {
 
         every { riskLevelStorage.latestEwRiskLevelResults } returns flowOf(
             listOf(
-                createRiskLevel(
+                createEwRiskLevel(
                     INCREASED_RISK,
                     calculatedAt = Instant.EPOCH.plus(1),
                     ewAggregatedRiskResult = mockk<EwAggregatedRiskResult>().apply {
@@ -275,7 +419,7 @@ class RiskLevelChangeDetectorTest : BaseTest() {
                         every { isIncreasedRisk() } returns false
                     }
                 ),
-                createRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
+                createEwRiskLevel(LOW_RISK, calculatedAt = Instant.EPOCH)
             )
         )
 
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 099afd385a811e046f64a17a1cc4e9a742d1cbaa..03c6758900c2520d87a5dbf7fa83f0224ad034a7 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
@@ -1,7 +1,11 @@
 package de.rki.coronawarnapp.risk.storage
 
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
+import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelResult
+import de.rki.coronawarnapp.risk.RiskState
+import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.ewCalculatedAt
 import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testAggregatedRiskPerDateResult
 import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindow
 import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testExposureWindowDaoWrapper
@@ -14,6 +18,9 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.AggregatedR
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.Factory
 import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResultsDao
+import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
@@ -32,6 +39,7 @@ import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.TestCoroutineScope
 import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
@@ -70,8 +78,9 @@ class BaseRiskLevelStorageTest : BaseTest() {
         // TODO proper tests
         coEvery { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow()
         coEvery { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow()
-        coEvery { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow()
+        coEvery { presenceTracingRiskRepository.allEntries() } returns emptyFlow()
         coEvery { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow()
+        coEvery { presenceTracingRiskRepository.clearAllTables() } just Runs
     }
 
     private fun createInstance(
@@ -104,7 +113,9 @@ class BaseRiskLevelStorageTest : BaseTest() {
             val instance = createInstance()
             val allEntries = instance.aggregatedRiskPerDateResultTables.allEntries()
             allEntries shouldBe testPersistedAggregatedRiskPerDateResultFlow
-            allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf(testAggregatedRiskPerDateResult)
+            allEntries.first().map { it.toAggregatedRiskPerDateResult() } shouldBe listOf(
+                testAggregatedRiskPerDateResult
+            )
 
             val aggregatedRiskPerDateResults = instance.ewDayRiskStates.first()
             aggregatedRiskPerDateResults shouldNotBe listOf(testPersistedAggregatedRiskPerDateResult)
@@ -112,6 +123,19 @@ class BaseRiskLevelStorageTest : BaseTest() {
         }
     }
 
+    @Test
+    fun `ptDayRiskStates are returned from database`() {
+        val testPresenceTracingDayRiskFlow = flowOf(listOf(testPresenceTracingDayRisk))
+        every { presenceTracingRiskRepository.presenceTracingDayRisk } returns testPresenceTracingDayRiskFlow
+
+        runBlockingTest {
+            val instance = createInstance()
+
+            val states = instance.ptDayRiskStates.first()
+            states shouldBe listOf(testPresenceTracingDayRisk)
+        }
+    }
+
     @Test
     fun `exposureWindows are returned from database and mapped`() {
         val testDaoWrappers = flowOf(listOf(testExposureWindowDaoWrapper))
@@ -170,6 +194,142 @@ class BaseRiskLevelStorageTest : BaseTest() {
         }
     }
 
+    @Test
+    fun `latestCombinedEwPtRiskLevelResults works when 2 pt result and 1 ew result are available`() {
+        every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(testRiskLevelResultDao))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+        val calculatedAt = ewCalculatedAt.plus(6000L)
+        every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf(
+            listOf(
+                PtRiskLevelResult(
+                    calculatedAt = calculatedAt,
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.CALCULATION_FAILED
+                ),
+                PtRiskLevelResult(
+                    calculatedAt = ewCalculatedAt.minus(1000L),
+                    presenceTracingDayRisk = listOf(testPresenceTracingDayRisk),
+                    riskState = RiskState.INCREASED_RISK
+                )
+            )
+        )
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first()
+            riskLevelResults.size shouldBe 2
+
+            riskLevelResults[0].calculatedAt shouldBe calculatedAt
+            riskLevelResults[0].riskState shouldBe RiskState.INCREASED_RISK
+            riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt
+            riskLevelResults[1].riskState shouldBe RiskState.INCREASED_RISK
+
+            verify {
+                riskResultTables.latestEntries(2)
+                presenceTracingRiskRepository.latestEntries(2)
+            }
+        }
+    }
+
+    @Test
+    fun `latestCombinedEwPtRiskLevelResults works when only one calc each is available`() {
+        every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(testRiskLevelResultDao))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+        val calculatedAt = ewCalculatedAt.minus(400L)
+        every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf(
+            listOf(
+                PtRiskLevelResult(
+                    calculatedAt = calculatedAt,
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.CALCULATION_FAILED
+                )
+            )
+        )
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first()
+            riskLevelResults.size shouldBe 2
+
+            riskLevelResults[0].calculatedAt shouldBe ewCalculatedAt
+            riskLevelResults[0].riskState shouldBe RiskState.INCREASED_RISK
+
+            // result from the combination with initial ew low risk result
+            riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(400L)
+            riskLevelResults[1].riskState shouldBe RiskState.CALCULATION_FAILED
+
+            verify {
+                riskResultTables.latestEntries(2)
+                presenceTracingRiskRepository.latestEntries(2)
+            }
+        }
+    }
+
+    @Test
+    fun `latestCombinedEwPtRiskLevelResults works when two calc each are available`() {
+        val ewResultDao1 = PersistedRiskLevelResultDao(
+            id = "id1",
+            calculatedAt = ewCalculatedAt,
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW,
+                totalMinimumDistinctEncountersWithLowRisk = 1,
+                totalMinimumDistinctEncountersWithHighRisk = 2,
+                mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+                mostRecentDateWithHighRisk = Instant.ofEpochMilli(4),
+                numberOfDaysWithLowRisk = 5,
+                numberOfDaysWithHighRisk = 6
+            )
+        )
+        val ewResultDao2 = PersistedRiskLevelResultDao(
+            id = "id2",
+            calculatedAt = ewCalculatedAt.minus(200L),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
+                totalMinimumDistinctEncountersWithLowRisk = 1,
+                totalMinimumDistinctEncountersWithHighRisk = 2,
+                mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+                mostRecentDateWithHighRisk = Instant.ofEpochMilli(4),
+                numberOfDaysWithLowRisk = 5,
+                numberOfDaysWithHighRisk = 6
+            )
+        )
+        every { riskResultTables.latestEntries(any()) } returns flowOf(listOf(ewResultDao1, ewResultDao2))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+        val calculatedAt = ewCalculatedAt.minus(400L)
+        every { presenceTracingRiskRepository.latestEntries(2) } returns flowOf(
+            listOf(
+                PtRiskLevelResult(
+                    calculatedAt = calculatedAt,
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.CALCULATION_FAILED
+                ),
+                PtRiskLevelResult(
+                    calculatedAt = calculatedAt.minus(400L),
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.LOW_RISK
+                )
+            )
+        )
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResults = instance.latestCombinedEwPtRiskLevelResults.first()
+            riskLevelResults.size shouldBe 2
+
+            riskLevelResults[0].calculatedAt shouldBe ewCalculatedAt
+            riskLevelResults[0].riskState shouldBe RiskState.LOW_RISK
+            riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(200L)
+            riskLevelResults[1].riskState shouldBe RiskState.INCREASED_RISK
+
+            verify {
+                riskResultTables.latestEntries(2)
+                presenceTracingRiskRepository.latestEntries(2)
+            }
+        }
+    }
+
     // This just tests the mapping, the correctness of the SQL statement is validated in an instrumentation test
     @Test
     fun `latestAndLastSuccessful with exposure windows are returned from database and mapped`() {
@@ -190,7 +350,214 @@ class BaseRiskLevelStorageTest : BaseTest() {
     }
 
     @Test
-    fun `errors when storing risklevel result are rethrown`() = runBlockingTest {
+    fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined`() {
+        val calculatedAt = ewCalculatedAt.plus(6000L)
+        every { presenceTracingRiskRepository.allEntries() } returns flowOf(
+            listOf(
+                PtRiskLevelResult(
+                    calculatedAt = calculatedAt,
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.CALCULATION_FAILED
+                ),
+                PtRiskLevelResult(
+                    calculatedAt = ewCalculatedAt.minus(1000L),
+                    presenceTracingDayRisk = listOf(testPresenceTracingDayRisk),
+                    riskState = RiskState.INCREASED_RISK
+                )
+            )
+        )
+        every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk))
+
+        every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(testRiskLevelResultDao))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first()
+
+            riskLevelResult.lastCalculated.calculatedAt shouldBe calculatedAt
+            riskLevelResult.lastCalculated.riskState shouldBe RiskState.INCREASED_RISK
+            riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe calculatedAt
+            riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.INCREASED_RISK
+
+            verify {
+                presenceTracingRiskRepository.allEntries()
+                presenceTracingRiskRepository.presenceTracingDayRisk
+            }
+        }
+    }
+
+    @Test
+    fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined 2`() {
+        every { presenceTracingRiskRepository.allEntries() } returns flowOf(
+            listOf(
+                PtRiskLevelResult(
+                    calculatedAt = ewCalculatedAt.plus(6000L),
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.CALCULATION_FAILED
+                ),
+                PtRiskLevelResult(
+                    calculatedAt = ewCalculatedAt.minus(1000L),
+                    presenceTracingDayRisk = listOf(testPresenceTracingDayRisk),
+                    riskState = RiskState.INCREASED_RISK
+                )
+            )
+        )
+
+        val ewResultDao1 = PersistedRiskLevelResultDao(
+            id = "id1",
+            calculatedAt = ewCalculatedAt,
+            failureReason = EwRiskLevelResult.FailureReason.UNKNOWN,
+            aggregatedRiskResult = null
+        )
+        val ewResultDao2 = PersistedRiskLevelResultDao(
+            id = "id2",
+            calculatedAt = ewCalculatedAt.minus(2000L),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW,
+                totalMinimumDistinctEncountersWithLowRisk = 1,
+                totalMinimumDistinctEncountersWithHighRisk = 0,
+                mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+                mostRecentDateWithHighRisk = Instant.ofEpochMilli(0),
+                numberOfDaysWithLowRisk = 5,
+                numberOfDaysWithHighRisk = 6
+            )
+        )
+        every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk))
+
+        every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first()
+
+            riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt.plus(6000L)
+            riskLevelResult.lastCalculated.riskState shouldBe RiskState.CALCULATION_FAILED
+            riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt
+            riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.INCREASED_RISK
+
+            verify {
+                presenceTracingRiskRepository.allEntries()
+                presenceTracingRiskRepository.presenceTracingDayRisk
+            }
+        }
+    }
+
+    @Test
+    fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult are combined 3`() {
+        every { presenceTracingRiskRepository.allEntries() } returns flowOf(
+            listOf(
+                PtRiskLevelResult(
+                    calculatedAt = ewCalculatedAt.plus(6000L),
+                    presenceTracingDayRisk = null,
+                    riskState = RiskState.CALCULATION_FAILED
+                ),
+                PtRiskLevelResult(
+                    calculatedAt = ewCalculatedAt.minus(100L),
+                    presenceTracingDayRisk = listOf(testPresenceTracingDayRisk),
+                    riskState = RiskState.LOW_RISK
+                )
+            )
+        )
+
+        val ewResultDao1 = PersistedRiskLevelResultDao(
+            id = "id1",
+            calculatedAt = ewCalculatedAt,
+            failureReason = EwRiskLevelResult.FailureReason.UNKNOWN,
+            aggregatedRiskResult = null
+        )
+        val ewResultDao2 = PersistedRiskLevelResultDao(
+            id = "id2",
+            calculatedAt = ewCalculatedAt.minus(200L),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
+                totalMinimumDistinctEncountersWithLowRisk = 1,
+                totalMinimumDistinctEncountersWithHighRisk = 2,
+                mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+                mostRecentDateWithHighRisk = Instant.ofEpochMilli(4),
+                numberOfDaysWithLowRisk = 5,
+                numberOfDaysWithHighRisk = 6
+            )
+        )
+        every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf(testPresenceTracingDayRisk))
+
+        every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first()
+
+            riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt.plus(6000L)
+            riskLevelResult.lastCalculated.riskState shouldBe RiskState.CALCULATION_FAILED
+            riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt
+            riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.LOW_RISK
+
+            verify {
+                presenceTracingRiskRepository.allEntries()
+                presenceTracingRiskRepository.presenceTracingDayRisk
+            }
+        }
+    }
+
+    @Test
+    fun `latestAndLastSuccessfulCombinedEwPtRiskLevelResult works when no pt result yet`() {
+
+        every { presenceTracingRiskRepository.allEntries() } returns flowOf(listOf())
+        every { presenceTracingRiskRepository.presenceTracingDayRisk } returns flowOf(listOf())
+
+        val ewResultDao1 = PersistedRiskLevelResultDao(
+            id = "id1",
+            calculatedAt = ewCalculatedAt,
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW,
+                totalMinimumDistinctEncountersWithLowRisk = 1,
+                totalMinimumDistinctEncountersWithHighRisk = 2,
+                mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+                mostRecentDateWithHighRisk = Instant.ofEpochMilli(4),
+                numberOfDaysWithLowRisk = 5,
+                numberOfDaysWithHighRisk = 6
+            )
+        )
+        val ewResultDao2 = PersistedRiskLevelResultDao(
+            id = "id2",
+            calculatedAt = ewCalculatedAt.minus(200L),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
+                totalMinimumDistinctEncountersWithLowRisk = 1,
+                totalMinimumDistinctEncountersWithHighRisk = 2,
+                mostRecentDateWithLowRisk = Instant.ofEpochMilli(3),
+                mostRecentDateWithHighRisk = Instant.ofEpochMilli(4),
+                numberOfDaysWithLowRisk = 5,
+                numberOfDaysWithHighRisk = 6
+            )
+        )
+
+        every { riskResultTables.latestAndLastSuccessful() } returns flowOf(listOf(ewResultDao1, ewResultDao2))
+        every { exposureWindowTables.getWindowsForResult(any()) } returns flowOf(listOf(testExposureWindowDaoWrapper))
+
+        runBlockingTest2(ignoreActive = true) {
+            val instance = createInstance(scope = this)
+
+            val riskLevelResult = instance.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.first()
+
+            riskLevelResult.lastCalculated.calculatedAt shouldBe ewCalculatedAt
+            riskLevelResult.lastCalculated.riskState shouldBe RiskState.LOW_RISK
+            riskLevelResult.lastSuccessfullyCalculated.calculatedAt shouldBe ewCalculatedAt
+            riskLevelResult.lastSuccessfullyCalculated.riskState shouldBe RiskState.LOW_RISK
+        }
+    }
+
+    @Test
+    fun `errors when storing risk level result are rethrown`() = runBlockingTest {
         coEvery { riskResultTables.insertEntry(any()) } throws IllegalStateException("No body expects the...")
         val instance = createInstance()
         shouldThrow<java.lang.IllegalStateException> {
@@ -242,6 +609,14 @@ class BaseRiskLevelStorageTest : BaseTest() {
     @Test
     fun `clear works`() = runBlockingTest {
         createInstance().clear()
-        verify { database.clearAllTables() }
+        coVerify {
+            database.clearAllTables()
+            presenceTracingRiskRepository.clearAllTables()
+        }
     }
 }
+
+private val testPresenceTracingDayRisk = PresenceTracingDayRisk(
+    Instant.now().toLocalDateUtc(),
+    RiskState.INCREASED_RISK
+)
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/CombineRiskTest.kt
index 824be93cc148c67284dc133d0de47990018141c5..a1099568aa19e32b4143ce8da45613664c804346 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/CombineRiskTest.kt
@@ -1,13 +1,17 @@
 package de.rki.coronawarnapp.risk.storage
 
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingDayRisk
+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.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk
 import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel
 import io.kotest.matchers.shouldBe
+import org.joda.time.Instant
 import org.joda.time.LocalDate
 import org.junit.jupiter.api.Test
-import java.time.Instant
 
 class CombineRiskTest {
 
@@ -26,33 +30,33 @@ class CombineRiskTest {
             riskState = RiskState.CALCULATION_FAILED
         )
         val ewRisk = ExposureWindowDayRisk(
-            dateMillisSinceEpoch = Instant.parse("2021-03-22T14:00:00.000Z").toEpochMilli(),
+            dateMillisSinceEpoch = Instant.parse("2021-03-22T14:00:00.000Z").millis,
             riskLevel = RiskLevel.HIGH,
             0,
             0
         )
         val ewRisk2 = ExposureWindowDayRisk(
-            dateMillisSinceEpoch = Instant.parse("2021-03-19T14:00:00.000Z").toEpochMilli(),
+            dateMillisSinceEpoch = Instant.parse("2021-03-19T14:00:00.000Z").millis,
             riskLevel = RiskLevel.LOW,
             0,
             0
         )
         val ewRisk3 = ExposureWindowDayRisk(
-            dateMillisSinceEpoch = Instant.parse("2021-03-20T14:00:00.000Z").toEpochMilli(),
+            dateMillisSinceEpoch = Instant.parse("2021-03-20T14:00:00.000Z").millis,
             riskLevel = RiskLevel.UNSPECIFIED,
             0,
             0
         )
         val ewRisk4 = ExposureWindowDayRisk(
-            dateMillisSinceEpoch = Instant.parse("2021-03-15T14:00:00.000Z").toEpochMilli(),
+            dateMillisSinceEpoch = Instant.parse("2021-03-15T14:00:00.000Z").millis,
             riskLevel = RiskLevel.UNSPECIFIED,
             0,
             0
         )
 
-        val ptRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk, ptRisk2, ptRisk3)
-        val exposureWindowDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk, ewRisk2, ewRisk3, ewRisk4)
-        val result = combineRisk(ptRiskList, exposureWindowDayRiskList)
+        val ptDayRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk, ptRisk2, ptRisk3)
+        val ewDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk, ewRisk2, ewRisk3, ewRisk4)
+        val result = combineRisk(ptDayRiskList, ewDayRiskList)
         result.size shouldBe 5
         result.find {
             it.localDate == LocalDate(2021, 3, 15)
@@ -71,15 +75,87 @@ class CombineRiskTest {
         }!!.riskState shouldBe RiskState.INCREASED_RISK
     }
 
+    @Test
+    fun `combineEwPtRiskLevelResults works`() {
+        val startInstant = Instant.ofEpochMilli(10000)
+
+        val ptResult = PtRiskLevelResult(
+            calculatedAt = startInstant.plus(1000L),
+            riskState = RiskState.LOW_RISK
+        )
+        val ptResult2 = PtRiskLevelResult(
+            calculatedAt = startInstant.plus(3000L),
+            riskState = RiskState.CALCULATION_FAILED
+        )
+        val ptResult3 = PtRiskLevelResult(
+            calculatedAt = startInstant.plus(6000L),
+            riskState = RiskState.CALCULATION_FAILED
+        )
+        val ptResult4 = PtRiskLevelResult(
+            calculatedAt = startInstant.plus(7000L),
+            riskState = RiskState.CALCULATION_FAILED
+        )
+
+        val ptResults = listOf(ptResult, ptResult2, ptResult4, ptResult3)
+        val ewResult = createEwRiskLevelResult(
+            calculatedAt = startInstant.plus(2000L),
+            riskState = RiskState.CALCULATION_FAILED
+        )
+        val ewResult2 = createEwRiskLevelResult(
+            calculatedAt = startInstant.plus(4000L),
+            riskState = RiskState.INCREASED_RISK
+        )
+        val ewResult3 = createEwRiskLevelResult(
+            calculatedAt = startInstant.plus(5000L),
+            riskState = RiskState.CALCULATION_FAILED
+        )
+        val ewResult4 = createEwRiskLevelResult(
+            calculatedAt = startInstant.plus(8000L),
+            riskState = RiskState.CALCULATION_FAILED
+        )
+        val ewResults = listOf(ewResult, ewResult4, ewResult2, ewResult3)
+        val result = combineEwPtRiskLevelResults(ptResults, ewResults).sortedByDescending { it.calculatedAt }
+        result.size shouldBe 8
+        result[0].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[0].calculatedAt shouldBe startInstant.plus(8000L)
+        result[1].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[1].calculatedAt shouldBe startInstant.plus(7000L)
+        result[2].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[2].calculatedAt shouldBe startInstant.plus(6000L)
+        result[3].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[3].calculatedAt shouldBe startInstant.plus(5000L)
+        result[4].riskState shouldBe RiskState.INCREASED_RISK
+        result[4].calculatedAt shouldBe startInstant.plus(4000L)
+        result[5].riskState shouldBe RiskState.CALCULATION_FAILED
+        result[5].calculatedAt shouldBe startInstant.plus(3000L)
+        result[6].riskState shouldBe RiskState.LOW_RISK
+        result[6].calculatedAt shouldBe startInstant.plus(2000L)
+        result[7].riskState shouldBe RiskState.LOW_RISK
+        result[7].calculatedAt shouldBe startInstant.plus(1000L)
+    }
+
     @Test
     fun `max RiskState works`() {
-        max(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
-        max(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK
-        max(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.INCREASED_RISK
-        max(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
-        max(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
-        max(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK
-        max(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK
-        max(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED
+        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.INCREASED_RISK
+        combine(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
+        combine(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK
+        combine(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK
+        combine(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK
+        combine(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED
     }
 }
+
+private fun createEwRiskLevelResult(
+    calculatedAt: Instant,
+    riskState: RiskState
+): EwRiskLevelResult = object : EwRiskLevelResult {
+    override val calculatedAt: Instant = calculatedAt
+    override val riskState: RiskState = riskState
+    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
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt
index 34c79dac15a342d5194b7f53f23d8db624fad6e8..5cb8e090ce739ed51abda4b3e1dc11f3327d2983 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/RiskStorageTestData.kt
@@ -14,9 +14,11 @@ import org.joda.time.Instant
 
 object RiskStorageTestData {
 
+    val ewCalculatedAt = Instant.ofEpochMilli(9999L)
+
     val testRiskLevelResultDao = PersistedRiskLevelResultDao(
         id = "riskresult-id",
-        calculatedAt = Instant.ofEpochMilli(9999L),
+        calculatedAt = ewCalculatedAt,
         failureReason = null,
         aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
             totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
@@ -40,7 +42,7 @@ object RiskStorageTestData {
     )
 
     val testRisklevelResult = EwRiskLevelTaskResult(
-        calculatedAt = Instant.ofEpochMilli(9999L),
+        calculatedAt = ewCalculatedAt,
         ewAggregatedRiskResult = testAggregatedRiskResult,
         exposureWindows = null
     )
@@ -76,14 +78,14 @@ object RiskStorageTestData {
     }.build()
 
     val testAggregatedRiskPerDateResult = ExposureWindowDayRisk(
-        dateMillisSinceEpoch = 9999L,
+        dateMillisSinceEpoch = ewCalculatedAt.millis,
         riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
         minimumDistinctEncountersWithLowRisk = 0,
         minimumDistinctEncountersWithHighRisk = 0
     )
 
     val testPersistedAggregatedRiskPerDateResult = PersistedAggregatedRiskPerDateResult(
-        dateMillisSinceEpoch = 9999L,
+        dateMillisSinceEpoch = ewCalculatedAt.millis,
         riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH,
         minimumDistinctEncountersWithLowRisk = 0,
         minimumDistinctEncountersWithHighRisk = 0
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt
index 30de3e9892402fc4c54632028487059802e9ea8c..38282aaf96e40847a257eaca7047f53b675770d1 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt
@@ -18,6 +18,7 @@ import de.rki.coronawarnapp.util.di.ApplicationComponent
 import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory
 import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool
 import de.rki.coronawarnapp.util.formatter.TestResult
+import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -57,6 +58,7 @@ class SubmissionRepositoryTest : BaseTest() {
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
     @MockK lateinit var tracingSettings: TracingSettings
     @MockK lateinit var testResultDataCollector: TestResultDataCollector
+    @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
 
     private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
     private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064"
@@ -99,6 +101,10 @@ class SubmissionRepositoryTest : BaseTest() {
         every { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } just Runs
         coEvery { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } just Runs
         every { testResultDataCollector.clear() } just Runs
+
+        backgroundWorkScheduler.apply {
+            every { startWorkScheduler() } just Runs
+        }
     }
 
     fun createInstance(scope: CoroutineScope) = SubmissionRepository(
@@ -111,7 +117,8 @@ class SubmissionRepositoryTest : BaseTest() {
         backgroundNoise = backgroundNoise,
         analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
         tracingSettings = tracingSettings,
-        testResultDataCollector = testResultDataCollector
+        testResultDataCollector = testResultDataCollector,
+        backgroundWorkScheduler = backgroundWorkScheduler
     )
 
     @Test
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt
index 0cb6a4ad75326870cbe5b8c7487448a2a64689bd..2802029978aae963e691b4abaa25e40ec5cac9cd 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt
@@ -30,7 +30,6 @@ import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.just
 import io.mockk.mockk
-import io.mockk.mockkObject
 import io.mockk.verify
 import kotlinx.coroutines.flow.emptyFlow
 import kotlinx.coroutines.flow.flowOf
@@ -63,6 +62,7 @@ class SubmissionTaskTest : BaseTest() {
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
     @MockK lateinit var checkInsTransformer: CheckInsTransformer
     @MockK lateinit var checkInRepository: CheckInRepository
+    @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
 
     private lateinit var settingSymptomsPreference: FlowPreference<Symptoms?>
 
@@ -80,9 +80,8 @@ class SubmissionTaskTest : BaseTest() {
         every { submissionSettings.registrationToken } returns registrationToken
         every { submissionSettings.isSubmissionSuccessful = any() } just Runs
 
-        mockkObject(BackgroundWorkScheduler)
-        every { BackgroundWorkScheduler.stopWorkScheduler() } just Runs
-        every { BackgroundWorkScheduler.startWorkScheduler() } just Runs
+        every { backgroundWorkScheduler.stopWorkScheduler() } just Runs
+        every { backgroundWorkScheduler.startWorkScheduler() } just Runs
 
         every { tekBatch.keys } returns listOf(tek)
         every { tekHistoryStorage.tekData } returns flowOf(listOf(tekBatch))
@@ -131,7 +130,8 @@ class SubmissionTaskTest : BaseTest() {
         testResultAvailableNotificationService = testResultAvailableNotificationService,
         analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
         checkInsRepository = checkInRepository,
-        checkInsTransformer = checkInsTransformer
+        checkInsTransformer = checkInsTransformer,
+        backgroundWorkScheduler = backgroundWorkScheduler,
     )
 
     @Test
@@ -183,9 +183,9 @@ class SubmissionTaskTest : BaseTest() {
 
             autoSubmission.updateMode(AutoSubmission.Mode.DISABLED)
 
-            BackgroundWorkScheduler.stopWorkScheduler()
+            backgroundWorkScheduler.stopWorkScheduler()
             submissionSettings.isSubmissionSuccessful = true
-            BackgroundWorkScheduler.startWorkScheduler()
+            backgroundWorkScheduler.startWorkScheduler()
 
             shareTestResultNotificationService.cancelSharePositiveTestResultNotification()
             testResultAvailableNotificationService.cancelTestResultAvailableNotification()
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt
index 27175e36d1849b303abc331b0bf1ebbe25b838ce..daf12a7211a3c53bb238e1cb84de4f73bda21e4c 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt
@@ -5,12 +5,11 @@ import android.content.res.Resources
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
 import de.rki.coronawarnapp.datadonation.survey.Surveys
 import de.rki.coronawarnapp.installTime.InstallTimeProvider
-import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult
+import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult
 import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult
-import de.rki.coronawarnapp.risk.ProtoRiskLevel
+import de.rki.coronawarnapp.risk.LastCombinedRiskResults
 import de.rki.coronawarnapp.risk.RiskState
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
-import de.rki.coronawarnapp.risk.storage.CombinedEwPtRiskLevelResult
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.tracing.GeneralTracingStatus
 import de.rki.coronawarnapp.tracing.ui.details.items.additionalinfos.AdditionalInfoLowRiskBox
@@ -46,6 +45,8 @@ class TracingDetailsItemProviderTest : BaseTest() {
     @MockK lateinit var installTimeProvider: InstallTimeProvider
     @MockK lateinit var surveys: Surveys
 
+    @MockK(relaxed = true) lateinit var combinedResult: CombinedEwPtRiskLevelResult
+
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
@@ -61,22 +62,23 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
     private fun prepare(
         status: GeneralTracingStatus.Status,
-        riskLevel: ProtoRiskLevel,
+        riskState: RiskState,
         matchedKeyCount: Int,
         daysSinceInstallation: Long,
         availableSurveys: List<Surveys.Type> = emptyList()
     ) {
         every { tracingStatus.generalStatus } returns flowOf(status)
-        every { ewAggregatedRiskResult.totalRiskLevel } returns riskLevel
         every { installTimeProvider.daysSinceInstallation } returns daysSinceInstallation
         every { surveys.availableSurveys } returns flowOf(availableSurveys)
 
-        if (riskLevel == ProtoRiskLevel.LOW) {
+        if (riskState == RiskState.LOW_RISK) {
             every { ewAggregatedRiskResult.isLowRisk() } returns true
-        } else if (riskLevel == ProtoRiskLevel.HIGH) {
+        } else if (riskState == RiskState.INCREASED_RISK) {
             every { ewAggregatedRiskResult.isIncreasedRisk() } returns true
         }
 
+        every { combinedResult.riskState } returns riskState
+
         val exposureWindow: ExposureWindow = mockk()
 
         val ewRiskLevelTaskResult = EwRiskLevelTaskResult(
@@ -85,18 +87,16 @@ class TracingDetailsItemProviderTest : BaseTest() {
             exposureWindows = listOf(exposureWindow)
         )
 
-        val ptRiskLevelResult = PtRiskLevelResult(
-            calculatedAt = Instant.EPOCH,
-            riskState = RiskState.CALCULATION_FAILED
-        )
-        val combined = CombinedEwPtRiskLevelResult(
-            ewRiskLevelResult = ewRiskLevelTaskResult,
-            ptRiskLevelResult = ptRiskLevelResult
+        every { combinedResult.ewRiskLevelResult } returns ewRiskLevelTaskResult
+
+        val lastCombined = LastCombinedRiskResults(
+            lastCalculated = combinedResult,
+            lastSuccessfullyCalculated = combinedResult
         )
         every { ewRiskLevelTaskResult.matchedKeyCount } returns matchedKeyCount
-        every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns flowOf(listOf(ewRiskLevelTaskResult))
-        // TODO tests
-        every { riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult } returns flowOf(listOf(combined))
+        every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns
+            flowOf(listOf(ewRiskLevelTaskResult))
+        every { riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult } returns flowOf(lastCombined)
     }
 
     @Test
@@ -104,7 +104,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 1
         )
@@ -121,7 +121,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -138,7 +138,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.HIGH,
+            riskState = RiskState.INCREASED_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -155,7 +155,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.HIGH,
+            riskState = RiskState.INCREASED_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -174,7 +174,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -193,7 +193,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0,
         )
@@ -213,7 +213,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.HIGH,
+            riskState = RiskState.INCREASED_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -233,7 +233,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.UNRECOGNIZED,
+            riskState = RiskState.CALCULATION_FAILED,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -253,7 +253,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_INACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -273,7 +273,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_INACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -296,7 +296,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.UNRECOGNIZED,
+            riskState = RiskState.CALCULATION_FAILED,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -319,7 +319,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -342,7 +342,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.HIGH,
+            riskState = RiskState.INCREASED_RISK,
             daysSinceInstallation = 4,
             matchedKeyCount = 0
         )
@@ -365,7 +365,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.LOW,
+            riskState = RiskState.LOW_RISK,
             matchedKeyCount = 0,
             daysSinceInstallation = 4,
             availableSurveys = listOf(Surveys.Type.HIGH_RISK_ENCOUNTER)
@@ -386,7 +386,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.HIGH,
+            riskState = RiskState.INCREASED_RISK,
             matchedKeyCount = 0,
             daysSinceInstallation = 4,
             availableSurveys = emptyList()
@@ -407,7 +407,7 @@ class TracingDetailsItemProviderTest : BaseTest() {
 
         prepare(
             status = GeneralTracingStatus.Status.TRACING_ACTIVE,
-            riskLevel = ProtoRiskLevel.HIGH,
+            riskState = RiskState.INCREASED_RISK,
             matchedKeyCount = 0,
             daysSinceInstallation = 4,
             availableSurveys = listOf(Surveys.Type.HIGH_RISK_ENCOUNTER)
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 77d37b6285bcc0b9d4981ce580d366e7440d0ed8..bfbd074700697d48cbfc6eebf01d6c887164530c 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
@@ -5,6 +5,7 @@ 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.presencetracing.checkins.checkout.CheckOutHandler
+import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.CameraPermissionVH
 import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.PastCheckInVH
@@ -23,7 +24,6 @@ import io.mockk.verify
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runBlockingTest
-import okio.ByteString
 import org.joda.time.Instant
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
@@ -80,11 +80,12 @@ class CheckInsViewModelTest : BaseTest() {
     @Test
     fun `DeepLink verification`() = runBlockingTest {
         every { savedState.get<String>(any()) } returns null
-        every { qrCodeUriParser.getQrCodePayload(any()) } returns ByteString.EMPTY
+        coEvery { qrCodeUriParser.getQrCodePayload(any()) } returns
+            TraceLocationOuterClass.QRCodePayload.newBuilder().build()
 
         createInstance(deepLink = DEEP_LINK, scope = this).apply {
             events.getOrAwaitValue().shouldBeInstanceOf<CheckInEvent.ConfirmCheckIn>()
-            verify {
+            coVerify {
                 savedState.get<String>(any())
                 qrCodeUriParser.getQrCodePayload(any())
                 savedState.set(any(), any<String>())
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7ead8a9e213ea9be2b9fb76e8e05cfda3975a3ed
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/organizer/poster/QrCodePosterViewModelTest.kt
@@ -0,0 +1,77 @@
+package de.rki.coronawarnapp.ui.eventregistration.organizer.poster
+
+import android.graphics.Bitmap
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.PosterTemplateProvider
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QrCodeGenerator
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.Template
+import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
+import de.rki.coronawarnapp.eventregistration.storage.repo.TraceLocationRepository
+import de.rki.coronawarnapp.server.protocols.internal.pt.QrCodePosterTemplate.QRCodePosterTemplateAndroid.QRCodeTextBoxAndroid
+import de.rki.coronawarnapp.util.files.FileSharing
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+import testhelpers.BaseTest
+import testhelpers.TestDispatcherProvider
+import testhelpers.extensions.InstantExecutorExtension
+import testhelpers.extensions.getOrAwaitValue
+
+@ExtendWith(InstantExecutorExtension::class)
+class QrCodePosterViewModelTest : BaseTest() {
+
+    @MockK lateinit var qrCodeGenerator: QrCodeGenerator
+    @MockK lateinit var posterTemplateProvider: PosterTemplateProvider
+    @MockK lateinit var traceLocationRepository: TraceLocationRepository
+    @MockK lateinit var fileSharing: FileSharing
+    @MockK lateinit var qrCodeBitmap: Bitmap
+    @MockK lateinit var templateBitmap: Bitmap
+    @MockK lateinit var textBox: QRCodeTextBoxAndroid
+    @MockK lateinit var traceLocation: TraceLocation
+    private lateinit var template: Template
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        template = Template(
+            bitmap = templateBitmap,
+            width = 500,
+            height = 600,
+            offsetX = 0.15f,
+            offsetY = 0.14f,
+            qrCodeLength = 500,
+            textBox = textBox
+        )
+
+        coEvery { qrCodeGenerator.createQrCode("locationUrl", any(), any()) } returns qrCodeBitmap
+        coEvery { posterTemplateProvider.template() } returns template
+        coEvery { traceLocationRepository.traceLocationForId(any()) } returns traceLocation.apply {
+            every { description } returns "description"
+            every { address } returns "address"
+            every { locationUrl } returns "locationUrl"
+        }
+    }
+
+    @Test
+    fun `Poster is requested in init`() {
+        createInstance().poster.getOrAwaitValue() shouldBe Poster(
+            qrCode = qrCodeBitmap,
+            template = template,
+            infoText = "description\naddress"
+        )
+    }
+
+    private fun createInstance() = QrCodePosterViewModel(
+        traceLocationId = 1,
+        dispatcher = TestDispatcherProvider(),
+        qrCodeGenerator = qrCodeGenerator,
+        posterTemplateProvider = posterTemplateProvider,
+        traceLocationRepository = traceLocationRepository,
+        fileSharing = fileSharing
+    )
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b1678a5b59974ee94f11483bd2965ec4fc1c0008
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/ProtoBufKtTest.kt
@@ -0,0 +1,28 @@
+package de.rki.coronawarnapp.util
+
+import com.google.protobuf.ByteString
+import io.kotest.matchers.shouldBe
+import okio.ByteString.Companion.toByteString
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ProtoBufKtTest : BaseTest() {
+
+    @Test
+    fun toProtoByteString() {
+        val okioByteString = KEY.toByteArray().toByteString()
+
+        okioByteString.toProtoByteString() shouldBe ByteString.copyFromUtf8(KEY)
+    }
+
+    @Test
+    fun toOkioByteString() {
+        val protoByteString = ByteString.copyFromUtf8(KEY)
+
+        protoByteString.toOkioByteString() shouldBe KEY.toByteArray().toByteString()
+    }
+
+    companion object {
+        private const val KEY = "No generated key"
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt
index a02df66dd129ca32d506ebb2ae18343436000264..4c7411bca6a60422e7bdd71bb58d47e0c792f4e1 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt
@@ -4,6 +4,7 @@ import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.calculateDays
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.derive10MinutesInterval
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.getCurrentHourUTC
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant
@@ -99,4 +100,16 @@ class TimeAndDateExtensionsTest : BaseTest() {
         Instant.parse("1970-01-01T00:00:00.000Z")
             .derive10MinutesInterval() shouldBe 0
     }
+
+    @Test
+    fun `derive 1 hour interval should be 0`() {
+        Instant.parse("1970-01-01T00:00:00.000Z")
+            .deriveHourInterval() shouldBe 0
+    }
+
+    @Test
+    fun `derive 1 hour interval`() {
+        Instant.parse("2021-02-15T13:52:05+00:00")
+            .deriveHourInterval() shouldBe 448165
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt
index 32de838776c99e74a17e6837df6e5165e24a7b8f..9a63024547371943a6998ac85d9ed57d096590b1 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterStatisticsHelperTest.kt
@@ -13,10 +13,10 @@ import io.mockk.every
 import io.mockk.impl.annotations.MockK
 import io.mockk.mockkStatic
 import io.mockk.slot
-import org.joda.time.DateTime
+import org.joda.time.Duration
 import org.joda.time.Instant
-import org.junit.Before
-import org.junit.Test
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 import java.util.Locale
 
@@ -26,9 +26,9 @@ class FormatterStatisticsHelperTest : BaseTest() {
     private lateinit var context: Context
 
     private val today = Instant()
-    private val yesterday = DateTime().minusDays(1).toInstant()
+    private val yesterday = today.minus(Duration.standardDays(1))
 
-    @Before
+    @BeforeEach
     fun setUp() {
         val slot = slot<String>()
         MockKAnnotations.init(this)
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt
index 4fdcc480084e1ab05272eeff493aa49a5880a4ca..ef18b7fd626748acc031103cd5e3adf605803a74 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt
@@ -4,7 +4,7 @@ import android.content.Context
 import androidx.work.ListenableWorker
 import androidx.work.WorkerParameters
 import androidx.work.impl.workers.DiagnosticsWorker
-import de.rki.coronawarnapp.worker.DiagnosisKeyRetrievalOneTimeWorker
+import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker
 import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
 import io.mockk.MockKAnnotations
@@ -22,15 +22,15 @@ class CWAWorkerFactoryTest : BaseTest() {
 
     @MockK lateinit var context: Context
     @MockK lateinit var workerParameters: WorkerParameters
-    @MockK lateinit var ourWorker: DiagnosisKeyRetrievalOneTimeWorker
-    @MockK lateinit var ourFactory: DiagnosisKeyRetrievalOneTimeWorker.Factory
+    @MockK lateinit var ourWorker: DiagnosisKeyRetrievalWorker
+    @MockK lateinit var ourFactory: DiagnosisKeyRetrievalWorker.Factory
 
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
 
         every { ourFactory.create(context, workerParameters) } returns ourWorker
-        workerFactories[DiagnosisKeyRetrievalOneTimeWorker::class.java] = Provider { ourFactory }
+        workerFactories[DiagnosisKeyRetrievalWorker::class.java] = Provider { ourFactory }
     }
 
     fun createInstance() = CWAWorkerFactory(
@@ -42,7 +42,7 @@ class CWAWorkerFactoryTest : BaseTest() {
         val instance = createInstance()
         instance.createWorker(
             context,
-            DiagnosisKeyRetrievalOneTimeWorker::class.qualifiedName!!,
+            DiagnosisKeyRetrievalWorker::class.qualifiedName!!,
             workerParameters
         ) shouldBe ourWorker
     }
@@ -55,4 +55,20 @@ class CWAWorkerFactoryTest : BaseTest() {
         val worker2 = instance.createWorker(context, DiagnosticsWorker::class.qualifiedName!!, workerParameters)
         worker1 shouldNotBe worker2
     }
+
+    /**
+     * Workers are initialized based on their class name.
+     * That class name is stored as part of the worker arguments.
+     * If we refactor a worker, the previously used classname will be unknown.
+     * Returning null will cause the periodic worker to be dequeued.
+     */
+    @Test
+    fun `class names that can not be instantiated are treated like an unknown worker`() {
+        val instance = createInstance()
+        instance.createWorker(
+            context,
+            "abc.im.a.ghost.def",
+            workerParameters
+        ) shouldBe null
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
index 4c7049e9fff4c1c9efa7929b797834418261c971..67bdaf6650019a2e6368f755d05c613af186b8d9 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt
@@ -18,6 +18,7 @@ import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutNotification
 import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut
+import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler
 import de.rki.coronawarnapp.risk.storage.RiskLevelStorage
 import de.rki.coronawarnapp.task.TaskController
 import de.rki.coronawarnapp.util.di.AppContext
@@ -143,4 +144,7 @@ class MockProvider {
 
     @Provides
     fun checkOutNotification(): CheckOutNotification = mockk()
+
+    @Provides
+    fun riskWorkScheduler(): RiskWorkScheduler = mockk()
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt
deleted file mode 100644
index f77fd6a7e8ebd20758a1949af5652484ac69ac48..0000000000000000000000000000000000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilderTest.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.rki.coronawarnapp.worker
-
-import io.kotest.matchers.shouldBe
-import org.joda.time.Duration
-import org.junit.jupiter.api.Test
-import testhelpers.BaseTest
-
-class BackgroundWorkBuilderTest : BaseTest() {
-
-    @Test
-    fun `worker interval for key retrieval is 60 minutes, once every hour`() {
-        buildDiagnosisKeyRetrievalPeriodicWork().apply {
-            workSpec.intervalDuration shouldBe Duration.standardMinutes(60).millis
-        }
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt
index 499dada21cc74a244210f6dacbb57dec888023ba..a59702bd5d771dbc90c8fe80cf09ec523e6f83bb 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt
@@ -2,7 +2,6 @@ package de.rki.coronawarnapp.worker
 
 import android.content.Context
 import androidx.work.ListenableWorker
-import androidx.work.Operation
 import androidx.work.WorkRequest
 import androidx.work.WorkerParameters
 import de.rki.coronawarnapp.notification.GeneralNotifications
@@ -18,7 +17,6 @@ import de.rki.coronawarnapp.util.di.ApplicationComponent
 import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory
 import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool
 import de.rki.coronawarnapp.util.formatter.TestResult
-import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.Runs
@@ -48,9 +46,10 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
     @MockK lateinit var appComponent: ApplicationComponent
     @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory
     @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool
-    @MockK lateinit var operation: Operation
     @MockK lateinit var timeStamper: TimeStamper
     @MockK lateinit var tracingSettings: TracingSettings
+    @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler
+
     @RelaxedMockK lateinit var workerParams: WorkerParameters
     private val currentInstant = Instant.ofEpochSecond(1611764225)
     private val registrationToken = "test token"
@@ -72,8 +71,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
 
         every { submissionSettings.registrationToken } returns mockFlowPreference(registrationToken)
 
-        mockkObject(BackgroundWorkScheduler)
-        every { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } returns operation
+        every { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } just Runs
     }
 
     @Test
@@ -83,7 +81,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
             val worker = createWorker()
             val result = worker.doWork()
             coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) }
-            verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() }
+            verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() }
             result shouldBe ListenableWorker.Result.success()
         }
     }
@@ -95,7 +93,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
             val worker = createWorker()
             val result = worker.doWork()
             coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) }
-            verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() }
+            verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() }
             result shouldBe ListenableWorker.Result.success()
         }
     }
@@ -109,7 +107,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
             val worker = createWorker()
             val result = worker.doWork()
             coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) }
-            verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() }
+            verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() }
             result shouldBe ListenableWorker.Result.success()
         }
     }
@@ -228,7 +226,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
                     NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID
                 )
             }
-            coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() }
+            coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() }
             result shouldBe ListenableWorker.Result.success()
         }
     }
@@ -240,7 +238,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
             val worker = createWorker()
             val result = worker.doWork()
             coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) }
-            coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() }
+            coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() }
             result shouldBe ListenableWorker.Result.retry()
         }
     }
@@ -253,6 +251,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() {
         submissionSettings,
         submissionService,
         timeStamper,
-        tracingSettings
+        tracingSettings,
+        backgroundWorkScheduler,
     )
 }
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 8c676d6ab2f8cd5353ff3445e8919809507c5722..274a5e5f941e266f6db258ecf57cad71bd51e806 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
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.test.risk.storage
 
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage
@@ -88,7 +88,7 @@ class DefaultRiskLevelStorageTest : BaseTest() {
 
         every { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow()
         every { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow()
-        every { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow()
+        every { presenceTracingRiskRepository.allEntries() } returns emptyFlow()
         every { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow()
     }
 
diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt
index ed0342077389eb08c9590844454473015094eb94..bc5ae9e45a58d362c2de5a11464488638ac300c2 100644
--- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt
+++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt
@@ -36,7 +36,9 @@ class DebugOptionsFragmentViewModelTest : BaseTestInstrumentation() {
         every { environmentSetup.downloadCdnUrl } returns "downloadUrl"
         every { environmentSetup.verificationCdnUrl } returns "verificationUrl"
         every { environmentSetup.dataDonationCdnUrl } returns "dataDonationUrl"
-        every { environmentSetup.qrCodePosterTemplateCdnUrl } returns "qrCodePosterTemplateUrl"
+        every { environmentSetup.logUploadServerUrl } returns "logUploadServerUrl"
+        every { environmentSetup.crowdNotifierPublicKey } returns "crowdNotifierPublicKey"
+        every { environmentSetup.appConfigPublicKey } returns "appConfigPublicKey"
 
         every { environmentSetup.currentEnvironment = any() } answers {
             currentEnvironment = arg(0)
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 24478cb801c98a653901968debb4b0b5e1374967..72eae29914ebfe630caea23002104922563ca4dd 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
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.test.risk.storage
 
 import com.google.android.gms.nearby.exposurenotification.ExposureWindow
-import de.rki.coronawarnapp.presencetracing.risk.PresenceTracingRiskRepository
+import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository
 import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult
 import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult
 import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage
@@ -87,7 +87,7 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() {
 
         every { presenceTracingRiskRepository.traceLocationCheckInRiskStates } returns emptyFlow()
         every { presenceTracingRiskRepository.presenceTracingDayRisk } returns emptyFlow()
-        every { presenceTracingRiskRepository.latestAndLastSuccessful() } returns emptyFlow()
+        every { presenceTracingRiskRepository.allEntries() } returns emptyFlow()
         every { presenceTracingRiskRepository.latestEntries(any()) } returns emptyFlow()
     }
 
diff --git a/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto b/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto
index 76a0b921cabfb724875d5ea13db33ca63dc39a51..6fdbbcdd9957131b034b0d8c5c6cecda1b1d447d 100644
--- a/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto
+++ b/Server-Protocol-Buffer/src/main/proto/internal/pt/qr_code_poster_template.proto
@@ -15,8 +15,8 @@ message QRCodePosterTemplateAndroid {
   QRCodeTextBoxAndroid descriptionTextBox = 5;
 
   message QRCodeTextBoxAndroid {
-    uint32 offsetX = 1;
-    uint32 offsetY = 2;
+    float offsetX = 1;
+    float offsetY = 2;
 
     uint32 width = 3;
     uint32 height = 4;
diff --git a/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto b/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto
index baf364df29eddd47d9dde80956f7b884b7225a20..d169f942a70706d1243379d263f70f69f657029d 100644
--- a/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto
+++ b/Server-Protocol-Buffer/src/main/proto/internal/pt/trace_location.proto
@@ -2,6 +2,7 @@
 syntax = "proto3";
 package de.rki.coronawarnapp.server.protocols.internal.pt;
 
+
 message QRCodePayload {
   uint32 version = 1;
   TraceLocation locationData = 2;
diff --git a/build.gradle b/build.gradle
index 533ae7cd654a253ff06be9890da5be66db25f37b..271da8127e5db7a2db945d1c3a4ada30695be281 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,7 +18,7 @@ buildscript {
         // Can be upgraded once new Android version is released or
         // the specific version is available via maven
         classpath 'com.android.tools:r8:2.0.88'
-        classpath 'com.android.tools.build:gradle:4.1.2'
+        classpath 'com.android.tools.build:gradle:4.1.3'
         classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
         classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufVersion"
         classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion"
diff --git a/gradle.properties b/gradle.properties
index 85d774408956be697bebcd055517b68670eb71cb..de45338cd079f8b39f38ac723b6539fac91ac262 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -20,4 +20,4 @@ org.gradle.dependency.verification.console=verbose
 VERSION_MAJOR=2
 VERSION_MINOR=1
 VERSION_PATCH=0
-VERSION_BUILD=0
+VERSION_BUILD=3
diff --git a/prod_environments.json b/prod_environments.json
index f500312f51d59c251fd51267c5ed8ff6f2713fbf..303606b6bddcec348ec65236a6204446b6f51ef3 100644
--- a/prod_environments.json
+++ b/prod_environments.json
@@ -5,8 +5,9 @@
     "DOWNLOAD_CDN_URL": "https://svc90.main.px.t-online.de",
     "VERIFICATION_CDN_URL": "https://verification.coronawarn.app",
     "DATA_DONATION_CDN_URL": "https://data.coronawarn.app",
-    "LOG_UPLOAD_SERVER_URL": "https://logupload.coronawarn.app",
+    "LOG_UPLOAD_SERVER_URL": "https://data.coronawarn.app",
     "SAFETYNET_API_KEY": "placeholder",
-    "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="
+    "PUB_KEYS_SIGNATURE_VERIFICATION": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==",
+    "CROWD_NOTIFIER_PUBLIC_KEY": "gwLMzE153tQwAOf2MZoUXXfzWTdlSpfS99iZffmcmxOG9njSK4RTimFOFwDh6t0Tyw8XR01ugDYjtuKwjjuK49Oh83FWct6XpefPi9Skjxvvz53i9gaMmUEc96pbtoaA"
   }
 }