From 24ba3c02ea5c6c672b562fe8f7b669cd0199af50 Mon Sep 17 00:00:00 2001
From: Mohamed <mohamed.metwalli@sap.com>
Date: Mon, 19 Apr 2021 15:38:00 +0200
Subject: [PATCH] Check-in selection in submission flow (EXPOSUREAPP-6514) 
 (#2851)

* Pump version patch

* CheckIns Consent (EXPOSUREAPP-6530) (#2855)

* Initial implementation

* Bind selectable check-ins item

* Select items

* Update strings

* lint

* show dialog

* Update strings.xml

* Delegate to ViewModel

* Correct text

* Consider pre-consent

* Move screen to actual graph

* Add unit tests

* Select All behaviour

* Tweak padding and background color in darkmode.

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>

* Add submission consent property to checkin database (EXPOSUREAPP-6514) (#2859)

* Add new Check-In property for submission consent.

* Fix unit tests.

* Wire up new consent (EXPOSUREAPP-6532) (#2860)

* Wire CheckIns consent

* Move to business logic

* Handle back navigation

* Fix test

* Use completedCheckIns extension

* Update CheckIns for submission

* Renaming

* Extend unit tests, test for CheckIn validity.

* Fix KLINT

* More unit tests

* Lint

* PR Comments

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>

* Reset old selection (DEV) (#2870)

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com>
---
 .../2.json                                    | 204 +++++++++++
 .../storage/CheckInDatabaseData.kt            |   2 +
 .../PresenceTracingDatabaseMigrationTest.kt   | 133 +++++++
 ...bmissionTestResultAvailableFragmentTest.kt |  13 +-
 .../ui/PresenceTracingTestFragment.kt         |   5 +
 .../layout/fragment_test_presence_tracing.xml |  11 +-
 .../eventregistration/checkins/CheckIn.kt     |   2 +
 .../checkins/CheckInRepository.kt             |  19 +-
 .../storage/TraceLocationDatabase.kt          |   8 +-
 .../storage/dao/CheckInDao.kt                 |   3 +
 .../entity/TraceLocationCheckInEntity.kt      |  11 +-
 .../PresenceTracingDatabaseMigration1To2.kt   |  38 ++
 .../common/CheckInRepositoryExtension.kt      |  13 +
 .../submission/task/SubmissionTask.kt         |   7 +-
 .../EventRegistrationUIModule.kt              |   5 +
 .../attendee/checkins/items/PastCheckInVH.kt  |  15 +-
 .../checkins/common/CompletedCheckIn.kt       |  16 +
 .../consent/CheckInsConsentAdapter.kt         |  37 ++
 .../consent/CheckInsConsentFragment.kt        | 106 ++++++
 .../consent/CheckInsConsentFragmentModule.kt  |  19 +
 .../checkins/consent/CheckInsConsentItem.kt   |   5 +
 .../consent/CheckInsConsentNavigation.kt      |   9 +
 .../consent/CheckInsConsentViewModel.kt       | 165 +++++++++
 .../checkins/consent/HeaderCheckInsVH.kt      |  31 ++
 .../checkins/consent/SelectableCheckInVH.kt   |  44 +++
 .../SubmissionTestResultAvailableViewModel.kt |  34 +-
 ...tPositiveOtherWarningNoConsentViewModel.kt |  39 +-
 .../res/layout/check_ins_consent_fragment.xml |  56 +++
 ...trace_location_attendee_consent_header.xml |  25 ++
 ...n_attendee_consent_selectable_check_in.xml |  67 ++++
 .../src/main/res/navigation/nav_graph.xml     |  34 ++
 .../src/main/res/values/styles.xml            |  10 +-
 .../checkins/CheckInRepositoryTest.kt         |   3 +
 .../submission/task/SubmissionTaskTest.kt     |  28 +-
 .../consent/CheckInsConsentViewModelTest.kt   | 338 ++++++++++++++++++
 ...missionTestResultAvailableViewModelTest.kt |   5 +-
 ...itiveOtherWarningNoConsentViewModelTest.kt |   5 +-
 gradle.properties                             |   2 +-
 38 files changed, 1510 insertions(+), 57 deletions(-)
 create mode 100644 Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json
 create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt
 create mode 100644 Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml
 create mode 100644 Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml
 create mode 100644 Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt

diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json
new file mode 100644
index 000000000..006ae7f97
--- /dev/null
+++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json
@@ -0,0 +1,204 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "3950e8c7f3123a41f0960bc30b4f07f4",
+    "entities": [
+      {
+        "tableName": "checkin",
+        "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, `submitted` INTEGER NOT NULL, `submissionConsent` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "traceLocationIdBase64",
+            "columnName": "traceLocationIdBase64",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "version",
+            "columnName": "version",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "address",
+            "columnName": "address",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "traceLocationStart",
+            "columnName": "traceLocationStart",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "traceLocationEnd",
+            "columnName": "traceLocationEnd",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "defaultCheckInLengthInMinutes",
+            "columnName": "defaultCheckInLengthInMinutes",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "cryptographicSeedBase64",
+            "columnName": "cryptographicSeedBase64",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "cnPublicKey",
+            "columnName": "cnPublicKey",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "checkInStart",
+            "columnName": "checkInStart",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "checkInEnd",
+            "columnName": "checkInEnd",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "completed",
+            "columnName": "completed",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "createJournalEntry",
+            "columnName": "createJournalEntry",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isSubmitted",
+            "columnName": "submitted",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "hasSubmissionConsent",
+            "columnName": "submissionConsent",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "traceLocations",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `startDate` TEXT, `endDate` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "version",
+            "columnName": "version",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "address",
+            "columnName": "address",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "startDate",
+            "columnName": "startDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "endDate",
+            "columnName": "endDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "defaultCheckInLengthInMinutes",
+            "columnName": "defaultCheckInLengthInMinutes",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "cryptographicSeedBase64",
+            "columnName": "cryptographicSeedBase64",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "cnPublicKey",
+            "columnName": "cnPublicKey",
+            "affinity": "TEXT",
+            "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, '3950e8c7f3123a41f0960bc30b4f07f4')"
+    ]
+  }
+}
\ No newline at end of file
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 1ab9a7725..bcc28c18c 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
@@ -23,6 +23,7 @@ object CheckInDatabaseData {
         completed = false,
         createJournalEntry = true,
         isSubmitted = false,
+        hasSubmissionConsent = false,
     )
 
     val testCheckInWithoutCheckOutTime = TraceLocationCheckInEntity(
@@ -41,5 +42,6 @@ object CheckInDatabaseData {
         completed = false,
         createJournalEntry = true,
         isSubmitted = false,
+        hasSubmissionConsent = false,
     )
 }
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt
new file mode 100644
index 000000000..dcb7f5e09
--- /dev/null
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt
@@ -0,0 +1,133 @@
+package de.rki.coronawarnapp.presencetracing.migration
+
+import androidx.room.testing.MigrationTestHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase
+import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity
+import de.rki.coronawarnapp.eventregistration.storage.migration.PresenceTracingDatabaseMigration1To2
+import io.kotest.matchers.shouldBe
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import okio.ByteString.Companion.encode
+import org.joda.time.Instant
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import testhelpers.BaseTestInstrumentation
+import java.io.IOException
+
+@RunWith(AndroidJUnit4::class)
+class PresenceTracingDatabaseMigrationTest : BaseTestInstrumentation() {
+    private val DB_NAME = "TraceLocations_test_db"
+
+    @get:Rule
+    val helper: MigrationTestHelper = MigrationTestHelper(
+        InstrumentationRegistry.getInstrumentation(),
+        TraceLocationDatabase::class.java.canonicalName,
+        FrameworkSQLiteOpenHelperFactory()
+    )
+
+    @Test
+    fun migrate1To2() {
+        val locationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64()
+        val cryptoGraphicSeed = "cryptographicSeed".encode().base64()
+        val locationStart = Instant.parse("2021-01-01T12:30:00.000Z")
+        val locationEnd = Instant.parse("2021-01-01T18:30:00.000Z")
+        val checkInStart = Instant.parse("2021-01-01T14:30:00.000Z")
+        val checkInEnd = Instant.parse("2021-01-01T16:30:00.000Z")
+        helper.createDatabase(DB_NAME, 1).apply {
+            execSQL(
+                """
+                    INSERT INTO "checkin" (
+                        "id",
+                        "traceLocationIdBase64",
+                        "version",
+                        "type",
+                        "description",
+                        "address",
+                        "traceLocationStart",
+                        "traceLocationEnd",
+                        "defaultCheckInLengthInMinutes",
+                        "cryptographicSeedBase64",
+                        "cnPublicKey",
+                        "checkInStart",
+                        "checkInEnd",
+                        "completed",
+                        "createJournalEntry",
+                        "submitted"
+                    ) VALUES (
+                        '1',
+                        '$locationIdBase64',
+                        '1',
+                        '2',
+                        'brothers birthday',
+                        'Malibu',
+                        '$locationStart',
+                        '$locationEnd',
+                        '42',
+                        '$cryptoGraphicSeed',
+                        'cnPublicKey',
+                        '$checkInStart',
+                        '$checkInEnd',
+                        '0',
+                        '1',
+                        '0'
+                    );
+                """.trimIndent()
+            )
+            close()
+        }
+
+        // Run migration
+        helper.runMigrationsAndValidate(
+            DB_NAME,
+            2,
+            true,
+            PresenceTracingDatabaseMigration1To2
+        )
+
+        val daoDb = TraceLocationDatabase.Factory(
+            context = ApplicationProvider.getApplicationContext()
+        ).create(databaseName = DB_NAME)
+
+        val checkin = TraceLocationCheckInEntity(
+            id = 1L,
+            traceLocationIdBase64 = locationIdBase64,
+            version = 1,
+            type = 2,
+            description = "brothers birthday",
+            address = "Malibu",
+            traceLocationStart = locationStart,
+            traceLocationEnd = locationEnd,
+            defaultCheckInLengthInMinutes = 42,
+            cryptographicSeedBase64 = cryptoGraphicSeed,
+            cnPublicKey = "cnPublicKey",
+            checkInStart = checkInStart,
+            checkInEnd = checkInEnd,
+            completed = false,
+            createJournalEntry = true,
+            isSubmitted = false,
+            hasSubmissionConsent = false,
+        )
+        runBlocking { daoDb.eventCheckInDao().allEntries().first() }.single() shouldBe checkin
+    }
+
+    @Test
+    @Throws(IOException::class)
+    fun migrateAll() {
+        helper.createDatabase(DB_NAME, 1).apply {
+            close()
+        }
+
+        // Open latest version of the database. Room will validate the schema once all migrations execute.
+        TraceLocationDatabase.Factory(
+            context = ApplicationProvider.getApplicationContext()
+        ).create(databaseName = DB_NAME).apply {
+            openHelper.writableDatabase
+            close()
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt
index 33dc537e4..b92a122c6 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt
@@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
 import dagger.Module
 import dagger.android.ContributesAndroidInjector
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater_Factory_Impl
@@ -39,6 +40,7 @@ class SubmissionTestResultAvailableFragmentTest : BaseUITest() {
     @MockK lateinit var autoSubmission: AutoSubmission
     @MockK lateinit var appShortcutsHelper: AppShortcutsHelper
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
+    @MockK lateinit var checkInRepository: CheckInRepository
 
     @Rule
     @JvmField
@@ -57,11 +59,12 @@ class SubmissionTestResultAvailableFragmentTest : BaseUITest() {
 
         viewModel = spyk(
             SubmissionTestResultAvailableViewModel(
-                TestDispatcherProvider(),
-                tekHistoryUpdaterFactory,
-                submissionRepository,
-                autoSubmission,
-                analyticsKeySubmissionCollector
+                dispatcherProvider = TestDispatcherProvider(),
+                tekHistoryUpdaterFactory = tekHistoryUpdaterFactory,
+                submissionRepository = submissionRepository,
+                autoSubmission = autoSubmission,
+                analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
+                checkInRepository = checkInRepository
             )
         )
 
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt
index 8e54311d1..d80da8bff 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt
@@ -10,6 +10,7 @@ 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.FragmentTestPresenceTracingBinding
 import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocation
@@ -62,6 +63,10 @@ class PresenceTracingTestFragment : Fragment(R.layout.fragment_test_presence_tra
             viewModel.runPresenceTracingWarningTask()
         }
 
+        binding.openConsent.setOnClickListener {
+            findNavController().navigate(R.id.checkInsConsentFragment)
+        }
+
         viewModel.lastOrganiserLocation.observe(viewLifecycleOwner) {
             binding.lastOrganiserLocationCard.isVisible = it != null
             it?.let { traceLocation ->
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml
index dba2acc1b..19c37f7ec 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml
@@ -16,8 +16,8 @@
             style="@style/Card"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:layout_marginTop="10dp"
             android:layout_marginHorizontal="@dimen/spacing_tiny"
+            android:layout_marginTop="10dp"
             android:orientation="vertical">
 
             <TextView
@@ -168,5 +168,14 @@
                 tools:text="ID" />
         </LinearLayout>
 
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/open_consent"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:layout_marginTop="16dp"
+            android:text="Consent" />
+
     </LinearLayout>
 </androidx.core.widget.NestedScrollView>
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 8fea0b27a..36db8dd52 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
@@ -24,6 +24,7 @@ data class CheckIn(
     val completed: Boolean,
     val createJournalEntry: Boolean,
     val isSubmitted: Boolean = false,
+    val hasSubmissionConsent: Boolean = false,
 ) {
     /**
      *  Returns SHA-256 hash of [traceLocationId] which itself may also be SHA-256 hash.
@@ -51,4 +52,5 @@ fun CheckIn.toEntity() = TraceLocationCheckInEntity(
     completed = completed,
     createJournalEntry = createJournalEntry,
     isSubmitted = isSubmitted,
+    hasSubmissionConsent = hasSubmissionConsent,
 )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt
index ea9d8b74b..a1a12c842 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt
@@ -65,10 +65,14 @@ class CheckInRepository @Inject constructor(
         checkInDao.updateEntityById(checkInId, update)
     }
 
-    suspend fun markCheckInAsSubmitted(checkInId: Long) {
+    suspend fun updatePostSubmissionFlags(checkInId: Long) {
         Timber.d("markCheckInAsSubmitted(checkInId=$checkInId)")
         checkInDao.updateEntity(
-            TraceLocationCheckInEntity.SubmissionUpdate(checkInId = checkInId, isSubmitted = true)
+            TraceLocationCheckInEntity.SubmissionUpdate(
+                checkInId = checkInId,
+                isSubmitted = true,
+                hasSubmissionConsent = false,
+            )
         )
     }
 
@@ -88,4 +92,15 @@ class CheckInRepository @Inject constructor(
 
         return checkIn.toCheckIn()
     }
+
+    suspend fun updateSubmissionConsents(checkInIds: Collection<Long>, consent: Boolean) {
+        Timber.d("updateSubmissionConsents(checkInIds=%s, consent=%b)", checkInIds, consent)
+        val consentUpdates = checkInIds.map {
+            TraceLocationCheckInEntity.SubmissionConsentUpdate(
+                checkInId = it,
+                hasSubmissionConsent = consent
+            )
+        }
+        checkInDao.updateSubmissionConsents(consentUpdates)
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt
index 30e508cf4..8a5232531 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt
@@ -10,6 +10,7 @@ import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao
 import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity
 import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationConverters
 import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity
+import de.rki.coronawarnapp.eventregistration.storage.migration.PresenceTracingDatabaseMigration1To2
 import de.rki.coronawarnapp.util.database.CommonConverters
 import de.rki.coronawarnapp.util.di.AppContext
 import javax.inject.Inject
@@ -19,7 +20,7 @@ import javax.inject.Inject
         TraceLocationCheckInEntity::class,
         TraceLocationEntity::class
     ],
-    version = 1,
+    version = 2,
     exportSchema = true
 )
 @TypeConverters(CommonConverters::class, TraceLocationConverters::class)
@@ -29,8 +30,9 @@ abstract class TraceLocationDatabase : RoomDatabase() {
     abstract fun traceLocationDao(): TraceLocationDao
 
     class Factory @Inject constructor(@AppContext private val context: Context) {
-        fun create() = Room
-            .databaseBuilder(context, TraceLocationDatabase::class.java, TRACE_LOCATIONS_DATABASE_NAME)
+        fun create(databaseName: String = TRACE_LOCATIONS_DATABASE_NAME): TraceLocationDatabase = Room
+            .databaseBuilder(context, TraceLocationDatabase::class.java, databaseName)
+            .addMigrations(PresenceTracingDatabaseMigration1To2)
             .build()
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt
index 56ed45b2a..fd75d2f76 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt
@@ -40,6 +40,9 @@ interface CheckInDao {
     @Update(entity = TraceLocationCheckInEntity::class)
     suspend fun updateEntity(update: TraceLocationCheckInEntity.SubmissionUpdate)
 
+    @Update(entity = TraceLocationCheckInEntity::class)
+    suspend fun updateSubmissionConsents(update: Collection<TraceLocationCheckInEntity.SubmissionConsentUpdate>)
+
     @Query("DELETE FROM checkin")
     suspend fun deleteAll()
 
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 8ddcadd2a..0017cb099 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
@@ -25,12 +25,20 @@ data class TraceLocationCheckInEntity(
     @ColumnInfo(name = "completed") val completed: Boolean,
     @ColumnInfo(name = "createJournalEntry") val createJournalEntry: Boolean,
     @ColumnInfo(name = "submitted") val isSubmitted: Boolean,
+    @ColumnInfo(name = "submissionConsent") val hasSubmissionConsent: Boolean,
 ) {
 
     @Entity
     data class SubmissionUpdate(
         @PrimaryKey @ColumnInfo(name = "id") val checkInId: Long,
         @ColumnInfo(name = "submitted") val isSubmitted: Boolean,
+        @ColumnInfo(name = "submissionConsent") val hasSubmissionConsent: Boolean,
+    )
+
+    @Entity
+    data class SubmissionConsentUpdate(
+        @PrimaryKey @ColumnInfo(name = "id") val checkInId: Long,
+        @ColumnInfo(name = "submissionConsent") val hasSubmissionConsent: Boolean,
     )
 }
 
@@ -50,5 +58,6 @@ fun TraceLocationCheckInEntity.toCheckIn() = CheckIn(
     checkInEnd = checkInEnd,
     completed = completed,
     createJournalEntry = createJournalEntry,
-    isSubmitted = isSubmitted
+    isSubmitted = isSubmitted,
+    hasSubmissionConsent = hasSubmissionConsent
 )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt
new file mode 100644
index 000000000..e055de482
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt
@@ -0,0 +1,38 @@
+package de.rki.coronawarnapp.eventregistration.storage.migration
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import de.rki.coronawarnapp.exception.ExceptionCategory
+import de.rki.coronawarnapp.exception.reporting.report
+import timber.log.Timber
+
+/**
+ * Migrates the presence tracing database from version 1 to 2.
+ * The additional attribute:
+ * PresenceTracingCheckInEntity.submissionConsent was added
+ */
+object PresenceTracingDatabaseMigration1To2 : Migration(1, 2) {
+
+    override fun migrate(database: SupportSQLiteDatabase) {
+        try {
+            Timber.i("Attempting migration 1->2...")
+            performMigration(database)
+            Timber.i("Migration 1->2 successful.")
+        } catch (e: Exception) {
+            Timber.e(e, "Migration 1->2 failed")
+            e.report(ExceptionCategory.INTERNAL, "PresenceTracing database migration failed.")
+            throw e
+        }
+    }
+
+    private fun performMigration(database: SupportSQLiteDatabase) = with(database) {
+        Timber.d("Running MIGRATION_1_2")
+
+        migrateTraceLocationCheckInEntity()
+    }
+
+    private val migrateTraceLocationCheckInEntity: SupportSQLiteDatabase.() -> Unit = {
+        Timber.d("Table 'checkin': Add column 'submissionConsent'")
+        execSQL("ALTER TABLE `checkin` ADD COLUMN `submissionConsent` INTEGER NOT NULL DEFAULT '0'")
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt
new file mode 100644
index 000000000..811c995ea
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt
@@ -0,0 +1,13 @@
+package de.rki.coronawarnapp.presencetracing.checkins.common
+
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import kotlinx.coroutines.flow.map
+
+/**
+ * Returns completed [CheckIn]s only
+ */
+val CheckInRepository.completedCheckIns
+    get() = checkInsWithinRetention.map { checkIns ->
+        checkIns.filter { checkIn -> checkIn.completed }
+    }
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 5f801d507..718359832 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
@@ -10,6 +10,7 @@ import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException
 import de.rki.coronawarnapp.notification.ShareTestResultNotificationService
 import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService
 import de.rki.coronawarnapp.playbook.Playbook
+import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns
 import de.rki.coronawarnapp.submission.SubmissionSettings
 import de.rki.coronawarnapp.submission.Symptoms
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
@@ -143,7 +144,9 @@ class SubmissionTask @Inject constructor(
         )
         Timber.tag(TAG).d("Transformed keys with symptoms %s from %s to %s", symptoms, keys, transformedKeys)
 
-        val checkIns = checkInsRepository.checkInsWithinRetention.first()
+        val checkIns = checkInsRepository.completedCheckIns.first().filter {
+            it.hasSubmissionConsent && !it.isSubmitted
+        }
         val transformedCheckIns = checkInsTransformer.transform(checkIns, symptoms)
 
         Timber.tag(TAG).d("Transformed CheckIns from: %s to: %s", checkIns, transformedCheckIns)
@@ -171,7 +174,7 @@ class SubmissionTask @Inject constructor(
         Timber.tag(TAG).d("Marking %d submitted CheckIns.", checkIns.size)
         checkIns.forEach { checkIn ->
             try {
-                checkInsRepository.markCheckInAsSubmitted(checkIn.id)
+                checkInsRepository.updatePostSubmissionFlags(checkIn.id)
             } catch (e: Exception) {
                 e.reportProblem(TAG, "CheckIn $checkIn could not be marked as submitted")
             }
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 07b90907c..d97eb3591 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
@@ -24,6 +24,8 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFr
 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
+import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment
+import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragmentModule
 
 @Module
 internal abstract class EventRegistrationUIModule {
@@ -60,4 +62,7 @@ internal abstract class EventRegistrationUIModule {
 
     @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class])
     abstract fun showEventDetail(): QrCodeDetailFragment
+
+    @ContributesAndroidInjector(modules = [CheckInsConsentFragmentModule::class])
+    abstract fun checkInsConsentFragment(): CheckInsConsentFragment
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt
index 4ae061dcb..8f8a84313 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt
@@ -4,10 +4,9 @@ import android.view.ViewGroup
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsItemPastBinding
 import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
-import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
+import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.common.checkoutInfo
 import de.rki.coronawarnapp.util.list.SwipeConsumer
 import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer
-import org.joda.time.format.DateTimeFormat
 
 class PastCheckInVH(parent: ViewGroup) :
     BaseCheckInVH<PastCheckInVH.Item, TraceLocationAttendeeCheckinsItemPastBinding>(
@@ -24,20 +23,10 @@ class PastCheckInVH(parent: ViewGroup) :
         payloads: List<Any>
     ) -> Unit = { item, payloads ->
         val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item
-
-        val checkInStartUserTZ = curItem.checkin.checkInStart.toUserTimeZone()
-        val checkInEndUserTZ = curItem.checkin.checkInEnd.toUserTimeZone()
-
         description.text = curItem.checkin.description
         address.text = curItem.checkin.address
 
-        checkoutInfo.text = run {
-            val dayFormatted = checkInStartUserTZ.toLocalDate().toString(DateTimeFormat.mediumDate())
-            val startTimeFormatted = checkInStartUserTZ.toLocalTime().toString(DateTimeFormat.shortTime())
-            val endTimeFormatted = checkInEndUserTZ.toLocalTime().toString(DateTimeFormat.shortTime())
-
-            "$dayFormatted, $startTimeFormatted - $endTimeFormatted"
-        }
+        checkoutInfo.text = curItem.checkin.checkoutInfo
 
         menuAction.setupMenu(R.menu.menu_trace_location_attendee_checkin_item) {
             when (it.itemId) {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt
new file mode 100644
index 000000000..37acd4f5e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.common
+
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone
+import org.joda.time.format.DateTimeFormat
+
+inline val CheckIn.checkoutInfo: String
+    get() {
+        val checkInStartUserTZ = checkInStart.toUserTimeZone()
+        val checkInEndUserTZ = checkInEnd.toUserTimeZone()
+
+        val dayFormatted = checkInStartUserTZ.toLocalDate().toString(DateTimeFormat.mediumDate())
+        val startTimeFormatted = checkInStartUserTZ.toLocalTime().toString(DateTimeFormat.shortTime())
+        val endTimeFormatted = checkInEndUserTZ.toLocalTime().toString(DateTimeFormat.shortTime())
+        return "$dayFormatted, $startTimeFormatted - $endTimeFormatted"
+    }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt
new file mode 100644
index 000000000..fbd39ba15
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt
@@ -0,0 +1,37 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.viewbinding.ViewBinding
+import de.rki.coronawarnapp.util.lists.BindableVH
+import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter
+import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffer
+import de.rki.coronawarnapp.util.lists.modular.ModularAdapter
+import de.rki.coronawarnapp.util.lists.modular.mods.DataBinderMod
+import de.rki.coronawarnapp.util.lists.modular.mods.StableIdMod
+import de.rki.coronawarnapp.util.lists.modular.mods.TypedVHCreatorMod
+
+class CheckInsConsentAdapter :
+    ModularAdapter<CheckInsConsentAdapter.ItemVH<CheckInsConsentItem, ViewBinding>>(),
+    AsyncDiffUtilAdapter<CheckInsConsentItem> {
+
+    override val asyncDiffer: AsyncDiffer<CheckInsConsentItem> = AsyncDiffer(adapter = this)
+
+    init {
+        modules.addAll(
+            listOf(
+                StableIdMod(data),
+                DataBinderMod<CheckInsConsentItem, ItemVH<CheckInsConsentItem, ViewBinding>>(data),
+                TypedVHCreatorMod({ data[it] is HeaderCheckInsVH.Item }) { HeaderCheckInsVH(it) },
+                TypedVHCreatorMod({ data[it] is SelectableCheckInVH.Item }) { SelectableCheckInVH(it) },
+            )
+        )
+    }
+
+    override fun getItemCount(): Int = data.size
+
+    abstract class ItemVH<Item : CheckInsConsentItem, VB : ViewBinding>(
+        @LayoutRes layoutRes: Int,
+        parent: ViewGroup
+    ) : ModularAdapter.VH(layoutRes, parent), BindableVH<Item, VB>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt
new file mode 100644
index 000000000..048e2edd9
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt
@@ -0,0 +1,106 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.activity.OnBackPressedCallback
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.CheckInsConsentFragmentBinding
+import de.rki.coronawarnapp.util.DialogHelper
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.lists.diffutil.update
+import de.rki.coronawarnapp.util.ui.doNavigate
+import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted
+import timber.log.Timber
+import javax.inject.Inject
+
+class CheckInsConsentFragment : Fragment(R.layout.check_ins_consent_fragment), AutoInject {
+
+    private val binding: CheckInsConsentFragmentBinding by viewBindingLazy()
+
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val viewModel: CheckInsConsentViewModel by cwaViewModelsAssisted(
+        factoryProducer = { viewModelFactory },
+        constructorCall = { factory, savedState ->
+            factory as CheckInsConsentViewModel.Factory
+            factory.create(
+                savedState = savedState
+            )
+        }
+    )
+
+    private val adapter = CheckInsConsentAdapter()
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+
+        val backCallback = object : OnBackPressedCallback(true) {
+            override fun handleOnBackPressed() = viewModel.onCloseClick()
+        }
+        requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback)
+
+        with(binding) {
+            checkInsRecycler.adapter = adapter
+            toolbar.setNavigationOnClickListener {
+                viewModel.onCloseClick()
+            }
+            skipButton.setOnClickListener { viewModel.onSkipClick() }
+            continueButton.setOnClickListener { viewModel.shareSelectedCheckIns() }
+        }
+
+        viewModel.checkIns.observe(viewLifecycleOwner) {
+            adapter.update(it)
+            binding.continueButton.isEnabled = it.any { item ->
+                item is SelectableCheckInVH.Item && item.checkIn.hasSubmissionConsent
+            }
+        }
+
+        viewModel.events.observe(viewLifecycleOwner) {
+            when (it) {
+                CheckInsConsentNavigation.OpenCloseDialog -> showCloseDialog()
+                CheckInsConsentNavigation.OpenSkipDialog -> showSkipDialog()
+                CheckInsConsentNavigation.ToHomeFragment -> doNavigate(
+                    CheckInsConsentFragmentDirections.actionCheckInsConsentFragmentToMainFragment()
+                )
+                CheckInsConsentNavigation.ToSubmissionResultReadyFragment -> doNavigate(
+                    CheckInsConsentFragmentDirections.actionCheckInsConsentFragmentToSubmissionResultReadyFragment()
+                )
+                CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment -> doNavigate(
+                    CheckInsConsentFragmentDirections
+                        .actionCheckInsConsentFragmentToSubmissionTestResultConsentGivenFragment()
+                )
+            }
+        }
+    }
+
+    private fun showSkipDialog() {
+        MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.trace_location_attendee_consent_dialog_title)
+            .setMessage(R.string.trace_location_attendee_consent_dialog_message)
+            .setPositiveButton(R.string.trace_location_attendee_consent_dialog_positive_button) { _, _ ->
+                Timber.d("showSkipDialog:Stay on CheckInsConsentFragment")
+            }
+            .setNegativeButton(R.string.trace_location_attendee_consent_dialog_negative_button) { _, _ ->
+                viewModel.doNotShareCheckIns()
+            }
+            .show()
+    }
+
+    private fun showCloseDialog() {
+        val closeDialogInstance = DialogHelper.DialogInstance(
+            context = requireActivity(),
+            title = R.string.submission_test_result_available_close_dialog_title_consent_given,
+            message = R.string.submission_test_result_available_close_dialog_body_consent_given,
+            positiveButton = R.string.submission_test_result_available_close_dialog_continue_button,
+            negativeButton = R.string.submission_test_result_available_close_dialog_cancel_button,
+            cancelable = true,
+            positiveButtonFunction = {
+                Timber.d("showCloseDialog:Stay on CheckInsConsentFragment")
+            },
+            negativeButtonFunction = { viewModel.onCancelConfirmed() }
+        )
+        DialogHelper.showDialog(closeDialogInstance)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt
new file mode 100644
index 000000000..d7fd545fd
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt
@@ -0,0 +1,19 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+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 CheckInsConsentFragmentModule {
+
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(CheckInsConsentViewModel::class)
+    abstract fun checkInsConsentFragment(
+        factory: CheckInsConsentViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt
new file mode 100644
index 000000000..53f9a7859
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt
@@ -0,0 +1,5 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import de.rki.coronawarnapp.util.lists.HasStableId
+
+interface CheckInsConsentItem : HasStableId
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt
new file mode 100644
index 000000000..ff3bbef59
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+sealed class CheckInsConsentNavigation {
+    object OpenCloseDialog : CheckInsConsentNavigation()
+    object OpenSkipDialog : CheckInsConsentNavigation()
+    object ToHomeFragment : CheckInsConsentNavigation()
+    object ToSubmissionTestResultConsentGivenFragment : CheckInsConsentNavigation()
+    object ToSubmissionResultReadyFragment : CheckInsConsentNavigation()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt
new file mode 100644
index 000000000..0945210df
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt
@@ -0,0 +1,165 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.asLiveData
+import dagger.assisted.Assisted
+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.submission.SubmissionRepository
+import de.rki.coronawarnapp.submission.auto.AutoSubmission
+import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.ui.SingleLiveEvent
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import timber.log.Timber
+
+class CheckInsConsentViewModel @AssistedInject constructor(
+    @Assisted private val savedState: SavedStateHandle,
+    dispatcherProvider: DispatcherProvider,
+    private val checkInRepository: CheckInRepository,
+    private val submissionRepository: SubmissionRepository,
+    private val autoSubmission: AutoSubmission
+) : CWAViewModel(dispatcherProvider) {
+
+    private val selectedSetFlow = MutableStateFlow(initialSet())
+
+    val checkIns: LiveData<List<CheckInsConsentItem>> = combine(
+        checkInRepository.completedCheckIns,
+        selectedSetFlow
+    ) { checkIns, ids ->
+        mutableListOf<CheckInsConsentItem>().apply {
+            add(headerItem(checkIns))
+            addAll(mapCheckIns(checkIns, ids))
+        }
+    }.asLiveData(context = dispatcherProvider.Default)
+
+    val events = SingleLiveEvent<CheckInsConsentNavigation>()
+
+    fun shareSelectedCheckIns() = launch {
+        // Reset selected check-ins from previous selection
+        resetPreviousSubmissionConsents()
+
+        Timber.d("Navigate to shareSelectedCheckIns")
+        autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+
+        // Update CheckIns for new submission
+        val idsWithConsent = selectedSetFlow.value
+        checkInRepository.updateSubmissionConsents(
+            checkInIds = idsWithConsent,
+            consent = true,
+        )
+
+        val event = if (submissionRepository.hasViewedTestResult.first()) {
+            Timber.d("Navigate to SubmissionResultReadyFragment")
+            CheckInsConsentNavigation.ToSubmissionResultReadyFragment
+        } else {
+            Timber.d("Navigate to SubmissionTestResultConsentGivenFragment")
+            CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment
+        }
+        events.postValue(event)
+    }
+
+    fun doNotShareCheckIns() = launch {
+        // Reset selected check-ins from previous selection
+        resetPreviousSubmissionConsents()
+
+        Timber.d("Navigate to doNotShareCheckIns")
+        autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+        val event = if (submissionRepository.hasViewedTestResult.first()) {
+            Timber.d("Navigate to SubmissionResultReadyFragment")
+            CheckInsConsentNavigation.ToSubmissionResultReadyFragment
+        } else {
+            Timber.d("Navigate to SubmissionTestResultConsentGivenFragment")
+            CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment
+        }
+        events.postValue(event)
+    }
+
+    fun onCloseClick() = launch {
+        val event = if (submissionRepository.hasViewedTestResult.first()) {
+            Timber.d("openSkipDialog")
+            CheckInsConsentNavigation.OpenSkipDialog
+        } else {
+            Timber.d("openCloseDialog")
+            CheckInsConsentNavigation.OpenCloseDialog
+        }
+        events.postValue(event)
+    }
+
+    fun onCancelConfirmed() {
+        Timber.d("onCancelConfirmed")
+        events.postValue(CheckInsConsentNavigation.ToHomeFragment)
+    }
+
+    fun onSkipClick() {
+        Timber.d("onSkipClick")
+        events.postValue(CheckInsConsentNavigation.OpenSkipDialog)
+    }
+
+    private fun headerItem(checkIns: List<CheckIn>) = HeaderCheckInsVH.Item(
+        selectAll = {
+            val ids = checkIns.map { it.id }
+            if (!selectedSetFlow.value.containsAll(ids)) {
+                selectedSetFlow.value = updateSet(ids)
+            }
+        }
+    )
+
+    private fun mapCheckIns(checkIns: List<CheckIn>, ids: Set<Long>): List<CheckInsConsentItem> =
+        checkIns.sortedByDescending { it.checkInEnd }
+            .map { checkIn ->
+                SelectableCheckInVH.Item(
+                    checkIn = checkIn.copy(hasSubmissionConsent = ids.contains(checkIn.id)),
+                    onItemSelected = { selectedSetFlow.value = updateSet(listOf(it.id)) }
+                )
+            }
+
+    private fun updateSet(ids: List<Long>) =
+        mutableSetOf<Long>().apply {
+            if (!selectedSetFlow.value.containsAll(ids)) {
+                addAll(ids) // New Ids
+                addAll(selectedSetFlow.value) // Existing Ids
+            } else {
+                addAll(
+                    selectedSetFlow.value.toMutableSet().apply { removeAll(ids) }
+                )
+            }
+        }.also {
+            savedState.set(SET_KEY, it)
+            Timber.d("SelectedCheckIns=$it")
+        }
+
+    private fun initialSet(): Set<Long> = savedState.get(SET_KEY) ?: emptySet()
+
+    private fun resetPreviousSubmissionConsents() = launch {
+        try {
+            Timber.d("Trying to reset submission consents")
+            checkInRepository.apply {
+                val ids = completedCheckIns.first().filter { it.hasSubmissionConsent }.map { it.id }
+                updateSubmissionConsents(ids, consent = false)
+            }
+
+            Timber.d("Resetting submission consents was successful")
+        } catch (error: Exception) {
+            Timber.e(error, "Failed to reset SubmissionConsents")
+        }
+    }
+
+    @AssistedFactory
+    interface Factory : CWAViewModelFactory<CheckInsConsentViewModel> {
+        fun create(
+            savedState: SavedStateHandle,
+        ): CheckInsConsentViewModel
+    }
+
+    companion object {
+        private const val SET_KEY = "selected_checkIn_set"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt
new file mode 100644
index 000000000..397b621ac
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt
@@ -0,0 +1,31 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import android.view.ViewGroup
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.TraceLocationAttendeeConsentHeaderBinding
+import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer
+
+class HeaderCheckInsVH(parent: ViewGroup) :
+    CheckInsConsentAdapter.ItemVH<HeaderCheckInsVH.Item, TraceLocationAttendeeConsentHeaderBinding>(
+        layoutRes = R.layout.trace_location_attendee_consent_header,
+        parent = parent
+    ) {
+
+    override val viewBinding: Lazy<TraceLocationAttendeeConsentHeaderBinding> = lazy {
+        TraceLocationAttendeeConsentHeaderBinding.bind(itemView)
+    }
+
+    override val onBindData: TraceLocationAttendeeConsentHeaderBinding.(
+        item: Item,
+        payloads: List<Any>
+    ) -> Unit = { item, _ ->
+        selectAllButton.setOnClickListener { item.selectAll() }
+    }
+
+    data class Item(
+        val selectAll: () -> Unit
+    ) : CheckInsConsentItem, HasPayloadDiffer {
+        override val stableId: Long = Item::class.simpleName.hashCode().toLong()
+        override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt
new file mode 100644
index 000000000..ae2060f42
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt
@@ -0,0 +1,44 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import android.view.ViewGroup
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.TraceLocationAttendeeConsentSelectableCheckInBinding
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.common.checkoutInfo
+import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer
+
+class SelectableCheckInVH(parent: ViewGroup) :
+    CheckInsConsentAdapter.ItemVH<SelectableCheckInVH.Item, TraceLocationAttendeeConsentSelectableCheckInBinding>(
+        layoutRes = R.layout.trace_location_attendee_consent_selectable_check_in,
+        parent = parent
+    ) {
+
+    override val viewBinding: Lazy<TraceLocationAttendeeConsentSelectableCheckInBinding> = lazy {
+        TraceLocationAttendeeConsentSelectableCheckInBinding.bind(itemView)
+    }
+
+    override val onBindData: TraceLocationAttendeeConsentSelectableCheckInBinding.(
+        item: Item,
+        payloads: List<Any>
+    ) -> Unit = { item, payloads ->
+        val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item
+        val checkIn = curItem.checkIn
+        val imageResource = if (checkIn.hasSubmissionConsent) R.drawable.ic_selected else R.drawable.ic_unselected
+
+        checkbox.setImageResource(imageResource)
+        title.text = checkIn.description
+        subtitle.text = checkIn.address
+        checkoutInfo.text = checkIn.checkoutInfo
+
+        checkbox.setOnClickListener { item.onItemSelected(checkIn) }
+        itemView.setOnClickListener { item.onItemSelected(checkIn) }
+    }
+
+    data class Item(
+        val checkIn: CheckIn,
+        val onItemSelected: (CheckIn) -> Unit
+    ) : CheckInsConsentItem, HasPayloadDiffer {
+        override val stableId: Long = checkIn.id
+        override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt
index 3e5d0a7cb..78d338084 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt
@@ -8,11 +8,13 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater
+import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -24,13 +26,14 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor(
     dispatcherProvider: DispatcherProvider,
     tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory,
     submissionRepository: SubmissionRepository,
+    private val checkInRepository: CheckInRepository,
     private val autoSubmission: AutoSubmission,
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
     val routeToScreen = SingleLiveEvent<NavDirections>()
 
-    val consentFlow = submissionRepository.hasGivenConsentToSubmission
+    private val consentFlow = submissionRepository.hasGivenConsentToSubmission
     val consent = consentFlow.asLiveData(dispatcherProvider.Default)
     val showPermissionRequest = SingleLiveEvent<(Activity) -> Unit>()
     val showCloseDialog = SingleLiveEvent<Unit>()
@@ -39,14 +42,21 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor(
 
     private val tekHistoryUpdater = tekHistoryUpdaterFactory.create(
         object : TEKHistoryUpdater.Callback {
-            override fun onTEKAvailable(teks: List<TemporaryExposureKey>) {
-                Timber.d("onTEKAvailable(teks.size=%d)", teks.size)
-                autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+            override fun onTEKAvailable(teks: List<TemporaryExposureKey>) = launch {
+                Timber.tag(TAG).d("onTEKAvailable(teks.size=%d)", teks.size)
                 showKeysRetrievalProgress.postValue(false)
-                routeToScreen.postValue(
+                val completedCheckInsExist = checkInRepository.completedCheckIns.first().isNotEmpty()
+                val navDirections = if (completedCheckInsExist) {
+                    Timber.tag(TAG).d("Navigate to CheckInsConsentFragment")
+                    SubmissionTestResultAvailableFragmentDirections
+                        .actionSubmissionTestResultAvailableFragmentToCheckInsConsentFragment()
+                } else {
+                    autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+                    Timber.tag(TAG).d("Navigate to SubmissionTestResultConsentGivenFragment")
                     SubmissionTestResultAvailableFragmentDirections
                         .actionSubmissionTestResultAvailableFragmentToSubmissionTestResultConsentGivenFragment()
-                )
+                }
+                routeToScreen.postValue(navDirections)
             }
 
             override fun onTEKPermissionDeclined() {
@@ -59,13 +69,13 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor(
             }
 
             override fun onTracingConsentRequired(onConsentResult: (given: Boolean) -> Unit) {
-                Timber.d("onTracingConsentRequired")
+                Timber.tag(TAG).d("onTracingConsentRequired")
                 showKeysRetrievalProgress.postValue(false)
                 showTracingConsentDialog.postValue(onConsentResult)
             }
 
             override fun onPermissionRequired(permissionRequest: (Activity) -> Unit) {
-                Timber.d("onPermissionRequired")
+                Timber.tag(TAG).d("onPermissionRequired")
                 showKeysRetrievalProgress.postValue(false)
                 showPermissionRequest.postValue(permissionRequest)
             }
@@ -109,10 +119,10 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor(
         showKeysRetrievalProgress.value = true
         launch {
             if (consentFlow.first()) {
-                Timber.d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission")
+                Timber.tag(TAG).d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission")
                 tekHistoryUpdater.updateTEKHistoryOrRequestPermission()
             } else {
-                Timber.d("routeToScreen:SubmissionTestResultNoConsentFragment")
+                Timber.tag(TAG).d("routeToScreen:SubmissionTestResultNoConsentFragment")
                 analyticsKeySubmissionCollector.reportConsentWithdrawn()
                 showKeysRetrievalProgress.postValue(false)
                 routeToScreen.postValue(
@@ -130,4 +140,8 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor(
 
     @AssistedFactory
     interface Factory : SimpleCWAViewModelFactory<SubmissionTestResultAvailableViewModel>
+
+    companion object {
+        private const val TAG = "TestAvailableViewModel"
+    }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt
index da6c4e514..0b6bee532 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt
@@ -9,6 +9,7 @@ import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.exception.ExceptionCategory
 import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.nearby.ENFClient
@@ -16,6 +17,7 @@ import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater
+import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -30,6 +32,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con
     tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory,
     interoperabilityRepository: InteroperabilityRepository,
     private val submissionRepository: SubmissionRepository,
+    private val checkInRepository: CheckInRepository,
     private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
 
@@ -48,36 +51,44 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con
 
     private val tekHistoryUpdater = tekHistoryUpdaterFactory.create(
         object : TEKHistoryUpdater.Callback {
-            override fun onTEKAvailable(teks: List<TemporaryExposureKey>) {
-                Timber.d("onTEKAvailable(tek.size=%d)", teks.size)
-                autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+            override fun onTEKAvailable(teks: List<TemporaryExposureKey>) = launch {
+                Timber.tag(TAG).d("onTEKAvailable(tek.size=%d)", teks.size)
                 showKeysRetrievalProgress.postValue(false)
-                routeToScreen.postValue(
+
+                val completedCheckInsExist = checkInRepository.completedCheckIns.first().isNotEmpty()
+                val navDirections = if (completedCheckInsExist) {
+                    Timber.tag(TAG).d("Navigate to CheckInsConsentFragment")
+                    SubmissionResultPositiveOtherWarningNoConsentFragmentDirections
+                        .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToCheckInsConsentFragment()
+                } else {
+                    autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+                    Timber.tag(TAG).d("Navigate to SubmissionResultReadyFragment")
                     SubmissionResultPositiveOtherWarningNoConsentFragmentDirections
                         .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToSubmissionResultReadyFragment()
-                )
+                }
+                routeToScreen.postValue(navDirections)
             }
 
             override fun onTEKPermissionDeclined() {
-                Timber.d("onTEKPermissionDeclined")
+                Timber.tag(TAG).d("onTEKPermissionDeclined")
                 showKeysRetrievalProgress.postValue(false)
                 // stay on screen
             }
 
             override fun onTracingConsentRequired(onConsentResult: (given: Boolean) -> Unit) {
-                Timber.d("onTracingConsentRequired")
+                Timber.tag(TAG).d("onTracingConsentRequired")
                 showKeysRetrievalProgress.postValue(false)
                 showTracingConsentDialog.postValue(onConsentResult)
             }
 
             override fun onPermissionRequired(permissionRequest: (Activity) -> Unit) {
-                Timber.d("onPermissionRequired")
+                Timber.tag(TAG).d("onPermissionRequired")
                 showKeysRetrievalProgress.postValue(false)
                 showPermissionRequest.postValue(permissionRequest)
             }
 
             override fun onError(error: Throwable) {
-                Timber.e(error, "Couldn't access temporary exposure key history.")
+                Timber.tag(TAG).e(error, "Couldn't access temporary exposure key history.")
                 showKeysRetrievalProgress.postValue(false)
                 error.report(ExceptionCategory.EXPOSURENOTIFICATION, "Failed to obtain TEKs.")
             }
@@ -96,10 +107,10 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con
         submissionRepository.giveConsentToSubmission()
         launch {
             if (enfClient.isTracingEnabled.first()) {
-                Timber.d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission()")
+                Timber.tag(TAG).d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission()")
                 tekHistoryUpdater.updateTEKHistoryOrRequestPermission()
             } else {
-                Timber.d("showEnableTracingEvent:Unit")
+                Timber.tag(TAG).d("showEnableTracingEvent:Unit")
                 showKeysRetrievalProgress.postValue(false)
                 showEnableTracingEvent.postValue(Unit)
             }
@@ -107,6 +118,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con
     }
 
     fun onDataPrivacyClick() {
+        Timber.tag(TAG).d("onDataPrivacyClick")
         routeToScreen.postValue(
             SubmissionResultPositiveOtherWarningNoConsentFragmentDirections
                 .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToInformationPrivacyFragment()
@@ -114,6 +126,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con
     }
 
     fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+        Timber.tag(TAG).d("handleActivityResult($resultCode)")
         showKeysRetrievalProgress.value = true
         tekHistoryUpdater.handleActivityResult(requestCode, resultCode, data)
     }
@@ -126,4 +139,8 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con
     interface Factory : CWAViewModelFactory<SubmissionResultPositiveOtherWarningNoConsentViewModel> {
         fun create(): SubmissionResultPositiveOtherWarningNoConsentViewModel
     }
+
+    companion object {
+        private const val TAG = "WarnNoConsentViewModel"
+    }
 }
diff --git a/Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml b/Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml
new file mode 100644
index 000000000..37cd803b9
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml
@@ -0,0 +1,56 @@
+<?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"
+    tools:context=".ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment">
+
+    <com.google.android.material.appbar.MaterialToolbar
+        android:id="@+id/toolbar"
+        style="@style/CWAToolbar.Close"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:title="@string/trace_location_attendee_consent_title" />
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/checkInsRecycler"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginBottom="16dp"
+        android:orientation="vertical"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+        app:layout_constraintBottom_toTopOf="@id/continue_button"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/toolbar"
+        tools:listitem="@layout/trace_location_attendee_consent_selectable_check_in" />
+
+    <Button
+        android:id="@+id/continue_button"
+        style="@style/buttonPrimary"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:layout_marginBottom="16dp"
+        android:text="@string/trace_location_attendee_consent_continue"
+        app:layout_constraintBottom_toTopOf="@id/skip_button"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+    <Button
+        android:id="@+id/skip_button"
+        style="@style/buttonPrimary"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:layout_marginBottom="24dp"
+        android:text="@string/trace_location_attendee_consent_skip"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml
new file mode 100644
index 000000000..04f98963f
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical"
+    android:paddingStart="24dp"
+    android:paddingTop="18dp"
+    android:paddingEnd="24dp"
+    android:paddingBottom="8dp">
+
+    <TextView
+        style="@style/subtitleMedium"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/trace_location_attendee_consent_header_description" />
+
+    <com.google.android.material.button.MaterialButton
+        android:id="@+id/select_all_button"
+        style="@style/materialTextButton"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:layout_marginTop="8dp"
+        android:text="@string/trace_location_attendee_consent_header_button" />
+</LinearLayout>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml
new file mode 100644
index 000000000..703729940
--- /dev/null
+++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml
@@ -0,0 +1,67 @@
+<?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"
+    style="@style/contactDiaryCardRipple"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="16dp"
+    android:layout_marginVertical="8dp"
+    android:focusable="true">
+
+    <ImageView
+        android:id="@+id/checkbox"
+        android:layout_width="@dimen/spacing_medium"
+        android:layout_height="@dimen/spacing_medium"
+        android:layout_marginStart="@dimen/spacing_small"
+        android:layout_marginTop="13dp"
+        android:background="?selectableItemBackgroundBorderless"
+        android:clickable="false"
+        android:focusable="false"
+        android:importantForAccessibility="no"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:srcCompat="@drawable/ic_unselected"
+        tools:srcCompat="@drawable/ic_selected" />
+
+    <TextView
+        android:id="@+id/title"
+        style="@style/materialSubtitleSixteen"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="15dp"
+        android:layout_marginTop="13dp"
+        android:layout_marginEnd="10dp"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/checkbox"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:text="Hairdresser" />
+
+    <TextView
+        android:id="@+id/subtitle"
+        style="@style/body2"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:layout_marginEnd="10dp"
+        android:textColor="@color/colorTextPrimary2"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/title"
+        app:layout_constraintTop_toBottomOf="@id/title"
+        tools:text="Berlin" />
+
+    <TextView
+        android:id="@+id/checkoutInfo"
+        style="@style/body2"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="10dp"
+        android:layout_marginEnd="10dp"
+        android:layout_marginBottom="10dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/title"
+        app:layout_constraintTop_toBottomOf="@id/subtitle"
+        tools:text="21.01.21, 18:01 - 21:00 Uhr" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ 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 6a025e839..44416b8d9 100644
--- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
+++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml
@@ -278,6 +278,11 @@
             app:destination="@id/submissionResultReadyFragment"
             app:popUpTo="@id/mainFragment"
             app:popUpToInclusive="false" />
+        <action
+            android:id="@+id/action_submissionResultPositiveOtherWarningNoConsentFragment_to_checkInsConsentFragment"
+            app:destination="@id/checkInsConsentFragment"
+            app:popUpTo="@id/mainFragment"
+            app:popUpToInclusive="false" />
     </fragment>
     <fragment
         android:id="@+id/submissionTestResultPendingFragment"
@@ -488,6 +493,11 @@
             app:destination="@id/submissionTestResultConsentGivenFragment"
             app:popUpTo="@id/mainFragment"
             app:popUpToInclusive="false" />
+        <action
+            android:id="@+id/action_submissionTestResultAvailableFragment_to_checkInsConsentFragment"
+            app:destination="@id/checkInsConsentFragment"
+            app:popUpTo="@id/mainFragment"
+            app:popUpToInclusive="false" />
         <action
             android:id="@+id/action_submissionTestResultAvailableFragment_to_submissionTestResultNoConsentFragment"
             app:destination="@id/submissionTestResultNoConsentFragment"
@@ -609,4 +619,28 @@
         android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalFragment"
         android:label="DebugLogLegalFragment"
         tools:layout="@layout/bugreporting_legal_fragment" />
+
+    <fragment
+        android:id="@+id/checkInsConsentFragment"
+        android:name="de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment"
+        android:label="check_ins_consent_fragment"
+        tools:layout="@layout/check_ins_consent_fragment">
+        <action
+            android:id="@+id/action_checkInsConsentFragment_to_mainFragment"
+            app:destination="@id/mainFragment"
+            app:popUpTo="@id/nav_graph"
+            app:popUpToInclusive="true" />
+
+        <action
+            android:id="@+id/action_checkInsConsentFragment_to_submissionResultReadyFragment"
+            app:destination="@id/submissionResultReadyFragment"
+            app:popUpTo="@id/mainFragment"
+            app:popUpToInclusive="false" />
+
+        <action
+            android:id="@+id/action_checkInsConsentFragment_to_submissionTestResultConsentGivenFragment"
+            app:destination="@id/submissionTestResultConsentGivenFragment"
+            app:popUpTo="@id/mainFragment"
+            app:popUpToInclusive="false" />
+    </fragment>
 </navigation>
diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml
index ecacc05a6..263879c3f 100644
--- a/Corona-Warn-App/src/main/res/values/styles.xml
+++ b/Corona-Warn-App/src/main/res/values/styles.xml
@@ -155,6 +155,10 @@
         <item name="android:textColor">@color/colorAccent</item>
     </style>
 
+    <style name="materialTextButton" parent="Widget.MaterialComponents.Button.TextButton.Dialog.Flush">
+        <item name="android:textColor">@color/colorAccent</item>
+    </style>
+
     <style name="buttonIcon">
         <item name="android:background">@drawable/circle_ripple</item>
         <item name="android:backgroundTint">@color/button_back</item>
@@ -289,9 +293,13 @@
         <item name="android:textColor">@color/colorTextPrimary1</item>
     </style>
 
-    <style name="subtitleBoldSixteen" parent="@style/TextAppearance.MaterialComponents.Subtitle1">
+
+    <style name="materialSubtitleSixteen" parent="@style/TextAppearance.MaterialComponents.Subtitle1">
         <item name="android:textColor">@color/colorTextPrimary1</item>
         <item name="android:textSize">16sp</item>
+    </style>
+
+    <style name="subtitleBoldSixteen" parent="materialSubtitleSixteen">
         <item name="android:textStyle">bold</item>
     </style>
 
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 23ea1802a..393a07fcf 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
@@ -115,6 +115,7 @@ class CheckInRepositoryTest : BaseTest() {
                         completed = false,
                         createJournalEntry = false,
                         isSubmitted = true,
+                        hasSubmissionConsent = false,
                     )
                 )
             }
@@ -160,6 +161,7 @@ class CheckInRepositoryTest : BaseTest() {
                 completed = false,
                 createJournalEntry = false,
                 isSubmitted = true,
+                hasSubmissionConsent = true,
             )
         )
         runBlockingTest {
@@ -181,6 +183,7 @@ class CheckInRepositoryTest : BaseTest() {
                     completed = false,
                     createJournalEntry = false,
                     isSubmitted = true,
+                    hasSubmissionConsent = true,
                 )
             )
         }
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 778d26c72..e36271385 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
@@ -74,7 +74,7 @@ class SubmissionTaskTest : BaseTest() {
 
     private val settingLastUserActivityUTC: FlowPreference<Instant> = mockFlowPreference(Instant.EPOCH.plus(1))
 
-    private val testCheckIn1 = CheckIn(
+    private val validCheckIn = CheckIn(
         id = 1L,
         traceLocationId = mockk(),
         version = 1,
@@ -88,11 +88,16 @@ class SubmissionTaskTest : BaseTest() {
         cnPublicKey = "cnPublicKey",
         checkInStart = Instant.EPOCH,
         checkInEnd = Instant.EPOCH.plus(9000),
-        completed = false,
+        completed = true,
         createJournalEntry = false,
-        isSubmitted = true
+        isSubmitted = false,
+        hasSubmissionConsent = true
     )
 
+    private val invalidCheckIn1 = validCheckIn.copy(id = 2L, completed = false)
+    private val invalidCheckIn2 = validCheckIn.copy(id = 3L, isSubmitted = true)
+    private val invalidCheckIn3 = validCheckIn.copy(id = 4L, hasSubmissionConsent = false)
+
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
@@ -133,7 +138,14 @@ class SubmissionTaskTest : BaseTest() {
 
         every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1))
 
-        every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(testCheckIn1))
+        every { checkInRepository.checkInsWithinRetention } returns flowOf(
+            listOf(
+                validCheckIn,
+                invalidCheckIn1,
+                invalidCheckIn2,
+                invalidCheckIn3
+            )
+        )
         coEvery { checkInsTransformer.transform(any(), any()) } returns emptyList()
     }
 
@@ -199,7 +211,7 @@ class SubmissionTaskTest : BaseTest() {
             submissionSettings.symptoms
             settingSymptomsPreference.update(match { it.invoke(mockk()) == null })
 
-            checkInRepository.markCheckInAsSubmitted(testCheckIn1.id)
+            checkInRepository.updatePostSubmissionFlags(validCheckIn.id)
 
             autoSubmission.updateMode(AutoSubmission.Mode.DISABLED)
 
@@ -210,6 +222,12 @@ class SubmissionTaskTest : BaseTest() {
             shareTestResultNotificationService.cancelSharePositiveTestResultNotification()
             testResultAvailableNotificationService.cancelTestResultAvailableNotification()
         }
+
+        coVerify(exactly = 0) {
+            checkInRepository.updatePostSubmissionFlags(invalidCheckIn1.id)
+            checkInRepository.updatePostSubmissionFlags(invalidCheckIn2.id)
+            checkInRepository.updatePostSubmissionFlags(invalidCheckIn3.id)
+        }
     }
 
     @Test
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt
new file mode 100644
index 000000000..6f8c68bbd
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt
@@ -0,0 +1,338 @@
+package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent
+
+import androidx.lifecycle.SavedStateHandle
+import de.rki.coronawarnapp.eventregistration.checkins.CheckIn
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
+import de.rki.coronawarnapp.submission.SubmissionRepository
+import de.rki.coronawarnapp.submission.auto.AutoSubmission
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import kotlinx.coroutines.flow.flowOf
+import okio.ByteString.Companion.decodeBase64
+import okio.ByteString.Companion.encode
+import org.joda.time.Instant
+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 CheckInsConsentViewModelTest : BaseTest() {
+
+    @MockK lateinit var savedState: SavedStateHandle
+    @MockK lateinit var checkInRepository: CheckInRepository
+    @MockK lateinit var submissionRepository: SubmissionRepository
+    @MockK lateinit var autoSubmission: AutoSubmission
+
+    private val checkIn1 = CheckIn(
+        id = 1L,
+        traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".decodeBase64()!!,
+        version = 1,
+        type = 2,
+        description = "brothers birthday",
+        address = "Malibu",
+        traceLocationStart = Instant.EPOCH,
+        traceLocationEnd = null,
+        defaultCheckInLengthInMinutes = null,
+        cryptographicSeed = "cryptographicSeed".encode(),
+        cnPublicKey = "cnPublicKey",
+        checkInStart = Instant.EPOCH,
+        checkInEnd = Instant.EPOCH,
+        completed = true,
+        createJournalEntry = false
+    )
+
+    private val checkIn2 = CheckIn(
+        id = 2L,
+        traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".decodeBase64()!!,
+        version = 1,
+        type = 2,
+        description = "brothers birthday",
+        address = "Malibu",
+        traceLocationStart = Instant.EPOCH,
+        traceLocationEnd = null,
+        defaultCheckInLengthInMinutes = null,
+        cryptographicSeed = "cryptographicSeed".encode(),
+        cnPublicKey = "cnPublicKey",
+        checkInStart = Instant.EPOCH,
+        checkInEnd = Instant.EPOCH,
+        completed = true,
+        createJournalEntry = false
+    )
+
+    private val checkIn3 = CheckIn(
+        id = 3L,
+        traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".decodeBase64()!!,
+        version = 1,
+        type = 2,
+        description = "brothers birthday",
+        address = "Malibu",
+        traceLocationStart = Instant.EPOCH,
+        traceLocationEnd = null,
+        defaultCheckInLengthInMinutes = null,
+        cryptographicSeed = "cryptographicSeed".encode(),
+        cnPublicKey = "cnPublicKey",
+        checkInStart = Instant.EPOCH,
+        checkInEnd = Instant.EPOCH,
+        completed = false,
+        createJournalEntry = false
+    )
+
+    @BeforeEach
+    fun setUp() {
+        MockKAnnotations.init(this)
+
+        every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkIn1, checkIn2, checkIn3))
+        coEvery { checkInRepository.updateSubmissionConsents(any(), true) } just Runs
+        coEvery { checkInRepository.updateSubmissionConsents(any(), false) } just Runs
+        every { savedState.set(any(), any<Set<Long>>()) } just Runs
+        every { autoSubmission.updateMode(any()) } just Runs
+        every { submissionRepository.hasViewedTestResult } returns flowOf(false)
+        every { savedState.get<Set<Long>>(any()) } returns emptySet()
+    }
+
+    @Test
+    fun `Nothing is selected initially`() {
+        every { savedState.get<Set<Long>>(any()) } returns emptySet()
+
+        val viewModel = createViewModel()
+        viewModel.checkIns.getOrAwaitValue().apply {
+            size shouldBe 3
+            get(0).apply {
+                this is HeaderCheckInsVH.Item
+            }
+
+            get(1).apply {
+                this as SelectableCheckInVH.Item
+                this.checkIn.hasSubmissionConsent shouldBe false
+            }
+
+            get(2).apply {
+                this as SelectableCheckInVH.Item
+                this.checkIn.hasSubmissionConsent shouldBe false
+            }
+        }
+    }
+
+    @Test
+    fun `Saved state is restored`() {
+        every { savedState.get<Set<Long>>(any()) } returns setOf(1L)
+        val viewModel = createViewModel()
+        viewModel.checkIns.getOrAwaitValue().apply {
+            size shouldBe 3
+            get(0).apply {
+                this is HeaderCheckInsVH.Item
+            }
+
+            get(1).apply {
+                this as SelectableCheckInVH.Item
+                this.checkIn.hasSubmissionConsent shouldBe true
+            }
+
+            get(2).apply {
+                this as SelectableCheckInVH.Item
+                this.checkIn.hasSubmissionConsent shouldBe false
+            }
+        }
+    }
+
+    @Test
+    fun `Select all`() {
+        every { savedState.get<Set<Long>>(any()) } returns emptySet()
+        val viewModel = createViewModel()
+        viewModel.checkIns.getOrAwaitValue().apply {
+            size shouldBe 3
+            get(0).apply {
+                this as HeaderCheckInsVH.Item
+                (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false
+                (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false
+
+                this.selectAll()
+
+                viewModel.checkIns.getOrAwaitValue().apply {
+                    (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+                    (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `Select all does not un-select all`() {
+        every { savedState.get<Set<Long>>(any()) } returns emptySet()
+        val viewModel = createViewModel()
+        viewModel.checkIns.getOrAwaitValue().apply {
+            size shouldBe 3
+            get(0).apply {
+                this as HeaderCheckInsVH.Item
+                (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false
+                (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false
+
+                this.selectAll()
+
+                viewModel.checkIns.getOrAwaitValue().apply {
+                    (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+                    (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+                }
+
+                this.selectAll()
+
+                viewModel.checkIns.getOrAwaitValue().apply {
+                    (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+                    (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `Single selection`() {
+        every { savedState.get<Set<Long>>(any()) } returns emptySet()
+        val viewModel = createViewModel()
+        viewModel.checkIns.getOrAwaitValue().apply {
+
+            (get(1) as SelectableCheckInVH.Item).apply {
+                checkIn.hasSubmissionConsent shouldBe false
+                onItemSelected(checkIn)
+            }
+
+            viewModel.checkIns.getOrAwaitValue().apply {
+                (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true
+            }
+        }
+    }
+
+    @Test
+    fun `Single deselection`() {
+        every { savedState.get<Set<Long>>(any()) } returns emptySet()
+        val viewModel = createViewModel()
+        viewModel.checkIns.getOrAwaitValue().apply {
+            (get(1) as SelectableCheckInVH.Item).apply {
+                checkIn.hasSubmissionConsent shouldBe false
+                onItemSelected(checkIn)
+            }
+
+            viewModel.checkIns.getOrAwaitValue().apply {
+                (get(1) as SelectableCheckInVH.Item).apply {
+                    checkIn.hasSubmissionConsent shouldBe true
+                    onItemSelected(checkIn)
+                }
+            }
+
+            viewModel.checkIns.getOrAwaitValue().apply {
+                (get(1) as SelectableCheckInVH.Item).apply {
+                    checkIn.hasSubmissionConsent shouldBe false
+                }
+            }
+        }
+    }
+
+    @Test
+    fun `Confirming cancel goes to home screen`() {
+        createViewModel().apply {
+            onCancelConfirmed()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToHomeFragment
+        }
+    }
+
+    @Test
+    fun `Skip opens skipDialog`() {
+        createViewModel().apply {
+            onSkipClick()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenSkipDialog
+        }
+    }
+
+    @Test
+    fun `Close opens skipDialog when test result has been shown`() {
+        every { submissionRepository.hasViewedTestResult } returns flowOf(true)
+        createViewModel().apply {
+            onCloseClick()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenSkipDialog
+        }
+    }
+
+    @Test
+    fun `Close opens closeDialog when test result has not been shown`() {
+        every { submissionRepository.hasViewedTestResult } returns flowOf(false)
+        createViewModel().apply {
+            onCloseClick()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenCloseDialog
+        }
+    }
+
+    @Test
+    fun `shareSelectedCheckIns when test result has been shown`() {
+        every { submissionRepository.hasViewedTestResult } returns flowOf(true)
+        createViewModel().apply {
+            shareSelectedCheckIns()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionResultReadyFragment
+        }
+
+        coVerify {
+            checkInRepository.updateSubmissionConsents(any(), false)
+            autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+            checkInRepository.updateSubmissionConsents(any(), true)
+        }
+    }
+
+    @Test
+    fun `shareSelectedCheckIns when test result has not been shown`() {
+        every { submissionRepository.hasViewedTestResult } returns flowOf(false)
+        createViewModel().apply {
+            shareSelectedCheckIns()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment
+        }
+
+        coVerify {
+            checkInRepository.updateSubmissionConsents(any(), false)
+            autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+            checkInRepository.updateSubmissionConsents(any(), true)
+        }
+    }
+
+    @Test
+    fun `doNotShareCheckIns when test result has been shown`() {
+        every { submissionRepository.hasViewedTestResult } returns flowOf(true)
+        createViewModel().apply {
+            doNotShareCheckIns()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionResultReadyFragment
+        }
+
+        coVerify {
+            checkInRepository.updateSubmissionConsents(any(), false)
+            autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+        }
+    }
+
+    @Test
+    fun `doNotShareCheckIns when test result has not been shown`() {
+        every { submissionRepository.hasViewedTestResult } returns flowOf(false)
+        createViewModel().apply {
+            doNotShareCheckIns()
+            events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment
+        }
+
+        coVerify {
+            checkInRepository.updateSubmissionConsents(any(), false)
+            autoSubmission.updateMode(AutoSubmission.Mode.MONITOR)
+        }
+    }
+
+    private fun createViewModel() = CheckInsConsentViewModel(
+        savedState = savedState,
+        dispatcherProvider = TestDispatcherProvider(),
+        checkInRepository = checkInRepository,
+        submissionRepository = submissionRepository,
+        autoSubmission = autoSubmission
+    )
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt
index f06833355..18f920516 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.ui.submission.testavailable
 
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
 import de.rki.coronawarnapp.submission.auto.AutoSubmission
 import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater
@@ -31,6 +32,7 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() {
     @MockK lateinit var tekHistoryUpdater: TEKHistoryUpdater
     @MockK lateinit var tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
+    @MockK lateinit var checkInRepository: CheckInRepository
 
     @BeforeEach
     fun setUp() {
@@ -49,7 +51,8 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() {
         dispatcherProvider = TestDispatcherProvider(),
         tekHistoryUpdaterFactory = tekHistoryUpdaterFactory,
         autoSubmission = autoSubmission,
-        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector
+        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
+        checkInRepository = checkInRepository
     )
 
     @Test
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt
index 6b3e2fdc4..e4aab954d 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.ui.submission.warnothers
 
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
+import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository
 import de.rki.coronawarnapp.nearby.ENFClient
 import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository
 import de.rki.coronawarnapp.submission.SubmissionRepository
@@ -33,6 +34,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() {
     @MockK lateinit var interoperabilityRepository: InteroperabilityRepository
     @MockK lateinit var enfClient: ENFClient
     @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector
+    @MockK lateinit var checkInRepository: CheckInRepository
 
     @BeforeEach
     fun setUp() {
@@ -53,7 +55,8 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() {
         enfClient = enfClient,
         interoperabilityRepository = interoperabilityRepository,
         submissionRepository = submissionRepository,
-        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector
+        analyticsKeySubmissionCollector = analyticsKeySubmissionCollector,
+        checkInRepository = checkInRepository
     )
 
     @Test
diff --git a/gradle.properties b/gradle.properties
index d80947505..c87a56f93 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -19,5 +19,5 @@ org.gradle.dependency.verification.console=verbose
 # Versioning, this is used by the app & pipelines to calculate the current versionCode & versionName
 VERSION_MAJOR=2
 VERSION_MINOR=0
-VERSION_PATCH=2
+VERSION_PATCH=3
 VERSION_BUILD=0
-- 
GitLab