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 298354d970bbf4dc537899eeeb5d4f55efdfbc81..a6719b32124e2e25c4c78600f41ab1c030eee8a3 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 @@ -83,11 +83,12 @@ class DownloadDiagnosisKeysTask @Inject constructor( if (wasLastDetectionPerformedRecently(now, exposureConfig, trackedExposureDetections)) { // At most one detection every 6h + Timber.tag(TAG).i("task aborted, because detection was performed recently") return object : Task.Result {} } if (hasRecentDetectionAndNoNewFiles(now, keySyncResult, trackedExposureDetections)) { - // Last check was within 24h, and there are no new files. + Timber.tag(TAG).i("task aborted, last check was within 24h, and there are no new files") return object : Task.Result {} } @@ -111,13 +112,14 @@ class DownloadDiagnosisKeysTask @Inject constructor( ) Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", isSubmissionSuccessful, token) - internalProgress.send(Progress.ApiSubmissionFinished) - throwIfCancelled() - + // 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 {} } catch (error: Exception) { Timber.tag(TAG).e(error) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt index 46499c31230bf37e06c79f5c124a5e02f9f61215..767788b052571ee50d306089dec02aba0a7513d6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt @@ -1,9 +1,11 @@ package de.rki.coronawarnapp.diagnosiskeys.download +import android.annotation.SuppressLint import android.content.Context import com.google.gson.Gson import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.preferences.FlowPreference +import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.serialization.BaseGson import org.joda.time.Instant import javax.inject.Inject @@ -32,6 +34,11 @@ class KeyPackageSyncSettings @Inject constructor( writer = FlowPreference.gsonWriter(gson) ) + @SuppressLint("ApplySharedPref") + fun clear() { + prefs.clearAndNotify() + } + data class LastDownload( val startedAt: Instant, val finishedAt: Instant? = null, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt index c5e4b8073deb172dcb4856afb9d2406af38ef94d..b40e1e6d01cea3bac0e0a3e5a38b781ee3c3ef35 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt @@ -134,6 +134,13 @@ class DefaultExposureDetectionTracker @Inject constructor( } } + override fun clear() { + Timber.i("clear()") + detectionStates.updateSafely { + emptyMap() + } + } + companion object { private const val TAG = "DefaultExposureDetectionTracker" private const val MAX_ENTRY_SIZE = 5 diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt index 4d9bcf0c6e318c96a314d71bdd0050f0ecf2bd88..ee1a302ea67b14b76516cd5c7bb738e22430814a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt @@ -8,4 +8,6 @@ interface ExposureDetectionTracker { fun trackNewExposureDetection(identifier: String) fun finishExposureDetection(identifier: String, result: TrackedExposureDetection.Result) + + fun clear() } 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 2e0347c40203980058112cec2e5e6e660055134e..26a4902fceed1cfef67b084fc1a084202bd2d216 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 @@ -726,4 +726,8 @@ object LocalData { putBoolean(PREFERENCE_INTEROPERABILITY_IS_USED_AT_LEAST_ONCE, value) } } + + fun clear() { + lastTimeDiagnosisKeysFetchedFlowPref.update { 0L } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index 86690e22520a4689a14f591cf5e5edab61ef853a..fd1064bbfef209742a3e763e9ecd386543311e80 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -22,8 +22,11 @@ package de.rki.coronawarnapp.util import android.annotation.SuppressLint import android.content.Context import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.diagnosiskeys.download.KeyPackageSyncSettings import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.storage.AppDatabase +import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository @@ -43,7 +46,9 @@ class DataReset @Inject constructor( @AppContext private val context: Context, private val keyCacheRepository: KeyCacheRepository, private val appConfigProvider: AppConfigProvider, - private val interoperabilityRepository: InteroperabilityRepository + private val interoperabilityRepository: InteroperabilityRepository, + private val exposureDetectionTracker: ExposureDetectionTracker, + private val keyPackageSyncSettings: KeyPackageSyncSettings ) { private val mutex = Mutex() @@ -56,6 +61,8 @@ class DataReset @Inject constructor( Timber.w("CWA LOCAL DATA DELETION INITIATED.") // Database Reset AppDatabase.reset(context) + // Because LocalData does not behave like a normal shared preference + LocalData.clear() // Shared Preferences Reset SecurityHelper.resetSharedPrefs() // Reset the current risk level stored in LiveData @@ -65,6 +72,8 @@ class DataReset @Inject constructor( keyCacheRepository.clear() appConfigProvider.clear() interoperabilityRepository.clear() + exposureDetectionTracker.clear() + keyPackageSyncSettings.clear() Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt index c64cbedfde89f3c164c26e3eb1ea2204102d2bd3..2e97359373b6de42a333ade72b7b4542c5e42a73 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt @@ -6,6 +6,7 @@ import com.google.gson.Gson import de.rki.coronawarnapp.util.serialization.fromJson import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber class FlowPreference<T> constructor( private val preferences: SharedPreferences, @@ -17,6 +18,21 @@ class FlowPreference<T> constructor( private val flowInternal = MutableStateFlow(internalValue) val flow: Flow<T> = flowInternal + private val preferenceChangeListener = + SharedPreferences.OnSharedPreferenceChangeListener { changedPrefs, changedKey -> + if (changedKey != key) return@OnSharedPreferenceChangeListener + + val newValue = reader(changedPrefs, changedKey) + val currentvalue = flowInternal.value + if (currentvalue != newValue && flowInternal.compareAndSet(currentvalue, newValue)) { + Timber.v("%s:%s changed to %s", changedPrefs, changedKey, newValue) + } + } + + init { + preferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + } + private var internalValue: T get() = reader(preferences, key) set(newValue) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..0469b3840d2b9417a4ad728314f57cfa871a63ec --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.util.preferences + +import android.content.SharedPreferences +import androidx.core.content.edit +import timber.log.Timber + +fun SharedPreferences.clearAndNotify() { + val currentKeys = this.all.keys.toSet() + Timber.v("%s clearAndNotify(): %s", this, currentKeys) + edit { + currentKeys.forEach { remove(it) } + } + // Clear does not notify anyone using registerOnSharedPreferenceChangeListener + edit(commit = true) { + clear() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt index 370ab58fd4ce02e2358bc1dd9f66557020403546..c1c145e4965c2cd874b65a4c107f0976e59557dd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt @@ -27,6 +27,7 @@ import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.exception.CwaSecurityException import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent +import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.security.SecurityConstants.CWA_APP_SQLITE_DB_PW import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MAX_LENGTH import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MIN_LENGTH @@ -80,7 +81,7 @@ object SecurityHelper { @SuppressLint("ApplySharedPref") fun resetSharedPrefs() { - globalEncryptedSharedPreferencesInstance.edit().clear().commit() + globalEncryptedSharedPreferencesInstance.clearAndNotify() } private fun getStoredDbPassword(): ByteArray? = diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt index 33d46578e1a956c645d549edc3f793d2fc9b27df..0b855e1b9938ed9d47650a26c85ba5bbe9ba4927 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt @@ -16,5 +16,6 @@ fun <T> mockFlowPreference( val updateCall = arg<(T) -> T>(0) flow.value = updateCall(flow.value) } + return instance } diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt index a6294b00f54bcdbc96ab6bd18195389fc1a79e51..c094e560e5401f80bbf7bedff36af3537cc8a67e 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt @@ -3,6 +3,7 @@ package testhelpers.preferences import android.content.SharedPreferences class MockSharedPreferences : SharedPreferences { + private val listeners = mutableListOf<SharedPreferences.OnSharedPreferenceChangeListener>() private val dataMap = mutableMapOf<String, Any>() val dataMapPeek: Map<String, Any> get() = dataMap.toMap() @@ -36,12 +37,12 @@ class MockSharedPreferences : SharedPreferences { dataMap.putAll(newData) } - override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) { - throw NotImplementedError() + override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + listeners.add(listener) } - override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) { - throw NotImplementedError() + override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + listeners.remove(listener) } private fun createEditor(