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 +}