diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 191fbbb7e347cdd8d6df9fc373f71139b67b7667..38ce5d0924bf439d02e79b8d87b85ccf0ae25b15 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -254,6 +254,7 @@ android {
         }
         androidTest {
             java.srcDirs += "$projectDir/src/testShared/java"
+            androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
         }
     }
 }
@@ -416,6 +417,7 @@ dependencies {
     implementation "androidx.room:room-guava:$room_version"
     kapt "androidx.room:room-compiler:$room_version"
     implementation "androidx.sqlite:sqlite:2.1.0"
+    androidTestImplementation "androidx.room:room-testing:$room_version"
 
     // UTILS
     implementation project(":Server-Protocol-Buffer")
diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase/2.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase/2.json
new file mode 100644
index 0000000000000000000000000000000000000000..5e35d70ab24482260df16038420f12db1d297824
--- /dev/null
+++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase/2.json
@@ -0,0 +1,209 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 2,
+    "identityHash": "c9b87dec5a9991bf14dc60dee54d4181",
+    "entities": [
+      {
+        "tableName": "riskresults",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`monotonicId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `calculatedAt` TEXT NOT NULL, `failureReason` TEXT, `totalRiskLevel` INTEGER, `totalMinimumDistinctEncountersWithLowRisk` INTEGER, `totalMinimumDistinctEncountersWithHighRisk` INTEGER, `mostRecentDateWithLowRisk` TEXT, `mostRecentDateWithHighRisk` TEXT, `numberOfDaysWithLowRisk` INTEGER, `numberOfDaysWithHighRisk` INTEGER)",
+        "fields": [
+          {
+            "fieldPath": "monotonicId",
+            "columnName": "monotonicId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "calculatedAt",
+            "columnName": "calculatedAt",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "failureReason",
+            "columnName": "failureReason",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.totalRiskLevel",
+            "columnName": "totalRiskLevel",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.totalMinimumDistinctEncountersWithLowRisk",
+            "columnName": "totalMinimumDistinctEncountersWithLowRisk",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.totalMinimumDistinctEncountersWithHighRisk",
+            "columnName": "totalMinimumDistinctEncountersWithHighRisk",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.mostRecentDateWithLowRisk",
+            "columnName": "mostRecentDateWithLowRisk",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.mostRecentDateWithHighRisk",
+            "columnName": "mostRecentDateWithHighRisk",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.numberOfDaysWithLowRisk",
+            "columnName": "numberOfDaysWithLowRisk",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "aggregatedRiskResult.numberOfDaysWithHighRisk",
+            "columnName": "numberOfDaysWithHighRisk",
+            "affinity": "INTEGER",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "monotonicId"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "exposurewindows",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `riskLevelResultId` TEXT NOT NULL, `dateMillisSinceEpoch` INTEGER NOT NULL, `calibrationConfidence` INTEGER NOT NULL, `infectiousness` INTEGER NOT NULL, `reportType` INTEGER NOT NULL)",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "riskLevelResultId",
+            "columnName": "riskLevelResultId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "dateMillisSinceEpoch",
+            "columnName": "dateMillisSinceEpoch",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "calibrationConfidence",
+            "columnName": "calibrationConfidence",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "infectiousness",
+            "columnName": "infectiousness",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "reportType",
+            "columnName": "reportType",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "scaninstances",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `exposureWindowId` INTEGER NOT NULL, `minAttenuationDb` INTEGER NOT NULL, `secondsSinceLastScan` INTEGER NOT NULL, `typicalAttenuationDb` INTEGER NOT NULL, FOREIGN KEY(`exposureWindowId`) REFERENCES `exposurewindows`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "exposureWindowId",
+            "columnName": "exposureWindowId",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "minAttenuationDb",
+            "columnName": "minAttenuationDb",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "secondsSinceLastScan",
+            "columnName": "secondsSinceLastScan",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "typicalAttenuationDb",
+            "columnName": "typicalAttenuationDb",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "indices": [
+          {
+            "name": "index_scaninstances_exposureWindowId",
+            "unique": false,
+            "columnNames": [
+              "exposureWindowId"
+            ],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_scaninstances_exposureWindowId` ON `${TABLE_NAME}` (`exposureWindowId`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "exposurewindows",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "exposureWindowId"
+            ],
+            "referencedColumns": [
+              "id"
+            ]
+          }
+        ]
+      }
+    ],
+    "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, 'c9b87dec5a9991bf14dc60dee54d4181')"
+    ]
+  }
+}
\ No newline at end of file
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseMigrationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..af16ce9d74ebb2c11e86b1689e934988951ab379
--- /dev/null
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseMigrationTest.kt
@@ -0,0 +1,224 @@
+package de.rki.coronawarnapp.risk.storage
+
+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.risk.RiskLevelResult
+import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase
+import de.rki.coronawarnapp.risk.storage.internal.migrations.RiskResultDatabaseMigration1To2
+import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
+import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass
+import io.kotest.matchers.shouldBe
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.joda.time.Instant
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import testhelpers.BaseTest
+import timber.log.Timber
+
+@RunWith(AndroidJUnit4::class)
+class RiskResultDatabaseMigrationTest : BaseTest() {
+    private val DB_NAME = "riskresults_migration_test.db"
+
+    @get:Rule
+    val helper: MigrationTestHelper = MigrationTestHelper(
+        InstrumentationRegistry.getInstrumentation(),
+        RiskResultDatabase::class.java.canonicalName,
+        FrameworkSQLiteOpenHelperFactory()
+    )
+
+    /**
+     * Test migration to create new primary key "monotonicId" column
+     */
+    @Test
+    fun migrate1To2() {
+        helper.createDatabase(DB_NAME, 1).apply {
+            execSQL(
+                """
+                    INSERT INTO "riskresults" (
+                        "id",
+                        "calculatedAt",
+                        "totalRiskLevel",
+                        "totalMinimumDistinctEncountersWithLowRisk",
+                        "totalMinimumDistinctEncountersWithHighRisk",
+                        "mostRecentDateWithLowRisk",
+                        "mostRecentDateWithHighRisk",
+                        "numberOfDaysWithLowRisk",
+                        "numberOfDaysWithHighRisk"
+                    ) VALUES (
+                        '72c4084a-43a9-4fcf-86d4-36103bfbd492',
+                        '2020-12-31T16:41:50.207Z',
+                        '2',
+                        '8',
+                        '1',
+                        '2020-12-29T16:41:50.038Z',
+                        '2020-12-30T16:41:50.038Z',
+                        '3',
+                        '1'
+                    );
+                """.trimIndent()
+            )
+            execSQL(
+                """
+                    INSERT INTO "riskresults" (
+                        "id",
+                        "calculatedAt",
+                        "totalRiskLevel",
+                        "totalMinimumDistinctEncountersWithLowRisk",
+                        "totalMinimumDistinctEncountersWithHighRisk",
+                        "mostRecentDateWithLowRisk",
+                        "mostRecentDateWithHighRisk",
+                        "numberOfDaysWithLowRisk",
+                        "numberOfDaysWithHighRisk"
+                    ) VALUES (
+                        '48a57f54-467b-4a0b-89c4-3c14e7ce65b5',
+                        '2020-12-31T16:41:38.663Z',
+                        '1',
+                        '0',
+                        '0',
+                        NULL,
+                        NULL,
+                        '0',
+                        '0'
+                    );
+                """.trimIndent()
+            )
+            execSQL(
+                """
+                    INSERT INTO "riskresults" (
+                        "id",
+                        "calculatedAt",
+                        "failureReason"
+                    ) VALUES (
+                        '0235fef8-4332-4a43-b7d8-f5eacb54a6ee',
+                        '2020-12-31T16:28:25.400Z',
+                        'tracingOff'
+                    );
+                """.trimIndent()
+            )
+
+            close()
+        }
+
+        // Run migration
+        helper.runMigrationsAndValidate(
+            DB_NAME,
+            2,
+            true,
+            RiskResultDatabaseMigration1To2
+        )
+
+        val daoDb = RiskResultDatabase.Factory(
+            context = ApplicationProvider.getApplicationContext()
+        ).create(databaseName = DB_NAME)
+
+        val allEntries = runBlocking { daoDb.riskResults().allEntries().first() }
+
+        allEntries.size shouldBe 3
+        // Newest entry
+        allEntries[0] shouldBe PersistedRiskLevelResultDao(
+            monotonicId = 3,
+            id = "0235fef8-4332-4a43-b7d8-f5eacb54a6ee",
+            calculatedAt = Instant.parse("2020-12-31T16:28:25.400Z"),
+            failureReason = RiskLevelResult.FailureReason.TRACING_OFF,
+            aggregatedRiskResult = null
+        )
+
+        allEntries[1] shouldBe PersistedRiskLevelResultDao(
+            monotonicId = 2,
+            id = "48a57f54-467b-4a0b-89c4-3c14e7ce65b5",
+            calculatedAt = Instant.parse("2020-12-31T16:41:38.663Z"),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(
+                    1
+                ),
+                totalMinimumDistinctEncountersWithLowRisk = 0,
+                totalMinimumDistinctEncountersWithHighRisk = 0,
+                mostRecentDateWithLowRisk = null,
+                mostRecentDateWithHighRisk = null,
+                numberOfDaysWithLowRisk = 0,
+                numberOfDaysWithHighRisk = 0
+            )
+        )
+        // Oldest entry, i.e. first one inserted
+        allEntries[2] shouldBe PersistedRiskLevelResultDao(
+            monotonicId = 1,
+            id = "72c4084a-43a9-4fcf-86d4-36103bfbd492",
+            calculatedAt = Instant.parse("2020-12-31T16:41:50.207Z"),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(
+                    2
+                ),
+                totalMinimumDistinctEncountersWithLowRisk = 8,
+                totalMinimumDistinctEncountersWithHighRisk = 1,
+                mostRecentDateWithLowRisk = Instant.parse("2020-12-29T16:41:50.038Z"),
+                mostRecentDateWithHighRisk = Instant.parse("2020-12-30T16:41:50.038Z"),
+                numberOfDaysWithLowRisk = 3,
+                numberOfDaysWithHighRisk = 1
+            )
+        )
+    }
+
+    /**
+     * If migration fails, drop the whole table and recreate it according to v2 schema
+     */
+    @Test
+    fun migrate1To2_failure_drops_db() {
+        helper.createDatabase(DB_NAME, 1).apply {
+            execSQL("DROP TABLE IF EXISTS riskresults")
+            execSQL("CREATE TABLE IF NOT EXISTS `riskresults` (`id` TEXT NOT NULL, `calculatedAt` INTEGER, `failureReason` INTEGER)")
+            execSQL("INSERT INTO `riskresults` (`id`, `calculatedAt`, `failureReason`) VALUES ('1', '2', '3')")
+
+            close()
+        }
+
+        // Run migration
+        helper.runMigrationsAndValidate(
+            DB_NAME,
+            2,
+            true,
+            RiskResultDatabaseMigration1To2
+        )
+
+        val daoDb = RiskResultDatabase.Factory(
+            context = ApplicationProvider.getApplicationContext()
+        ).create(databaseName = DB_NAME)
+
+        val emptyResults = runBlocking { daoDb.riskResults().allEntries().first() }
+        emptyResults.size shouldBe 0
+
+        val expectedResult = PersistedRiskLevelResultDao(
+            id = "48a57f54-467b-4a0b-89c4-3c14e7ce65b5",
+            calculatedAt = Instant.parse("2020-12-31T16:41:38.663Z"),
+            failureReason = null,
+            aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
+                totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(
+                    1
+                ),
+                totalMinimumDistinctEncountersWithLowRisk = 0,
+                totalMinimumDistinctEncountersWithHighRisk = 0,
+                mostRecentDateWithLowRisk = null,
+                mostRecentDateWithHighRisk = null,
+                numberOfDaysWithLowRisk = 0,
+                numberOfDaysWithHighRisk = 0
+            )
+        )
+
+        val insertedResult = runBlocking {
+            daoDb.riskResults().insertEntry(expectedResult)
+            daoDb.riskResults().allEntries().first().let {
+                it.size shouldBe 1
+                it.first()
+            }
+        }
+
+        Timber.v("insertedResult=%s", insertedResult)
+        insertedResult shouldBe expectedResult.copy(monotonicId = 1)
+    }
+}
diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseTest.kt
index 36f8696601620b48a4d3863f5c2a4948a47ff02f..8dffeef996f4c1efbdc79e9e4a3f1018b5b90685 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/risk/storage/RiskResultDatabaseTest.kt
@@ -22,6 +22,7 @@ class RiskResultDatabaseTest {
     private val riskResultDao = database.riskResults()
 
     private val oldestSuccessfulEntry = PersistedRiskLevelResultDao(
+        monotonicId = 1,
         id = UUID.randomUUID().toString(),
         calculatedAt = Instant.now().minus(9000),
         aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
@@ -37,6 +38,7 @@ class RiskResultDatabaseTest {
     )
 
     private val olderEntryFailedEntry = PersistedRiskLevelResultDao(
+        monotonicId = 2,
         id = UUID.randomUUID().toString(),
         calculatedAt = Instant.now().minus(4500),
         aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
@@ -52,6 +54,7 @@ class RiskResultDatabaseTest {
     )
 
     private val newestEntryFailed = PersistedRiskLevelResultDao(
+        monotonicId = 3,
         id = UUID.randomUUID().toString(),
         calculatedAt = Instant.now(),
         aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt
index 5110334a3cbb27f5b89cf0671428fc95b3de6b32..900ce5bef4f3179a4b9cc020ab53cdf4316407ae 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt
@@ -54,7 +54,7 @@ fun ExposureDetectionParametersAndroid?.maxExposureDetectionsPerDay(): Int =
 fun ExposureDetectionParametersAndroid?.minTimeBetweenExposureDetections(): Duration {
     val detectionsPerDay = this.maxExposureDetectionsPerDay()
     return if (detectionsPerDay == 0) {
-        Duration.standardDays(99)
+        Duration.standardDays(1)
     } else {
         (24 / detectionsPerDay).let { Duration.standardHours(it.toLong()) }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt
index c5315a98646599c1a8773a3f99fe58e9cdb11a90..d4192af041ce8789f72bb20a020d7e0d645126e0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.contactdiary.model
 
 import de.rki.coronawarnapp.util.lists.HasStableId
+import java.util.Locale
 
 interface ContactDiaryLocation : HasStableId {
     val locationId: Long
@@ -8,4 +9,4 @@ interface ContactDiaryLocation : HasStableId {
 }
 
 fun List<ContactDiaryLocation>.sortByNameAndIdASC(): List<ContactDiaryLocation> =
-    this.sortedWith(compareBy({ it.locationName }, { it.locationId }))
+    this.sortedWith(compareBy({ it.locationName.toLowerCase(Locale.ROOT) }, { it.locationId }))
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt
index a1f4061d7966086715a548e5c50784a8f9f1a085..2d477bcee18248a08aa8ecdfd9d4937f7e635ed2 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.contactdiary.model
 
 import org.joda.time.LocalDate
+import java.util.Locale
 
 interface ContactDiaryLocationVisit {
     val id: Long
@@ -9,4 +10,9 @@ interface ContactDiaryLocationVisit {
 }
 
 fun List<ContactDiaryLocationVisit>.sortByNameAndIdASC(): List<ContactDiaryLocationVisit> =
-    this.sortedWith(compareBy({ it.contactDiaryLocation.locationName }, { it.contactDiaryLocation.locationId }))
+    this.sortedWith(
+        compareBy(
+            { it.contactDiaryLocation.locationName.toLowerCase(Locale.ROOT) },
+            { it.contactDiaryLocation.locationId }
+        )
+    )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt
index 95badab5576060f02a888551d0c416be8ad9ba9b..de391152bcfba7267c0d095124025137b2fc3214 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.contactdiary.model
 
 import de.rki.coronawarnapp.util.lists.HasStableId
+import java.util.Locale
 
 interface ContactDiaryPerson : HasStableId {
     val personId: Long
@@ -8,4 +9,4 @@ interface ContactDiaryPerson : HasStableId {
 }
 
 fun List<ContactDiaryPerson>.sortByNameAndIdASC(): List<ContactDiaryPerson> =
-    this.sortedWith(compareBy({ it.fullName }, { it.personId }))
+    this.sortedWith(compareBy({ it.fullName.toLowerCase(Locale.ROOT) }, { it.personId }))
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt
index 60bf3887a3680388cbb6ec527eaf5e0bfc86ffec..47975a267757d5a18018d5a21c46d088bf32da3f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.contactdiary.model
 
 import org.joda.time.LocalDate
+import java.util.Locale
 
 interface ContactDiaryPersonEncounter {
     val id: Long
@@ -9,4 +10,9 @@ interface ContactDiaryPersonEncounter {
 }
 
 fun List<ContactDiaryPersonEncounter>.sortByNameAndIdASC(): List<ContactDiaryPersonEncounter> =
-    this.sortedWith(compareBy({ it.contactDiaryPerson.fullName }, { it.contactDiaryPerson.personId }))
+    this.sortedWith(
+        compareBy(
+            { it.contactDiaryPerson.fullName.toLowerCase(Locale.ROOT) },
+            { it.contactDiaryPerson.personId }
+        )
+    )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
index 7be1a88e9bbe32a26abafa3bf6784c0c9cf9888b..6a741421493b0958044f56fd38a3d6775b35eaf6 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt
@@ -140,9 +140,23 @@ class DownloadDiagnosisKeysTask @Inject constructor(
         trackedDetections: Collection<TrackedExposureDetection>
     ): Boolean {
         val lastDetection = trackedDetections.maxByOrNull { it.startedAt }
-        val nextDetectionAt = lastDetection?.startedAt?.plus(exposureConfig.minTimeBetweenDetections)
+        if (lastDetection == null) {
+            Timber.tag(TAG).d("No previous detections exist, don't abort.")
+            return false
+        }
+
+        if (lastDetection.startedAt.isAfter(now.plus(Duration.standardHours(1)))) {
+            Timber.tag(TAG).w("Last detection happened in our future? Don't abort as precaution.")
+            return false
+        }
+
+        val nextDetectionAt = lastDetection.startedAt.plus(exposureConfig.minTimeBetweenDetections)
+
+        Duration(now, nextDetectionAt).also {
+            Timber.tag(TAG).d("Next detection is available in %d min", it.standardMinutes)
+        }
 
-        return (nextDetectionAt != null && now.isBefore(nextDetectionAt)).also {
+        return (now.isBefore(nextDetectionAt)).also {
             if (it) Timber.tag(TAG).w("Aborting. Last detection is recent: %s (now=%s)", lastDetection, now)
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt
index 0bb56250707a0e0321608056295f547868a7be92..83c786c58bf2531cfe654e160ad6588ddbd12f31 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt
@@ -9,6 +9,7 @@ import androidx.room.Query
 import androidx.room.Room
 import androidx.room.RoomDatabase
 import androidx.room.TypeConverters
+import de.rki.coronawarnapp.risk.storage.internal.migrations.RiskResultDatabaseMigration1To2
 import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao
 import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao
 import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper
@@ -25,7 +26,7 @@ import javax.inject.Inject
         PersistedExposureWindowDao::class,
         PersistedExposureWindowDao.PersistedScanInstance::class
     ],
-    version = 1,
+    version = 2,
     exportSchema = true
 )
 @TypeConverters(
@@ -41,20 +42,20 @@ abstract class RiskResultDatabase : RoomDatabase() {
 
     @Dao
     interface RiskResultsDao {
-        @Query("SELECT * FROM riskresults ORDER BY calculatedAt DESC")
+        @Query("SELECT * FROM riskresults ORDER BY monotonicId DESC")
         fun allEntries(): Flow<List<PersistedRiskLevelResultDao>>
 
-        @Query("SELECT * FROM riskresults ORDER BY calculatedAt DESC LIMIT :limit")
+        @Query("SELECT * FROM riskresults ORDER BY monotonicId DESC LIMIT :limit")
         fun latestEntries(limit: Int): Flow<List<PersistedRiskLevelResultDao>>
 
-        @Query("SELECT * FROM (SELECT * FROM riskresults ORDER BY calculatedAt DESC LIMIT 1) UNION ALL SELECT * FROM (SELECT * FROM riskresults where failureReason is null ORDER BY calculatedAt DESC LIMIT 1)")
+        @Query("SELECT * FROM (SELECT * FROM riskresults ORDER BY monotonicId DESC LIMIT 1) UNION ALL SELECT * FROM (SELECT * FROM riskresults where failureReason is null ORDER BY monotonicId DESC LIMIT 1)")
         fun latestAndLastSuccessful(): Flow<List<PersistedRiskLevelResultDao>>
 
         @Insert(onConflict = OnConflictStrategy.ABORT)
         suspend fun insertEntry(riskResultDao: PersistedRiskLevelResultDao)
 
         @Query(
-            "DELETE FROM riskresults where id NOT IN (SELECT id from riskresults ORDER BY calculatedAt DESC LIMIT :keep)"
+            "DELETE FROM riskresults where id NOT IN (SELECT id from riskresults ORDER BY monotonicId DESC LIMIT :keep)"
         )
         suspend fun deleteOldest(keep: Int): Int
     }
@@ -81,11 +82,11 @@ abstract class RiskResultDatabase : RoomDatabase() {
 
     class Factory @Inject constructor(@AppContext private val context: Context) {
 
-        fun create(): RiskResultDatabase {
+        fun create(databaseName: String = DATABASE_NAME): RiskResultDatabase {
             Timber.d("Instantiating risk result database.")
             return Room
-                .databaseBuilder(context, RiskResultDatabase::class.java, DATABASE_NAME)
-                .fallbackToDestructiveMigrationFrom()
+                .databaseBuilder(context, RiskResultDatabase::class.java, databaseName)
+                .addMigrations(RiskResultDatabaseMigration1To2)
                 .build()
         }
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/migrations/RiskResultDatabaseMigration1To2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/migrations/RiskResultDatabaseMigration1To2.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b396d72fcc20f85a5091d7b903dd03d8e3e99e73
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/migrations/RiskResultDatabaseMigration1To2.kt
@@ -0,0 +1,117 @@
+package de.rki.coronawarnapp.risk.storage.internal.migrations
+
+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 RiskResultDataBase from schema version 1 to schema version 2
+ * The primary key column "id" was replaced with the newly added "monotonicId" column
+ * This was done to allow the app to determine the "latest" calculation independent of the calculation timestamp,
+ * which could be problematic if timetravel happens.
+ */
+object RiskResultDatabaseMigration1To2 : 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, dropping tables...")
+            e.report(ExceptionCategory.INTERNAL, "RiskResult database migration failed.")
+
+            try {
+                recreateRiskResults(database)
+            } catch (e: Exception) {
+                e.report(ExceptionCategory.INTERNAL, "Migration failed, table recreation failed too!")
+                throw e
+            }
+
+            Timber.w("Migration failed, but fallback via reset was successful.")
+        }
+    }
+
+    private fun performMigration(database: SupportSQLiteDatabase) = with(database) {
+        Timber.i("Running MIGRATION_1_2: Create new table.")
+        execSQL(
+            """
+                CREATE TABLE IF NOT EXISTS `riskresults_new` (
+                    `monotonicId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+                    `id` TEXT NOT NULL,
+                    `calculatedAt` TEXT NOT NULL,
+                    `failureReason` TEXT,
+                    `totalRiskLevel` INTEGER,
+                    `totalMinimumDistinctEncountersWithLowRisk` INTEGER,
+                    `totalMinimumDistinctEncountersWithHighRisk` INTEGER,
+                    `mostRecentDateWithLowRisk` TEXT,
+                    `mostRecentDateWithHighRisk` TEXT,
+                    `numberOfDaysWithLowRisk` INTEGER,
+                    `numberOfDaysWithHighRisk` INTEGER
+                )
+            """.trimIndent()
+        )
+
+        Timber.i("Running MIGRATION_1_2: Insert old data.")
+        execSQL(
+            """
+                INSERT INTO riskresults_new(
+                    id,
+                    calculatedAt,
+                    failureReason,
+                    totalRiskLevel,
+                    totalMinimumDistinctEncountersWithLowRisk,
+                    totalMinimumDistinctEncountersWithHighRisk,
+                    mostRecentDateWithLowRisk,
+                    mostRecentDateWithHighRisk,
+                    numberOfDaysWithLowRisk,
+                    numberOfDaysWithHighRisk
+                ) SELECT
+                    id,
+                    calculatedAt,
+                    failureReason,
+                    totalRiskLevel,
+                    totalMinimumDistinctEncountersWithLowRisk,
+                    totalMinimumDistinctEncountersWithHighRisk,
+                    mostRecentDateWithLowRisk,
+                    mostRecentDateWithHighRisk,
+                    numberOfDaysWithLowRisk,
+                    numberOfDaysWithHighRisk
+                FROM riskresults
+            """.trimIndent()
+        )
+
+        Timber.i("Running MIGRATION_1_2: Drop old table.")
+        execSQL("DROP TABLE riskresults")
+
+        Timber.i("Running MIGRATION_1_2: Rename temporary table.")
+        execSQL("ALTER TABLE riskresults_new RENAME TO riskresults")
+    }
+
+    private fun recreateRiskResults(database: SupportSQLiteDatabase) = with(database) {
+        Timber.i("Dropping and creating new riskResults v2 table.")
+
+        execSQL("DROP TABLE IF EXISTS riskresults")
+        execSQL("DROP TABLE IF EXISTS riskresults_new")
+
+        execSQL(
+            """
+                CREATE TABLE `riskresults` (
+                    `monotonicId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+                    `id` TEXT NOT NULL,
+                    `calculatedAt` TEXT NOT NULL,
+                    `failureReason` TEXT,
+                    `totalRiskLevel` INTEGER,
+                    `totalMinimumDistinctEncountersWithLowRisk` INTEGER,
+                    `totalMinimumDistinctEncountersWithHighRisk` INTEGER,
+                    `mostRecentDateWithLowRisk` TEXT,
+                    `mostRecentDateWithHighRisk` TEXT,
+                    `numberOfDaysWithLowRisk` INTEGER,
+                    `numberOfDaysWithHighRisk` INTEGER
+                )
+            """.trimIndent()
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt
index 51a596d47376a04e95a941f6177d565969312e84..1be333a7f979ebae4eb0c95cb2fda7c4bf9e4616 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt
@@ -15,7 +15,8 @@ import timber.log.Timber
 
 @Entity(tableName = "riskresults")
 data class PersistedRiskLevelResultDao(
-    @PrimaryKey @ColumnInfo(name = "id") val id: String,
+    @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "monotonicId") val monotonicId: Long = 0,
+    @ColumnInfo(name = "id") val id: String,
     @ColumnInfo(name = "calculatedAt") val calculatedAt: Instant,
     @ColumnInfo(name = "failureReason") val failureReason: FailureReason?,
     @Embedded val aggregatedRiskResult: PersistedAggregatedRiskResult?
@@ -81,7 +82,7 @@ data class PersistedRiskLevelResultDao(
     class Converter {
         @TypeConverter
         fun toType(value: String?): FailureReason? = value?.let {
-            FailureReason.values().singleOrNull { it.failureCode == value } ?: FailureReason.UNKNOWN
+            FailureReason.values().singleOrNull { it.failureCode == value }
         }
 
         @TypeConverter
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt
index f39e4f9dee0a89f0d4d073cca27b76c99cabfe1b..7a6e080ae54e1f5908871d854a9e0045b2432faf 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt
@@ -21,13 +21,13 @@ class ExposureDetectionConfigMapperTest : BaseTest() {
     }
 
     @Test
-    fun `detection interval 0 defaults to almost infinite delay`() {
+    fun `detection interval 0 defaults to sane delay`() {
         val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder()
         val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder()
             .setExposureDetectionParameters(exposureDetectionParameters)
             .build()
         createInstance().map(rawConfig).apply {
-            minTimeBetweenDetections shouldBe Duration.standardDays(99)
+            minTimeBetweenDetections shouldBe Duration.standardDays(1)
             maxExposureDetectionsPerUTCDay shouldBe 0
         }
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt
index ea4feec334a5b47f0792f5c24d615b686902b27f..ee98a610d59b9994cbd3d652d0ca837c7bd877a9 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt
@@ -1,5 +1,8 @@
 package de.rki.coronawarnapp.contactdiary.util
 
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryPerson
+import de.rki.coronawarnapp.contactdiary.model.sortByNameAndIdASC
 import org.junit.Assert
 import org.junit.jupiter.api.Test
 
@@ -12,4 +15,78 @@ class ContactDiaryExtensionsTest {
         Assert.assertEquals("Granny   ".formatContactDiaryNameField(5), "Grann")
         Assert.assertEquals("    ".formatContactDiaryNameField(2), "  ")
     }
+
+    @Test
+    fun `upper and lowercase mix sorting for names`() {
+        val testList = listOf(
+            DefaultContactDiaryPerson(1, "Max Mustermann"),
+            DefaultContactDiaryPerson(2, "Erika Musterfrau"),
+            DefaultContactDiaryPerson(3, "erika musterfrau2"),
+        )
+
+        val expectedResult = listOf(
+            DefaultContactDiaryPerson(2, "Erika Musterfrau"),
+            DefaultContactDiaryPerson(3, "erika musterfrau2"),
+            DefaultContactDiaryPerson(1, "Max Mustermann"),
+        )
+
+        // Test that lowercase "erika musterfrau2" is sorted to the 2nd position instead of the end
+        Assert.assertEquals(expectedResult, testList.sortByNameAndIdASC())
+    }
+
+    @Test
+    fun `sort by id when names are equal for names`() {
+        val testList = listOf(
+            DefaultContactDiaryPerson(1, "Max Mustermann"),
+            DefaultContactDiaryPerson(3, "Erika Musterfrau"),
+            DefaultContactDiaryPerson(2, "Erika Musterfrau"),
+        )
+
+        val expectedResult = listOf(
+            DefaultContactDiaryPerson(2, "Erika Musterfrau"),
+            DefaultContactDiaryPerson(3, "Erika Musterfrau"),
+            DefaultContactDiaryPerson(1, "Max Mustermann"),
+        )
+
+        // Test that "Erika Musterfrau" with lower personId comes before the other one, even though it was
+        // added as the last entry to the testList
+        Assert.assertEquals(expectedResult, testList.sortByNameAndIdASC())
+    }
+
+    @Test
+    fun `upper and lowercase mix sorting for places`() {
+        val testList = listOf(
+            DefaultContactDiaryLocation(1, "Berlin"),
+            DefaultContactDiaryLocation(2, "At home"),
+            DefaultContactDiaryLocation(3, "at home"),
+        )
+
+        val expectedResult = listOf(
+            DefaultContactDiaryLocation(2, "At home"),
+            DefaultContactDiaryLocation(3, "at home"),
+            DefaultContactDiaryLocation(1, "Berlin"),
+        )
+
+        // Test that lowercase "at home" is sorted to the 2nd position instead of the end
+        Assert.assertEquals(expectedResult, testList.sortByNameAndIdASC())
+    }
+
+    @Test
+    fun `sort by id when names are equal for places`() {
+        val testList = listOf(
+            DefaultContactDiaryLocation(1, "Berlin"),
+            DefaultContactDiaryLocation(3, "At home"),
+            DefaultContactDiaryLocation(2, "At home"),
+        )
+
+        val expectedResult = listOf(
+            DefaultContactDiaryLocation(2, "At home"),
+            DefaultContactDiaryLocation(3, "At home"),
+            DefaultContactDiaryLocation(1, "Berlin"),
+        )
+
+        // Test that "At home" with lower locationId comes before the other one, even though it was
+        // added as the last entry to the testList
+        Assert.assertEquals(expectedResult, testList.sortByNameAndIdASC())
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt
index f85ce9af769bebddcbdd955c45a40b1268c79e07..c22593689bdb3fb649c31a47c0c244abc18a0b52 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt
@@ -141,7 +141,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() {
     }
 
     @Test
-    fun `execution is skipped if last detection was recent via`() = runBlockingTest {
+    fun `execution is skipped if last detection was recent`() = runBlockingTest {
         // Last detection was at T+2h
         every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(2))
 
@@ -157,6 +157,18 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() {
         }
     }
 
+    @Test
+    fun `execution is NOT skipped if last detection is in our future`() = runBlockingTest {
+        // Last detection was at T, i.e. our time is now T-1h, so it was in our future.
+        every { timeStamper.nowUTC } returns Instant.EPOCH.minus(Duration.standardHours(1).plus(1))
+
+        createInstance().run(DownloadDiagnosisKeysTask.Arguments())
+
+        coVerify {
+            enfClient.provideDiagnosisKeys(any(), any())
+        }
+    }
+
     @Test
     fun `wasLastDetectionPerformedRecently honors paramters from config`() = runBlockingTest {
         every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(4))
diff --git a/Corona-Warn-App/src/test/java/testhelpers/IsAUnitTest.kt b/Corona-Warn-App/src/testShared/java/testhelpers/IsAUnitTest.kt
similarity index 100%
rename from Corona-Warn-App/src/test/java/testhelpers/IsAUnitTest.kt
rename to Corona-Warn-App/src/testShared/java/testhelpers/IsAUnitTest.kt