diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..5d067c9d65353d79255bb7171e7ba427332443de
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt
@@ -0,0 +1,56 @@
+package de.rki.coronawarnapp.test.contactdiary.ui
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.databinding.FragmentTestContactDiaryBinding
+import de.rki.coronawarnapp.test.menu.ui.TestMenuItem
+import de.rki.coronawarnapp.util.di.AutoInject
+import de.rki.coronawarnapp.util.ui.observe2
+import de.rki.coronawarnapp.util.ui.viewBindingLazy
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider
+import de.rki.coronawarnapp.util.viewmodel.cwaViewModels
+import javax.inject.Inject
+
+@SuppressLint("SetTextI18n")
+class ContactDiaryTestFragment : Fragment(R.layout.fragment_test_contact_diary), AutoInject {
+    @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory
+    private val vm: ContactDiaryTestFragmentViewModel by cwaViewModels { viewModelFactory }
+
+    private val binding: FragmentTestContactDiaryBinding by viewBindingLazy()
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        vm.locationVisits.observe2(this) {
+            binding.locationVisitsFancy.text = vm.getFancyLocationVisitString(it)
+            binding.locationVisitsStatus.text = vm.getLocationVisitStatusString(it)
+        }
+
+        vm.personEncounters.observe2(this) {
+            binding.personEncountersFancy.text = vm.getFancyPersonEncounterString(it)
+            binding.personEncountersStatus.text = vm.getPersonEncounterStatusString(it)
+        }
+
+        binding.apply {
+            wipeAllButton.setOnClickListener { vm.clearAll() }
+            outdatedLocationVisitsButton.setOnClickListener { vm.createLocationVisit(true) }
+            normalLocationVisitsButton.setOnClickListener { vm.createLocationVisit(false) }
+            outdatedPersonEncountersButton.setOnClickListener { vm.createPersonEncounters(true) }
+            normalPersonEncountersButton.setOnClickListener { vm.createPersonEncounters(false) }
+            locationVisitsCleanButton.setOnClickListener { vm.clearLocationVisits() }
+            personEncountersCleanButton.setOnClickListener { vm.clearPersonEncounters() }
+        }
+    }
+
+    companion object {
+        val TAG: String = ContactDiaryTestFragment::class.simpleName!!
+        val MENU_ITEM = TestMenuItem(
+            title = "Contact Diary Test Options",
+            description = "Contact Diary related test options..",
+            targetId = R.id.test_contact_diary_fragment
+        )
+    }
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragmentModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..695f96b0aa58df079472a1ed7d366bd01aaaa0f4
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragmentModule.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.test.contactdiary.ui
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey
+
+@Module
+abstract class ContactDiaryTestFragmentModule {
+    @Binds
+    @IntoMap
+    @CWAViewModelKey(ContactDiaryTestFragmentViewModel::class)
+    abstract fun testContactDiaryFragment(
+        factory: ContactDiaryTestFragmentViewModel.Factory
+    ): CWAViewModelFactory<out CWAViewModel>
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragmentViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9bf0a9e8db8933e62877b6061f64e43bf5382dd7
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragmentViewModel.kt
@@ -0,0 +1,113 @@
+package de.rki.coronawarnapp.test.contactdiary.ui
+
+import androidx.lifecycle.asLiveData
+import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryPerson
+import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionCalculation
+import de.rki.coronawarnapp.contactdiary.storage.repo.DefaultContactDiaryRepository
+import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
+import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
+import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
+import org.joda.time.LocalDate
+import java.lang.StringBuilder
+import kotlin.random.Random
+
+class ContactDiaryTestFragmentViewModel @AssistedInject constructor(
+    dispatcherProvider: DispatcherProvider,
+    private val repository: DefaultContactDiaryRepository,
+    private val retentionCalculation: ContactDiaryRetentionCalculation
+) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
+
+    val locationVisits = repository.locationVisits.asLiveData(context = dispatcherProvider.Default)
+    val personEncounters = repository.personEncounters.asLiveData(context = dispatcherProvider.Default)
+
+    fun getFancyLocationVisitString(list: List<ContactDiaryLocationVisit>): String {
+        val sortedList = list.sortedBy { it.date }
+        val builder = StringBuilder()
+        for (entry in sortedList) {
+            builder.append("[${entry.date.dayOfMonth}]")
+        }
+        return builder.toString()
+    }
+
+    fun getLocationVisitStatusString(list: List<ContactDiaryLocationVisit>): String {
+        val filtered = retentionCalculation.filterContactDiaryLocationVisits(list)
+        return "Outdated: ${filtered.count()} Normal: ${list.count() - filtered.count()} Total: ${list.count()}"
+    }
+
+    fun getFancyPersonEncounterString(list: List<ContactDiaryPersonEncounter>): String {
+        val sortedList = list.sortedBy { it.date }
+        val builder = StringBuilder()
+        for (entry in sortedList) {
+            builder.append("[${entry.date.dayOfMonth}]")
+        }
+        return builder.toString()
+    }
+
+    fun getPersonEncounterStatusString(list: List<ContactDiaryPersonEncounter>): String {
+        val filtered = retentionCalculation.filterContactDiaryPersonEncounters(list)
+        return "Outdated: ${filtered.count()} Normal: ${list.count() - filtered.count()} Total: ${list.count()}"
+    }
+
+    fun createLocationVisit(outdated: Boolean) {
+        launch {
+            val locationId = Random.nextLong()
+            val contactDiaryLocation = DefaultContactDiaryLocation(locationId, "Test location $locationId")
+            repository.addLocation(contactDiaryLocation)
+
+            val locationVisit =
+                DefaultContactDiaryLocationVisit(Random.nextLong(), getDate(outdated), contactDiaryLocation)
+            repository.addLocationVisit(locationVisit)
+        }
+    }
+
+    fun createPersonEncounters(outdated: Boolean) {
+        launch {
+            val contactPersonId = Random.nextLong()
+            val contactPerson = DefaultContactDiaryPerson(contactPersonId, "Suspect #$contactPersonId")
+            repository.addPerson(contactPerson)
+
+            val personEncounter =
+                DefaultContactDiaryPersonEncounter(Random.nextLong(), getDate(outdated), contactPerson)
+            repository.addPersonEncounter(personEncounter)
+        }
+    }
+
+    fun clearLocationVisits() {
+        launch {
+            retentionCalculation.clearObsoleteContactDiaryLocationVisits()
+        }
+    }
+
+    fun clearPersonEncounters() {
+        launch {
+            retentionCalculation.clearObsoleteContactDiaryPersonEncounters()
+        }
+    }
+
+    fun clearAll() {
+        launch {
+            repository.deleteAllLocationVisits()
+            repository.deleteAllPersonEncounters()
+            repository.deleteAllLocations()
+            repository.deleteAllPeople()
+        }
+    }
+
+    private fun getDate(outdated: Boolean): LocalDate {
+        val date = LocalDate.now()
+        return if (outdated) {
+            date.minusDays(Random.nextInt(17, 25))
+        } else {
+            date.minusDays(Random.nextInt(0, 16))
+        }
+    }
+
+    @AssistedInject.Factory
+    interface Factory : SimpleCWAViewModelFactory<ContactDiaryTestFragmentViewModel>
+}
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
index 86aac8783ba37f942b002fc494892ae2efb0e30e..e77ffd41f8af9eb568535b6b5db26d3d53798345 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData
 import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.miscinfo.MiscInfoFragment
 import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragment
+import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment
 import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment
 import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment
 import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment
@@ -25,7 +26,8 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() {
             TestTaskControllerFragment.MENU_ITEM,
             SubmissionTestFragment.MENU_ITEM,
             SettingsCrashReportFragment.MENU_ITEM,
-            MiscInfoFragment.MENU_ITEM
+            MiscInfoFragment.MENU_ITEM,
+            ContactDiaryTestFragment.MENU_ITEM
         ).let { MutableLiveData(it) }
     }
     val showTestScreenEvent = SingleLiveEvent<TestMenuItem>()
diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
index e368eab9f32d175fa0b6c15d29c3143eb52255a4..43dba449de04bbbc4635103a3c6293b97fac17ff 100644
--- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
+++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt
@@ -6,6 +6,8 @@ import de.rki.coronawarnapp.miscinfo.MiscInfoFragment
 import de.rki.coronawarnapp.miscinfo.MiscInfoFragmentModule
 import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragment
 import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragmentModule
+import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment
+import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragmentModule
 import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment
 import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragmentModule
 import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment
@@ -45,4 +47,7 @@ abstract class MainActivityTestModule {
 
     @ContributesAndroidInjector(modules = [SubmissionTestFragmentModule::class])
     abstract fun submissionTest(): SubmissionTestFragment
+
+    @ContributesAndroidInjector(modules = [ContactDiaryTestFragmentModule::class])
+    abstract fun contactDiaryTest(): ContactDiaryTestFragment
 }
diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml
new file mode 100644
index 0000000000000000000000000000000000000000..c288be592188370f1485e593f8f7cd5f5bbffff1
--- /dev/null
+++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:ignore="HardcodedText">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_margin="8dp"
+        android:orientation="vertical"
+        android:paddingBottom="32dp">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            style="@style/card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_tiny"
+            android:layout_marginStart="8dp"
+            android:layout_marginEnd="8dp"
+            android:orientation="vertical">
+            <TextView
+                android:id="@+id/generate_title"
+                style="@style/body1"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Generate entities"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+            <Button
+                android:id="@+id/outdated_location_visits_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Outdated\nLocation Visits"
+                app:layout_constraintEnd_toStartOf="@+id/normal_location_visits_button"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/generate_title" />
+            <Button
+                android:id="@+id/normal_location_visits_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Normal\nLocation Visits"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/outdated_location_visits_button"
+                app:layout_constraintTop_toBottomOf="@+id/generate_title" />
+            <Button
+                android:id="@+id/outdated_person_encounters_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Outdated Person Encounters"
+                app:layout_constraintEnd_toStartOf="@+id/normal_person_encounters_button"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/outdated_location_visits_button" />
+            <Button
+                android:id="@+id/normal_person_encounters_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Normal Person Encounters"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/outdated_person_encounters_button"
+                app:layout_constraintTop_toBottomOf="@+id/normal_location_visits_button" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            style="@style/card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_tiny">
+
+            <TextView
+                android:id="@+id/clean_title"
+                style="@style/body1"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Clean outdated"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <TextView
+                android:id="@+id/location_visits_title"
+                style="@style/TextAppearance.AppCompat.Caption"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:layout_weight="1"
+                android:text="Location Visits:"
+                app:layout_constraintEnd_toStartOf="@+id/location_visits_status"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/clean_title" />
+
+            <TextView
+                android:id="@+id/location_visits_status"
+                style="@style/TextAppearance.AppCompat.Caption"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:layout_weight="1"
+                android:text="Outdated: X Normal: Y"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/location_visits_title"
+                app:layout_constraintTop_toBottomOf="@+id/clean_title" />
+
+            <TextView
+                android:id="@+id/location_visits_fancy"
+                style="@style/TextAppearance.AppCompat.Caption"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="- none -"
+                app:layout_constraintTop_toBottomOf="@id/location_visits_status" />
+
+            <View
+                android:id="@+id/divider_top"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="@dimen/card_divider"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:background="@color/colorHairline"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/location_visits_fancy" />
+
+            <TextView
+                android:id="@+id/person_encounters_title"
+                style="@style/TextAppearance.AppCompat.Caption"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:layout_weight="1"
+                android:text="Person Encounters:"
+                app:layout_constraintEnd_toStartOf="@+id/person_encounters_status"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/divider_top" />
+
+            <TextView
+                android:id="@+id/person_encounters_status"
+                style="@style/TextAppearance.AppCompat.Caption"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:layout_weight="1"
+                android:text="Outdated: X Normal: Y"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/person_encounters_title"
+                app:layout_constraintTop_toBottomOf="@+id/divider_top" />
+
+            <TextView
+                android:id="@+id/person_encounters_fancy"
+                style="@style/TextAppearance.AppCompat.Caption"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:text="- none -"
+                app:layout_constraintTop_toBottomOf="@id/person_encounters_status" />
+
+            <View
+                android:id="@+id/divider_bottom"
+                android:layout_width="@dimen/match_constraint"
+                android:layout_height="@dimen/card_divider"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:background="@color/colorHairline"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/person_encounters_fancy" />
+
+            <Button
+                android:id="@+id/location_visits_clean_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:layout_weight="1"
+                android:text="Clean Location Visits"
+                app:layout_constraintEnd_toStartOf="@+id/person_encounters_clean_button"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/divider_bottom" />
+            <Button
+                android:id="@+id/person_encounters_clean_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_small"
+                android:layout_weight="1"
+                android:text="Clean Person Encounters"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/location_visits_clean_button"
+                app:layout_constraintTop_toBottomOf="@+id/divider_bottom" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            style="@style/card"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_margin="@dimen/spacing_tiny">
+
+            <TextView
+                android:id="@+id/wipe_all"
+                style="@style/body1"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Sudo drop table"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <Button
+                android:id="@+id/wipe_all_button"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/spacing_tiny"
+                android:layout_weight="1"
+                android:text="Delete all"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/wipe_all" />
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </LinearLayout>
+</androidx.core.widget.NestedScrollView>
\ No newline at end of file
diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
index d1919dec94661e5fbd9c0c907f427690f2705e1d..4fc36433886d94f0e938517b327bbb1c765f8208 100644
--- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
+++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml
@@ -34,6 +34,9 @@
         <action
             android:id="@+id/action_test_menu_fragment_to_miscInfoFragment"
             app:destination="@id/miscInfoFragment" />
+        <action
+            android:id="@+id/action_test_menu_fragment_to_contactDiaryTestFragment"
+            app:destination="@id/test_contact_diary_fragment" />
     </fragment>
 
     <fragment
@@ -92,5 +95,10 @@
         android:id="@+id/miscInfoFragment"
         android:name="de.rki.coronawarnapp.miscinfo.MiscInfoFragment"
         android:label="MiscInfoFragment" />
+    <fragment
+        android:id="@+id/test_contact_diary_fragment"
+        android:name="de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment"
+        android:label="ContactDiaryTestFragment"
+        tools:layout="@layout/fragment_test_contact_diary" />
 
 </navigation>
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
index faa23d485c765f197205d21ed105fe9626362720..ad64710eb8d5ab009b8a01d980f0f4bb2553dcab 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt
@@ -13,6 +13,7 @@ import dagger.android.DispatchingAndroidInjector
 import dagger.android.HasAndroidInjector
 import de.rki.coronawarnapp.appconfig.ConfigChangeDetector
 import de.rki.coronawarnapp.bugreporting.loghistory.LogHistoryTree
+import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
 import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver
 import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL
@@ -48,6 +49,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
     @Inject lateinit var configChangeDetector: ConfigChangeDetector
     @Inject lateinit var riskLevelChangeDetector: RiskLevelChangeDetector
     @Inject lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler
+    @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler
     @Inject lateinit var notificationHelper: NotificationHelper
     @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree
 
@@ -78,6 +80,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector {
 
         if (LocalData.onboardingCompletedTimestamp() != null) {
             deadmanNotificationScheduler.schedulePeriodic()
+            contactDiaryWorkScheduler.schedulePeriodic()
         }
 
         configChangeDetector.launch()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ContactDiaryRootModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ContactDiaryRootModule.kt
index 4db7d6fde159cac51ef4e19a7c47005533b0a947..4215c795d879a7f4620f8cbfa7ad13317eb8a91a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ContactDiaryRootModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ContactDiaryRootModule.kt
@@ -2,13 +2,15 @@ package de.rki.coronawarnapp.contactdiary
 
 import dagger.Module
 import dagger.android.ContributesAndroidInjector
+import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionModule
 import de.rki.coronawarnapp.contactdiary.storage.ContactDiaryStorageModule
 import de.rki.coronawarnapp.contactdiary.ui.ContactDiaryActivity
 import de.rki.coronawarnapp.contactdiary.ui.ContactDiaryUIModule
 
 @Module(
     includes = [
-        ContactDiaryStorageModule::class
+        ContactDiaryStorageModule::class,
+        ContactDiaryRetentionModule::class
     ]
 )
 abstract class ContactDiaryRootModule {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d6bfd5a34a79ae1aefc2c5c940892a748c5224eb
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanTask.kt
@@ -0,0 +1,66 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.common.DefaultProgress
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import org.joda.time.Duration
+import timber.log.Timber
+import javax.inject.Inject
+import javax.inject.Provider
+
+class ContactDiaryCleanTask @Inject constructor(
+    private val retentionCalculation: ContactDiaryRetentionCalculation
+) : Task<DefaultProgress, Task.Result> {
+
+    private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>()
+    override val progress: Flow<DefaultProgress> = internalProgress.asFlow()
+
+    private var isCanceled = false
+
+    override suspend fun run(arguments: Task.Arguments) = try {
+        Timber.d("Running with arguments=%s", arguments)
+
+        retentionCalculation.clearObsoleteContactDiaryLocationVisits()
+        Timber.tag(TAG).d("Obsolete contact diary location visits cleaned up")
+
+        retentionCalculation.clearObsoleteContactDiaryPersonEncounters()
+        Timber.tag(TAG).d("Obsolete contact diary person encounters cleaned up")
+
+        object : Task.Result {}
+    } catch (error: Exception) {
+        Timber.tag(TAG).e(error)
+        throw error
+    } finally {
+        Timber.i("Finished (isCanceled=$isCanceled).")
+        internalProgress.close()
+    }
+
+    override suspend fun cancel() {
+        Timber.w("cancel() called.")
+        isCanceled = true
+    }
+
+    class Config : TaskFactory.Config {
+        override val executionTimeout: Duration = Duration.standardMinutes(9)
+        override val collisionBehavior: TaskFactory.Config.CollisionBehavior =
+            TaskFactory.Config.CollisionBehavior.SKIP_IF_SIBLING_RUNNING
+    }
+
+    class Factory @Inject constructor(
+        private val taskByDagger: Provider<ContactDiaryCleanTask>
+    ) : TaskFactory<DefaultProgress, Task.Result> {
+
+        override suspend fun createConfig(): TaskFactory.Config = Config()
+
+        override val taskProvider: () -> Task<DefaultProgress, Task.Result> = {
+            taskByDagger.get()
+        }
+    }
+
+    companion object {
+        private val TAG: String? = ContactDiaryCleanTask::class.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionCalculation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionCalculation.kt
new file mode 100644
index 0000000000000000000000000000000000000000..fb90610e64d2411828caa572ce39d89888118750
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionCalculation.kt
@@ -0,0 +1,57 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import dagger.Reusable
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.storage.repo.DefaultContactDiaryRepository
+import de.rki.coronawarnapp.util.TimeStamper
+import kotlinx.coroutines.flow.first
+import org.joda.time.Days
+import org.joda.time.LocalDate
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class ContactDiaryRetentionCalculation @Inject constructor(
+    private val timeStamper: TimeStamper,
+    private val repository: DefaultContactDiaryRepository
+) {
+
+    fun getDaysDiff(dateSaved: LocalDate): Int {
+        val today = LocalDate(timeStamper.nowUTC)
+        return Days.daysBetween(dateSaved, today).days
+    }
+
+    fun filterContactDiaryLocationVisits(list: List<ContactDiaryLocationVisit>): List<ContactDiaryLocationVisit> {
+        return list.filter { entity -> RETENTION_DAYS < getDaysDiff(entity.date) }
+    }
+
+    fun filterContactDiaryPersonEncounters(list: List<ContactDiaryPersonEncounter>): List<ContactDiaryPersonEncounter> {
+        return list.filter { entity -> RETENTION_DAYS < getDaysDiff(entity.date) }
+    }
+
+    suspend fun clearObsoleteContactDiaryLocationVisits() {
+        val list = repository.locationVisits.first()
+        Timber.d("Contact Diary Location Visits total count: ${list.size}")
+        val toDeleteList =
+            list.filter { entity -> RETENTION_DAYS < getDaysDiff(entity.date).also { Timber.d("Days diff: $it") } }
+        Timber.d("Contact Diary Location Visits to be deleted: ${toDeleteList.size}")
+        repository.deleteLocationVisits(toDeleteList)
+    }
+
+    suspend fun clearObsoleteContactDiaryPersonEncounters() {
+        val list = repository.personEncounters.first()
+        Timber.d("Contact Diary Persons Encounters total count: ${list.size}")
+        val toDeleteList =
+            list.filter { entity -> RETENTION_DAYS < getDaysDiff(entity.date).also { Timber.d("Days diff: $it") } }
+        Timber.d("Contact Diary Persons Encounters to be deleted: ${toDeleteList.size}")
+        repository.deletePersonEncounters(toDeleteList)
+    }
+
+    companion object {
+        /**
+         * Contact diary data retention in days 14+2
+         */
+        const val RETENTION_DAYS = 16
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionModule.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e48d823605bc14a14233920d107dc006553756c8
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionModule.kt
@@ -0,0 +1,19 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.TaskTypeKey
+
+@Module
+abstract class ContactDiaryRetentionModule {
+
+    @Binds
+    @IntoMap
+    @TaskTypeKey(ContactDiaryCleanTask::class)
+    abstract fun contactDiaryCleanTaskFactory(
+        factory: ContactDiaryCleanTask.Factory
+    ): TaskFactory<out Task.Progress, out Task.Result>
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionWorker.kt
new file mode 100644
index 0000000000000000000000000000000000000000..129c4b2c87dd0d8ef5d404ff9beeb4976c4d4bc9
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryRetentionWorker.kt
@@ -0,0 +1,50 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import android.content.Context
+import androidx.work.CoroutineWorker
+import androidx.work.WorkerParameters
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
+import de.rki.coronawarnapp.task.submitBlocking
+import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
+import timber.log.Timber
+
+/**
+ * Periodic background contact diary clean worker
+ */
+class ContactDiaryRetentionWorker @AssistedInject constructor(
+    @Assisted val context: Context,
+    @Assisted workerParams: WorkerParameters,
+    private val taskController: TaskController
+) :
+    CoroutineWorker(context, workerParams) {
+
+    override suspend fun doWork(): Result {
+        Timber.tag(TAG).d("Background job started. No backoff criteria")
+        try {
+            taskController.submitBlocking(
+                DefaultTaskRequest(
+                    ContactDiaryCleanTask::class,
+                    originTag = "ContactDiaryCleanWorker"
+                )
+            ).error?.also { error: Throwable ->
+                Timber.tag(TAG).w(error, "$id: Error when cleaning contact diary.")
+                return Result.failure()
+            }
+        } catch (e: Exception) {
+            Timber.tag(TAG).d(e)
+            return Result.failure()
+        }
+        Timber.tag(TAG).d("Background job completed")
+        return Result.success()
+    }
+
+    @AssistedInject.Factory
+    interface Factory : InjectedWorkerFactory<ContactDiaryRetentionWorker>
+
+    companion object {
+        private val TAG = ContactDiaryRetentionWorker::class.java.simpleName
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkBuilder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a1d220c9bb0c566114177786d676bdfe86544c01
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkBuilder.kt
@@ -0,0 +1,22 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import androidx.work.PeriodicWorkRequest
+import androidx.work.PeriodicWorkRequestBuilder
+import dagger.Reusable
+import de.rki.coronawarnapp.worker.BackgroundConstants
+import org.joda.time.DateTimeConstants
+import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+
+@Reusable
+class ContactDiaryWorkBuilder @Inject constructor() {
+
+    fun buildPeriodicWork(): PeriodicWorkRequest = PeriodicWorkRequestBuilder<ContactDiaryRetentionWorker>(
+        DateTimeConstants.HOURS_PER_DAY.toLong(), TimeUnit.HOURS
+    )
+        .setInitialDelay(
+            BackgroundConstants.KIND_DELAY,
+            TimeUnit.MINUTES
+        )
+        .build()
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d989e8146ceca7b951e43f8b005ad9c3a20e1108
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkScheduler.kt
@@ -0,0 +1,30 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.WorkManager
+import dagger.Reusable
+import javax.inject.Inject
+
+@Reusable
+class ContactDiaryWorkScheduler @Inject constructor(
+    val workManager: WorkManager,
+    private val workBuilder: ContactDiaryWorkBuilder
+) {
+
+    /**
+     * Enqueue background contact diary clean periodic worker
+     * Replace with new if older work exists.
+     */
+    fun schedulePeriodic() {
+        // Create unique work and enqueue
+        workManager.enqueueUniquePeriodicWork(
+            PERIODIC_WORK_NAME,
+            ExistingPeriodicWorkPolicy.REPLACE,
+            workBuilder.buildPeriodicWork()
+        )
+    }
+
+    companion object {
+        const val PERIODIC_WORK_NAME = "ContactDiaryPeriodicWork"
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt
index 04d042a3cd64b99ccab73af2ef2b0f2fdfaa44d6..a75ebda35f2bac15096256b0db8450dfea1fd7ae 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt
@@ -7,6 +7,7 @@ import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
 import kotlinx.coroutines.flow.Flow
 import org.joda.time.LocalDate
 
+@Suppress("TooManyFunctions")
 interface ContactDiaryRepository {
 
     // Location
@@ -22,6 +23,7 @@ interface ContactDiaryRepository {
     fun locationVisitsForDate(date: LocalDate): Flow<List<ContactDiaryLocationVisit>>
     suspend fun addLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit)
     suspend fun deleteLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit)
+    suspend fun deleteLocationVisits(contactDiaryLocationVisits: List<ContactDiaryLocationVisit>)
     suspend fun deleteAllLocationVisits()
 
     // Person
@@ -37,5 +39,6 @@ interface ContactDiaryRepository {
     fun personEncountersForDate(date: LocalDate): Flow<List<ContactDiaryPersonEncounter>>
     suspend fun addPersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter)
     suspend fun deletePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter)
+    suspend fun deletePersonEncounters(contactDiaryPersonEncounters: List<ContactDiaryPersonEncounter>)
     suspend fun deleteAllPersonEncounters()
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt
index 6e2edbb9f5d0e2da601da730237d322b54012d21..5339351e82bcb3a85df32545c42184e075a57afa 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt
@@ -23,6 +23,7 @@ import javax.inject.Inject
 import javax.inject.Singleton
 
 @Singleton
+@Suppress("TooManyFunctions")
 class DefaultContactDiaryRepository @Inject constructor(
     private val contactDiaryLocationDao: ContactDiaryLocationDao,
     private val contactDiaryLocationVisitDao: ContactDiaryLocationVisitDao,
@@ -97,6 +98,17 @@ class DefaultContactDiaryRepository @Inject constructor(
         }
     }
 
+    override suspend fun deleteLocationVisits(contactDiaryLocationVisits: List<ContactDiaryLocationVisit>) {
+        Timber.d("Deleting location visits $contactDiaryLocationVisits")
+        val contactDiaryLocationVisitsEntities = contactDiaryLocationVisits
+            .map {
+                val contactDiaryLocationVisitsEntity = it.toContactDiaryLocationVisitEntity()
+                executeWhenIdNotDefault(contactDiaryLocationVisitsEntity.id)
+                return@map contactDiaryLocationVisitsEntity
+            }
+        contactDiaryLocationVisitDao.delete(contactDiaryLocationVisitsEntities)
+    }
+
     override suspend fun deleteAllLocationVisits() {
         Timber.d("Clearing contact diary location visit table")
         contactDiaryLocationVisitDao.deleteAll()
@@ -169,6 +181,17 @@ class DefaultContactDiaryRepository @Inject constructor(
         }
     }
 
+    override suspend fun deletePersonEncounters(contactDiaryPersonEncounters: List<ContactDiaryPersonEncounter>) {
+        Timber.d("Deleting person encounter $contactDiaryPersonEncounters")
+        val contactDiaryPersonEncounterEntities = contactDiaryPersonEncounters
+            .map {
+                val contactDiaryPersonEncounterEntity = it.toContactDiaryPersonEncounterEntity()
+                executeWhenIdNotDefault(contactDiaryPersonEncounterEntity.id)
+                return@map contactDiaryPersonEncounterEntity
+            }
+        contactDiaryPersonEncounterDao.delete(contactDiaryPersonEncounterEntities)
+    }
+
     override suspend fun deleteAllPersonEncounters() {
         Timber.d("Clearing contact diary person encounter table")
         contactDiaryPersonEncounterDao.deleteAll()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt
index cf81612bf0d33fb2b88f2d9c50b4f96a504864e5..554bf6d88ccd8dac7ac5c3f110df500b931e0402 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt
@@ -6,8 +6,11 @@ import com.squareup.inject.assisted.AssistedInject
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit
 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryCleanTask
 import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository
 import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.ListItem
+import de.rki.coronawarnapp.task.TaskController
+import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.ui.SingleLiveEvent
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
 import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
@@ -20,6 +23,7 @@ import timber.log.Timber
 import java.util.Locale
 
 class ContactDiaryOverviewViewModel @AssistedInject constructor(
+    taskController: TaskController,
     dispatcherProvider: DispatcherProvider,
     contactDiaryRepository: ContactDiaryRepository
 ) : CWAViewModel(dispatcherProvider = dispatcherProvider) {
@@ -40,6 +44,15 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor(
         createListItemList(dateList, locationVisitList, personEncounterList)
     }.asLiveData()
 
+    init {
+        taskController.submit(
+            DefaultTaskRequest(
+                ContactDiaryCleanTask::class,
+                originTag = "ContactDiaryOverviewViewModelInit"
+            )
+        )
+    }
+
     private fun createListItemList(
         dateList: List<LocalDate>,
         locationVisitList: List<ContactDiaryLocationVisit>,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt
index eb6c8aab8c7527e6c9dc8d1e6250c98fa16ec7d8..fe2c288a07b0de8a74ae7476549c647e230ce225 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt
@@ -166,7 +166,9 @@ class TaskController @Inject constructor(
                     state.job.getCompleted()
                 } else {
                     Timber.tag(TAG).e(error, "Task failed: %s", state)
-                    error.report(ExceptionCategory.INTERNAL)
+                    if (state.config.errorHandling == TaskFactory.Config.ErrorHandling.ALERT) {
+                        error.report(ExceptionCategory.INTERNAL)
+                    }
                     error.reportProblem(tag = state.request.type.simpleName)
                     null
                 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt
index 25dfd34f5f19af7d6a7ebbfc0f055dd0936dd5de..198b3c981b826f2af6e626c3efded7ba6347829c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt
@@ -15,6 +15,9 @@ interface TaskFactory<
 
         val collisionBehavior: CollisionBehavior
 
+        val errorHandling: ErrorHandling
+            get() = ErrorHandling.ALERT
+
         val preconditions: List<suspend () -> Boolean>
             get() = emptyList()
 
@@ -22,6 +25,11 @@ interface TaskFactory<
             ENQUEUE,
             SKIP_IF_SIBLING_RUNNING
         }
+
+        enum class ErrorHandling {
+            SILENT,
+            ALERT
+        }
     }
 
     suspend fun createConfig(): Config
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
index 5b917458ed343c32040004ff837bf99582208249..5f65fc8a7d6a226f5ab48abf94bdd9d0418d4ece 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt
@@ -14,6 +14,7 @@ import dagger.android.AndroidInjector
 import dagger.android.DispatchingAndroidInjector
 import dagger.android.HasAndroidInjector
 import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler
 import de.rki.coronawarnapp.contactdiary.ui.ContactDiaryActivity
 import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler
 import de.rki.coronawarnapp.storage.LocalData
@@ -67,6 +68,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
     @Inject lateinit var powerManagement: PowerManagement
 
     @Inject lateinit var deadmanScheduler: DeadmanNotificationScheduler
+    @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler
 
     /**
      * Register connection callback.
@@ -104,6 +106,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector {
         checkShouldDisplayBackgroundWarning()
         vm.doBackgroundNoiseCheck()
         deadmanScheduler.schedulePeriodic()
+        contactDiaryWorkScheduler.schedulePeriodic()
     }
 
     private fun showEnergyOptimizedEnabledForBackground() {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
index 99541ed60f1dfc69b41bb3a354af2dc78798d937..0a6bdf779804a17cd668e16cf24740570142c19a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt
@@ -4,6 +4,7 @@ import androidx.work.ListenableWorker
 import dagger.Binds
 import dagger.Module
 import dagger.multibindings.IntoMap
+import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker
 import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker
 import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker
@@ -71,4 +72,11 @@ abstract class WorkerBinder {
     abstract fun deadmanNotificationPeriodic(
         factory: DeadmanNotificationPeriodicWorker.Factory
     ): InjectedWorkerFactory<out ListenableWorker>
+
+    @Binds
+    @IntoMap
+    @WorkerKey(ContactDiaryRetentionWorker::class)
+    abstract fun contactDiaryCleanWorker(
+        factory: ContactDiaryRetentionWorker.Factory
+    ): InjectedWorkerFactory<out ListenableWorker>
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanTaskTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cee8408ed09c68521e4f2f0349e571a12bb24116
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanTaskTest.kt
@@ -0,0 +1,83 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import io.kotest.matchers.shouldNotBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.coVerifyOrder
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockk
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import testhelpers.BaseTest
+
+class ContactDiaryCleanTaskTest : BaseTest() {
+
+    @MockK lateinit var retentionCalculation: ContactDiaryRetentionCalculation
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+
+        coEvery { retentionCalculation.clearObsoleteContactDiaryPersonEncounters() } returns Unit
+        coEvery { retentionCalculation.clearObsoleteContactDiaryLocationVisits() } returns Unit
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun createInstance() = ContactDiaryCleanTask(
+        retentionCalculation = retentionCalculation
+    )
+
+    @Test
+    fun `no errors`() = runBlockingTest {
+        val result = createInstance().run(mockk())
+
+        coVerifyOrder {
+            retentionCalculation.clearObsoleteContactDiaryLocationVisits()
+            retentionCalculation.clearObsoleteContactDiaryPersonEncounters()
+        }
+        result shouldNotBe null
+    }
+
+    @Test
+    fun `location visits fails`() = runBlockingTest {
+        coEvery { retentionCalculation.clearObsoleteContactDiaryLocationVisits() } throws Exception()
+
+        val result = assertThrows<Exception> { createInstance().run(mockk()) }
+
+        coVerify(exactly = 1) { retentionCalculation.clearObsoleteContactDiaryLocationVisits() }
+        coVerify(exactly = 0) { retentionCalculation.clearObsoleteContactDiaryPersonEncounters() }
+        result shouldNotBe null
+    }
+
+    @Test
+    fun `person encounters fails`() = runBlockingTest {
+        coEvery { retentionCalculation.clearObsoleteContactDiaryPersonEncounters() } throws Exception()
+
+        val result = assertThrows<Exception> { createInstance().run(mockk()) }
+
+        coVerify(exactly = 1) { retentionCalculation.clearObsoleteContactDiaryLocationVisits() }
+        coVerify(exactly = 1) { retentionCalculation.clearObsoleteContactDiaryPersonEncounters() }
+        result shouldNotBe null
+    }
+
+    @Test
+    fun `everything fails =(`() = runBlockingTest {
+        coEvery { retentionCalculation.clearObsoleteContactDiaryLocationVisits() } throws Exception()
+        coEvery { retentionCalculation.clearObsoleteContactDiaryPersonEncounters() } throws Exception()
+
+        val result = assertThrows<Exception> { createInstance().run(mockk()) }
+
+        coVerify(exactly = 1) { retentionCalculation.clearObsoleteContactDiaryLocationVisits() }
+        coVerify(exactly = 0) { retentionCalculation.clearObsoleteContactDiaryPersonEncounters() }
+        result shouldNotBe null
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanWorkerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..dbb0f02848fde33ba12537e5ec0eefbfe18c32f4
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryCleanWorkerTest.kt
@@ -0,0 +1,41 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import android.content.Context
+import androidx.work.WorkerParameters
+import de.rki.coronawarnapp.task.TaskController
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ContactDiaryCleanWorkerTest : BaseTest() {
+
+    @MockK lateinit var context: Context
+    @RelaxedMockK lateinit var workerParams: WorkerParameters
+    @RelaxedMockK lateinit var taskController: TaskController
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun createWorker() = ContactDiaryRetentionWorker(
+        context = context,
+        workerParams = workerParams,
+        taskController = taskController
+    )
+
+    @Test
+    fun `create worker`() {
+        createWorker()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryDataRetentionCalculationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryDataRetentionCalculationTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..12856945f0abb14dfe8d5b38ede8e18c2813b08b
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryDataRetentionCalculationTest.kt
@@ -0,0 +1,107 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit
+import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter
+import de.rki.coronawarnapp.contactdiary.storage.repo.DefaultContactDiaryRepository
+import de.rki.coronawarnapp.util.TimeStamper
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.runs
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ContactDiaryDataRetentionCalculationTest : BaseTest() {
+
+    @MockK lateinit var timeStamper: TimeStamper
+    @MockK lateinit var contactDiaryRepository: DefaultContactDiaryRepository
+
+    private val testDates = arrayListOf<String>("2020-08-20T14:00:00.000Z",
+        "2020-08-20T13:00:00.000Z",
+        "2020-08-19T14:00:00.000Z",
+        "2020-08-05T14:00:00.000Z",
+        "2020-08-04T14:00:00.000Z",
+        "2020-08-03T14:00:00.000Z"
+    )
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-20T23:00:00.000Z")
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun createInstance() = ContactDiaryRetentionCalculation(
+        timeStamper = timeStamper,
+        repository = contactDiaryRepository
+    )
+
+    @Test
+    fun `test days diff`() {
+        every { timeStamper.nowUTC } returns Instant.parse("2020-08-20T14:00:00.000Z")
+
+        val instance = createInstance()
+
+        instance.getDaysDiff(LocalDate(Instant.parse("2020-08-20T14:00:00.000Z"))) shouldBe 0
+        instance.getDaysDiff(LocalDate(Instant.parse("2020-08-20T13:00:00.000Z"))) shouldBe 0
+        instance.getDaysDiff(LocalDate(Instant.parse("2020-08-19T14:00:00.000Z"))) shouldBe 1
+        instance.getDaysDiff(LocalDate(Instant.parse("2020-08-05T14:00:00.000Z"))) shouldBe 15
+        instance.getDaysDiff(LocalDate(Instant.parse("2020-08-04T14:00:00.000Z"))) shouldBe 16
+        instance.getDaysDiff(LocalDate(Instant.parse("2020-08-03T14:00:00.000Z"))) shouldBe 17
+    }
+
+    @Test
+    fun `test location visit deletion`() = runBlockingTest {
+        val list: List<ContactDiaryLocationVisit> = testDates.map { createContactDiaryLocationVisit(Instant.parse(it)) }
+
+        every { contactDiaryRepository.locationVisits } returns flowOf(list)
+        coEvery { contactDiaryRepository.deleteLocationVisits(any()) } just runs
+
+        val instance = createInstance()
+        instance.filterContactDiaryLocationVisits(list).size shouldBe 1
+
+        instance.clearObsoleteContactDiaryLocationVisits()
+        coVerify(exactly = 1) { contactDiaryRepository.deleteLocationVisits(any()) }
+    }
+
+    private fun createContactDiaryLocationVisit(date: Instant): ContactDiaryLocationVisit {
+        val locationVisit: ContactDiaryLocationVisit = mockk()
+        every { locationVisit.date } returns LocalDate(date)
+        return locationVisit
+    }
+
+    @Test
+    fun `test person encounters`() = runBlockingTest {
+        val list: List<ContactDiaryPersonEncounter> = testDates.map { createContactDiaryPersonEncounter(Instant.parse(it)) }
+
+        every { contactDiaryRepository.personEncounters } returns flowOf(list)
+        coEvery { contactDiaryRepository.deletePersonEncounters(any()) } just runs
+
+        val instance = createInstance()
+        instance.filterContactDiaryPersonEncounters(list).size shouldBe 1
+        instance.clearObsoleteContactDiaryPersonEncounters()
+        coVerify(exactly = 1) { contactDiaryRepository.deletePersonEncounters(any()) }
+    }
+
+    private fun createContactDiaryPersonEncounter(date: Instant): ContactDiaryPersonEncounter {
+        val personEncounter: ContactDiaryPersonEncounter = mockk()
+        every { personEncounter.date } returns LocalDate(date)
+        return personEncounter
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkBuilderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkBuilderTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8122dc1ec477993b202e3833d0c8cfc1e5fe9bab
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkBuilderTest.kt
@@ -0,0 +1,29 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import io.kotest.matchers.shouldBe
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ContactDiaryWorkBuilderTest : BaseTest() {
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    @Test
+    fun `periodic work test`() {
+        val periodicWork = ContactDiaryWorkBuilder().buildPeriodicWork()
+
+        periodicWork.workSpec.intervalDuration shouldBe 24 * 60 * 60 * 1000
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkSchedulerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkSchedulerTest.kt
new file mode 100644
index 0000000000000000000000000000000000000000..bf9f2150e4b7907b5ae7320ad9d8c16ea8e3bfad
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/retention/ContactDiaryWorkSchedulerTest.kt
@@ -0,0 +1,59 @@
+package de.rki.coronawarnapp.contactdiary.retention
+
+import androidx.work.ExistingPeriodicWorkPolicy
+import androidx.work.Operation
+import androidx.work.PeriodicWorkRequest
+import androidx.work.WorkManager
+import io.mockk.MockKAnnotations
+import io.mockk.clearAllMocks
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.verifySequence
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class ContactDiaryWorkSchedulerTest : BaseTest() {
+
+    @MockK lateinit var workManager: WorkManager
+    @MockK lateinit var operation: Operation
+    @MockK lateinit var workBuilder: ContactDiaryWorkBuilder
+    @MockK lateinit var periodicWorkRequest: PeriodicWorkRequest
+
+    @BeforeEach
+    fun setup() {
+        MockKAnnotations.init(this)
+        every { workBuilder.buildPeriodicWork() } returns periodicWorkRequest
+        every {
+            workManager.enqueueUniquePeriodicWork(
+                ContactDiaryWorkScheduler.PERIODIC_WORK_NAME,
+                ExistingPeriodicWorkPolicy.REPLACE,
+                any()
+            )
+        } returns operation
+    }
+
+    @AfterEach
+    fun teardown() {
+        clearAllMocks()
+    }
+
+    private fun createScheduler() = ContactDiaryWorkScheduler(
+        workManager = workManager,
+        workBuilder = workBuilder
+    )
+
+    @Test
+    fun `test periodic work was scheduled`() {
+        createScheduler().schedulePeriodic()
+
+        verifySequence {
+            workManager.enqueueUniquePeriodicWork(
+                ContactDiaryWorkScheduler.PERIODIC_WORK_NAME,
+                ExistingPeriodicWorkPolicy.REPLACE,
+                periodicWorkRequest
+            )
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt
index 1db3f96d9a29c871609aa58e5fc93aff11f7db4e..9aec6e4ec37ca222cbe3d78ed21b2a4593f5b470 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt
@@ -1,9 +1,13 @@
 package de.rki.coronawarnapp.task
 
+import de.rki.coronawarnapp.bugreporting.reportProblem
+import de.rki.coronawarnapp.exception.reporting.report
 import de.rki.coronawarnapp.task.common.DefaultTaskRequest
 import de.rki.coronawarnapp.task.example.QueueingTask
 import de.rki.coronawarnapp.task.testtasks.SkippingTask
+import de.rki.coronawarnapp.task.testtasks.alerterror.AlertErrorTask
 import de.rki.coronawarnapp.task.testtasks.precondition.PreconditionTask
+import de.rki.coronawarnapp.task.testtasks.silenterror.SilentErrorTask
 import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTask
 import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTask2
 import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTaskArguments
@@ -16,12 +20,16 @@ import io.kotest.matchers.shouldBe
 import io.kotest.matchers.shouldNotBe
 import io.kotest.matchers.types.instanceOf
 import io.mockk.MockKAnnotations
+import io.mockk.Runs
 import io.mockk.clearAllMocks
 import io.mockk.coVerifySequence
 import io.mockk.every
 import io.mockk.impl.annotations.MockK
+import io.mockk.just
 import io.mockk.mockk
+import io.mockk.mockkStatic
 import io.mockk.spyk
+import io.mockk.verify
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.TimeoutCancellationException
 import kotlinx.coroutines.delay
@@ -56,6 +64,8 @@ class TaskControllerTest : BaseIOTest() {
     private val queueingFactory = spyk(QueueingTask.Factory(Provider { QueueingTask() }))
     private val skippingFactory = spyk(SkippingTask.Factory(Provider { SkippingTask() }))
     private val preconditionFactory = spyk(PreconditionTask.Factory(Provider { PreconditionTask() }))
+    private val silentErrorFactory = spyk(SilentErrorTask.Factory(Provider { SilentErrorTask() }))
+    private val alertErrorFactory = spyk(AlertErrorTask.Factory(Provider { AlertErrorTask() }))
 
     @BeforeEach
     fun setup() {
@@ -66,6 +76,8 @@ class TaskControllerTest : BaseIOTest() {
         taskFactoryMap[TimeoutTask::class.java] = timeoutFactory
         taskFactoryMap[TimeoutTask2::class.java] = timeoutFactory2
         taskFactoryMap[PreconditionTask::class.java] = preconditionFactory
+        taskFactoryMap[SilentErrorTask::class.java] = silentErrorFactory
+        taskFactoryMap[AlertErrorTask::class.java] = alertErrorFactory
 
         every { timeStamper.nowUTC } answers {
             Instant.now()
@@ -619,4 +631,62 @@ class TaskControllerTest : BaseIOTest() {
 
         instance.close()
     }
+
+    @Test
+    fun `silent error handling`() = runBlockingTest {
+
+        val error: Throwable = spyk(Throwable())
+
+        mockkStatic("de.rki.coronawarnapp.exception.reporting.ExceptionReporterKt")
+        mockkStatic("de.rki.coronawarnapp.bugreporting.BugReporterKt")
+
+        every { error.report(any(), any(), any()) } just Runs
+        every { error.reportProblem(any()) } just Runs
+
+        val instance = createInstance(scope = this)
+
+        val request = DefaultTaskRequest(type = SilentErrorTask::class, arguments = SilentErrorTask.Arguments(error = error))
+        instance.submit(request)
+
+        val infoFinished = instance.tasks
+            .first { it.single().taskState.executionState == TaskState.ExecutionState.FINISHED }
+            .single()
+
+        infoFinished.apply {
+            taskState.error shouldNotBe null
+            verify(exactly = 0) { any<Throwable>().report(any()) }
+            verify(exactly = 1) { any<Throwable>().reportProblem(any()) }
+        }
+
+        instance.close()
+    }
+
+    @Test
+    fun `alert error handling`() = runBlockingTest {
+
+        val error: Throwable = spyk(Throwable())
+
+        mockkStatic("de.rki.coronawarnapp.exception.reporting.ExceptionReporterKt")
+        mockkStatic("de.rki.coronawarnapp.bugreporting.BugReporterKt")
+
+        every { error.report(any(), any(), any()) } just Runs
+        every { error.reportProblem(any()) } just Runs
+
+        val instance = createInstance(scope = this)
+
+        val request = DefaultTaskRequest(type = AlertErrorTask::class, arguments = AlertErrorTask.Arguments(error = error))
+        instance.submit(request)
+
+        val infoFinished = instance.tasks
+            .first { it.single().taskState.executionState == TaskState.ExecutionState.FINISHED }
+            .single()
+
+        infoFinished.apply {
+            taskState.error shouldNotBe null
+            verify(exactly = 1) { any<Throwable>().report(any()) }
+            verify(exactly = 1) { any<Throwable>().reportProblem(any()) }
+        }
+
+        instance.close()
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/alerterror/AlertErrorTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/alerterror/AlertErrorTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..05572d15905f6830542c4e80a6c5f82eefc788d9
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/alerterror/AlertErrorTask.kt
@@ -0,0 +1,45 @@
+package de.rki.coronawarnapp.task.testtasks.alerterror
+
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.common.DefaultProgress
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import timber.log.Timber
+import javax.inject.Provider
+
+class AlertErrorTask : Task<DefaultProgress, AlertErrorTask.Result> {
+
+    private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>()
+    override val progress: Flow<DefaultProgress> = internalProgress.asFlow()
+
+    private var isCanceled = false
+
+    override suspend fun run(arguments: Task.Arguments): Result = try {
+        Timber.d("Running with arguments=%s", arguments)
+        arguments as Arguments
+        throw arguments.error
+    } finally {
+        Timber.i("Finished (isCanceled=$isCanceled).")
+        internalProgress.close()
+    }
+
+    override suspend fun cancel() {
+        Timber.w("cancel() called.")
+        isCanceled = true
+    }
+
+    data class Arguments(
+        val error: Throwable
+    ) : Task.Arguments
+
+    class Result : Task.Result
+
+    class Factory constructor(
+        private val taskByDagger: Provider<AlertErrorTask>
+    ) : TaskFactory<DefaultProgress, Result> {
+        override suspend fun createConfig(): TaskFactory.Config = AlertErrorTaskConfig()
+        override val taskProvider: () -> Task<DefaultProgress, Result> = { taskByDagger.get() }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/alerterror/AlertErrorTaskConfig.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/alerterror/AlertErrorTaskConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..449f8e7f309edaac32e38b6df731656a26b9e8c7
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/alerterror/AlertErrorTaskConfig.kt
@@ -0,0 +1,13 @@
+package de.rki.coronawarnapp.task.testtasks.alerterror
+
+import de.rki.coronawarnapp.task.TaskFactory
+import org.joda.time.Duration
+
+class AlertErrorTaskConfig : TaskFactory.Config {
+    override val executionTimeout: Duration = Duration.standardSeconds(10)
+
+    override val errorHandling: TaskFactory.Config.ErrorHandling = TaskFactory.Config.ErrorHandling.ALERT
+
+    override val collisionBehavior: TaskFactory.Config.CollisionBehavior =
+        TaskFactory.Config.CollisionBehavior.ENQUEUE
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/silenterror/SilentErrorTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/silenterror/SilentErrorTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..81d6dfc42ee1f5d74ad11ad86d3c899d5530eed9
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/silenterror/SilentErrorTask.kt
@@ -0,0 +1,45 @@
+package de.rki.coronawarnapp.task.testtasks.silenterror
+
+import de.rki.coronawarnapp.task.Task
+import de.rki.coronawarnapp.task.TaskFactory
+import de.rki.coronawarnapp.task.common.DefaultProgress
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
+import timber.log.Timber
+import javax.inject.Provider
+
+class SilentErrorTask : Task<DefaultProgress, SilentErrorTask.Result> {
+
+    private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>()
+    override val progress: Flow<DefaultProgress> = internalProgress.asFlow()
+
+    private var isCanceled = false
+
+    override suspend fun run(arguments: Task.Arguments): Result = try {
+        Timber.d("Running with arguments=%s", arguments)
+        arguments as Arguments
+        throw arguments.error
+    } finally {
+        Timber.i("Finished (isCanceled=$isCanceled).")
+        internalProgress.close()
+    }
+
+    override suspend fun cancel() {
+        Timber.w("cancel() called.")
+        isCanceled = true
+    }
+
+    data class Arguments(
+        val error: Throwable
+    ) : Task.Arguments
+
+    class Result : Task.Result
+
+    class Factory constructor(
+        private val taskByDagger: Provider<SilentErrorTask>
+    ) : TaskFactory<DefaultProgress, Result> {
+        override suspend fun createConfig(): TaskFactory.Config = SilentErrorTaskConfig()
+        override val taskProvider: () -> Task<DefaultProgress, Result> = { taskByDagger.get() }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/silenterror/SilentErrorTaskConfig.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/silenterror/SilentErrorTaskConfig.kt
new file mode 100644
index 0000000000000000000000000000000000000000..00c66a2d374531516c308052a7f6a6db3694f617
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/silenterror/SilentErrorTaskConfig.kt
@@ -0,0 +1,13 @@
+package de.rki.coronawarnapp.task.testtasks.silenterror
+
+import de.rki.coronawarnapp.task.TaskFactory
+import org.joda.time.Duration
+
+class SilentErrorTaskConfig : TaskFactory.Config {
+    override val executionTimeout: Duration = Duration.standardSeconds(10)
+
+    override val errorHandling: TaskFactory.Config.ErrorHandling = TaskFactory.Config.ErrorHandling.SILENT
+
+    override val collisionBehavior: TaskFactory.Config.CollisionBehavior =
+        TaskFactory.Config.CollisionBehavior.ENQUEUE
+}