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 e3b014dea87c13623842857fbe2667f4ab791b85..d6c542fe27fa24962e8e1f7cecd46ba6dfde2302 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 ca4ed7c6094acda5e0f635abeb35af7304ece19e..f69b6f8899cb4818d826257f270bb7d210844324 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 1e6d0bf1b5e927068a3b9b833f1473cffb91eaee..b11dc97e7c883c490333a4e70fde9e1bc1c03323 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 8f34bcc8ced3d56d3c19ba2269e5a02e4be119b4..cd5bc298faa82904ddbf2c2610ed954ed4a98750 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 0000000000000000000000000000000000000000..a05ccdf39d2d6e6ce63cd0b147d4c2f2ce89abfb --- /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 326f0579081966e92e4e4aeaf4a0394e81fe2974..1c2adaaf16b39eb53644cb8744ce02ec6f49a55e 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 8174877c2a3ff71247ee328dbc671add9d8d321e..e107f9754d38e32023ecfdab4cb7a2ce6d525eb0 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 3737a45265d7cdf50261cb7e7cfbe6030ac999c8..a2f71aeb12586afed13237c5b390e337bcbb5e91 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 1f8e51ee7dc53a2824e8f09ea6d83f56faa31ab6..5fa194430033216fce6d63dc6848fe1a2bf89053 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 0000000000000000000000000000000000000000..18702738c8cc0c58bde2bf7263f94895c3387d80 --- /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 52e718e79e61f7f33daea36d9556ba134e6b9851..dd06def65231a1bba01790341fbe48f662a6edb4 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) } }