From e90f5e3d4822d65ef34b5369abf930237250cf1c Mon Sep 17 00:00:00 2001 From: Alex Paulescu <alex.paulescu@gmail.com> Date: Wed, 28 Oct 2020 17:08:14 +0200 Subject: [PATCH] Delete country list cache (EXPOSUREAPP-3154) (#1431) * Refactored DataRetentionHelper class to use injection * Added ViewModel to SettingsReset * Add a clear cache method to AppConfigProvider.kt * Fixed repository live data containing stale data after app reset. * Moved logic from fragment to view model. * Adjusted tests to accommodate AppConfigProvider changes * Reverted back last 2 commits and downgraded ktlint plugin * Fixed DI issue * Put the scope in the hands of the VM * Deleted unused imports * Removed unused imports from VM * Use injected dispatcher provider to simplify testing. Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> Co-authored-by: AlexanderAlferov <64849422+AlexanderAlferov@users.noreply.github.com> Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> --- .../appconfig/AppConfigHttpCache.kt | 8 ++ .../appconfig/AppConfigModule.kt | 19 ++- .../appconfig/AppConfigProvider.kt | 44 ++++-- .../appconfig/AppConfigStorage.kt | 48 ++++--- .../storage/RiskLevelRepository.kt | 2 +- .../InteroperabilityRepository.kt | 4 + .../ui/main/MainActivityModule.kt | 5 + .../ui/settings/SettingsEvents.kt | 7 + .../ui/settings/SettingsResetFragment.kt | 71 +++------- .../ui/settings/SettingsResetModule.kt | 22 +++ .../ui/settings/SettingsResetViewModel.kt | 56 ++++++++ .../{DataRetentionHelper.kt => DataReset.kt} | 28 ++-- .../appconfig/AppConfigApiTest.kt | 9 +- .../appconfig/AppConfigProviderTest.kt | 131 ++++++++---------- .../appconfig/AppConfigStorageTest.kt | 32 ++--- 15 files changed, 293 insertions(+), 193 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigHttpCache.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsEvents.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/{DataRetentionHelper.kt => DataReset.kt} (72%) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigHttpCache.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigHttpCache.kt new file mode 100644 index 000000000..a3aff4add --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigHttpCache.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.appconfig + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class AppConfigHttpCache diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt index d742d7370..9216419bb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt @@ -18,18 +18,25 @@ import javax.inject.Singleton @Module class AppConfigModule { + @Singleton + @Provides + @AppConfigHttpCache + fun provideAppConfigCache( + @AppContext context: Context + ): Cache { + val cacheSize = 1 * 1024 * 1024L // 1MB + val cacheDir = File(context.cacheDir, "http_app-config") + return Cache(cacheDir, cacheSize) + } + @Singleton @Provides fun provideAppConfigApi( - @AppContext context: Context, @DownloadCDNHttpClient client: OkHttpClient, @DownloadCDNServerUrl url: String, - gsonConverterFactory: GsonConverterFactory + gsonConverterFactory: GsonConverterFactory, + @AppConfigHttpCache cache: Cache ): AppConfigApiV1 { - val cacheSize = 1 * 1024 * 1024L // 1MB - - val cacheDir = File(context.cacheDir, "http_app-config") - val cache = Cache(cacheDir, cacheSize) val cachingClient = client.newBuilder().apply { cache(cache) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt index c4d2d982d..4cbedfc93 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt @@ -8,7 +8,10 @@ import de.rki.coronawarnapp.server.protocols.internal.AppConfig.ApplicationConfi import de.rki.coronawarnapp.util.ZipHelper.unzip import de.rki.coronawarnapp.util.security.VerificationKeys import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import okhttp3.Cache import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -18,9 +21,11 @@ class AppConfigProvider @Inject constructor( private val appConfigAPI: Lazy<AppConfigApiV1>, private val verificationKeys: VerificationKeys, @DownloadCDNHomeCountry private val homeCountry: LocationCode, - private val configStorage: AppConfigStorage + private val configStorage: AppConfigStorage, + @AppConfigHttpCache private val cache: Cache ) { + private val mutex = Mutex() private val configApi: AppConfigApiV1 get() = appConfigAPI.get() @@ -58,7 +63,7 @@ class AppConfigProvider @Inject constructor( downloadAppConfig() } catch (e: Exception) { Timber.w(e, "Failed to download latest AppConfig.") - if (configStorage.isAppConfigAvailable) { + if (configStorage.isAppConfigAvailable()) { null } else { Timber.e("No fallback available, rethrowing!") @@ -76,12 +81,12 @@ class AppConfigProvider @Inject constructor( return newConfigParsed?.also { Timber.d("Saving new valid config.") Timber.v("New Config.supportedCountries: %s", it.supportedCountriesList) - configStorage.appConfigRaw = newConfigRaw + configStorage.setAppConfigRaw(newConfigRaw) } } - private fun getFallback(): ApplicationConfiguration { - val lastValidConfig = tryParseConfig(configStorage.appConfigRaw) + private suspend fun getFallback(): ApplicationConfiguration { + val lastValidConfig = tryParseConfig(configStorage.getAppConfigRaw()) return if (lastValidConfig != null) { Timber.d("Using fallback AppConfig.") lastValidConfig @@ -91,16 +96,29 @@ class AppConfigProvider @Inject constructor( } } - suspend fun getAppConfig(): ApplicationConfiguration = withContext(Dispatchers.IO) { - val newAppConfig = getNewAppConfig() + suspend fun getAppConfig(): ApplicationConfiguration = mutex.withLock { + withContext(Dispatchers.IO) { - return@withContext if (newAppConfig != null) { - newAppConfig - } else { - Timber.w("No new config available, using last valid.") - getFallback() + val newAppConfig = getNewAppConfig() + + return@withContext if (newAppConfig != null) { + newAppConfig + } else { + Timber.w("No new config available, using last valid.") + getFallback() + } + }.performSanityChecks() + } + + suspend fun clear() = mutex.withLock { + withContext(Dispatchers.IO) { + configStorage.setAppConfigRaw(null) + + // We are using Dispatchers IO to make it appropriate + @Suppress("BlockingMethodInNonBlockingContext") + cache.evictAll() } - }.performSanityChecks() + } private fun ApplicationConfiguration.performSanityChecks(): ApplicationConfiguration { var sanityChecked = this diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt index f81d9bee0..54b6351b1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigStorage.kt @@ -2,6 +2,8 @@ package de.rki.coronawarnapp.appconfig import android.content.Context import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import timber.log.Timber import java.io.File import javax.inject.Inject @@ -13,33 +15,35 @@ class AppConfigStorage @Inject constructor( ) { private val configDir = File(context.filesDir, "appconfig_storage") private val configFile = File(configDir, "appconfig") + private val mutex = Mutex() - val isAppConfigAvailable: Boolean - get() = configFile.exists() && configFile.length() > MIN_VALID_CONFIG_BYTES + suspend fun isAppConfigAvailable(): Boolean = mutex.withLock { + configFile.exists() && configFile.length() > MIN_VALID_CONFIG_BYTES + } + + suspend fun getAppConfigRaw(): ByteArray? = mutex.withLock { + Timber.v("get() AppConfig") + if (!configFile.exists()) return null + + val value = configFile.readBytes() + Timber.v("Read AppConfig of size %s and date %s", value.size, configFile.lastModified()) + return value + } + + suspend fun setAppConfigRaw(value: ByteArray?): Unit = mutex.withLock { + Timber.v("set(...) AppConfig: %dB", value?.size) - var appConfigRaw: ByteArray? - get() { - Timber.v("get() AppConfig") - if (!configFile.exists()) return null + if (configDir.mkdirs()) Timber.v("Parent folder created.") - val value = configFile.readBytes() - Timber.v("Read AppConfig of size %s and date %s", value.size, configFile.lastModified()) - return value + if (configFile.exists()) { + Timber.v("Overwriting %d from %s", configFile.length(), configFile.lastModified()) } - set(value) { - Timber.v("set(...) AppConfig: %dB", value?.size) - - if (configDir.mkdirs()) Timber.v("Parent folder created.") - - if (configFile.exists()) { - Timber.v("Overwriting %d from %s", configFile.length(), configFile.lastModified()) - } - if (value != null) { - configFile.writeBytes(value) - } else { - configFile.delete() - } + if (value != null) { + configFile.writeBytes(value) + } else { + configFile.delete() } + } companion object { // The normal config is ~512B+, we just need to check for a non 0 value, 128 is fine. diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt index ad835cd20..6fc200f81 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt @@ -35,7 +35,7 @@ object RiskLevelRepository { /** * Resets the data in the [RiskLevelRepository] * - * @see de.rki.coronawarnapp.util.DataRetentionHelper + * @see de.rki.coronawarnapp.util.DataReset * */ fun reset() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt index 90768259c..81327c8e1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/interoperability/InteroperabilityRepository.kt @@ -52,4 +52,8 @@ class InteroperabilityRepository @Inject constructor( } } } + + fun clear() { + _countryList.postValue(emptyList()) + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt index 83ffa5dff..af1a59dfd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt @@ -7,6 +7,8 @@ import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFra import de.rki.coronawarnapp.ui.main.home.HomeFragmentModule import de.rki.coronawarnapp.ui.onboarding.OnboardingDeltaInteroperabilityModule import de.rki.coronawarnapp.ui.settings.SettingFragmentsModule +import de.rki.coronawarnapp.ui.settings.SettingsResetFragment +import de.rki.coronawarnapp.ui.settings.SettingsResetModule import de.rki.coronawarnapp.ui.submission.SubmissionFragmentModule import de.rki.coronawarnapp.ui.tracing.details.RiskDetailsFragmentModule @@ -29,4 +31,7 @@ abstract class MainActivityModule { @ContributesAndroidInjector(modules = [InteroperabilityConfigurationFragmentModule::class]) abstract fun intertopConfigScreen(): InteroperabilityConfigurationFragment + + @ContributesAndroidInjector(modules = [SettingsResetModule::class]) + abstract fun settingsResetScreen(): SettingsResetFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsEvents.kt new file mode 100644 index 000000000..8f44c39bc --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsEvents.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.ui.settings + +sealed class SettingsEvents { + object ResetApp : SettingsEvents() + object GoBack : SettingsEvents() + object GoToOnboarding : SettingsEvents() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt index 74bf48594..f59335d80 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetFragment.kt @@ -5,45 +5,41 @@ import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.google.android.gms.common.api.ApiException import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSettingsResetBinding -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.ui.onboarding.OnboardingActivity -import de.rki.coronawarnapp.util.DataRetentionHelper import de.rki.coronawarnapp.util.DialogHelper +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.worker.BackgroundWorkScheduler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject /** * The user is informed what a reset means and he can perform it. * */ -class SettingsResetFragment : Fragment(R.layout.fragment_settings_reset) { - - companion object { - private val TAG: String? = SettingsResetFragment::class.simpleName - } +class SettingsResetFragment : Fragment(R.layout.fragment_settings_reset), AutoInject { + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: SettingsResetViewModel by cwaViewModels { viewModelFactory } private val binding: FragmentSettingsResetBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - binding.settingsResetButtonDelete.setOnClickListener { - confirmReset() - } - binding.settingsResetButtonCancel.setOnClickListener { - (activity as MainActivity).goBack() + binding.apply { + settingsResetButtonDelete.setOnClickListener { vm.resetAllData() } + settingsResetButtonCancel.setOnClickListener { vm.goBack() } + settingsResetHeader.headerButtonBack.buttonIcon.setOnClickListener { vm.goBack() } } - binding.settingsResetHeader.headerButtonBack.buttonIcon.setOnClickListener { - (activity as MainActivity).goBack() + vm.clickEvent.observe2(this) { + when (it) { + is SettingsEvents.ResetApp -> confirmReset() + is SettingsEvents.GoBack -> (activity as MainActivity).goBack() + is SettingsEvents.GoToOnboarding -> navigateToOnboarding() + } } } @@ -52,36 +48,11 @@ class SettingsResetFragment : Fragment(R.layout.fragment_settings_reset) { binding.settingsResetContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) } - private fun deleteAllAppContent() { - lifecycleScope.launch { - try { - val isTracingEnabled = InternalExposureNotificationClient.asyncIsEnabled() - // only stop tracing if it is currently enabled - if (isTracingEnabled) { - InternalExposureNotificationClient.asyncStop() - BackgroundWorkScheduler.stopWorkScheduler() - } - } catch (apiException: ApiException) { - apiException.report( - ExceptionCategory.EXPOSURENOTIFICATION, TAG, null - ) - } - withContext(Dispatchers.IO) { - deleteLocalAppContent() - } - navigateToOnboarding() - } - } - private fun navigateToOnboarding() { OnboardingActivity.start(requireContext()) activity?.finish() } - private fun deleteLocalAppContent() { - DataRetentionHelper.clearAllLocalData(requireContext()) - } - private fun confirmReset() { val resetDialog = DialogHelper.DialogInstance( requireActivity(), @@ -89,10 +60,8 @@ class SettingsResetFragment : Fragment(R.layout.fragment_settings_reset) { R.string.settings_reset_dialog_body, R.string.settings_reset_dialog_button_confirm, R.string.settings_reset_dialog_button_cancel, - true, - { - deleteAllAppContent() - } + cancelable = true, + positiveButtonFunction = vm::deleteAllAppContent ) DialogHelper.showDialog(resetDialog).apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetModule.kt new file mode 100644 index 000000000..ae89d82f4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetModule.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.ui.settings + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +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 SettingsResetModule { + @Binds + @IntoMap + @CWAViewModelKey(SettingsResetViewModel::class) + abstract fun settingsResetVM( + factory: SettingsResetViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun settingsResetFragment(): SettingsResetFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt new file mode 100644 index 000000000..2b559cd3a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt @@ -0,0 +1,56 @@ +package de.rki.coronawarnapp.ui.settings + +import com.google.android.gms.common.api.ApiException +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.DataReset +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler + +class SettingsResetViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + private val dataReset: DataReset +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + val clickEvent: SingleLiveEvent<SettingsEvents> = SingleLiveEvent() + + fun resetAllData() { + clickEvent.postValue(SettingsEvents.ResetApp) + } + + fun goBack() { + clickEvent.postValue(SettingsEvents.GoBack) + } + + fun deleteAllAppContent() { + launch { + try { + val isTracingEnabled = InternalExposureNotificationClient.asyncIsEnabled() + // only stop tracing if it is currently enabled + if (isTracingEnabled) { + InternalExposureNotificationClient.asyncStop() + BackgroundWorkScheduler.stopWorkScheduler() + } + } catch (apiException: ApiException) { + apiException.report( + ExceptionCategory.EXPOSURENOTIFICATION, TAG, null + ) + } + + dataReset.clearAllLocalData() + clickEvent.postValue(SettingsEvents.GoToOnboarding) + } + } + + companion object { + private val TAG: String? = SettingsResetFragment::class.simpleName + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<SettingsResetViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataRetentionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt similarity index 72% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataRetentionHelper.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index 1ce93fc10..b0aae9004 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataRetentionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -21,25 +21,37 @@ package de.rki.coronawarnapp.util import android.annotation.SuppressLint import android.content.Context +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.storage.AppDatabase import de.rki.coronawarnapp.storage.RiskLevelRepository -import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.security.SecurityHelper -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton /** * Helper for supplying functionality regarding Data Retention */ -object DataRetentionHelper { - private val TAG: String? = DataRetentionHelper::class.simpleName +@Singleton +class DataReset @Inject constructor( + @AppContext private val context: Context, + private val keyCacheRepository: KeyCacheRepository, + private val appConfigProvider: AppConfigProvider, + private val interoperabilityRepository: InteroperabilityRepository +) { + private val mutex = Mutex() /** * Deletes all data known to the Application * */ @SuppressLint("ApplySharedPref") // We need a commit here to ensure consistency - fun clearAllLocalData(context: Context) { + suspend fun clearAllLocalData() = mutex.withLock { Timber.w("CWA LOCAL DATA DELETION INITIATED.") // Database Reset AppDatabase.reset(context) @@ -47,9 +59,9 @@ object DataRetentionHelper { SecurityHelper.resetSharedPrefs() // Reset the current risk level stored in LiveData RiskLevelRepository.reset() - // Export File Reset - // TODO runBlocking, but also all of the above is BLOCKING and should be called more nicely - runBlocking { AppInjector.component.keyCacheRepository.clear() } + keyCacheRepository.clear() + appConfigProvider.clear() + interoperabilityRepository.clear() Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigApiTest.kt index 24c628143..2e2d2cb5a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigApiTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigApiTest.kt @@ -8,6 +8,7 @@ import io.mockk.MockKAnnotations import io.mockk.clearAllMocks import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import kotlinx.coroutines.runBlocking import okhttp3.ConnectionSpec import okhttp3.mockwebserver.MockResponse @@ -31,6 +32,7 @@ class AppConfigApiTest : BaseIOTest() { private val cacheFiles = File(testDir, "cache") private val cacheDir = File(cacheFiles, "http_app-config") + @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -59,14 +61,15 @@ class AppConfigApiTest : BaseIOTest() { .connectionSpecs(listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)) .build() + val cache = AppConfigModule().provideAppConfigCache(context) return AppConfigModule().provideAppConfigApi( - context = context, client = cdnHttpClient, url = serverAddress, - gsonConverterFactory = gsonConverterFactory + gsonConverterFactory = gsonConverterFactory, + cache = cache ) } - + @Test fun `application config download`() { val api = createAPI() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt index e744b2fc3..972136da0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt @@ -7,10 +7,11 @@ 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.mockk import io.mockk.verify -import kotlinx.coroutines.runBlocking import okhttp3.ResponseBody.Companion.toResponseBody import okio.ByteString.Companion.decodeHex import org.junit.jupiter.api.AfterEach @@ -38,9 +39,9 @@ class AppConfigProviderTest : BaseIOTest() { testDir.mkdirs() testDir.exists() shouldBe true - every { appConfigStorage.isAppConfigAvailable } answers { mockConfigStorage != null } - every { appConfigStorage.appConfigRaw } answers { mockConfigStorage } - every { appConfigStorage.appConfigRaw = any() } answers { mockConfigStorage = arg(0) } + coEvery { appConfigStorage.isAppConfigAvailable() } answers { mockConfigStorage != null } + coEvery { appConfigStorage.getAppConfigRaw() } answers { mockConfigStorage } + coEvery { appConfigStorage.setAppConfigRaw(any()) } answers { mockConfigStorage = arg(0) } } @AfterEach @@ -55,27 +56,26 @@ class AppConfigProviderTest : BaseIOTest() { appConfigAPI = { api }, verificationKeys = verificationKeys, homeCountry = homeCountry, - configStorage = appConfigStorage + configStorage = appConfigStorage, + cache = mockk() ) @Test - fun `application config download`() { + suspend fun `application config download`() { coEvery { api.getApplicationConfiguration("DE") } returns APPCONFIG_BUNDLE.toResponseBody() every { verificationKeys.hasInvalidSignature(any(), any()) } returns false val downloadServer = createDownloadServer() - runBlocking { - val rawConfig = downloadServer.downloadAppConfig() - rawConfig shouldBe APPCONFIG_RAW.toByteArray() - } + val rawConfig = downloadServer.downloadAppConfig() + rawConfig shouldBe APPCONFIG_RAW.toByteArray() verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) } } @Test - fun `application config data is faulty`() { + suspend fun `application config data is faulty`() { coEvery { api.getApplicationConfiguration("DE") } returns "123ABC".decodeHex() .toResponseBody() @@ -83,15 +83,13 @@ class AppConfigProviderTest : BaseIOTest() { val downloadServer = createDownloadServer() - runBlocking { - shouldThrow<ApplicationConfigurationInvalidException> { - downloadServer.downloadAppConfig() - } + shouldThrow<ApplicationConfigurationInvalidException> { + downloadServer.downloadAppConfig() } } @Test - fun `application config verification fails`() { + suspend fun `application config verification fails`() { coEvery { api.getApplicationConfiguration("DE") } returns APPCONFIG_BUNDLE .toResponseBody() @@ -99,126 +97,113 @@ class AppConfigProviderTest : BaseIOTest() { val downloadServer = createDownloadServer() - runBlocking { - shouldThrow<ApplicationConfigurationCorruptException> { - downloadServer.downloadAppConfig() - } + shouldThrow<ApplicationConfigurationCorruptException> { + downloadServer.downloadAppConfig() } } @Test - fun `successful download stores new config`() { + suspend fun `successful download stores new config`() { coEvery { api.getApplicationConfiguration("DE") } returns APPCONFIG_BUNDLE .toResponseBody() every { verificationKeys.hasInvalidSignature(any(), any()) } returns false val downloadServer = createDownloadServer() + downloadServer.getAppConfig() - runBlocking { - downloadServer.getAppConfig() - - mockConfigStorage shouldBe APPCONFIG_RAW.toByteArray() - verify { appConfigStorage.appConfigRaw = APPCONFIG_RAW.toByteArray() } - } + mockConfigStorage shouldBe APPCONFIG_RAW.toByteArray() + coVerify { appConfigStorage.setAppConfigRaw(APPCONFIG_RAW.toByteArray()) } } @Test - fun `failed download doesn't overwrite valid config`() { + suspend fun `failed download doesn't overwrite valid config`() { mockConfigStorage = APPCONFIG_RAW.toByteArray() coEvery { api.getApplicationConfiguration("DE") } throws IOException() every { verificationKeys.hasInvalidSignature(any(), any()) } returns false - runBlocking { - createDownloadServer().getAppConfig() - } + createDownloadServer().getAppConfig() - verify(exactly = 0) { appConfigStorage.appConfigRaw = any() } + coVerify(exactly = 0) { appConfigStorage.setAppConfigRaw(any()) } mockConfigStorage shouldBe APPCONFIG_RAW.toByteArray() } @Test - fun `failed verification doesn't overwrite valid config`() { + suspend fun `failed verification doesn't overwrite valid config`() { mockConfigStorage = APPCONFIG_RAW.toByteArray() coEvery { api.getApplicationConfiguration("DE") } returns APPCONFIG_BUNDLE .toResponseBody() every { verificationKeys.hasInvalidSignature(any(), any()) } returns true - runBlocking { - createDownloadServer().getAppConfig() - } + createDownloadServer().getAppConfig() - verify(exactly = 0) { appConfigStorage.appConfigRaw = any() } + coVerify(exactly = 0) { appConfigStorage.setAppConfigRaw(any()) } mockConfigStorage shouldBe APPCONFIG_RAW.toByteArray() } @Test - fun `fallback to last config if verification fails`() { + suspend fun `fallback to last config if verification fails`() { mockConfigStorage = APPCONFIG_RAW.toByteArray() coEvery { api.getApplicationConfiguration("DE") } returns "123ABC".decodeHex() .toResponseBody() every { verificationKeys.hasInvalidSignature(any(), any()) } throws Exception() - runBlocking { - createDownloadServer().getAppConfig().minRiskScore shouldBe 11 - } + createDownloadServer().getAppConfig().minRiskScore shouldBe 11 every { verificationKeys.hasInvalidSignature(any(), any()) } returns true - runBlocking { - createDownloadServer().getAppConfig().minRiskScore shouldBe 11 - } + createDownloadServer().getAppConfig().minRiskScore shouldBe 11 } @Test - fun `fallback to last config if download fails`() { + suspend fun `fallback to last config if download fails`() { mockConfigStorage = APPCONFIG_RAW.toByteArray() coEvery { api.getApplicationConfiguration("DE") } throws Exception() every { verificationKeys.hasInvalidSignature(any(), any()) } returns false - runBlocking { - createDownloadServer().getAppConfig().minRiskScore shouldBe 11 - } + createDownloadServer().getAppConfig().minRiskScore shouldBe 11 } // Because the UI requires this to detect when to show alternative UI elements @Test - fun `if supportedCountryList is empty, we do not insert DE as fallback`() { + suspend fun `if supportedCountryList is empty, we do not insert DE as fallback`() { coEvery { api.getApplicationConfiguration("DE") } returns APPCONFIG_BUNDLE.toResponseBody() every { verificationKeys.hasInvalidSignature(any(), any()) } returns false - runBlocking { - createDownloadServer().getAppConfig().supportedCountriesList shouldBe emptyList() - } + createDownloadServer().getAppConfig().supportedCountriesList shouldBe emptyList() } companion object { private val APPCONFIG_BUNDLE = - ("504b0304140008080800856b22510000000000000000000000000a0000006578706f72742e62696ee3e016" + - "f2e552e662f6f10f97e05792ca28292928b6d2d72f2f2fd74bce2fcacf4b2c4f2ccad34b2c28e0" + - "52e362f1f074f710e097f0c0a74e2a854b80835180498259814583d580cd82dd814390010c3c1d" + - "a4b8141835180d182d181d181561825a021cac02ac12ac0aac40f5ac16ac0eac86102913072b3e" + - "01460946841e47981e25192e160e73017b21214e88d0077ba8250fec1524b5a4b8b8b858043824" + - "98849804588578806a19255884c02400504b0708df2c788daf000000f1000000504b0304140008" + - "080800856b22510000000000000000000000000a0000006578706f72742e736967018a0075ff0a" + - "87010a380a1864652e726b692e636f726f6e617761726e6170702d6465761a0276312203323632" + - "2a13312e322e3834302e31303034352e342e332e321001180122473045022100cf32ff24ea18a1" + - "ffcc7ff4c9fe8d1808cecbc5a37e3e1d4c9ce682120450958c022064bf124b6973a9b510a43d47" + - "9ff93e0ef97a5b893c7af4abc4a8d399969cd8a0504b070813c517c68f0000008a000000504b01" + - "021400140008080800856b2251df2c788daf000000f10000000a00000000000000000000000000" + - "000000006578706f72742e62696e504b01021400140008080800856b225113c517c68f0000008a" + - "0000000a00000000000000000000000000e70000006578706f72742e736967504b050600000000" + - "0200020070000000ae0100000000").decodeHex() + ( + "504b0304140008080800856b22510000000000000000000000000a0000006578706f72742e62696ee3e016" + + "f2e552e662f6f10f97e05792ca28292928b6d2d72f2f2fd74bce2fcacf4b2c4f2ccad34b2c28e0" + + "52e362f1f074f710e097f0c0a74e2a854b80835180498259814583d580cd82dd814390010c3c1d" + + "a4b8141835180d182d181d181561825a021cac02ac12ac0aac40f5ac16ac0eac86102913072b3e" + + "01460946841e47981e25192e160e73017b21214e88d0077ba8250fec1524b5a4b8b8b858043824" + + "98849804588578806a19255884c02400504b0708df2c788daf000000f1000000504b0304140008" + + "080800856b22510000000000000000000000000a0000006578706f72742e736967018a0075ff0a" + + "87010a380a1864652e726b692e636f726f6e617761726e6170702d6465761a0276312203323632" + + "2a13312e322e3834302e31303034352e342e332e321001180122473045022100cf32ff24ea18a1" + + "ffcc7ff4c9fe8d1808cecbc5a37e3e1d4c9ce682120450958c022064bf124b6973a9b510a43d47" + + "9ff93e0ef97a5b893c7af4abc4a8d399969cd8a0504b070813c517c68f0000008a000000504b01" + + "021400140008080800856b2251df2c788daf000000f10000000a00000000000000000000000000" + + "000000006578706f72742e62696e504b01021400140008080800856b225113c517c68f0000008a" + + "0000000a00000000000000000000000000e70000006578706f72742e736967504b050600000000" + + "0200020070000000ae0100000000" + ).decodeHex() private val APPCONFIG_RAW = - ("080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + - "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" + - "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" + - "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" + - "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" + - "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804").decodeHex() + ( + "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + + "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" + + "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" + + "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" + + "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" + + "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804" + ).decodeHex() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigStorageTest.kt index dd637fbb7..aea45f221 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigStorageTest.kt @@ -37,51 +37,51 @@ class AppConfigStorageTest : BaseIOTest() { private fun createStorage() = AppConfigStorage(context) @Test - fun `config availability is determined by file existence and min size`() { + suspend fun `config availability is determined by file existence and min size`() { storageDir.mkdirs() val storage = createStorage() - storage.isAppConfigAvailable shouldBe false + storage.isAppConfigAvailable() shouldBe false configPath.createNewFile() - storage.isAppConfigAvailable shouldBe false + storage.isAppConfigAvailable() shouldBe false configPath.writeBytes(ByteArray(128) { 1 }) - storage.isAppConfigAvailable shouldBe false + storage.isAppConfigAvailable() shouldBe false configPath.writeBytes(ByteArray(129) { 1 }) - storage.isAppConfigAvailable shouldBe true + storage.isAppConfigAvailable() shouldBe true } @Test - fun `simple read and write config`() { + suspend fun `simple read and write config`() { configPath.exists() shouldBe false val storage = createStorage() configPath.exists() shouldBe false - storage.appConfigRaw = testByteArray + storage.setAppConfigRaw(testByteArray) configPath.exists() shouldBe true configPath.readBytes() shouldBe testByteArray - storage.appConfigRaw shouldBe testByteArray + storage.getAppConfigRaw() shouldBe testByteArray } @Test - fun `nulling and overwriting`() { + suspend fun `nulling and overwriting`() { val storage = createStorage() configPath.exists() shouldBe false - storage.appConfigRaw shouldBe null - storage.appConfigRaw = null + storage.getAppConfigRaw() shouldBe null + storage.setAppConfigRaw(null) configPath.exists() shouldBe false - storage.appConfigRaw shouldBe null - storage.appConfigRaw = testByteArray - storage.appConfigRaw shouldBe testByteArray + storage.getAppConfigRaw() shouldBe null + storage.setAppConfigRaw(testByteArray) + storage.getAppConfigRaw() shouldBe testByteArray configPath.exists() shouldBe true configPath.readBytes() shouldBe testByteArray - storage.appConfigRaw = null - storage.appConfigRaw shouldBe null + storage.setAppConfigRaw(null) + storage.getAppConfigRaw() shouldBe null configPath.exists() shouldBe false } } -- GitLab