From dc5d765933eca10895e5d391e481145e1e2aefc7 Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Tue, 1 Dec 2020 16:12:18 +0100 Subject: [PATCH] Fix refresh check and improve test menu (EXPOSUREAPP-4049) (#1772) * Remove "lastTimeDiagnosisKeysFromServerFetch" and replace it with less missleading data. While it was called "lastTimeDiagnosisKeysFromServerFetch" it was actually "last time we submitted keys to google". * To decide whether to refresh in "onResume", we now use "has there been any submission to the ENF?" * To display a timestamp on the risk card, we take the last successful submission to the ENF as the risk card displays the calculation results based on the latest submission. While we could use the last calculated risk level result timestamp, we currently also trigger risk calculations if there are no new submissions to the ENF, which would mean the timestamp is updated even though the result is not based on new data. I've also fixed the test fragment button behavior and added descriptions, the "Reset risk level" button surfaced the initial issue because it behaved like a "do a 75% data reset" button. * Tests and LINTs Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com> --- .../ui/TestRiskLevelCalculationFragment.kt | 11 +-- ...iskLevelCalculationFragmentCWAViewModel.kt | 55 +++++------ .../fragment_test_risk_level_calculation.xml | 52 +++++++--- .../download/DownloadDiagnosisKeysTask.kt | 19 ---- .../ExposureDetectionTrackerExtensions.kt | 22 +++++ .../de/rki/coronawarnapp/storage/LocalData.kt | 28 +----- .../storage/TracingRepository.kt | 16 ++-- .../ui/tracing/card/TracingCardState.kt | 19 ++-- .../tracing/card/TracingCardStateProvider.kt | 13 ++- .../ExposureDetectionTrackerExtensionsTest.kt | 94 +++++++++++++++++++ .../ui/tracing/card/TracingCardStateTest.kt | 27 +++--- 11 files changed, 220 insertions(+), 136 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt index e3b014dea..d6c542fe2 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.View import android.widget.RadioButton import android.widget.RadioGroup -import android.widget.Toast import androidx.core.app.ShareCompat import androidx.core.content.FileProvider import androidx.core.view.ViewCompat @@ -13,6 +12,7 @@ import androidx.core.view.children import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs +import com.google.android.material.snackbar.Snackbar import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding import de.rki.coronawarnapp.storage.TestSettings @@ -59,12 +59,9 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le binding.buttonClearDiagnosisKeyCache.setOnClickListener { vm.clearKeyCache() } binding.buttonResetRiskLevel.setOnClickListener { vm.resetRiskLevel() } binding.buttonExposureWindowsShare.setOnClickListener { vm.shareExposureWindows() } - vm.riskLevelResetEvent.observe2(this) { - Toast.makeText( - requireContext(), "Reset done, please fetch diagnosis keys from server again", - Toast.LENGTH_SHORT - ).show() - } + + vm.dataResetEvent.observe2(this) { Snackbar.make(requireView(), it, Snackbar.LENGTH_SHORT).show() } + vm.additionalRiskCalcInfo.observe2(this) { binding.labelRiskAdditionalInfo.text = it } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt index ca4ed7c60..f69b6f889 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -10,16 +10,15 @@ import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.diagnosiskeys.download.KeyPackageSyncSettings import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.latestSubmission import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.risk.result.AggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.storage.AppDatabase -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.task.TaskController @@ -32,21 +31,17 @@ import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.withContext import org.joda.time.Instant import org.joda.time.format.DateTimeFormat import timber.log.Timber import java.io.File -import java.util.Date import java.util.concurrent.TimeUnit class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( @@ -60,7 +55,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( tracingCardStateProvider: TracingCardStateProvider, private val riskLevelStorage: RiskLevelStorage, private val testSettings: TestSettings, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val exposureDetectionTracker: ExposureDetectionTracker, + private val keyPackageSyncSettings: KeyPackageSyncSettings ) : CWAViewModel( dispatcherProvider = dispatcherProvider ) { @@ -78,7 +75,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( Timber.d("Example arg: %s", exampleArg) } - val riskLevelResetEvent = SingleLiveEvent<Unit>() + val dataResetEvent = SingleLiveEvent<String>() val shareFileEvent = SingleLiveEvent<File>() val showRiskStatusCard = SubmissionRepository.deviceUIStateFlow.map { @@ -151,8 +148,8 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( val additionalRiskCalcInfo = combine( riskLevelStorage.riskLevelResults, - LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() - ) { riskLevelResults, lastTimeDiagnosisKeysFromServerFetch -> + exposureDetectionTracker.latestSubmission() + ) { riskLevelResults, latestSubmission -> val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults() @@ -162,7 +159,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( riskLevelLastSuccessfulCalculated = latestSuccessfulCalc.riskState, matchedKeyCount = latestCalc.matchedKeyCount, daysSinceLastExposure = latestCalc.daysWithEncounters, - lastTimeDiagnosisKeysFromServerFetch = lastTimeDiagnosisKeysFromServerFetch + lastKeySubmission = latestSubmission?.startedAt ) }.asLiveData() @@ -172,13 +169,13 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( riskLevelLastSuccessfulCalculated: RiskState, matchedKeyCount: Int, daysSinceLastExposure: Int, - lastTimeDiagnosisKeysFromServerFetch: Date? + lastKeySubmission: Instant? ): String = StringBuilder() .appendLine("Risk Level: $riskLevel") .appendLine("Last successful Risk Level: $riskLevelLastSuccessfulCalculated") .appendLine("Matched key count: $matchedKeyCount") .appendLine("Days since last Exposure: $daysSinceLastExposure days") - .appendLine("Last Time Server Fetch: ${lastTimeDiagnosisKeysFromServerFetch?.time?.let { Instant.ofEpochMilli(it) }}") + .appendLine("Last key submission: $lastKeySubmission") .appendLine("Tracing Duration: ${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days") .appendLine("Tracing Duration in last 14 days: ${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days") .appendLine("Last time risk level calculation $lastTimeRiskLevelCalculation") @@ -210,24 +207,8 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( fun resetRiskLevel() { Timber.d("Resetting risk level") launch { - withContext(Dispatchers.IO) { - try { - // Preference reset - SecurityHelper.resetSharedPrefs() - // Database Reset - AppDatabase.reset(context) - // Export File Reset - keyCacheRepository.clear() - - riskLevelStorage.clear() - - LocalData.lastTimeDiagnosisKeysFromServerFetch(null) - } catch (e: Exception) { - e.report(ExceptionCategory.INTERNAL) - } - } - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) - riskLevelResetEvent.postValue(Unit) + riskLevelStorage.clear() + dataResetEvent.postValue("Risk level calculation related data reset.") } } @@ -257,7 +238,13 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( fun clearKeyCache() { Timber.d("Clearing key cache") - launch { keyCacheRepository.clear() } + launch { + keyCacheRepository.clear() + keyPackageSyncSettings.clear() + exposureDetectionTracker.clear() + + dataResetEvent.postValue("Download & Submission related data reset.") + } } fun selectFakeExposureWindowMode(newMode: TestSettings.FakeExposureWindowTypes) { diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml index 1e6d0bf1b..b11dc97e7 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml @@ -34,13 +34,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> - + <LinearLayout android:id="@+id/environment_container" style="@style/card" android:layout_width="match_parent" - android:orientation="vertical" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:orientation="vertical"> <TextView android:id="@+id/fake_windows_title" @@ -50,11 +50,11 @@ android:text="Fake exposure windows" /> <TextView - android:layout_width="match_parent" style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_tiny" - android:text="Takes effect the next time `ExposureNotificationClient.exposureWindows` is called, i.e. on risk level calculation." - android:layout_height="wrap_content" /> + android:text="Takes effect the next time `ExposureNotificationClient.exposureWindows` is called, i.e. on risk level calculation." /> <RadioGroup android:id="@+id/fake_windows_toggle_group" @@ -83,28 +83,48 @@ </FrameLayout> <Button - android:id="@+id/button_retrieve_diagnosis_keys" + android:id="@+id/button_calculate_risk_level" style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Retrieve Diagnosis Keys" /> + android:text="Calculate Risk Level" /> + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Start the task that gets the latest exposure windows and calculates a current risk state." /> <Button - android:id="@+id/button_calculate_risk_level" + android:id="@+id/button_reset_risk_level" style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Calculate Risk Level" /> + android:text="Reset Risk Level" /> + + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Delete the all stored calculated risk level results." /> <Button - android:id="@+id/button_reset_risk_level" + android:id="@+id/button_retrieve_diagnosis_keys" style="@style/buttonPrimary" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Reset Risk Level" /> + android:text="Download Diagnosis Keys" /> + + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Start the task syncs the local diagnosis key cache with the server and submits them to the exposure notification framework for detection (if constraints allow). " /> <Button android:id="@+id/button_clear_diagnosis_key_cache" @@ -112,7 +132,13 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="Clear Diagnosis-Key cache" /> + android:text="Reset Diagnosis-Keys" /> + <TextView + style="@style/TextAppearance.AppCompat.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Restore download task conditions to initial state. Remove cached keys, delete last download logs, reset tracked exposure detections. " /> <TextView android:id="@+id/label_aggregated_risk_result_title" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt index 8f34bcc8c..cd5bc298f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.risk.RollbackItem -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -109,12 +108,6 @@ class DownloadDiagnosisKeysTask @Inject constructor( ) Timber.tag(TAG).d("Diagnosis Keys provided (success=%s)", isSubmissionSuccessful) - // EXPOSUREAPP-3878 write timestamp immediately after submission, - // so that progress observers can rely on a clean app state - if (isSubmissionSuccessful) { - saveTimestamp(currentDate, rollbackItems) - } - internalProgress.send(Progress.ApiSubmissionFinished) return object : Task.Result {} @@ -159,18 +152,6 @@ class DownloadDiagnosisKeysTask @Inject constructor( } } - private fun saveTimestamp( - currentDate: Date, - rollbackItems: MutableList<RollbackItem> - ) { - val lastFetchDateForRollback = LocalData.lastTimeDiagnosisKeysFromServerFetch() - rollbackItems.add { - LocalData.lastTimeDiagnosisKeysFromServerFetch(lastFetchDateForRollback) - } - Timber.tag(TAG).d("dateUpdate(currentDate=%s)", currentDate) - LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate) - } - private fun rollback(rollbackItems: MutableList<RollbackItem>) { try { Timber.tag(TAG).d("Initiate Rollback") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt new file mode 100644 index 000000000..a05ccdf39 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensions.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.nearby.modules.detectiontracker + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +suspend fun ExposureDetectionTracker.lastSubmission( + onlyFinished: Boolean = true +): TrackedExposureDetection? = calculations + .first().values + .filter { it.isSuccessful || !onlyFinished } + .maxByOrNull { it.startedAt } + +fun ExposureDetectionTracker.latestSubmission( + onlySuccessful: Boolean = true +): Flow<TrackedExposureDetection?> = calculations + .map { entries -> + entries.values.filter { it.isSuccessful || !onlySuccessful } + } + .map { detections -> + detections.maxByOrNull { it.startedAt } + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt index 326f05790..1c2adaaf1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt @@ -4,12 +4,10 @@ import android.content.SharedPreferences import androidx.core.content.edit import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.util.preferences.createFlowPreference import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.map -import java.util.Date +import timber.log.Timber /** * LocalData is responsible for all access to the shared preferences. Each preference is accessible @@ -306,28 +304,6 @@ object LocalData { .edit(commit = true) { putBoolean(PREFERENCE_HAS_RISK_STATUS_LOWERED, value) } .also { isUserToBeNotifiedOfLoweredRiskLevelFlowInternal.value = value } - /**************************************************** - * SERVER FETCH DATA - ****************************************************/ - - private val dateMapperForFetchTime: (Long) -> Date? = { - if (it != 0L) Date(it) else null - } - - private val lastTimeDiagnosisKeysFetchedFlowPref by lazy { - getSharedPreferenceInstance() - .createFlowPreference<Long>(key = "preference_timestamp_diagnosis_keys_fetch", 0L) - } - - fun lastTimeDiagnosisKeysFromServerFetchFlow() = lastTimeDiagnosisKeysFetchedFlowPref.flow - .map { dateMapperForFetchTime(it) } - - fun lastTimeDiagnosisKeysFromServerFetch() = - dateMapperForFetchTime(lastTimeDiagnosisKeysFetchedFlowPref.value) - - fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) = - lastTimeDiagnosisKeysFetchedFlowPref.update { value?.time ?: 0L } - /**************************************************** * SETTINGS DATA ****************************************************/ @@ -583,6 +559,6 @@ object LocalData { } fun clear() { - lastTimeDiagnosisKeysFetchedFlowPref.update { 0L } + Timber.w("LocalData.clear()") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt index 8174877c2..e107f9754 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt @@ -4,6 +4,8 @@ import android.content.Context import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.lastSubmission import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPeriod import de.rki.coronawarnapp.task.TaskController @@ -24,7 +26,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.joda.time.Duration import timber.log.Timber -import java.util.Date import java.util.NoSuchElementException import javax.inject.Inject import javax.inject.Singleton @@ -43,11 +44,10 @@ class TracingRepository @Inject constructor( @AppScope private val scope: CoroutineScope, private val taskController: TaskController, enfClient: ENFClient, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val exposureDetectionTracker: ExposureDetectionTracker ) { - val lastTimeDiagnosisKeysFetched: Flow<Date?> = LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() - private val internalActiveTracingDaysInRetentionPeriod = MutableStateFlow(0L) val activeTracingDaysInRetentionPeriod: Flow<Long> = internalActiveTracingDaysInRetentionPeriod @@ -121,15 +121,15 @@ class TracingRepository @Inject constructor( // model the keys are only fetched on button press of the user val isBackgroundJobEnabled = ConnectivityHelper.autoModeEnabled(context) - val wasNotYetFetched = LocalData.lastTimeDiagnosisKeysFromServerFetch() == null - Timber.tag(TAG).v("Network is enabled $isNetworkEnabled") Timber.tag(TAG).v("Background jobs are enabled $isBackgroundJobEnabled") - Timber.tag(TAG).v("Was not yet fetched from server $wasNotYetFetched") if (isNetworkEnabled && isBackgroundJobEnabled) { scope.launch { - if (wasNotYetFetched || downloadDiagnosisKeysTaskDidNotRunRecently()) { + val lastSubmission = exposureDetectionTracker.lastSubmission(onlyFinished = false) + Timber.tag(TAG).v("Last submission was %s", lastSubmission) + + if (lastSubmission == null || downloadDiagnosisKeysTaskDidNotRunRecently()) { Timber.tag(TAG).v("Start the fetching and submitting of the diagnosis keys") taskController.submitBlocking( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt index 3737a4526..a2f71aeb1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt @@ -16,7 +16,6 @@ import de.rki.coronawarnapp.ui.tracing.common.BaseTracingState import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate import org.joda.time.Instant import org.joda.time.format.DateTimeFormat -import java.util.Date @Suppress("TooManyFunctions") data class TracingCardState( @@ -27,7 +26,7 @@ data class TracingCardState( val daysWithEncounters: Int, val lastEncounterAt: Instant?, val activeTracingDays: Long, - val lastTimeDiagnosisKeysFetched: Date?, + val lastExposureDetectionTime: Instant?, override val isManualKeyRetrievalEnabled: Boolean, override val showDetails: Boolean = false ) : BaseTracingState() { @@ -166,10 +165,10 @@ data class TracingCardState( else -> "" } - private fun formatRelativeDateTimeString(c: Context, date: Date): CharSequence? = + private fun formatRelativeDateTimeString(c: Context, date: Instant): CharSequence? = DateUtils.getRelativeDateTimeString( c, - date.time, + date.millis, DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS * 2, 0 @@ -183,10 +182,10 @@ data class TracingCardState( */ fun getTimeFetched(c: Context): String { if (isTracingOff()) { - return if (lastTimeDiagnosisKeysFetched != null) { + return if (lastExposureDetectionTime != null) { c.getString( R.string.risk_card_body_time_fetched, - formatRelativeDateTimeString(c, lastTimeDiagnosisKeysFetched) + formatRelativeDateTimeString(c, lastExposureDetectionTime) ) } else { c.getString(R.string.risk_card_body_not_yet_fetched) @@ -194,10 +193,10 @@ data class TracingCardState( } return when (riskState) { LOW_RISK, INCREASED_RISK -> { - if (lastTimeDiagnosisKeysFetched != null) { + if (lastExposureDetectionTime != null) { c.getString( R.string.risk_card_body_time_fetched, - formatRelativeDateTimeString(c, lastTimeDiagnosisKeysFetched) + formatRelativeDateTimeString(c, lastExposureDetectionTime) ) } else { c.getString(R.string.risk_card_body_not_yet_fetched) @@ -206,10 +205,10 @@ data class TracingCardState( CALCULATION_FAILED -> { when (lastSuccessfulRiskState) { LOW_RISK, INCREASED_RISK -> { - if (lastTimeDiagnosisKeysFetched != null) { + if (lastExposureDetectionTime != null) { c.getString( R.string.risk_card_body_time_fetched, - formatRelativeDateTimeString(c, lastTimeDiagnosisKeysFetched) + formatRelativeDateTimeString(c, lastExposureDetectionTime) ) } else { c.getString(R.string.risk_card_body_not_yet_fetched) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt index 1f8e51ee7..5fa194430 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.ui.tracing.card import dagger.Reusable +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.latestSubmission import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.TracingRepository @@ -20,7 +22,8 @@ class TracingCardStateProvider @Inject constructor( tracingStatus: GeneralTracingStatus, backgroundModeStatus: BackgroundModeStatus, tracingRepository: TracingRepository, - riskLevelStorage: RiskLevelStorage + riskLevelStorage: RiskLevelStorage, + exposureDetectionTracker: ExposureDetectionTracker ) { val state: Flow<TracingCardState> = combine( @@ -36,8 +39,8 @@ class TracingCardStateProvider @Inject constructor( tracingRepository.activeTracingDaysInRetentionPeriod.onEach { Timber.v("activeTracingDaysInRetentionPeriod: $it") }, - tracingRepository.lastTimeDiagnosisKeysFetched.onEach { - Timber.v("lastTimeDiagnosisKeysFetched: $it") + exposureDetectionTracker.latestSubmission().onEach { + Timber.v("latestSubmission: $it") }, backgroundModeStatus.isAutoModeEnabled.onEach { Timber.v("isAutoModeEnabled: $it") @@ -46,7 +49,7 @@ class TracingCardStateProvider @Inject constructor( tracingProgress, riskLevelResults, activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched, + latestSubmission, isBackgroundJobEnabled -> val ( @@ -61,7 +64,7 @@ class TracingCardStateProvider @Inject constructor( riskState = latestCalc.riskState, tracingProgress = tracingProgress, lastSuccessfulRiskState = latestSuccessfulCalc.riskState, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, + lastExposureDetectionTime = latestSubmission?.startedAt, daysWithEncounters = latestCalc.daysWithEncounters, lastEncounterAt = latestCalc.lastRiskEncounterAt, activeTracingDays = activeTracingDaysInRetentionPeriod, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt new file mode 100644 index 000000000..18702738c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerExtensionsTest.kt @@ -0,0 +1,94 @@ +package de.rki.coronawarnapp.nearby.modules.detectiontracker + +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import java.util.UUID + +class ExposureDetectionTrackerExtensionsTest : BaseTest() { + + @MockK lateinit var tracker: ExposureDetectionTracker + + private val fakeCalculations: MutableStateFlow<Map<String, TrackedExposureDetection>> = MutableStateFlow(emptyMap()) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { tracker.calculations } returns fakeCalculations + } + + @AfterEach + fun teardown() { + } + + private fun createFakeCalculation( + startedAt: Instant, + result: TrackedExposureDetection.Result? = TrackedExposureDetection.Result.NO_MATCHES + ) = TrackedExposureDetection( + identifier = UUID.randomUUID().toString(), + startedAt = startedAt, + finishedAt = if (result != null) startedAt.plus(100) else null, + result = result + ) + + @Test + fun `last submission`() { + val tr1 = createFakeCalculation(startedAt = Instant.EPOCH) + val tr2 = createFakeCalculation(startedAt = Instant.EPOCH.plus(1)) + val tr3 = createFakeCalculation(startedAt = Instant.EPOCH.plus(2), result = null) + fakeCalculations.value = mapOf( + tr1.identifier to tr1, + tr2.identifier to tr2, + tr3.identifier to tr3, + ) + runBlockingTest { + tracker.lastSubmission(onlyFinished = false) shouldBe tr3 + tracker.lastSubmission(onlyFinished = true) shouldBe tr2 + } + } + + @Test + fun `last submission on empty data`() { + runBlockingTest { + tracker.lastSubmission(onlyFinished = false) shouldBe null + tracker.lastSubmission(onlyFinished = true) shouldBe null + } + } + + @Test + fun `latest submission`() { + val tr1 = createFakeCalculation(startedAt = Instant.EPOCH) + val tr2 = createFakeCalculation(startedAt = Instant.EPOCH.plus(1)) + val tr3 = createFakeCalculation(startedAt = Instant.EPOCH.plus(2), result = null) + fakeCalculations.value = mapOf( + tr1.identifier to tr1, + tr2.identifier to tr2, + tr3.identifier to tr3, + ) + runBlockingTest { + tracker.latestSubmission(onlySuccessful = false).first() shouldBe tr3 + tracker.latestSubmission(onlySuccessful = true).first() shouldBe tr2 + } + } + + @Test + fun `latest submission on empty data`() = runBlockingTest { + tracker.latestSubmission(onlySuccessful = false).first() shouldBe null + tracker.latestSubmission(onlySuccessful = true).first() shouldBe null + + val tr1 = createFakeCalculation(startedAt = Instant.EPOCH) + fakeCalculations.value = mapOf(tr1.identifier to tr1) + + tracker.latestSubmission(onlySuccessful = false).first() shouldBe tr1 + tracker.latestSubmission(onlySuccessful = true).first() shouldBe tr1 + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt index 52e718e79..dd06def65 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt @@ -22,7 +22,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import java.util.Date class TracingCardStateTest : BaseTest() { @@ -46,7 +45,7 @@ class TracingCardStateTest : BaseTest() { daysWithEncounters: Int = 0, lastEncounterAt: Instant? = null, activeTracingDaysInRetentionPeriod: Long = 0, - lastTimeDiagnosisKeysFetched: Date? = mockk(), + lastExposureDetectionTime: Instant? = mockk(), isBackgroundJobEnabled: Boolean = false ) = TracingCardState( tracingStatus = tracingStatus, @@ -56,7 +55,7 @@ class TracingCardStateTest : BaseTest() { daysWithEncounters = daysWithEncounters, lastEncounterAt = lastEncounterAt, activeTracingDays = activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, + lastExposureDetectionTime = lastExposureDetectionTime, isManualKeyRetrievalEnabled = !isBackgroundJobEnabled ) @@ -251,11 +250,11 @@ class TracingCardStateTest : BaseTest() { @Test fun `text for last time diagnosis keys were fetched`() { - val date = Date() + val date = Instant() createInstance( riskState = INCREASED_RISK, lastSuccessfulRiskState = LOW_RISK, - lastTimeDiagnosisKeysFetched = date + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } @@ -264,7 +263,7 @@ class TracingCardStateTest : BaseTest() { createInstance( riskState = CALCULATION_FAILED, lastSuccessfulRiskState = LOW_RISK, - lastTimeDiagnosisKeysFetched = date + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } @@ -273,7 +272,7 @@ class TracingCardStateTest : BaseTest() { createInstance( riskState = CALCULATION_FAILED, lastSuccessfulRiskState = LOW_RISK, - lastTimeDiagnosisKeysFetched = date + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } @@ -282,36 +281,36 @@ class TracingCardStateTest : BaseTest() { createInstance( riskState = LOW_RISK, lastSuccessfulRiskState = LOW_RISK, - lastTimeDiagnosisKeysFetched = date + lastExposureDetectionTime = date ).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } - createInstance(riskState = INCREASED_RISK, lastTimeDiagnosisKeysFetched = date).apply { + createInstance(riskState = INCREASED_RISK, lastExposureDetectionTime = date).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } - createInstance(riskState = CALCULATION_FAILED, lastTimeDiagnosisKeysFetched = date).apply { + createInstance(riskState = CALCULATION_FAILED, lastExposureDetectionTime = date).apply { getTimeFetched(context) shouldBe "" } - createInstance(riskState = LOW_RISK, lastTimeDiagnosisKeysFetched = date).apply { + createInstance(riskState = LOW_RISK, lastExposureDetectionTime = date).apply { getTimeFetched(context) verify { context.getString(eq(R.string.risk_card_body_time_fetched), any()) } } - createInstance(riskState = INCREASED_RISK, lastTimeDiagnosisKeysFetched = null).apply { + createInstance(riskState = INCREASED_RISK, lastExposureDetectionTime = null).apply { getTimeFetched(context) verify { context.getString(R.string.risk_card_body_not_yet_fetched) } } - createInstance(riskState = CALCULATION_FAILED, lastTimeDiagnosisKeysFetched = null).apply { + createInstance(riskState = CALCULATION_FAILED, lastExposureDetectionTime = null).apply { getTimeFetched(context) shouldBe "" } - createInstance(riskState = LOW_RISK, lastTimeDiagnosisKeysFetched = null).apply { + createInstance(riskState = LOW_RISK, lastExposureDetectionTime = null).apply { getTimeFetched(context) verify { context.getString(R.string.risk_card_body_not_yet_fetched) } } -- GitLab