diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt index 292d1d2c64fa0004d0e3dc4e918cff094d7a617f..5f2f3a274e85e9c145fb7c1cab7cf55586869160 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt @@ -17,6 +17,7 @@ import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.statistics.source.StatisticsProvider import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard import de.rki.coronawarnapp.storage.TracingRepository +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider import de.rki.coronawarnapp.submission.ui.homecards.TestPositiveCard @@ -67,6 +68,7 @@ class HomeFragmentTest : BaseUITest() { @MockK lateinit var statisticsProvider: StatisticsProvider @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @MockK lateinit var appShortcutsHelper: AppShortcutsHelper + @MockK lateinit var tracingSettings: TracingSettings private lateinit var homeFragmentViewModel: HomeFragmentViewModel @@ -256,7 +258,8 @@ class HomeFragmentTest : BaseUITest() { cwaSettings = cwaSettings, statisticsProvider = statisticsProvider, deadmanNotificationScheduler = deadmanNotificationScheduler, - appShortcutsHelper = appShortcutsHelper + appShortcutsHelper = appShortcutsHelper, + tracingSettings = tracingSettings ) ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentTest.kt index 7eeb02ee3d9649bea978adb941dacde0312778da..a0362781c44750b755b88ca6f51228a89fd8d598 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentTest.kt @@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.nearby.TracingPermissionHelper +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import io.mockk.MockKAnnotations import io.mockk.Runs @@ -31,6 +32,7 @@ class OnboardingTracingFragmentTest : BaseUITest() { @MockK lateinit var interopRepo: InteroperabilityRepository @MockK lateinit var factory: TracingPermissionHelper.Factory + @MockK lateinit var tracingSettings: TracingSettings @Rule @JvmField @@ -47,7 +49,8 @@ class OnboardingTracingFragmentTest : BaseUITest() { OnboardingTracingFragmentViewModel( interoperabilityRepository = interopRepo, tracingPermissionHelperFactory = factory, - dispatcherProvider = TestDispatcherProvider() + dispatcherProvider = TestDispatcherProvider(), + tracingSettings = tracingSettings ) ) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt index 6ca53f71453eed67437e683a2aa449027b609d08..49daaf6fa2da7d161058154d3607cca0196ca57c 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/deltaonboarding/ui/DeltaOnboardingFragmentViewModel.kt @@ -7,7 +7,6 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory @@ -42,10 +41,10 @@ class DeltaOnboardingFragmentViewModel @AssistedInject constructor( ContactDiarySettings.OnboardingStatus.NOT_ONBOARDED } - fun isDeltaOnboardingDone() = LocalData.isInteroperabilityShownAtLeastOnce + fun isDeltaOnboardingDone() = settings.wasInteroperabilityShownAtLeastOnce fun setDeltaOboardinDone(value: Boolean) { - LocalData.isInteroperabilityShownAtLeastOnce = value + settings.wasInteroperabilityShownAtLeastOnce = value } @AssistedFactory diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt index 0212b6a3796ce9620ad366bddaf660bcc8427629..0218b306014d78f70d9cebd057cc2978cfbd7d66 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt @@ -8,8 +8,7 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import com.google.gson.Gson import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -17,7 +16,6 @@ import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import timber.log.Timber @@ -26,7 +24,7 @@ import java.util.UUID class SubmissionTestFragmentViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val tekHistoryStorage: TEKHistoryStorage, - private val submissionRepository: SubmissionRepository, + private val submissionSettings: SubmissionSettings, tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory, @BaseGson baseGson: Gson ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -60,8 +58,7 @@ class SubmissionTestFragmentViewModel @AssistedInject constructor( ) val errorEvents = SingleLiveEvent<Throwable>() - private val internalToken = MutableStateFlow(LocalData.registrationToken()) - val currentTestId = internalToken.asLiveData() + val currentTestId = submissionSettings.registrationToken.flow.asLiveData() val shareTEKsEvent = SingleLiveEvent<TEKExport>() @@ -85,13 +82,15 @@ class SubmissionTestFragmentViewModel @AssistedInject constructor( .asLiveData(context = dispatcherProvider.Default) fun scrambleRegistrationToken() { - LocalData.registrationToken(UUID.randomUUID().toString()) - internalToken.value = LocalData.registrationToken() + submissionSettings.registrationToken.update { + UUID.randomUUID().toString() + } } fun deleteRegistrationToken() { - LocalData.registrationToken(null) - internalToken.value = LocalData.registrationToken() + submissionSettings.registrationToken.update { + null + } } fun updateStorage() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index f797cf5b89cd738c7f47d106b854ef1f2ec08064..d57407ef4e48d7f63255544a925d6fdae4e261a8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -21,7 +21,9 @@ import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.risk.RiskLevelChangeDetector -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.storage.OnboardingSettings +import de.rki.coronawarnapp.storage.preferences.EncryptedPreferencesMigration import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.CWADebug @@ -29,6 +31,7 @@ import de.rki.coronawarnapp.util.WatchdogService import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -57,6 +60,9 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var notificationHelper: NotificationHelper @Inject lateinit var deviceTimeHandler: DeviceTimeHandler @Inject lateinit var autoSubmission: AutoSubmission + @Inject lateinit var submissionSettings: SubmissionSettings + @Inject lateinit var onboardingSettings: OnboardingSettings + @Inject lateinit var encryptedPreferencesMigration: EncryptedPreferencesMigration @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree @@ -70,6 +76,10 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { CWADebug.initAfterInjection(component) + encryptedPreferencesMigration.doMigration() + + BackgroundWorkScheduler.init(component) + Timber.plant(rollingLogHistory) Timber.v("onCreate(): WorkManager setup done: $workManager") @@ -87,8 +97,8 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { .onEach { isAppInForeground = it } .launchIn(GlobalScope) - if (LocalData.onboardingCompletedTimestamp() != null) { - if (!LocalData.isAllowedToSubmitDiagnosisKeys()) { + if (onboardingSettings.isOnboarded) { + if (!submissionSettings.isAllowedToSubmitKeys) { deadmanNotificationScheduler.schedulePeriodic() } contactDiaryWorkScheduler.schedulePeriodic() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt index 6d1d15f608315295e742272515b256d6320f2686..81f2bece36227c378fb5c212b3a27171c2598a64 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt @@ -3,14 +3,16 @@ package de.rki.coronawarnapp.bugreporting.censors import dagger.Reusable import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent import de.rki.coronawarnapp.bugreporting.debuglog.LogLine -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.CWADebug import javax.inject.Inject @Reusable -class RegistrationTokenCensor @Inject constructor() : BugCensor { +class RegistrationTokenCensor @Inject constructor( + private val submissionSettings: SubmissionSettings +) : BugCensor { override suspend fun checkLog(entry: LogLine): LogLine? { - val token = LocalData.registrationToken() ?: return null + val token = submissionSettings.registrationToken.value ?: return null if (!entry.message.contains(token)) return null val newMessage = if (CWADebug.isDeviceForTestersBuild) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt index 97949918f02c3590afca4dd76ab5fb8b3ae11f88..4c273c3bf87907bf9a2779923cade024be725462 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/Analytics.kt @@ -13,7 +13,7 @@ import de.rki.coronawarnapp.datadonation.safetynet.DeviceAttestation import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaDataRequestAndroid -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.util.TimeStamper import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.flow.Flow @@ -21,7 +21,6 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout import org.joda.time.Hours -import org.joda.time.Instant import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -36,7 +35,8 @@ class Analytics @Inject constructor( private val donorModules: Set<@JvmSuppressWildcards DonorModule>, private val settings: AnalyticsSettings, private val logger: LastAnalyticsSubmissionLogger, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val onboardingSettings: OnboardingSettings ) { private val submissionLockoutMutex = Mutex() @@ -173,7 +173,7 @@ class Analytics @Inject constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) fun stopDueToTimeSinceOnboarding(): Boolean { - val onboarding = LocalData.onboardingCompletedTimestamp()?.let { Instant.ofEpochMilli(it) } ?: return true + val onboarding = onboardingSettings.onboardingCompletedTimestamp ?: return true return onboarding.plus(Hours.hours(ONBOARDING_DELAY_HOURS).toStandardDuration()).isAfter(timeStamper.nowUTC) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt index 4a2053feca5bf156a479f8fb6085c4f70e80faf2..34f8be0bb2a6df2029de72f0de6a2888206da592 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt @@ -6,7 +6,7 @@ import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettin import de.rki.coronawarnapp.risk.RiskLevelSettings import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.formatter.TestResult import kotlinx.coroutines.flow.first @@ -22,6 +22,7 @@ class TestResultDonor @Inject constructor( private val riskLevelSettings: RiskLevelSettings, private val riskLevelStorage: RiskLevelStorage, private val timeStamper: TimeStamper, + private val submissionSettings: SubmissionSettings ) : DonorModule { override suspend fun beginDonation(request: DonorModule.Request): DonorModule.Contribution { @@ -31,7 +32,7 @@ class TestResultDonor @Inject constructor( return TestResultMetadataNoContribution } - val timestampAtRegistration = LocalData.initialTestResultReceivedTimestamp() + val timestampAtRegistration = submissionSettings.initialTestResultReceivedAt if (timestampAtRegistration == null) { Timber.d("Skipping TestResultMetadata donation timestampAtRegistration isn't found") @@ -43,8 +44,7 @@ class TestResultDonor @Inject constructor( .analytics .hoursSinceTestRegistrationToSubmitTestResultMetadata - val registrationTime = Instant.ofEpochMilli(timestampAtRegistration) - val hoursSinceTestRegistrationTime = Duration(registrationTime, timeStamper.nowUTC).standardHours.toInt() + val hoursSinceTestRegistrationTime = Duration(timestampAtRegistration, timeStamper.nowUTC).standardHours.toInt() val isDiffHoursMoreThanConfigHoursForPendingTest = hoursSinceTestRegistrationTime >= configHours val testResultAtRegistration = @@ -53,7 +53,7 @@ class TestResultDonor @Inject constructor( val daysSinceMostRecentDateAtRiskLevelAtTestRegistration = calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration( riskLevelSettings.lastChangeCheckedRiskLevelTimestamp, - registrationTime + timestampAtRegistration ) val riskLevelAtRegistration = testResultDonorSettings.riskLevelAtTestRegistration.value @@ -62,7 +62,7 @@ class TestResultDonor @Inject constructor( if (riskLevelAtRegistration == PpaData.PPARiskLevel.RISK_LEVEL_LOW) { DEFAULT_HOURS_SINCE_HIGH_RISK_WARNING } else { - calculatedHoursSinceHighRiskWarning(registrationTime) + calculatedHoursSinceHighRiskWarning(timestampAtRegistration) } return when { @@ -87,7 +87,7 @@ class TestResultDonor @Inject constructor( */ testResultAtRegistration.isFinal -> finalTestMetadataDonation( - registrationTime, + timestampAtRegistration, testResultAtRegistration, daysSinceMostRecentDateAtRiskLevelAtTestRegistration, hoursSinceHighRiskWarningAtTestRegistration 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 b545c807ec7d29864850a5c770e6ffd08100da77..66ecae8a8c6a1d467ce314b3c71db8ab90ef3893 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 @@ -8,7 +8,7 @@ import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.risk.RollbackItem -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -33,7 +33,8 @@ class DownloadDiagnosisKeysTask @Inject constructor( private val appConfigProvider: AppConfigProvider, private val keyPackageSyncTool: KeyPackageSyncTool, private val timeStamper: TimeStamper, - private val settings: DownloadDiagnosisKeysSettings + private val settings: DownloadDiagnosisKeysSettings, + private val submissionSettings: SubmissionSettings ) : Task<DownloadDiagnosisKeysTask.Progress, Task.Result> { private val internalProgress = ConflatedBroadcastChannel<Progress>() @@ -113,7 +114,7 @@ class DownloadDiagnosisKeysTask @Inject constructor( // remember version code of this execution for next time settings.updateLastVersionCodeToCurrent() - if (LocalData.isAllowedToSubmitDiagnosisKeys()) { + if (submissionSettings.isAllowedToSubmitKeys) { Timber.tag(TAG).i("task aborted, positive test result") return object : Task.Result {} } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt index b571c7f7f8b45e1fc65f5649fd00afa618bf62b1..e335d18c8d841cfbacf6813521731ef5a3908e3f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/main/CWASettings.kt @@ -12,8 +12,6 @@ import javax.inject.Inject /** * For general app related values, * e.g. "Has dialog been shown", as "OnBoarding been shown?" - * In future refactoring it should contain all values - * from **[de.rki.coronawarnapp.storage.LocalData]** that don't fit more specific classes. */ class CWASettings @Inject constructor( @AppContext val context: Context @@ -27,6 +25,10 @@ class CWASettings @Inject constructor( get() = prefs.getBoolean(PKEY_DEVICE_TIME_INCORRECT_ACK, false) set(value) = prefs.edit { putBoolean(PKEY_DEVICE_TIME_INCORRECT_ACK, value) } + var wasInteroperabilityShownAtLeastOnce: Boolean + get() = prefs.getBoolean(PKEY_INTEROPERABILITY_SHOWED_AT_LEAST_ONCE, false) + set(value) = prefs.edit { putBoolean(PKEY_INTEROPERABILITY_SHOWED_AT_LEAST_ONCE, value) } + var firstReliableDeviceTime: Instant get() = Instant.ofEpochMilli(prefs.getLong(PKEY_DEVICE_TIME_FIRST_RELIABLE, 0L)) set(value) = prefs.edit { putLong(PKEY_DEVICE_TIME_FIRST_RELIABLE, value.millis) } @@ -42,6 +44,20 @@ class CWASettings @Inject constructor( ).let { raw -> ConfigData.DeviceTimeState.values().single { it.key == raw } } set(value) = prefs.edit { putString(PKEY_DEVICE_TIME_LAST_STATE_CHANGE_STATE, value.key) } + var numberOfRemainingSharePositiveTestResultReminders: Int + get() = prefs.getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, Int.MIN_VALUE) + set(value) = prefs.edit { putInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, value) } + + val isNotificationsRiskEnabled = prefs.createFlowPreference( + key = PKEY_NOTIFICATIONS_RISK_ENABLED, + defaultValue = false + ) + + val isNotificationsTestEnabled = prefs.createFlowPreference( + key = PKEY_NOTIFICATIONS_TEST_ENABLED, + defaultValue = false + ) + val lastChangelogVersion = prefs.createFlowPreference( key = LAST_CHANGELOG_VERSION, defaultValue = DEFAULT_APP_VERSION @@ -53,9 +69,13 @@ class CWASettings @Inject constructor( companion object { private const val PKEY_DEVICE_TIME_INCORRECT_ACK = "devicetime.incorrect.acknowledged" + private const val PKEY_INTEROPERABILITY_SHOWED_AT_LEAST_ONCE = "interoperability.showed" private const val PKEY_DEVICE_TIME_FIRST_RELIABLE = "devicetime.correct.first" private const val PKEY_DEVICE_TIME_LAST_STATE_CHANGE_TIME = "devicetime.laststatechange.timestamp" private const val PKEY_DEVICE_TIME_LAST_STATE_CHANGE_STATE = "devicetime.laststatechange.state" + private const val PKEY_NOTIFICATIONS_RISK_ENABLED = "notifications.risk.enabled" + private const val PKEY_NOTIFICATIONS_TEST_ENABLED = "notifications.test.enabled" + private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT = "testresults.count" private const val LAST_CHANGELOG_VERSION = "update.changelog.lastversion" private const val DEFAULT_APP_VERSION = 1L } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/TracingPermissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/TracingPermissionHelper.kt index c726fe9f59792f8052772dc24dbf66c57a6bdeab..1c3ad7c8c64bb948aa51c69a4c3123f1922673a4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/TracingPermissionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/TracingPermissionHelper.kt @@ -6,7 +6,7 @@ import androidx.annotation.VisibleForTesting import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first @@ -16,6 +16,7 @@ import timber.log.Timber class TracingPermissionHelper @AssistedInject constructor( @Assisted private val callback: Callback, private val enfClient: ENFClient, + private val tracingSettings: TracingSettings, @AppScope private val scope: CoroutineScope ) { @@ -73,11 +74,7 @@ class TracingPermissionHelper @AssistedInject constructor( return true } - private fun isConsentGiven(): Boolean { - val firstTracingActivationAt = LocalData.initialTracingActivationTimestamp() - Timber.tag(TAG).v("isConsentGiven(): First tracing activationat: %d", firstTracingActivationAt) - return firstTracingActivationAt != null - } + private fun isConsentGiven(): Boolean = tracingSettings.isConsentGiven interface Callback { fun onUpdateTracingStatus(isTracingEnabled: Boolean) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt index 208465596fcc7d74c6eaf67970ed7232ac0c3197..e8c1e9f7d7ebfb442c6aa05bb584990db24ac6f6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt @@ -6,7 +6,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureNotificationCl import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.flow.shareLatest import kotlinx.coroutines.CancellationException @@ -30,6 +30,7 @@ import kotlin.coroutines.suspendCoroutine @Singleton class DefaultTracingStatus @Inject constructor( private val client: ExposureNotificationClient, + private val tracingSettings: TracingSettings, @AppScope val scope: CoroutineScope ) : TracingStatus { @@ -72,14 +73,15 @@ class DefaultTracingStatus @Inject constructor( client.start() .addOnSuccessListener { cont.resume(it) } .addOnFailureListener { cont.resumeWithException(it) } + .also { + tracingSettings.isConsentGiven = true + } } private suspend fun asyncStop() = suspendCoroutine<Void> { cont -> client.stop() .addOnSuccessListener { cont.resume(it) } .addOnFailureListener { cont.resumeWithException(it) } - }.also { - LocalData.lastNonActiveTracingTimestamp(System.currentTimeMillis()) } override val isTracingEnabled: Flow<Boolean> = flow { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/ShareTestResultNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/ShareTestResultNotificationService.kt index f18af548a82fda119acb5f6380691e98e73c6d72..ad5dac0ac493edcfc99e8d4ee4edf84f08ddad6b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/ShareTestResultNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/ShareTestResultNotificationService.kt @@ -3,11 +3,11 @@ package de.rki.coronawarnapp.notification import android.content.Context import androidx.navigation.NavDeepLinkBuilder import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_ID import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_INITIAL_OFFSET import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_INTERVAL import de.rki.coronawarnapp.notification.NotificationConstants.POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppContext @@ -17,13 +17,14 @@ import javax.inject.Inject class ShareTestResultNotificationService @Inject constructor( @AppContext private val context: Context, private val timeStamper: TimeStamper, - private val notificationHelper: NotificationHelper + private val notificationHelper: NotificationHelper, + private val cwaSettings: CWASettings ) { fun scheduleSharePositiveTestResultReminder() { - if (LocalData.numberOfRemainingSharePositiveTestResultReminders < 0) { + if (cwaSettings.numberOfRemainingSharePositiveTestResultReminders < 0) { Timber.v("Schedule positive test result notification") - LocalData.numberOfRemainingSharePositiveTestResultReminders = POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT + cwaSettings.numberOfRemainingSharePositiveTestResultReminders = POSITIVE_RESULT_NOTIFICATION_TOTAL_COUNT notificationHelper.scheduleRepeatingNotification( timeStamper.nowUTC.plus(POSITIVE_RESULT_NOTIFICATION_INITIAL_OFFSET), POSITIVE_RESULT_NOTIFICATION_INTERVAL, @@ -35,8 +36,8 @@ class ShareTestResultNotificationService @Inject constructor( } fun showSharePositiveTestResultNotification(notificationId: Int) { - if (LocalData.numberOfRemainingSharePositiveTestResultReminders > 0) { - LocalData.numberOfRemainingSharePositiveTestResultReminders -= 1 + if (cwaSettings.numberOfRemainingSharePositiveTestResultReminders > 0) { + cwaSettings.numberOfRemainingSharePositiveTestResultReminders -= 1 val pendingIntent = NavDeepLinkBuilder(context) .setGraph(R.navigation.nav_graph) .setComponentName(MainActivity::class.java) @@ -61,7 +62,7 @@ class ShareTestResultNotificationService @Inject constructor( fun resetSharePositiveTestResultNotification() { cancelSharePositiveTestResultNotification() - LocalData.numberOfRemainingSharePositiveTestResultReminders = Int.MIN_VALUE + cwaSettings.numberOfRemainingSharePositiveTestResultReminders = Int.MIN_VALUE Timber.v("Positive test result notification counter has been reset") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt index 9a9c3412fce65d9c1f45ae41135afd5335e5a436..9b5995b38cb1e31beb0f151aec5de801d971c95f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt @@ -3,7 +3,7 @@ package de.rki.coronawarnapp.notification import android.content.Context import androidx.navigation.NavDeepLinkBuilder import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppContext @@ -17,13 +17,14 @@ class TestResultAvailableNotificationService @Inject constructor( @AppContext private val context: Context, private val foregroundState: ForegroundState, private val navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder>, - private val notificationHelper: NotificationHelper + private val notificationHelper: NotificationHelper, + private val cwaSettings: CWASettings ) { suspend fun showTestResultAvailableNotification(testResult: TestResult) { if (foregroundState.isInForeground.first()) return - if (!LocalData.isNotificationsTestEnabled) { + if (!cwaSettings.isNotificationsTestEnabled.value) { Timber.i("Don't show test result available notification because user doesn't want to be informed") return } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt index 608c329c993ddcb7a6a263aa2df4f8a81b0814ed..cce89d6506f7ccdb7fb2bc872f11f5a76aa5991e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt @@ -1,35 +1,24 @@ package de.rki.coronawarnapp.playbook -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.worker.BackgroundConstants import de.rki.coronawarnapp.worker.BackgroundWorkScheduler +import javax.inject.Inject +import javax.inject.Singleton import kotlin.random.Random -class BackgroundNoise { - companion object { - @Volatile - private var instance: BackgroundNoise? = null - - fun getInstance(): BackgroundNoise { - return instance ?: synchronized(this) { - instance ?: BackgroundNoise().also { - instance = it - } - } - } - } - +@Singleton +class BackgroundNoise @Inject constructor( + private val submissionSettings: SubmissionSettings, private val playbook: Playbook - get() = AppInjector.component.playbook - +) { fun scheduleDummyPattern() { if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0) BackgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork() } suspend fun foregroundScheduleCheck() { - if (LocalData.isAllowedToSubmitDiagnosisKeys()) { + if (submissionSettings.isAllowedToSubmitKeys) { val chance = Random.nextFloat() * 100 if (chance < DefaultPlaybook.PROBABILITY_TO_EXECUTE_PLAYBOOK_ON_APP_OPEN) { playbook.dummy() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt index a746ab02146edcda3bf436f4f485e7cd93b318ed..8a6fe31799602ee50ff2aabe8522054b497c6d00 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -8,7 +8,8 @@ import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppContext @@ -22,6 +23,7 @@ import kotlinx.coroutines.flow.onEach import timber.log.Timber import javax.inject.Inject +@Suppress("LongParameterList") class RiskLevelChangeDetector @Inject constructor( @AppContext private val context: Context, @AppScope private val appScope: CoroutineScope, @@ -30,7 +32,9 @@ class RiskLevelChangeDetector @Inject constructor( private val notificationManagerCompat: NotificationManagerCompat, private val foregroundState: ForegroundState, private val notificationHelper: NotificationHelper, - private val surveys: Surveys + private val surveys: Surveys, + private val submissionSettings: SubmissionSettings, + private val tracingSettings: TracingSettings ) { fun launch() { @@ -64,7 +68,7 @@ class RiskLevelChangeDetector @Inject constructor( Timber.d("Last state was $oldRiskState and current state is $newRiskState") - if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !LocalData.submissionWasSuccessful()) { + if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !submissionSettings.isSubmissionSuccessful) { Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}") if (!foregroundState.isInForeground.first()) { @@ -80,7 +84,7 @@ class RiskLevelChangeDetector @Inject constructor( } if (oldRiskState == RiskState.INCREASED_RISK && newRiskState == RiskState.LOW_RISK) { - LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true + tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel.update { true } Timber.d("Risk level changed LocalData is updated. Current Risk level is $newRiskState") surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt index 058d658c79fd84cf764a8176046b2baf158f8a8b..0bdfeb13edd4bfc6c51bd722fbc0ebd21d0cdaf5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt @@ -15,7 +15,7 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetec import de.rki.coronawarnapp.risk.RiskLevelResult.FailureReason import de.rki.coronawarnapp.risk.result.AggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -45,6 +45,7 @@ class RiskLevelTask @Inject constructor( private val appConfigProvider: AppConfigProvider, private val riskLevelStorage: RiskLevelStorage, private val keyCacheRepository: KeyCacheRepository, + private val submissionSettings: SubmissionSettings, private val analyticsExposureWindowCollector: AnalyticsExposureWindowCollector ) : Task<DefaultProgress, RiskLevelTaskResult> { @@ -82,7 +83,7 @@ class RiskLevelTask @Inject constructor( Timber.d("The current time is %s", it) } - if (LocalData.isAllowedToSubmitDiagnosisKeys()) { + if (submissionSettings.isAllowedToSubmitKeys) { Timber.i("Positive test result, skip risk calculation") return RiskLevelTaskResult( calculatedAt = nowUTC, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt deleted file mode 100644 index 63733ba49fa6471e67ab037f2835dbcf677a8381..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.rki.coronawarnapp.storage - -import javax.inject.Qualifier - -@Qualifier -@MustBeDocumented -@Retention(AnnotationRetention.RUNTIME) -annotation class EncryptedPreferences 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 deleted file mode 100644 index a5cb1011cb3155c2f2275c19f7a7b6addd8253e4..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ /dev/null @@ -1,448 +0,0 @@ -package de.rki.coronawarnapp.storage - -import android.content.SharedPreferences -import androidx.core.content.edit -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import timber.log.Timber - -/** - * LocalData is responsible for all access to the shared preferences. Each preference is accessible - * by a string which is stored in strings.xml. - * - * @see SharedPreferences - */ -object LocalData { - - private const val PREFERENCE_INTEROPERABILITY_IS_USED_AT_LEAST_ONCE = - "preference_interoperability_is_used_at_least_once" - - private const val PREFERENCE_HAS_RISK_STATUS_LOWERED = - "preference_has_risk_status_lowered" - - /**************************************************** - * ONBOARDING DATA - ****************************************************/ - - /** - * Gets the boolean if the user has completed the onboarding - * from the EncryptedSharedPrefs - * - * @return boolean if user is onboarded - */ - fun isOnboarded(): Boolean = getSharedPreferenceInstance().getBoolean( - CoronaWarnApplication.getAppContext().getString(R.string.preference_onboarding_completed), - false - ) - - /** - * Sets the boolean if the user has completed the onboarding - * from the EncryptedSharedPrefs - * - * @param value boolean if onboarding was completed - */ - fun isOnboarded(value: Boolean) = getSharedPreferenceInstance().edit(true) { - putBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_onboarding_completed), - value - ) - } - - /** - * Gets the time when the user has completed the onboarding - * from the EncryptedSharedPrefs - * - * @return - */ - fun onboardingCompletedTimestamp(): Long? { - val timestamp = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_onboarding_completed_timestamp), - 0L - ) - - if (timestamp == 0L) return null - return timestamp - } - - /** - * Sets the time when the user has completed the onboarding - * from the EncryptedSharedPrefs - * @param value - */ - fun onboardingCompletedTimestamp(value: Long) = getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_onboarding_completed_timestamp), - value - ) - } - - /** - * Gets the boolean if the user has received the background warning - * from the EncryptedSharedPrefs - * - * @return boolean if background warning was shown - */ - fun isBackgroundCheckDone(): Boolean = getSharedPreferenceInstance().getBoolean( - CoronaWarnApplication.getAppContext().getString(R.string.preference_background_check_done), - false - ) - - /** - * Sets the boolean if the user has received the background warning - * from the EncryptedSharedPrefs - * - * @param value boolean if background warning was shown - */ - fun isBackgroundCheckDone(value: Boolean) = getSharedPreferenceInstance().edit(true) { - putBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_background_check_done), - value - ) - } - /**************************************************** - * TRACING DATA - ****************************************************/ - - /** - * Gets the initial timestamp when tracing was activated for the first time in ms - * - * @return timestamp in ms - */ - fun initialTracingActivationTimestamp(): Long? { - val timestamp = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_initial_tracing_activation_time), - 0L - ) - - if (timestamp == 0L) return null - return timestamp - } - - /** - * Sets the initial timestamp when tracing was activated for the first time in ms - * - * @param value timestamp in ms - */ - fun initialTracingActivationTimestamp(value: Long) = - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_initial_tracing_activation_time), - value - ) - } - - /** - * Gets the timestamp when the user stopped Exposure Notification tracing the last time - * from the EncryptedSharedPrefs - * - * @return timestamp in ms - */ - fun lastNonActiveTracingTimestamp(): Long? { - val timestamp = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_last_non_active_tracing_timestamp), - 0L - ) - if (timestamp == 0L) return null - return timestamp - } - - /** - * Sets the timestamp when the user stopped Exposure Notification tracing the last time - * from the EncryptedSharedPrefs - * - * @param value timestamp in ms - */ - fun lastNonActiveTracingTimestamp(value: Long?) = getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext().getString( - R.string.preference_last_non_active_tracing_timestamp - ), - value ?: 0L - ) - } - - /** - * Sets the total amount of time the tracing was not active - * from the EncryptedSharedPrefs - * - * @param value timestamp in ms - */ - fun totalNonActiveTracing(value: Long?) { - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_total_non_active_tracing), - value ?: 0L - ) - } - } - - /** - * Gets the total amount of time the tracing was not active - * from the EncryptedSharedPrefs - * - * @return timestamp in ms - */ - fun totalNonActiveTracing(): Long { - return getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_total_non_active_tracing), - 0L - ) - } - - /** - - * Gets the timestamp when the Background Polling Began initially - * from the EncryptedSharedPrefs - * - * @return timestamp in ms - */ - fun initialPollingForTestResultTimeStamp(): Long { - return getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_polling_test_result_started), - 0L - ) - } - - /** - * Sets the timestamp when the Background Polling Began initially - * from the EncryptedSharedPrefs - * - * @param value timestamp in ms - */ - fun initialPollingForTestResultTimeStamp(value: Long) = - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_polling_test_result_started), - value - ) - } - - /** - - * Gets the flag if notification is executed on Status Change - * from the EncryptedSharedPrefs - * - * @return boolean - */ - fun isTestResultAvailableNotificationSent(): Boolean { - return getSharedPreferenceInstance().getBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_test_result_notification), - false - ) - } - - /** - * Sets the flag if notification is executed on Status Change - * from the EncryptedSharedPrefs - * - * @param value boolean - */ - fun isTestResultAvailableNotificationSent(value: Boolean) = - getSharedPreferenceInstance().edit(true) { - putBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_test_result_notification), - value - ) - } - - /** - * Sets a boolean depending whether the risk level decreased or not. - */ - private val isUserToBeNotifiedOfLoweredRiskLevelFlowInternal by lazy { - MutableStateFlow(isUserToBeNotifiedOfLoweredRiskLevel) - } - val isUserToBeNotifiedOfLoweredRiskLevelFlow: Flow<Boolean> by lazy { - isUserToBeNotifiedOfLoweredRiskLevelFlowInternal - } - var isUserToBeNotifiedOfLoweredRiskLevel: Boolean - get() = getSharedPreferenceInstance().getBoolean( - PREFERENCE_HAS_RISK_STATUS_LOWERED, - false - ) - set(value) = getSharedPreferenceInstance() - .edit(commit = true) { putBoolean(PREFERENCE_HAS_RISK_STATUS_LOWERED, value) } - .also { isUserToBeNotifiedOfLoweredRiskLevelFlowInternal.value = value } - - /**************************************************** - * SETTINGS DATA - ****************************************************/ - - private const val PKEY_NOTIFICATIONS_RISK_ENABLED = "preference_notifications_risk_enabled" - - private val isNotificationsRiskEnabledFlowInternal by lazy { - MutableStateFlow(isNotificationsRiskEnabled) - } - val isNotificationsRiskEnabledFlow: Flow<Boolean> by lazy { - isNotificationsRiskEnabledFlowInternal - } - var isNotificationsRiskEnabled: Boolean - get() = getSharedPreferenceInstance().getBoolean(PKEY_NOTIFICATIONS_RISK_ENABLED, true) - set(value) = getSharedPreferenceInstance().edit(true) { - putBoolean(PKEY_NOTIFICATIONS_RISK_ENABLED, value) - isNotificationsRiskEnabledFlowInternal.value = value - } - - private const val PKEY_NOTIFICATIONS_TEST_ENABLED = "preference_notifications_test_enabled" - private val isNotificationsTestEnabledFlowInternal by lazy { - MutableStateFlow(isNotificationsTestEnabled) - } - val isNotificationsTestEnabledFlow: Flow<Boolean> by lazy { - isNotificationsTestEnabledFlowInternal - } - var isNotificationsTestEnabled: Boolean - get() = getSharedPreferenceInstance().getBoolean(PKEY_NOTIFICATIONS_TEST_ENABLED, true) - set(value) = getSharedPreferenceInstance().edit(true) { - putBoolean(PKEY_NOTIFICATIONS_TEST_ENABLED, value) - isNotificationsTestEnabledFlowInternal.value = value - } - - private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT = "preference_positive_test_result_reminder_count" - var numberOfRemainingSharePositiveTestResultReminders: Int - get() = getSharedPreferenceInstance().getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, Int.MIN_VALUE) - set(value) = getSharedPreferenceInstance().edit(true) { - putInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, value) - } - - /**************************************************** - * SUBMISSION DATA - ****************************************************/ - - private const val PREFERENCE_REGISTRATION_TOKEN = "preference_registration_token" - - /** - * Gets the registration token that is needed for the submission process - * - * @return the registration token - */ - fun registrationToken(): String? = getSharedPreferenceInstance() - .getString(PREFERENCE_REGISTRATION_TOKEN, null) - - /** - * Sets the registration token that is needed for the submission process - * - * @param value registration token as string - */ - fun registrationToken(value: String?) { - getSharedPreferenceInstance().edit(true) { - putString(PREFERENCE_REGISTRATION_TOKEN, value) - } - } - - fun initialTestResultReceivedTimestamp(value: Long) = - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_initial_result_received_time), - value - ) - } - - fun initialTestResultReceivedTimestamp(): Long? { - val timestamp = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_initial_result_received_time), - 0L - ) - - if (timestamp == 0L) return null - return timestamp - } - - fun devicePairingSuccessfulTimestamp(value: Long) = - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_device_pairing_successful_time), - value - ) - } - - fun devicePairingSuccessfulTimestamp(): Long { - return getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_device_pairing_successful_time), - 0L - ) - } - - fun numberOfSuccessfulSubmissions(value: Int) = - getSharedPreferenceInstance().edit(true) { - putInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_number_successful_submissions), - value - ) - } - - private fun numberOfSuccessfulSubmissions(): Int { - return getSharedPreferenceInstance().getInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_number_successful_submissions), - 0 - ) - } - - fun submissionWasSuccessful(): Boolean = numberOfSuccessfulSubmissions() >= 1 - - fun isAllowedToSubmitDiagnosisKeys(isAllowedToSubmitDiagnosisKeys: Boolean) { - getSharedPreferenceInstance().edit(true) { - putBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_is_allowed_to_submit_diagnosis_keys), - isAllowedToSubmitDiagnosisKeys - ) - } - } - - fun isAllowedToSubmitDiagnosisKeys(): Boolean { - return getSharedPreferenceInstance().getBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_is_allowed_to_submit_diagnosis_keys), - false - ) - } - - /**************************************************** - * ENCRYPTED SHARED PREFERENCES HANDLING - ****************************************************/ - - private fun getSharedPreferenceInstance(): SharedPreferences = globalEncryptedSharedPreferencesInstance - - /**************************************************** - * INTEROPERABILITY - ****************************************************/ - - var isInteroperabilityShownAtLeastOnce: Boolean - get() { - return getSharedPreferenceInstance().getBoolean( - PREFERENCE_INTEROPERABILITY_IS_USED_AT_LEAST_ONCE, - false - ) - } - set(value) { - getSharedPreferenceInstance().edit(true) { - putBoolean(PREFERENCE_INTEROPERABILITY_IS_USED_AT_LEAST_ONCE, value) - } - } - - fun clear() { - // If you make use of a FlowPreference, you need to manually clear it here - Timber.w("LocalData.clear()") - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f22c31b71311e5ad2a7b7e08a053151da93f8ae --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/OnboardingSettings.kt @@ -0,0 +1,40 @@ +package de.rki.coronawarnapp.storage + +import android.content.Context +import androidx.core.content.edit +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.clearAndNotify +import org.joda.time.Instant +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class OnboardingSettings @Inject constructor( + @AppContext private val context: Context +) { + private val prefs by lazy { + context.getSharedPreferences("onboarding_localdata", Context.MODE_PRIVATE) + } + + var onboardingCompletedTimestamp: Instant? + get() = prefs.getLong(ONBOARDING_COMPLETED_TIMESTAMP, 0L).let { + if (it != 0L) { + Instant.ofEpochMilli(it) + } else null + } + set(value) = prefs.edit { putLong(ONBOARDING_COMPLETED_TIMESTAMP, value?.millis ?: 0L) } + + val isOnboarded: Boolean + get() = onboardingCompletedTimestamp != null + + var isBackgroundCheckDone: Boolean + get() = prefs.getBoolean(BACKGROUND_CHECK_DONE, false) + set(value) = prefs.edit { putBoolean(BACKGROUND_CHECK_DONE, value) } + + fun clear() = prefs.clearAndNotify() + + companion object { + private const val ONBOARDING_COMPLETED_TIMESTAMP = "onboarding.done.timestamp" + private const val BACKGROUND_CHECK_DONE = "onboarding.background.checked" + } +} 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 197617c59e7036b4798387deadc399e128f48973..49488cc0e03fd3dd3dcb9f3add4fcc5edb6b154d 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 @@ -32,7 +32,6 @@ import javax.inject.Singleton * The Tracing Repository refreshes and triggers all tracing relevant data. Some functions get their * data directly from the Exposure Notification, others consume the shared preferences. * - * @see LocalData * @see InternalExposureNotificationClient */ @Singleton diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..63f4739e96fc9ebde92732f85661316e1d373638 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt @@ -0,0 +1,49 @@ +package de.rki.coronawarnapp.storage + +import android.content.Context +import androidx.core.content.edit +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.clearAndNotify +import de.rki.coronawarnapp.util.preferences.createFlowPreference +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TracingSettings @Inject constructor(@AppContext private val context: Context) { + + private val prefs by lazy { + context.getSharedPreferences("tracing_settings", Context.MODE_PRIVATE) + } + + var isConsentGiven: Boolean + get() = prefs.getBoolean(TRACING_ACTIVATION_TIMESTAMP, false) + set(value) = prefs.edit(true) { + putBoolean(TRACING_ACTIVATION_TIMESTAMP, value) + } + + var initialPollingForTestResultTimeStamp: Long + get() = prefs.getLong(TRACING_POOLING_TIMESTAMP, 0L) + set(value) = prefs.edit(true) { + putLong(TRACING_POOLING_TIMESTAMP, value) + } + + var isTestResultAvailableNotificationSent: Boolean + get() = prefs.getBoolean(TEST_RESULT_NOTIFICATION_SENT, false) + set(value) = prefs.edit(true) { + putBoolean(TEST_RESULT_NOTIFICATION_SENT, value) + } + + val isUserToBeNotifiedOfLoweredRiskLevel = prefs.createFlowPreference( + key = LOWERED_RISK_LEVEL, + defaultValue = false + ) + + fun clear() = prefs.clearAndNotify() + + companion object { + const val TRACING_POOLING_TIMESTAMP = "tracing.pooling.timestamp" + const val TRACING_ACTIVATION_TIMESTAMP = "tracing.activation.timestamp" + const val TEST_RESULT_NOTIFICATION_SENT = "test.notification.sent" + const val LOWERED_RISK_LEVEL = "notification.risk.lowered" + } +} 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 fb07dcbaef0e0ae6af55d59abc0e4393b00b9f8c..b17572eb8e5af1ab8d57d912c1de09249705b49e 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 @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.storage.interoperability import de.rki.coronawarnapp.appconfig.AppConfigProvider -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.ui.Country import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -12,7 +12,8 @@ import javax.inject.Singleton @Singleton class InteroperabilityRepository @Inject constructor( - private val appConfigProvider: AppConfigProvider + private val appConfigProvider: AppConfigProvider, + private val settings: CWASettings ) { val countryList = appConfigProvider.currentConfig @@ -39,6 +40,6 @@ class InteroperabilityRepository @Inject constructor( } fun saveInteroperabilityUsed() { - LocalData.isInteroperabilityShownAtLeastOnce = true + settings.wasInteroperabilityShownAtLeastOnce = true } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/preferences/EncryptedPreferencesHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/preferences/EncryptedPreferencesHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..45b15c4d4b07e2cfc3fe60a6c82221d300dfbca1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/preferences/EncryptedPreferencesHelper.kt @@ -0,0 +1,51 @@ +package de.rki.coronawarnapp.storage.preferences + +import android.content.SharedPreferences +import android.content.pm.ApplicationInfo +import androidx.core.content.edit +import de.rki.coronawarnapp.exception.CwaSecurityException +import de.rki.coronawarnapp.util.security.EncryptedPreferencesFactory +import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool +import de.rki.coronawarnapp.util.security.SecurityConstants +import java.io.File +import javax.inject.Inject + +class EncryptedPreferencesHelper @Inject constructor( + private val applicationInfo: ApplicationInfo, + factory: EncryptedPreferencesFactory, + encryptionErrorResetTool: EncryptionErrorResetTool +) { + + private val encryptedPreferencesFile by lazy { + File(applicationInfo.dataDir) + .resolve("shared_prefs/${SecurityConstants.ENCRYPTED_SHARED_PREFERENCES_FILE}.xml") + } + + val encryptedSharedPreferencesInstance: SharedPreferences? by lazy { + withSecurityCatch { + try { + if (encryptedPreferencesFile.exists()) { + factory.create(SecurityConstants.ENCRYPTED_SHARED_PREFERENCES_FILE) + } else { + null + } + } catch (e: Exception) { + encryptionErrorResetTool.isResetNoticeToBeShown = true + null + } + } + } + + fun clean() { + encryptedSharedPreferencesInstance?.edit(true) { + clear() + } + encryptedPreferencesFile.delete() + } + + private fun <T> withSecurityCatch(doInCatch: () -> T) = try { + doInCatch.invoke() + } catch (e: Exception) { + throw CwaSecurityException(e) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/preferences/EncryptedPreferencesMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/preferences/EncryptedPreferencesMigration.kt new file mode 100644 index 0000000000000000000000000000000000000000..cda45aade8aba1a42b5211b52bbfab33d807f92e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/preferences/EncryptedPreferencesMigration.kt @@ -0,0 +1,167 @@ +package de.rki.coronawarnapp.storage.preferences + +import android.content.Context +import android.content.SharedPreferences +import android.database.sqlite.SQLiteDatabase +import de.rki.coronawarnapp.main.CWASettings +import de.rki.coronawarnapp.storage.OnboardingSettings +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toInstantOrNull +import de.rki.coronawarnapp.util.di.AppContext +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject + +class EncryptedPreferencesMigration @Inject constructor( + @AppContext private val context: Context, + private val encryptedPreferencesHelper: EncryptedPreferencesHelper, + private val cwaSettings: CWASettings, + private val submissionSettings: SubmissionSettings, + private val tracingSettings: TracingSettings, + private val onboardingSettings: OnboardingSettings +) { + + fun doMigration() { + Timber.d("Migration start") + try { + copyData() + cleanData() + } catch (e: Exception) { + Timber.e(e, "Migration was not successful") + } + try { + dropDatabase() + } catch (e: Exception) { + Timber.e(e, "Database removing was not successful") + } + Timber.d("Migration finish") + } + + private fun copyData() { + val encryptedSharedPreferences = encryptedPreferencesHelper.encryptedSharedPreferencesInstance ?: return + Timber.d("EncryptedPreferences are available") + SettingsLocalData(encryptedSharedPreferences).apply { + cwaSettings.wasInteroperabilityShownAtLeastOnce = wasInteroperabilityShown() + cwaSettings.isNotificationsRiskEnabled.update { isNotificationsRiskEnabled() } + cwaSettings.isNotificationsTestEnabled.update { isNotificationsTestEnabled() } + cwaSettings.numberOfRemainingSharePositiveTestResultReminders = + numberOfRemainingSharePositiveTestResultReminders() + } + + OnboardingLocalData(encryptedSharedPreferences).apply { + onboardingSettings.onboardingCompletedTimestamp = onboardingCompletedTimestamp()?.let { + Instant.ofEpochMilli(it) + } + onboardingSettings.isBackgroundCheckDone = isBackgroundCheckDone() + } + + TracingLocalData(encryptedSharedPreferences).apply { + tracingSettings.initialPollingForTestResultTimeStamp = initialPollingForTestResultTimeStamp() + tracingSettings.isTestResultAvailableNotificationSent = isTestResultAvailableNotificationSent() + tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel.update { isUserToBeNotifiedOfLoweredRiskLevel() } + tracingSettings.isConsentGiven = initialTracingActivationTimestamp() != 0L + } + + SubmissionLocalData(encryptedSharedPreferences).apply { + submissionSettings.registrationToken.update { + registrationToken() + } + submissionSettings.initialTestResultReceivedAt = initialTestResultReceivedTimestamp().toInstantOrNull() + submissionSettings.devicePairingSuccessfulAt = devicePairingSuccessfulTimestamp().toInstantOrNull() + submissionSettings.isSubmissionSuccessful = numberOfSuccessfulSubmissions() >= 1 + submissionSettings.isAllowedToSubmitKeys = isAllowedToSubmitDiagnosisKeys() + } + } + + private fun cleanData() { + encryptedPreferencesHelper.clean() + } + + private fun dropDatabase() { + val file = context.getDatabasePath("coronawarnapp-db") + if (file.exists()) { + Timber.d("Removing database $file") + SQLiteDatabase.deleteDatabase(file) + } + } + + private class SettingsLocalData(private val sharedPreferences: SharedPreferences) { + + fun wasInteroperabilityShown() = sharedPreferences.getBoolean(PREFERENCE_INTEROPERABILITY_WAS_USED, false) + + fun isNotificationsRiskEnabled(): Boolean = sharedPreferences.getBoolean(PKEY_NOTIFICATIONS_RISK_ENABLED, true) + + fun isNotificationsTestEnabled(): Boolean = sharedPreferences.getBoolean(PKEY_NOTIFICATIONS_TEST_ENABLED, true) + + fun numberOfRemainingSharePositiveTestResultReminders(): Int = + sharedPreferences.getInt(PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT, Int.MIN_VALUE) + + companion object { + private const val PREFERENCE_INTEROPERABILITY_WAS_USED = "preference_interoperability_is_used_at_least_once" + private const val PKEY_NOTIFICATIONS_RISK_ENABLED = "preference_notifications_risk_enabled" + private const val PKEY_NOTIFICATIONS_TEST_ENABLED = "preference_notifications_test_enabled" + private const val PKEY_POSITIVE_TEST_RESULT_REMINDER_COUNT = + "preference_positive_test_result_reminder_count" + } + } + + private class OnboardingLocalData(private val sharedPreferences: SharedPreferences) { + fun onboardingCompletedTimestamp(): Long? { + val timestamp = sharedPreferences.getLong(PKEY_ONBOARDING_COMPLETED_TIMESTAMP, 0L) + + if (timestamp == 0L) return null + return timestamp + } + + fun isBackgroundCheckDone(): Boolean = sharedPreferences.getBoolean(PKEY_BACKGROUND_CHECK_DONE, false) + + companion object { + private const val PKEY_ONBOARDING_COMPLETED_TIMESTAMP = "preference_onboarding_completed_timestamp" + private const val PKEY_BACKGROUND_CHECK_DONE = "preference_background_check_done" + } + } + + private class TracingLocalData(private val sharedPreferences: SharedPreferences) { + + fun initialPollingForTestResultTimeStamp() = sharedPreferences.getLong(PKEY_POOLING_TEST_RESULT_STARTED, 0L) + + fun isTestResultAvailableNotificationSent() = sharedPreferences.getBoolean(PKEY_TEST_RESULT_NOTIFICATION, false) + + fun isUserToBeNotifiedOfLoweredRiskLevel() = sharedPreferences.getBoolean(PKEY_HAS_RISK_STATUS_LOWERED, false) + + fun initialTracingActivationTimestamp(): Long = sharedPreferences.getLong(PKEY_TRACING_ACTIVATION_TIME, 0L) + + companion object { + private const val PKEY_POOLING_TEST_RESULT_STARTED = "preference_polling_test_result_started" + private const val PKEY_TEST_RESULT_NOTIFICATION = "preference_test_result_notification" + private const val PKEY_HAS_RISK_STATUS_LOWERED = "preference_has_risk_status_lowered" + private const val PKEY_TRACING_ACTIVATION_TIME = "preference_initial_tracing_activation_time" + } + } + + private class SubmissionLocalData(private val sharedPreferences: SharedPreferences) { + fun registrationToken(): String? = sharedPreferences.getString(PKEY_REGISTRATION_TOKEN, null) + + fun initialTestResultReceivedTimestamp(): Long? { + val timestamp = sharedPreferences.getLong(PKEY_INITIAL_RESULT_RECEIVED_TIME, 0L) + + if (timestamp == 0L) return null + return timestamp + } + + fun devicePairingSuccessfulTimestamp(): Long = sharedPreferences.getLong(PKEY_DEVICE_PARING_SUCCESSFUL_TIME, 0L) + + fun numberOfSuccessfulSubmissions(): Int = sharedPreferences.getInt(PKEY_NUMBER_SUCCESSFUL_SUBMISSIONS, 0) + + fun isAllowedToSubmitDiagnosisKeys(): Boolean = sharedPreferences.getBoolean(PKEY_IS_ALLOWED_TO_SUBMIT, false) + + companion object { + private const val PKEY_REGISTRATION_TOKEN = "preference_registration_token" + private const val PKEY_INITIAL_RESULT_RECEIVED_TIME = "preference_initial_result_received_time" + private const val PKEY_DEVICE_PARING_SUCCESSFUL_TIME = "preference_device_pairing_successful_time" + private const val PKEY_NUMBER_SUCCESSFUL_SUBMISSIONS = "preference_number_successful_submissions" + private const val PKEY_IS_ALLOWED_TO_SUBMIT = "preference_is_allowed_to_submit_diagnosis_keys" + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt index 8f496e37159faa663aadcb93e23adf3709b988b2..392fcd548e782a4f8cbd0eef0e9ee28c12e931fb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt @@ -9,7 +9,7 @@ import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.service.submission.SubmissionService -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.NetworkRequestWrapper @@ -35,10 +35,12 @@ class SubmissionRepository @Inject constructor( private val timeStamper: TimeStamper, private val tekHistoryStorage: TEKHistoryStorage, private val deadmanNotificationScheduler: DeadmanNotificationScheduler, - private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + private val backgroundNoise: BackgroundNoise, + private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, + private val tracingSettings: TracingSettings ) { private val testResultReceivedDateFlowInternal = - MutableStateFlow(Date(LocalData.initialTestResultReceivedTimestamp() ?: System.currentTimeMillis())) + MutableStateFlow((submissionSettings.initialTestResultReceivedAt ?: timeStamper.nowUTC).toDate()) val testResultReceivedDateFlow: Flow<Date> = testResultReceivedDateFlowInternal private val deviceUIStateFlowInternal = @@ -77,18 +79,18 @@ class SubmissionRepository @Inject constructor( } fun refreshDeviceUIState(refreshTestResult: Boolean = true) { - if (LocalData.submissionWasSuccessful()) { + if (submissionSettings.isSubmissionSuccessful) { deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) return } - val registrationToken = LocalData.registrationToken() + val registrationToken = submissionSettings.registrationToken.value if (registrationToken == null) { deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) return } - if (LocalData.isAllowedToSubmitDiagnosisKeys()) { + if (submissionSettings.isAllowedToSubmitKeys) { deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE) return } @@ -123,20 +125,24 @@ class SubmissionRepository @Inject constructor( suspend fun asyncRegisterDeviceViaTAN(tan: String) { val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan) - LocalData.registrationToken(registrationData.registrationToken) + submissionSettings.registrationToken.update { + registrationData.registrationToken + } updateTestResult(registrationData.testResult) - LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) - BackgroundNoise.getInstance().scheduleDummyPattern() + submissionSettings.devicePairingSuccessfulAt = timeStamper.nowUTC + backgroundNoise.scheduleDummyPattern() analyticsKeySubmissionCollector.reportTestRegistered() analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() } suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid) - LocalData.registrationToken(registrationData.registrationToken) + submissionSettings.registrationToken.update { + registrationData.registrationToken + } updateTestResult(registrationData.testResult) - LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) - BackgroundNoise.getInstance().scheduleDummyPattern() + submissionSettings.devicePairingSuccessfulAt = timeStamper.nowUTC + backgroundNoise.scheduleDummyPattern() analyticsKeySubmissionCollector.reportTestRegistered() return registrationData.testResult } @@ -152,22 +158,22 @@ class SubmissionRepository @Inject constructor( testResultFlow.value = testResult if (testResult == TestResult.POSITIVE) { - LocalData.isAllowedToSubmitDiagnosisKeys(true) + submissionSettings.isAllowedToSubmitKeys = true analyticsKeySubmissionCollector.reportPositiveTestResultReceived() deadmanNotificationScheduler.cancelScheduledWork() } - val initialTestResultReceivedTimestamp = LocalData.initialTestResultReceivedTimestamp() + val initialTestResultReceivedTimestamp = submissionSettings.initialTestResultReceivedAt if (initialTestResultReceivedTimestamp == null) { - val currentTime = System.currentTimeMillis() - LocalData.initialTestResultReceivedTimestamp(currentTime) - testResultReceivedDateFlowInternal.value = Date(currentTime) + val currentTime = timeStamper.nowUTC + submissionSettings.initialTestResultReceivedAt = currentTime + testResultReceivedDateFlowInternal.value = currentTime.toDate() if (testResult == TestResult.PENDING) { BackgroundWorkScheduler.startWorkScheduler() } } else { - testResultReceivedDateFlowInternal.value = Date(initialTestResultReceivedTimestamp) + testResultReceivedDateFlowInternal.value = initialTestResultReceivedTimestamp.toDate() } } @@ -184,13 +190,13 @@ class SubmissionRepository @Inject constructor( submissionSettings.hasGivenConsent.update { false } analyticsKeySubmissionCollector.reset() revokeConsentToSubmission() - LocalData.registrationToken(null) - LocalData.devicePairingSuccessfulTimestamp(0L) - LocalData.initialPollingForTestResultTimeStamp(0L) - LocalData.initialTestResultReceivedTimestamp(0L) - LocalData.isAllowedToSubmitDiagnosisKeys(false) - LocalData.isTestResultAvailableNotificationSent(false) - LocalData.numberOfSuccessfulSubmissions(0) + submissionSettings.registrationToken.update { null } + submissionSettings.devicePairingSuccessfulAt = null + tracingSettings.initialPollingForTestResultTimeStamp = 0L + submissionSettings.initialTestResultReceivedAt = null + submissionSettings.isAllowedToSubmitKeys = false + tracingSettings.isTestResultAvailableNotificationSent = false + submissionSettings.isSubmissionSuccessful = false } private fun deriveUiState(testResult: TestResult?): DeviceUIState = when (testResult) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionSettings.kt index 486111f0e4c7cc6dc21e1cd88726451e93839cdd..1b362ef1a3eb843e762cba992a45ea33a5557c7f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionSettings.kt @@ -1,7 +1,9 @@ package de.rki.coronawarnapp.submission import android.content.Context +import androidx.core.content.edit import com.google.gson.Gson +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toInstantOrNull import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.preferences.FlowPreference import de.rki.coronawarnapp.util.preferences.clearAndNotify @@ -35,25 +37,46 @@ class SubmissionSettings @Inject constructor( context.getSharedPreferences("submission_localdata", Context.MODE_PRIVATE) } + val registrationToken = prefs.createFlowPreference<String?>( + key = TEST_REGISTRATION_TOKEN, + defaultValue = null + ) + + var initialTestResultReceivedAt: Instant? + get() = prefs.getLong(TEST_RESULT_RECEIVED_AT, 0L).toInstantOrNull() + set(value) = prefs.edit { putLong(TEST_RESULT_RECEIVED_AT, value?.millis ?: 0L) } + + var devicePairingSuccessfulAt: Instant? + get() = prefs.getLong(TEST_PARING_SUCCESSFUL_AT, 0L).toInstantOrNull() + set(value) = prefs.edit { putLong(TEST_PARING_SUCCESSFUL_AT, value?.millis ?: 0L) } + + var isSubmissionSuccessful: Boolean + get() = prefs.getBoolean(IS_KEY_SUBMISSION_SUCCESSFUL, false) + set(value) = prefs.edit { putBoolean(IS_KEY_SUBMISSION_SUCCESSFUL, value) } + + var isAllowedToSubmitKeys: Boolean + get() = prefs.getBoolean(IS_KEY_SUBMISSION_ALLOWED, false) + set(value) = prefs.edit { putBoolean(IS_KEY_SUBMISSION_ALLOWED, value) } + val hasGivenConsent = prefs.createFlowPreference( - key = "key_submission_consent", + key = SUBMISSION_CONSENT_GIVEN, defaultValue = false ) val hasViewedTestResult = prefs.createFlowPreference( - key = "key_submission_result_viewed", + key = SUBMISSION_RESULT_VIEWED, defaultValue = false ) val symptoms: FlowPreference<Symptoms?> = FlowPreference( prefs, - key = "submission.symptoms.latest", + key = SUBMISSION_SYMPTOMS_LATEST, reader = FlowPreference.gsonReader<Symptoms?>(gson, null), writer = FlowPreference.gsonWriter(gson) ) val lastSubmissionUserActivityUTC = prefs.createFlowPreference( - key = "submission.user.activity.last", + key = AUTO_SUBMISSION_LAST_USER_ACTIVITY, reader = { key -> Instant.ofEpochMilli(getLong(key, 0L)) }, @@ -63,17 +86,17 @@ class SubmissionSettings @Inject constructor( ) val autoSubmissionEnabled = prefs.createFlowPreference( - key = "submission.auto.enabled", + key = AUTO_SUBMISSION_ENABLED, defaultValue = false ) val autoSubmissionAttemptsCount = prefs.createFlowPreference( - key = "submission.auto.attempts.count", + key = AUTO_SUBMISSION_ATTEMPT_COUNT, defaultValue = 0 ) val autoSubmissionAttemptsLast = prefs.createFlowPreference( - key = "submission.auto.attempts.last", + key = AUTO_SUBMISSION_LAST_ATTEMPT, reader = { key -> Instant.ofEpochMilli(getLong(key, 0L)) }, @@ -82,7 +105,20 @@ class SubmissionSettings @Inject constructor( } ) - fun clear() { - prefs.clearAndNotify() + fun clear() = prefs.clearAndNotify() + + companion object { + private const val TEST_REGISTRATION_TOKEN = "submission.test.token" + private const val TEST_RESULT_RECEIVED_AT = "submission.test.result.receivedAt" + private const val TEST_PARING_SUCCESSFUL_AT = "submission.test.pairedAt" + private const val IS_KEY_SUBMISSION_ALLOWED = "submission.allowed" + private const val IS_KEY_SUBMISSION_SUCCESSFUL = "submission.successful" + private const val SUBMISSION_CONSENT_GIVEN = "key_submission_consent" + private const val SUBMISSION_RESULT_VIEWED = "key_submission_result_viewed" + private const val SUBMISSION_SYMPTOMS_LATEST = "submission.symptoms.latest" + private const val AUTO_SUBMISSION_LAST_USER_ACTIVITY = "submission.user.activity.last" + private const val AUTO_SUBMISSION_ENABLED = "submission.auto.enabled" + private const val AUTO_SUBMISSION_ATTEMPT_COUNT = "submission.auto.attempts.count" + private const val AUTO_SUBMISSION_LAST_ATTEMPT = "submission.auto.attempts.last" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index 2bd1faa295558ef435ff124e78fc28ac8ea8c828..8232893d4888ca535c04abee92c7f978bfeb015c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt @@ -7,7 +7,6 @@ import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.submission.auto.AutoSubmission @@ -119,7 +118,7 @@ class SubmissionTask @Inject constructor( } private suspend fun performSubmission(): Result { - val registrationToken = LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() + val registrationToken = submissionSettings.registrationToken.value ?: throw NoRegistrationTokenSetException() Timber.tag(TAG).d("Using registrationToken=$registrationToken") val keys: List<TemporaryExposureKey> = try { @@ -167,7 +166,7 @@ class SubmissionTask @Inject constructor( private fun setSubmissionFinished() { Timber.tag(TAG).d("setSubmissionFinished()") BackgroundWorkScheduler.stopWorkScheduler() - LocalData.numberOfSuccessfulSubmissions(1) + submissionSettings.isSubmissionSuccessful = true BackgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt index add526c6fc104d3731060df0bcd13176a2222b29..d94e448b7c7e0789219c48b7f1344edf27a82801 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt @@ -2,8 +2,8 @@ package de.rki.coronawarnapp.submission.ui.homecards import dagger.Reusable import de.rki.coronawarnapp.exception.http.CwaServerError -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.NetworkRequestWrapper @@ -18,18 +18,20 @@ import javax.inject.Inject @Reusable class SubmissionStateProvider @Inject constructor( - submissionRepository: SubmissionRepository + submissionRepository: SubmissionRepository, + submissionSettings: SubmissionSettings ) { val state: Flow<SubmissionState> = combine( submissionRepository.deviceUIStateFlow, submissionRepository.hasViewedTestResult, - submissionRepository.testResultReceivedDateFlow - ) { uiState, hasTestBeenSeen, testRegistrationDate -> + submissionRepository.testResultReceivedDateFlow, + submissionSettings.registrationToken.flow + ) { uiState, hasTestBeenSeen, testRegistrationDate, registrationToken -> val eval = Evaluation( deviceUiState = uiState, - isDeviceRegistered = LocalData.registrationToken() != null, + isDeviceRegistered = registrationToken != null, hasTestResultBeenSeen = hasTestBeenSeen ) Timber.d("eval: %s", eval) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt index acd7d63e6dc7763ab70d3da95733c8f1e61afded..5f83d8ea49767018da6f7c7a84879ec3296a7dde 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModel.kt @@ -4,7 +4,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.update.UpdateChecker import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -15,6 +15,7 @@ class LauncherActivityViewModel @AssistedInject constructor( private val updateChecker: UpdateChecker, dispatcherProvider: DispatcherProvider, private val cwaSettings: CWASettings, + private val onboardingSettings: OnboardingSettings ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val events = SingleLiveEvent<LauncherEvent>() @@ -31,7 +32,7 @@ class LauncherActivityViewModel @AssistedInject constructor( } private fun isJustInstalledOrUpdated() = - !LocalData.isOnboarded() || !LocalData.isInteroperabilityShownAtLeastOnce || + !onboardingSettings.isOnboarded || !cwaSettings.wasInteroperabilityShownAtLeastOnce || cwaSettings.lastChangelogVersion.value < BuildConfigWrap.VERSION_CODE @AssistedFactory diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index f6984c21e47c7cae1c08ef02749d3d15df21926b..545557ddd88760054a96560d618d8db498bc01f7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -22,7 +22,7 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.ContactDiaryOverviewFragmen import de.rki.coronawarnapp.databinding.ActivityMainBinding import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.ui.base.startActivitySafely import de.rki.coronawarnapp.ui.setupWithNavController2 import de.rki.coronawarnapp.util.AppShortcuts @@ -84,6 +84,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { @Inject lateinit var deadmanScheduler: DeadmanNotificationScheduler @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler @Inject lateinit var dataDonationAnalyticsScheduler: DataDonationAnalyticsScheduler + @Inject lateinit var submissionSettings: SubmissionSettings override fun onCreate(savedInstanceState: Bundle?) { AppInjector.setup(this) @@ -163,7 +164,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { vm.doBackgroundNoiseCheck() contactDiaryWorkScheduler.schedulePeriodic() dataDonationAnalyticsScheduler.schedulePeriodic() - if (!LocalData.isAllowedToSubmitDiagnosisKeys()) { + if (!submissionSettings.isAllowedToSubmitKeys) { deadmanScheduler.schedulePeriodic() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt index 7db7bcbe8087531746be32bc27e7c5473de74daa..028197ca86763fa9b9bcfe559620172fc7f959e6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt @@ -7,7 +7,7 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.playbook.BackgroundNoise -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.device.BackgroundModeStatus @@ -20,7 +20,9 @@ class MainActivityViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val environmentSetup: EnvironmentSetup, private val backgroundModeStatus: BackgroundModeStatus, - private val contactDiarySettings: ContactDiarySettings + private val contactDiarySettings: ContactDiarySettings, + private val backgroundNoise: BackgroundNoise, + private val onboardingSettings: OnboardingSettings ) : CWAViewModel( dispatcherProvider = dispatcherProvider ) { @@ -43,8 +45,8 @@ class MainActivityViewModel @AssistedInject constructor( } launch { - if (!LocalData.isBackgroundCheckDone()) { - LocalData.isBackgroundCheckDone(true) + if (!onboardingSettings.isBackgroundCheckDone) { + onboardingSettings.isBackgroundCheckDone = true if (backgroundModeStatus.isBackgroundRestricted.first()) { showBackgroundJobDisabledNotification.postValue(Unit) } else { @@ -56,7 +58,7 @@ class MainActivityViewModel @AssistedInject constructor( fun doBackgroundNoiseCheck() { launch { - BackgroundNoise.getInstance().foregroundScheduleCheck() + backgroundNoise.foregroundScheduleCheck() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index f10170180f96824889fb921897e7607b463acf56..0c72da282baddaed96138f6756ab8620763cc3b6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -11,8 +11,8 @@ import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.statistics.source.StatisticsProvider import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.TracingRepository +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.ui.homecards.FetchingResult import de.rki.coronawarnapp.submission.ui.homecards.NoTest @@ -79,7 +79,8 @@ class HomeFragmentViewModel @AssistedInject constructor( appConfigProvider: AppConfigProvider, statisticsProvider: StatisticsProvider, private val deadmanNotificationScheduler: DeadmanNotificationScheduler, - private val appShortcutsHelper: AppShortcutsHelper + private val appShortcutsHelper: AppShortcutsHelper, + private val tracingSettings: TracingSettings, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { private val tracingStateProvider by lazy { tracingStateProviderFactory.create(isDetailsMode = false) } @@ -262,8 +263,9 @@ class HomeFragmentViewModel @AssistedInject constructor( // TODO only lazy to keep tests going which would break because of LocalData access val showLoweredRiskLevelDialog: LiveData<Boolean> by lazy { - LocalData - .isUserToBeNotifiedOfLoweredRiskLevelFlow + tracingSettings + .isUserToBeNotifiedOfLoweredRiskLevel + .flow .map { shouldBeNotified -> val shouldBeShown = shouldBeNotified && !isLoweredRiskLevelDialogBeingShown if (shouldBeShown) { @@ -302,7 +304,7 @@ class HomeFragmentViewModel @AssistedInject constructor( fun userHasAcknowledgedTheLoweredRiskLevel() { isLoweredRiskLevelDialogBeingShown = false - LocalData.isUserToBeNotifiedOfLoweredRiskLevel = false + tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel.update { false } } fun userHasAcknowledgedIncorrectDeviceTime() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt index fe8a9252ff56112046101f42edf4e0a5c6ff8861..3888efc6398c11c741d3fc29bc6d796e1c36254e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingActivity.kt @@ -13,16 +13,16 @@ import dagger.android.HasAndroidInjector import de.rki.coronawarnapp.R import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.AppShortcuts +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppInjector import javax.inject.Inject /** * This activity holds all the onboarding fragments and isn't used after a successful onboarding flow. * - * @see LocalData */ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInjector { companion object { @@ -48,6 +48,8 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector @Inject lateinit var settings: CWASettings + @Inject lateinit var onboardingSettings: OnboardingSettings + @Inject lateinit var timeStamper: TimeStamper private val FragmentManager.currentNavigationFragment: Fragment? get() = primaryNavigationFragment?.childFragmentManager?.fragments?.first() @@ -71,9 +73,12 @@ class OnboardingActivity : AppCompatActivity(), LifecycleObserver, HasAndroidInj } fun completeOnboarding() { - LocalData.isOnboarded(true) - LocalData.onboardingCompletedTimestamp(System.currentTimeMillis()) - settings.lastChangelogVersion.update { BuildConfigWrap.VERSION_CODE } + onboardingSettings.onboardingCompletedTimestamp = timeStamper.nowUTC + + settings.lastChangelogVersion.update { + BuildConfigWrap.VERSION_CODE + } + MainActivity.start(this) finish() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingViewModel.kt index 57dbcfc194b293b008651726c8c4dc7124f45fc5..487f4be42bc2c8068136fcb0b79a05f51b03586b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingLoadingViewModel.kt @@ -4,21 +4,24 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -class OnboardingLoadingViewModel @AssistedInject constructor(private val cwaSettings: CWASettings) : CWAViewModel() { +class OnboardingLoadingViewModel @AssistedInject constructor( + private val cwaSettings: CWASettings, + private val onboardingSettings: OnboardingSettings +) : CWAViewModel() { val navigationEvents = SingleLiveEvent<OnboardingFragmentEvents>() fun navigate() { when { - !LocalData.isOnboarded() -> { + !onboardingSettings.isOnboarded -> { navigationEvents.postValue(OnboardingFragmentEvents.ShowOnboarding) } - !LocalData.isInteroperabilityShownAtLeastOnce -> { + !cwaSettings.wasInteroperabilityShownAtLeastOnce -> { navigationEvents.postValue(OnboardingFragmentEvents.ShowInteropDeltaOnboarding) } cwaSettings.lastChangelogVersion.value < BuildConfigWrap.VERSION_CODE -> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt index 844e900278f2dd137c08cddb441dd38f1f96ff03..dca5b110f9d065b6af5a29728cbeb3ae83dfa58b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt @@ -9,7 +9,7 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.TracingPermissionHelper -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -20,7 +20,8 @@ import timber.log.Timber class OnboardingTracingFragmentViewModel @AssistedInject constructor( private val interoperabilityRepository: InteroperabilityRepository, tracingPermissionHelperFactory: TracingPermissionHelper.Factory, - dispatcherProvider: DispatcherProvider + dispatcherProvider: DispatcherProvider, + private val tracingSettings: TracingSettings, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val countryList = interoperabilityRepository.countryList @@ -62,8 +63,7 @@ class OnboardingTracingFragmentViewModel @AssistedInject constructor( try { if (InternalExposureNotificationClient.asyncIsEnabled()) { InternalExposureNotificationClient.asyncStop() - // Reset initial activation timestamp - LocalData.initialTracingActivationTimestamp(0L) + tracingSettings.isConsentGiven = false } } catch (exception: Exception) { exception.report( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/notifications/NotificationSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/notifications/NotificationSettings.kt index 3388bef869673426fcd8a42689d568f25fbfad09..6ecc209f9e0f46a21ee7645c350f5d6bef2248d3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/notifications/NotificationSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/notifications/NotificationSettings.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.ui.settings.notifications import androidx.core.app.NotificationManagerCompat -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.util.device.ForegroundState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -11,6 +11,7 @@ import javax.inject.Singleton @Singleton class NotificationSettings @Inject constructor( foregroundState: ForegroundState, + private val cwaSettings: CWASettings, private val notificationManagerCompat: NotificationManagerCompat ) { @@ -20,25 +21,23 @@ class NotificationSettings @Inject constructor( notificationManagerCompat.areNotificationsEnabled() } - val isNotificationsRiskEnabled: Flow<Boolean> = LocalData.isNotificationsRiskEnabledFlow + val isNotificationsRiskEnabled: Flow<Boolean> = cwaSettings.isNotificationsRiskEnabled.flow /** * Toggle notifications risk updates. * - * @see LocalData */ fun toggleNotificationsRiskEnabled() { - LocalData.isNotificationsRiskEnabled = !LocalData.isNotificationsRiskEnabled + cwaSettings.isNotificationsRiskEnabled.update { !it } } - val isNotificationsTestEnabled: Flow<Boolean> = LocalData.isNotificationsTestEnabledFlow + val isNotificationsTestEnabled: Flow<Boolean> = cwaSettings.isNotificationsTestEnabled.flow /** * Toggle notifications for test updates in shared preferences and refresh it afterwards. * - * @see LocalData */ fun toggleNotificationsTestEnabled() { - LocalData.isNotificationsTestEnabled = !LocalData.isNotificationsTestEnabled + cwaSettings.isNotificationsTestEnabled.update { !it } } } 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 d78661d2b7a2a4ac2d2152c02ade522a4c19440c..08eec626f20d6005b8aec183c6fa79f9a2fc0a7c 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 @@ -15,10 +15,11 @@ import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.statistics.source.StatisticsProvider -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.security.SecurityHelper import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -45,7 +46,10 @@ class DataReset @Inject constructor( private val surveySettings: SurveySettings, private val analyticsSettings: AnalyticsSettings, private val analytics: Analytics, - private val bugReportingSettings: BugReportingSettings + private val bugReportingSettings: BugReportingSettings, + private val tracingSettings: TracingSettings, + private val onboardingSettings: OnboardingSettings, + private val submissionSettings: SubmissionSettings ) { private val mutex = Mutex() @@ -57,11 +61,6 @@ class DataReset @Inject constructor( @SuppressLint("ApplySharedPref") // We need a commit here to ensure consistency suspend fun clearAllLocalData() = mutex.withLock { Timber.w("CWA LOCAL DATA DELETION INITIATED.") - // Because LocalData does not behave like a normal shared preference - LocalData.clear() - // Shared Preferences Reset - SecurityHelper.resetSharedPrefs() - // Triggers deletion of all analytics contributed data analytics.setAnalyticsEnabled(false) @@ -76,6 +75,9 @@ class DataReset @Inject constructor( cwaSettings.clear() surveySettings.clear() analyticsSettings.clear() + tracingSettings.clear() + onboardingSettings.clear() + submissionSettings.clear() // Clear contact diary database contactDiaryRepository.clear() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/RetryMechanism.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/RetryMechanism.kt index ba2a372b51a99e49b2fdd04881f0d47d2b99f2c6..474bd237cdda0f3d3ae0a2ff9c284c6e3ef04780 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/RetryMechanism.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/RetryMechanism.kt @@ -40,7 +40,7 @@ object RetryMechanism { } } - private const val DEFAULT_TOTAL_MAX_RETRY = 15 * 1000L // 15 seconds total delay + private const val DEFAULT_TOTAL_MAX_RETRY = 7 * 1000L // 7 seconds total delay private const val DEFAULT_MAX_DELAY = 3 * 1000L // 3 seconds max between retries private const val DEFAULT_MIN_DELAY = 25L // Almost immediate retry private const val DEFAULT_RETRY_MULTIPLIER = 1.5 diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index 0a8833e9d86b937a49eab52c690c9e6a276cad6a..cfb9c9fca4ede8f16ae9bd4bf76ac2bfffde1c9c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -55,6 +55,14 @@ object TimeAndDateExtensions { } } + /** + * Converts a Long to Instant or null if the long is 0 or null + */ + fun Long?.toInstantOrNull(): Instant? = + if (this != null && this != 0L) { + Instant.ofEpochMilli(this) + } else null + /** * Converts milliseconds to human readable format hh:mm:ss * diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt index 3365de8462647f20759cd2856b946090ccac19e2..6ead4ba58ab9d791885e0f583891dcad02328f09 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt @@ -6,7 +6,7 @@ import android.os.PowerManager import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking @@ -27,7 +27,8 @@ class WatchdogService @Inject constructor( @AppContext private val context: Context, private val taskController: TaskController, private val backgroundModeStatus: BackgroundModeStatus, - @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner + @ProcessLifecycle private val processLifecycleOwner: LifecycleOwner, + private val onboardingSettings: OnboardingSettings ) { private val powerManager by lazy { @@ -73,7 +74,9 @@ class WatchdogService @Inject constructor( // if the user is onboarded we will schedule period background jobs // in case the app was force stopped and woken up again by the Google WakeUpService - if (LocalData.onboardingCompletedTimestamp() != null) BackgroundWorkScheduler.startWorkScheduler() + if (onboardingSettings.isOnboarded) { + BackgroundWorkScheduler.startWorkScheduler() + } } private fun createWakeLock(): PowerManager.WakeLock = powerManager diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt index d548e33cdd1043f678b41558e4d97da5c38a2a43..813ad1b51f51fff8377f658371239fc01d278f6f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt @@ -6,7 +6,7 @@ import android.app.NotificationManager import android.bluetooth.BluetoothAdapter import android.content.ContentResolver import android.content.Context -import android.content.SharedPreferences +import android.content.pm.ApplicationInfo import androidx.core.app.NotificationManagerCompat import androidx.core.content.getSystemService import androidx.lifecycle.LifecycleOwner @@ -18,8 +18,6 @@ import com.google.android.gms.safetynet.SafetyNetClient import dagger.Module import dagger.Provides import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.storage.EncryptedPreferences -import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.worker.WorkManagerProvider import javax.inject.Singleton @@ -57,11 +55,6 @@ class AndroidModule { workManagerProvider: WorkManagerProvider ): WorkManager = workManagerProvider.workManager - @EncryptedPreferences - @Provides - @Singleton - fun encryptedPreferences(): SharedPreferences = SecurityHelper.globalEncryptedSharedPreferencesInstance - @Provides fun navDeepLinkBuilder(@AppContext context: Context): NavDeepLinkBuilder = NavDeepLinkBuilder(context) @@ -80,4 +73,7 @@ class AndroidModule { @Provides fun contentResolver(@AppContext context: Context): ContentResolver = context.contentResolver + + @Provides + fun applicationInfo(@AppContext context: Context): ApplicationInfo = context.applicationInfo } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt index da4d4e081a5b64cb8664754d5b950c536c7513f6..35ee578c350d1feeaaaf43f1a4a3e9852c896171 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt @@ -41,6 +41,7 @@ import de.rki.coronawarnapp.util.security.SecurityModule import de.rki.coronawarnapp.util.serialization.SerializationModule import de.rki.coronawarnapp.util.worker.WorkerBinder import de.rki.coronawarnapp.verification.VerificationModule +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import javax.inject.Singleton @Singleton @@ -84,6 +85,7 @@ interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { val enfClient: ENFClient val encryptedPreferencesFactory: EncryptedPreferencesFactory + val errorResetTool: EncryptionErrorResetTool val playbook: Playbook @@ -96,6 +98,8 @@ interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { fun inject(logger: DebugLogger) + fun inject(backgroundWorkScheduler: BackgroundWorkScheduler) + @Component.Factory interface Factory { fun create(@BindsInstance app: CoronaWarnApplication): ApplicationComponent diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt index ae4f6a47b04c194e57d4a36a28ed30a3995579e5..fd87b8a0ab525bacf7f0e9640088e7e87aedc36d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptionErrorResetTool.kt @@ -3,127 +3,25 @@ package de.rki.coronawarnapp.util.security import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.errors.causes -import org.joda.time.Instant -import timber.log.Timber -import java.io.File -import java.security.GeneralSecurityException import javax.inject.Inject import javax.inject.Singleton -/** - * This tool determines the narrow scope for which we will recovery from an encryption error - * by resetting our encrypted data. - * This will allow users currently affected by it, that update the app, to keep using it without - * requiring any manual actions from their side. - * - * https://github.com/corona-warn-app/cwa-app-android/issues/642 - */ @Singleton class EncryptionErrorResetTool @Inject constructor( - @AppContext private val context: Context, - private val timeStamper: TimeStamper + @AppContext private val context: Context ) { - // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ContextImpl.java;drc=3b8e8d76315f6718a982d5e6a019b3aa4f634bcd;l=626 - private val encryptedPreferencesFile by lazy { - val appbaseDir = context.filesDir.parentFile!! - val sharedPrefsDir = File(appbaseDir, "shared_prefs") - File(sharedPrefsDir, "${SecurityConstants.ENCRYPTED_SHARED_PREFERENCES_FILE}.xml") - } private val prefs: SharedPreferences by lazy { context.getSharedPreferences("encryption_error_reset_tool", Context.MODE_PRIVATE) } - private var isResetWindowConsumed: Boolean - get() = prefs.getBoolean(PKEY_EA1851_WAS_WINDOW_CONSUMED, false) - set(value) = prefs.edit { - putBoolean(PKEY_EA1851_WAS_WINDOW_CONSUMED, value) - } - - private var resetPerformedAt: Instant? - get() = prefs.getLong(PKEY_EA1851_RESET_PERFORMED_AT, -1).let { - if (it == -1L) null else Instant.ofEpochMilli(it) - } - set(value) = prefs.edit { - if (value != null) { - putLong(PKEY_EA1851_RESET_PERFORMED_AT, value.millis) - } else { - remove(PKEY_EA1851_RESET_PERFORMED_AT) - } - } - var isResetNoticeToBeShown: Boolean get() = prefs.getBoolean(PKEY_EA1851_SHOW_RESET_NOTICE, false) set(value) = prefs.edit { putBoolean(PKEY_EA1851_SHOW_RESET_NOTICE, value) } - fun tryResetIfNecessary(error: Throwable): Boolean { - Timber.w(error, "tryResetIfNecessary()") - - // We only try to reset once, if the error reoccurs, on-going resets is not the solution. - if (isResetWindowConsumed) { - Timber.v("Reset window has been consumed -> no reset.") - return false - } - isResetWindowConsumed = true - - val keyException = error.causes().lastOrNull() - if (keyException == null || keyException !is GeneralSecurityException) { - Timber.v("Error has no GeneralSecurityException as cause -> no reset.") - return false - } - - // https://github.com/google/tink/blob/a8ec74d083068cd5e1ebed86fd8254630617b592/java_src/src/main/java/com/google/crypto/tink/aead/AeadWrapper.java#L83 - if (keyException.message?.trim()?.equals("decryption failed") != true) { - Timber.v("Not the GeneralSecurityException we are looking for -> no reset.") - return false - } - - if (!encryptedPreferencesFile.exists()) { - // The error we are looking for can only happen if there already is an encrypted preferences file - Timber.w( - "Error fits, but where is the existing preference file (%s)? -> no reset.", - encryptedPreferencesFile - ) - return false - } - - // So we have a GeneralSecurityException("decryption failed") and existing preferences - // And this is the first error we encountered after upgrading to 1.4.0, let's do this! - - return try { - performReset() - } catch (e: Exception) { - // If anything goes wrong, we return false, so that our caller can rethrow the original error. - Timber.e(e, "Error while performing the reset.") - false - } - } - - private fun performReset(): Boolean { - // Delete encrypted shared preferences file - if (!encryptedPreferencesFile.delete()) { - Timber.w("Couldn't delete %s", encryptedPreferencesFile) - // The encrypted preferences are a prerequisite for this error case - // If we can't delete that, we have to assume our reset approach failed. - return false - } - - resetPerformedAt = timeStamper.nowUTC - isResetNoticeToBeShown = true - - return true - } - companion object { - private const val PKEY_EA1851_RESET_PERFORMED_AT = "ea1851.reset.performedAt" - - @Suppress("unused") - private const val PKEY_EA1851_WAS_WINDOW_CONSUMED_OLD = "ea1851.reset.windowconsumed" - private const val PKEY_EA1851_WAS_WINDOW_CONSUMED = "ea1851.reset.windowconsumed.160" private const val PKEY_EA1851_SHOW_RESET_NOTICE = "ea1851.reset.shownotice" } } 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 deleted file mode 100644 index cad222cb23d445517cd1760ef0762b155740f74a..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt +++ /dev/null @@ -1,57 +0,0 @@ -package de.rki.coronawarnapp.util.security - -import android.annotation.SuppressLint -import android.content.SharedPreferences -import android.util.Base64 -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.ENCRYPTED_SHARED_PREFERENCES_FILE -import timber.log.Timber - -/** - * Key Store and Password Access - */ -object SecurityHelper { - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal val encryptedPreferencesProvider: (ApplicationComponent) -> SharedPreferences = { - val factory = it.encryptedPreferencesFactory - val encryptionErrorResetTool = it.errorResetTool - withSecurityCatch { - try { - factory.create(ENCRYPTED_SHARED_PREFERENCES_FILE) - } catch (e: Exception) { - if (encryptionErrorResetTool.tryResetIfNecessary(e)) { - Timber.w("We could recovery from this error via reset. Now retrying.") - factory.create(ENCRYPTED_SHARED_PREFERENCES_FILE) - } else { - throw e - } - } - } - } - - val globalEncryptedSharedPreferencesInstance: SharedPreferences by lazy { - encryptedPreferencesProvider(AppInjector.component) - } - - private val String.toPreservedByteArray: ByteArray - get() = Base64.decode(this, Base64.NO_WRAP) - - private val ByteArray.toPreservedString: String - get() = Base64.encodeToString(this, Base64.NO_WRAP) - - @SuppressLint("ApplySharedPref") - fun resetSharedPrefs() { - globalEncryptedSharedPreferencesInstance.clearAndNotify() - } - - fun <T> withSecurityCatch(doInCatch: () -> T) = try { - doInCatch.invoke() - } catch (e: Exception) { - throw CwaSecurityException(e) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt index f66cda4621d977c92e19d16da1c1fe0b5c8cf9cd..31922abf4120e6341508e056d82b84386aec82ab 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt @@ -6,11 +6,12 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.stop -import org.joda.time.DateTime -import org.joda.time.DateTimeZone +import org.joda.time.Duration +import org.joda.time.Instant import timber.log.Timber /** @@ -20,7 +21,9 @@ import timber.log.Timber */ class BackgroundNoisePeriodicWorker @AssistedInject constructor( @Assisted val context: Context, - @Assisted workerParams: WorkerParameters + @Assisted workerParams: WorkerParameters, + private val submissionSettings: SubmissionSettings, + private val timeStamper: TimeStamper ) : CoroutineWorker(context, workerParams) { /** @@ -31,13 +34,13 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( var result = Result.success() try { - val initialPairingDate = DateTime( - LocalData.devicePairingSuccessfulTimestamp(), - DateTimeZone.UTC - ) + val initialPairingDate = submissionSettings.devicePairingSuccessfulAt ?: Instant.ofEpochMilli(0) // Check if the numberOfDaysToRunPlaybook are over - if (initialPairingDate.plusDays(BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK).isBeforeNow) { + if (initialPairingDate + .plus(Duration.standardDays(NUMBER_OF_DAYS_TO_RUN_PLAYBOOK)) + .isBefore(timeStamper.nowUTC) + ) { stopWorker() return result } @@ -64,5 +67,6 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( companion object { private val TAG = BackgroundNoisePeriodicWorker::class.java.simpleName + private const val NUMBER_OF_DAYS_TO_RUN_PLAYBOOK = BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK.toLong() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt index 83804e32536b4875d0ae8917afbf951244699d5d..aaf6317d9be63bf12856a97c46cc9665512e65b7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt @@ -6,7 +6,7 @@ import androidx.work.Operation import androidx.work.WorkInfo import androidx.work.WorkManager import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.di.ApplicationComponent import timber.log.Timber import java.util.concurrent.ExecutionException @@ -17,7 +17,11 @@ import java.util.concurrent.ExecutionException * @see BackgroundConstants * @see BackgroundWorkHelper */ -object BackgroundWorkScheduler { +object BackgroundWorkScheduler : BackgroundWorkSchedulerBase() { + + fun init(component: ApplicationComponent) { + component.inject(this) + } /** * Enum class for work tags @@ -67,7 +71,7 @@ object BackgroundWorkScheduler { * Checks if periodic worker was already scheduled. If not - reschedule it again. * For [WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER] also checks if User is Registered * - * @see LocalData.registrationToken + * @see de.rki.coronawarnapp.submission.SubmissionSettings.registrationToken * @see isWorkActive */ fun startWorkScheduler() { @@ -82,12 +86,13 @@ object BackgroundWorkScheduler { WorkType.DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK.start() notificationBody.append("[DIAGNOSIS_KEY_BACKGROUND_PERIODIC_WORK] ") } - if (!LocalData.submissionWasSuccessful()) { + if (!submissionSettings.isSubmissionSuccessful) { if (!isWorkActive(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) && - LocalData.registrationToken() != null && !LocalData.isTestResultAvailableNotificationSent() + submissionSettings.registrationToken.value != null && + !tracingSettings.isTestResultAvailableNotificationSent ) { WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() - LocalData.initialPollingForTestResultTimeStamp(System.currentTimeMillis()) + tracingSettings.initialPollingForTestResultTimeStamp = System.currentTimeMillis() notificationBody.append("[DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER]") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt new file mode 100644 index 0000000000000000000000000000000000000000..9085a1645570aa546c4f8294904096753b46b17c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkSchedulerBase.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.worker + +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings +import javax.inject.Inject + +@Suppress("UnnecessaryAbstractClass") +abstract class BackgroundWorkSchedulerBase { + @Inject internal lateinit var submissionSettings: SubmissionSettings + @Inject internal lateinit var tracingSettings: TracingSettings +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt index 1540f8aa6716b3fe2f59563403557f5b3f6bc0ea..dc01b596996a6a4ba9f9e2da9e9ee1d4f944bcfc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt @@ -11,7 +11,7 @@ import de.rki.coronawarnapp.notification.NotificationConstants import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.service.submission.SubmissionService -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeAndDateExtensions import de.rki.coronawarnapp.util.TimeStamper @@ -32,7 +32,8 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private val notificationHelper: NotificationHelper, private val submissionSettings: SubmissionSettings, private val submissionService: SubmissionService, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val tracingSettings: TracingSettings, ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { @@ -55,7 +56,8 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( } else { Timber.tag(TAG).d(" $id Running worker.") - val registrationToken = LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() + val registrationToken = + submissionSettings.registrationToken.value ?: throw NoRegistrationTokenSetException() val testResult = submissionService.asyncRequestTestResult(registrationToken) Timber.tag(TAG).d("$id: Test Result retrieved is $testResult") @@ -80,7 +82,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( } private fun abortConditionsMet(currentMillis: Long): Boolean { - if (LocalData.isTestResultAvailableNotificationSent()) { + if (tracingSettings.isTestResultAvailableNotificationSent) { Timber.tag(TAG).d("$id: Notification already sent.") return true } @@ -90,7 +92,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( } val calculateDays = TimeAndDateExtensions.calculateDays( - LocalData.initialPollingForTestResultTimeStamp(), + tracingSettings.initialPollingForTestResultTimeStamp, currentMillis ) if (calculateDays >= BackgroundConstants.POLLING_VALIDITY_MAX_DAYS) { @@ -104,7 +106,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private suspend fun sendTestResultAvailableNotification(testResult: TestResult) { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) - LocalData.isTestResultAvailableNotificationSent(true) + tracingSettings.isTestResultAvailableNotificationSent = true } private fun cancelRiskLevelScoreNotification() { @@ -114,7 +116,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( } private fun stopWorker() { - LocalData.initialPollingForTestResultTimeStamp(0L) + tracingSettings.initialPollingForTestResultTimeStamp = 0L BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() Timber.tag(TAG).d("$id: Background worker stopped") } diff --git a/Corona-Warn-App/src/main/res/values-bg/strings.xml b/Corona-Warn-App/src/main/res/values-bg/strings.xml index 66b184830757a23d3d7ac65d9cba3efe7d7ef5e7..498b4a70adc6927a2bfcbac7324d23829ba114a6 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -1,47 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?><resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index a53ffdb8b040030d0a39ce0d8bacbf424fa4674b..6e6d5106ce8aac361f155b78f0b1aa73f19296d9 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -1,48 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml index 5c5ab53178ea2fc153b29af8d71ca2bbd6f09929..d21b9aeb089913ffc7d1e701b4293a6ab9c9c404 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -1,47 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?><resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/main/res/values-pl/strings.xml b/Corona-Warn-App/src/main/res/values-pl/strings.xml index 60216c9b7fa6bd68251c7c47ba9fdeda7bc08dc3..37ffe028420b7ee05fc4b06555231732833cf717 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -1,47 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?><resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/main/res/values-ro/strings.xml b/Corona-Warn-App/src/main/res/values-ro/strings.xml index e7339c1a902ed87a24aff5b842c5adc74fed36d3..ce6862eda5e9ecc090476dddc2648c28bec826a7 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -1,47 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?><resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/main/res/values-tr/strings.xml b/Corona-Warn-App/src/main/res/values-tr/strings.xml index d91a7055c97cad5de09393dd915a22dcb1dca29a..f408e7698649008c23232c2964420735064486cf 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -1,47 +1,4 @@ <?xml version="1.0" encoding="UTF-8"?><resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 9921b1dfa0ea944ef832477d8981fc90ea2d3df3..38eb2464b2fe251d64f1b1f0568a845cb2392325 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1,54 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> <resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> - - <!-- #################################### - Preference Keys - ###################################### - TODO: Check what is needed --> - - <!-- NOTR --> - <string name="preference_name"><xliff:g id="preference">"shared_preferences_cwa"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed"><xliff:g id="preference">"preference_onboarding_completed"</xliff:g></string> - <!-- NOTR --> - <string name="preference_onboarding_completed_timestamp"><xliff:g id="preference">"preference_onboarding_completed_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_check_done"><xliff:g id="preference">"preference_background_check_done"</xliff:g></string> - <!-- NOTR --> - <string name="preference_reset_app"><xliff:g id="preference">"preference_reset_app"</xliff:g></string> - <!-- NOTR --> - <string name="preference_only_wifi"><xliff:g id="preference">"preference_only_wifi"</xliff:g></string> - <!-- NOTR --> - <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> - <!-- NOTR --> - <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> - <!-- NOTR --> - <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> - <!-- NOTR --> - <string name="preference_registration_token"><xliff:g id="preference">"preference_registration_token"</xliff:g></string> - <!-- NOTR --> - <string name="preference_device_pairing_successful_time"><xliff:g id="preference">"preference_device_pairing_successful_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_tracing_activation_time"><xliff:g id="preference">"preference_initial_tracing_activation_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> - <!-- NOTR --> - <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> - <!-- NOTR --> - <string name="preference_database_password"><xliff:g id="preference">"preference_database_password"</xliff:g></string> - <!-- NOTR --> - <string name="preference_total_non_active_tracing"><xliff:g id="preference">"preference_total_non_active_tracing"</xliff:g></string> - <!-- NOTR --> - <string name="preference_last_non_active_tracing_timestamp"><xliff:g id="preference">"preference_last_non_active_tracing_timestamp"</xliff:g></string> - <!-- NOTR --> - <string name="preference_number_successful_submissions"><xliff:g id="preference">"preference_number_successful_submissions"</xliff:g></string> - <!-- NOTR --> - <string name="preference_polling_test_result_started"><xliff:g id="preference">"preference_polling_test_result_started"</xliff:g></string> - <!-- NOTR --> - <string name="preference_test_result_notification"><xliff:g id="preference">"preference_test_result_notification"</xliff:g></string> - <!-- #################################### Generics ###################################### --> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt index 10c54df0a834451588d7daddb3610dd29a6cc673..90bcddb7723145acd4131e3d4e6ba14343778a09 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.Provides import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import de.rki.coronawarnapp.submission.SubmissionSettings import io.github.classgraph.ClassGraph import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe @@ -63,4 +64,8 @@ class MockProvider { @Singleton @Provides fun diary(): ContactDiaryRepository = mockk() + + @Singleton + @Provides + fun submissionSettings(): SubmissionSettings = mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt index 55c64ca412d57ba704a113d26d5c71dc02798785..a00968c428c93e723309761c38e08af1634ce905 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryEncounterCensorTest.kt @@ -53,21 +53,23 @@ class DiaryEncounterCensorTest : BaseTest() { val censorMe = LogLine( timestamp = 1, priority = 3, - message = """ + message = + """ On A rainy day, two persons Spilled coffee on each others laptops, everyone disliked that. - """.trimIndent(), + """.trimIndent(), tag = "I'm a tag", throwable = null ) instance.checkLog(censorMe) shouldBe censorMe.copy( - message = """ + message = + """ On Encounter#2/Circumstances, two persons Encounter#3/Circumstances, everyone disliked that. - """.trimIndent() + """.trimIndent() ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt index 47c8eae21cb961cb31de04ce4620dccc8a1ec5a2..0c80fee73ed0eb5e43ee044c80a43214c73047d2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryLocationCensorTest.kt @@ -56,20 +56,22 @@ class DiaryLocationCensorTest : BaseTest() { val censorMe = LogLine( timestamp = 1, priority = 3, - message = """ + message = + """ Bürgermeister of Munich (+49 089 3333) and Karl of Aachen [+49 0241 9999] called each other. Both agreed that their emails (bürgermeister@münchen.de|karl@aachen.de) are awesome, and that Bielefeld doesn't exist as it has neither phonenumber (null) nor email (null). - """.trimIndent(), + """.trimIndent(), tag = "I'm a tag", throwable = null ) instance.checkLog(censorMe) shouldBe censorMe.copy( - message = """ + message = + """ Bürgermeister of Location#1/Name (Location#1/PhoneNumber) and Karl of Location#3/Name [Location#3/PhoneNumber] called each other. Both agreed that their emails (Location#1/EMail|Location#3/EMail) are awesome, and that Location#2/Name doesn't exist as it has neither phonenumber (null) nor email (null). - """.trimIndent() + """.trimIndent() ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt index 163faca82d6a3f14e9d68e8387e363b458681b39..e4e5e38c294d6f3b384f6a8e7191f725a29ed441 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryPersonCensorTest.kt @@ -57,20 +57,22 @@ class DiaryPersonCensorTest : BaseTest() { val censorMe = LogLine( timestamp = 1, priority = 3, - message = """ + message = + """ Ralf requested more coffee from +49 1234 7777, but Matthias thought he had enough has had enough for today. A quick mail to luka@sap.com confirmed this. - """.trimIndent(), + """.trimIndent(), tag = "I'm a tag", throwable = null ) instance.checkLog(censorMe) shouldBe censorMe.copy( - message = """ + message = + """ Person#2/Name requested more coffee from Person#1/PhoneNumber, but Person#3/Name thought he had enough has had enough for today. A quick mail to Person#1/EMail confirmed this. - """.trimIndent() + """.trimIndent() ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt index 6af281b9a25a6c87b5091015d4bae770f652fa82..cb9405722fde29918cb4d4a03fff8413fd518bea 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/DiaryVisitCensorTest.kt @@ -52,20 +52,22 @@ class DiaryVisitCensorTest : BaseTest() { val censorMe = LogLine( timestamp = 1, priority = 3, - message = """ + message = + """ After having a Döner that was too spicy, I got my beard shaved without mask, only to find out the supermarket was out of toiletpaper. - """.trimIndent(), + """.trimIndent(), tag = "I'm a tag", throwable = null ) instance.checkLog(censorMe) shouldBe censorMe.copy( - message = """ + message = + """ After having a Visit#1/Circumstances, I got my Visit#2/Circumstances, only to find out the supermarket was Visit#3/Circumstances. - """.trimIndent() + """.trimIndent() ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt index a1266ba8016ff84188722fbe1eee9020392cb410..284f8398badbfc1427c82fbfdaebc1bd6b488bc7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt @@ -1,22 +1,27 @@ package de.rki.coronawarnapp.bugreporting.censors import de.rki.coronawarnapp.bugreporting.debuglog.LogLine -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.CWADebug import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.every +import io.mockk.impl.annotations.MockK import io.mockk.mockkObject import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference class RegistrationTokenCensorTest : BaseTest() { + @MockK lateinit var submissionSettings: SubmissionSettings private val testToken = "63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f" + private val regtokenPreference = mockFlowPreference<String?>(testToken) + @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -24,11 +29,12 @@ class RegistrationTokenCensorTest : BaseTest() { mockkObject(CWADebug) every { CWADebug.isDeviceForTestersBuild } returns false - mockkObject(LocalData) - every { LocalData.registrationToken() } returns testToken + every { submissionSettings.registrationToken } returns regtokenPreference } - private fun createInstance() = RegistrationTokenCensor() + private fun createInstance() = RegistrationTokenCensor( + submissionSettings = submissionSettings + ) @Test fun `censoring replaces the logline message`() = runBlockingTest { @@ -49,12 +55,12 @@ class RegistrationTokenCensorTest : BaseTest() { message = "I'm a shy registration token: ########-e0de-4bd4-90c1-17c2bb683a2f" ) - verify { LocalData.registrationToken() } + verify { regtokenPreference.value } } @Test fun `censoring returns null if there is no token`() = runBlockingTest { - every { LocalData.registrationToken() } returns null + every { submissionSettings.registrationToken } returns mockFlowPreference(null) val instance = createInstance() val filterMeNot = LogLine( timestamp = 1, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt index a1b9812796d018fa5e09f415571e7fc4e87f6741..6cc0f482d8e03e279efd2d5565a51fc560f6477f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/AnalyticsTest.kt @@ -16,7 +16,7 @@ import de.rki.coronawarnapp.datadonation.safetynet.SafetyNetException import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaDataRequestAndroid import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpacAndroid -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -27,7 +27,6 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject import io.mockk.slot import io.mockk.spyk import kotlinx.coroutines.delay @@ -50,6 +49,7 @@ class AnalyticsTest : BaseTest() { @MockK lateinit var exposureRiskMetadataDonor: ExposureRiskMetadataDonor @MockK lateinit var lastAnalyticsSubmissionLogger: LastAnalyticsSubmissionLogger @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var onboardingSettings: OnboardingSettings private val baseTime: Instant = Instant.ofEpochMilli(0) @@ -57,8 +57,6 @@ class AnalyticsTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - mockkObject(LocalData) - coEvery { appConfigProvider.getAppConfig() } returns configData every { configData.analytics } returns analyticsConfig @@ -73,7 +71,7 @@ class AnalyticsTest : BaseTest() { val twoDaysAgo = baseTime.minus(Days.TWO.toStandardDuration()) every { settings.lastSubmittedTimestamp } returns mockFlowPreference(twoDaysAgo) - every { LocalData.onboardingCompletedTimestamp() } returns twoDaysAgo.millis + every { onboardingSettings.onboardingCompletedTimestamp } returns twoDaysAgo every { analyticsConfig.safetyNetRequirements } returns SafetyNetRequirementsContainer() @@ -88,7 +86,8 @@ class AnalyticsTest : BaseTest() { donorModules = modules, settings = settings, logger = lastAnalyticsSubmissionLogger, - timeStamper = timeStamper + timeStamper = timeStamper, + onboardingSettings = onboardingSettings ) ) @@ -196,7 +195,7 @@ class AnalyticsTest : BaseTest() { @Test fun `abort due to time since onboarding`() { - every { LocalData.onboardingCompletedTimestamp() } returns baseTime.millis + every { onboardingSettings.onboardingCompletedTimestamp } returns baseTime val analytics = createInstance() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/keysubmission/AnalyticsKeySubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/keysubmission/AnalyticsKeySubmissionRepositoryTest.kt index 136eaae38cf3cd1b3a7f1955e16bc2d6964e11f4..bbb25be9800a339a9614195855606218a98c33a5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/keysubmission/AnalyticsKeySubmissionRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/keysubmission/AnalyticsKeySubmissionRepositoryTest.kt @@ -26,7 +26,8 @@ class AnalyticsKeySubmissionRepositoryTest : BaseTest() { } private fun createInstance() = AnalyticsKeySubmissionRepository( - storage, riskLevelSettings + storage, + riskLevelSettings ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt index 193b4bf5f13c24bcc41501e8bc059c19d28f484e..f128a602166778abbbcae049ec4a8e24a0fefd15 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt @@ -7,7 +7,7 @@ import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettin import de.rki.coronawarnapp.risk.RiskLevelSettings import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.formatter.TestResult import io.kotest.matchers.shouldBe @@ -18,42 +18,43 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject import io.mockk.unmockkAll import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Duration 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 testhelpers.preferences.mockFlowPreference -import java.util.concurrent.TimeUnit class TestResultDonorTest : BaseTest() { @MockK lateinit var testResultDonorSettings: TestResultDonorSettings @MockK lateinit var riskLevelSettings: RiskLevelSettings @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var submissionSettings: SubmissionSettings private lateinit var testResultDonor: TestResultDonor + private val baseTime = Instant.ofEpochMilli(101010101) + @BeforeEach fun setUp() { MockKAnnotations.init(this, true) - mockkObject(LocalData) - every { timeStamper.nowUTC } returns Instant.now() - every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.now() + every { timeStamper.nowUTC } returns baseTime + every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns baseTime every { testResultDonorSettings.riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_LOW) - every { LocalData.initialTestResultReceivedTimestamp() } returns System.currentTimeMillis() + every { submissionSettings.initialTestResultReceivedAt } returns baseTime testResultDonor = TestResultDonor( testResultDonorSettings, riskLevelSettings, riskLevelStorage, timeStamper, + submissionSettings ) } @@ -71,7 +72,7 @@ class TestResultDonorTest : BaseTest() { @Test fun `No donation when timestamp at registration is missing`() = runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { LocalData.initialTestResultReceivedTimestamp() } returns null + every { submissionSettings.initialTestResultReceivedAt } returns null testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution } @@ -105,16 +106,16 @@ class TestResultDonorTest : BaseTest() { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.PENDING) - val timeDayBefore = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1) - every { LocalData.initialTestResultReceivedTimestamp() } returns timeDayBefore - every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns Instant.ofEpochMilli(timeDayBefore) + val timeDayBefore = baseTime.minus(Duration.standardDays(1)) + every { submissionSettings.initialTestResultReceivedAt } returns timeDayBefore + every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp } returns timeDayBefore val donation = testResultDonor.beginDonation(TestRequest) donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() with(donation.testResultMetadata) { riskLevelAtTestRegistration shouldBe PpaData.PPARiskLevel.RISK_LEVEL_LOW testResult shouldBe PpaData.PPATestResult.TEST_RESULT_PENDING - hoursSinceTestRegistration shouldBe 23 + hoursSinceTestRegistration shouldBe 24 hoursSinceHighRiskWarningAtTestRegistration shouldBe -1 daysSinceMostRecentDateAtRiskLevelAtTestRegistration shouldBe 0 } @@ -126,7 +127,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) - every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(Instant.now()) + every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(baseTime) val donation = testResultDonor.beginDonation(TestRequest) donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() @@ -145,7 +146,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) - every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(Instant.now()) + every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(baseTime) val donation = testResultDonor.beginDonation(TestRequest) donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt index 6e4b58f4d948b2a8937bf192edfa96af3c03e410..3201a04a7c24b83bb2c573c35c9982a59ac3c585 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt @@ -8,7 +8,7 @@ import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import io.mockk.MockKAnnotations import io.mockk.Runs @@ -46,6 +46,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { @MockK lateinit var newKey1: CachedKey @MockK lateinit var latestTrackedDetection: TrackedExposureDetection + @MockK lateinit var submissionSettings: SubmissionSettings @BeforeEach fun setup() { @@ -53,8 +54,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { mockkObject(BuildConfigWrap) every { BuildConfigWrap.VERSION_CODE } returns 1080005 - mockkObject(LocalData) - every { LocalData.isAllowedToSubmitDiagnosisKeys() } returns false + every { submissionSettings.isAllowedToSubmitKeys } returns false availableKey1.apply { every { path } returns File("availableKey1") @@ -101,7 +101,8 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { appConfigProvider = appConfigProvider, keyPackageSyncTool = keyPackageSyncTool, timeStamper = timeStamper, - settings = downloadSettings + settings = downloadSettings, + submissionSettings = submissionSettings ) @Test @@ -229,7 +230,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { @Test fun `we do not submit keys if user got positive test results`() = runBlockingTest { - every { LocalData.isAllowedToSubmitDiagnosisKeys() } returns true + every { submissionSettings.isAllowedToSubmitKeys } returns true createInstance().run(DownloadDiagnosisKeysTask.Arguments()) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt index 1e15c104b00d5c4d273af764f67e0b857bc9e9f0..aa8bab6568f0480baf3d800c487bcc9eacae1beb 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt @@ -2,7 +2,8 @@ package de.rki.coronawarnapp.main import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings import de.rki.coronawarnapp.environment.EnvironmentSetup -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.playbook.BackgroundNoise +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.ui.main.MainActivityViewModel import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.device.BackgroundModeStatus @@ -25,15 +26,16 @@ class MainActivityViewModelTest : BaseTest() { @MockK lateinit var environmentSetup: EnvironmentSetup @MockK lateinit var backgroundModeStatus: BackgroundModeStatus @MockK lateinit var diarySettings: ContactDiarySettings + @MockK lateinit var backgroundNoise: BackgroundNoise + @MockK lateinit var onboardingSettings: OnboardingSettings @BeforeEach fun setup() { MockKAnnotations.init(this) - mockkObject(LocalData) mockkObject(CWADebug) - every { LocalData.isBackgroundCheckDone() } returns true + every { onboardingSettings.isOnboarded } returns true every { environmentSetup.currentEnvironment } returns EnvironmentSetup.Type.WRU } @@ -41,7 +43,9 @@ class MainActivityViewModelTest : BaseTest() { dispatcherProvider = TestDispatcherProvider(), environmentSetup = environmentSetup, backgroundModeStatus = backgroundModeStatus, - contactDiarySettings = diarySettings + contactDiarySettings = diarySettings, + backgroundNoise = backgroundNoise, + onboardingSettings = onboardingSettings ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt index dbbcda67a7ea6f040e0f790cff68f8b5a95a138e..fe35faf99c0c09ab909c1ce2a86c2edbd7a928b6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt @@ -7,8 +7,8 @@ import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.statistics.source.StatisticsProvider -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.TracingRepository +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.ui.homecards.SubmissionDone import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider @@ -64,6 +64,7 @@ class HomeFragmentViewModelTest : BaseTest() { @MockK lateinit var statisticsProvider: StatisticsProvider @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @MockK lateinit var appShortcutsHelper: AppShortcutsHelper + @MockK lateinit var tracingSettings: TracingSettings @BeforeEach fun setup() { @@ -95,7 +96,8 @@ class HomeFragmentViewModelTest : BaseTest() { appConfigProvider = appConfigProvider, statisticsProvider = statisticsProvider, deadmanNotificationScheduler = deadmanNotificationScheduler, - appShortcutsHelper = appShortcutsHelper + appShortcutsHelper = appShortcutsHelper, + tracingSettings = tracingSettings ) @Test @@ -176,9 +178,7 @@ class HomeFragmentViewModelTest : BaseTest() { @Test fun `test correct order of displaying delta onboarding, release notes and popups`() { - - mockkObject(LocalData) - every { LocalData.isInteroperabilityShownAtLeastOnce } returns false andThen true + every { cwaSettings.wasInteroperabilityShownAtLeastOnce } returns false andThen true mockkObject(BuildConfigWrap) every { BuildConfigWrap.VERSION_CODE } returns 1120004 diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/TracingPermissionHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/TracingPermissionHelperTest.kt index de30f6d1da854d5d9ae23031569e496be0f4f31f..68c76f457bfbd3ea795cc687ba1ea51a02e31a62 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/TracingPermissionHelperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/TracingPermissionHelperTest.kt @@ -2,7 +2,7 @@ package de.rki.coronawarnapp.nearby import android.app.Activity import com.google.android.gms.common.api.Status -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import io.kotest.matchers.shouldBe import io.mockk.Called import io.mockk.MockKAnnotations @@ -13,7 +13,6 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.CoroutineScope @@ -25,6 +24,7 @@ import testhelpers.BaseTest class TracingPermissionHelperTest : BaseTest() { @MockK lateinit var enfClient: ENFClient + @MockK lateinit var tracingSettings: TracingSettings @BeforeEach fun setup() { @@ -33,14 +33,14 @@ class TracingPermissionHelperTest : BaseTest() { coEvery { enfClient.isTracingEnabled } returns flowOf(false) coEvery { enfClient.setTracing(any(), any(), any(), any()) } just Runs - mockkObject(LocalData) - every { LocalData.initialTracingActivationTimestamp() } returns 123L + every { tracingSettings.isConsentGiven } returns true } fun createInstance(scope: CoroutineScope, callback: TracingPermissionHelper.Callback) = TracingPermissionHelper( callback = callback, scope = scope, - enfClient = enfClient + enfClient = enfClient, + tracingSettings = tracingSettings ) @Test @@ -61,7 +61,7 @@ class TracingPermissionHelperTest : BaseTest() { @Test fun `if consent is missing then we continue after it was given`() = runBlockingTest { - every { LocalData.initialTracingActivationTimestamp() } returns null + every { tracingSettings.isConsentGiven } returns false val callback = mockk<TracingPermissionHelper.Callback>(relaxUnitFun = true) val consentCallbackSlot = slot<(Boolean) -> Unit>() @@ -85,7 +85,7 @@ class TracingPermissionHelperTest : BaseTest() { @Test fun `if consent was declined then we do nothing`() = runBlockingTest { - every { LocalData.initialTracingActivationTimestamp() } returns null + every { tracingSettings.isConsentGiven } returns false val callback = mockk<TracingPermissionHelper.Callback>(relaxUnitFun = true) val consentCallbackSlot = slot<(Boolean) -> Unit>() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt index a72581ca824c0513f308f8c3833727cb91d604b3..a9a402b39d5b9c41d76cc54c30b46daea87d7e18 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.nearby.modules.tracing import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.storage.TracingSettings import io.kotest.matchers.shouldBe import io.mockk.Called import io.mockk.MockKAnnotations @@ -22,6 +23,7 @@ import testhelpers.gms.MockGMSTask class DefaultTracingStatusTest : BaseTest() { @MockK lateinit var client: ExposureNotificationClient + @MockK lateinit var tracingSettings: TracingSettings @BeforeEach fun setup() { @@ -32,7 +34,8 @@ class DefaultTracingStatusTest : BaseTest() { private fun createInstance(scope: CoroutineScope): DefaultTracingStatus = DefaultTracingStatus( client = client, - scope = scope + scope = scope, + tracingSettings = tracingSettings ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt index 2a50eb7dbe82515e9f01165e50d1a1314fb99dcb..d2fc8112210d32d98c6c86005037f29adff4b63d 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt @@ -6,7 +6,7 @@ import android.content.Context import androidx.navigation.NavDeepLinkBuilder import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.formatter.TestResult import io.kotest.matchers.shouldBe @@ -35,26 +35,27 @@ class TestResultAvailableNotificationServiceTest : BaseTest() { @MockK lateinit var navDeepLinkBuilderProvider: Provider<NavDeepLinkBuilder> @MockK lateinit var notificationManager: NotificationManager @MockK lateinit var notificationHelper: NotificationHelper + @MockK lateinit var cwaSettings: CWASettings @BeforeEach fun setUp() { MockKAnnotations.init(this) mockkObject(CoronaWarnApplication) - mockkObject(LocalData) every { CoronaWarnApplication.getAppContext() } returns context every { context.getSystemService(Context.NOTIFICATION_SERVICE) } returns notificationManager every { navDeepLinkBuilderProvider.get() } returns navDeepLinkBuilder every { navDeepLinkBuilder.createPendingIntent() } returns pendingIntent - every { LocalData.isNotificationsTestEnabled } returns true + every { cwaSettings.isNotificationsTestEnabled.value } returns true } fun createInstance() = TestResultAvailableNotificationService( context = context, foregroundState = foregroundState, navDeepLinkBuilderProvider = navDeepLinkBuilderProvider, - notificationHelper = notificationHelper + notificationHelper = notificationHelper, + cwaSettings = cwaSettings ) @Test @@ -111,7 +112,7 @@ class TestResultAvailableNotificationServiceTest : BaseTest() { @Test fun `test notification in background disabled`() = runBlockingTest { coEvery { foregroundState.isInForeground } returns flow { emit(false) } - every { LocalData.isNotificationsTestEnabled } returns false + every { cwaSettings.isNotificationsTestEnabled.value } returns false createInstance().apply { showTestResultAvailableNotification(TestResult.POSITIVE) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt index 9ec5698f0d5b0529d2aff62a4fea3b64333d9c6b..13fea4bb9f7b0a2bb24c3848256cfe2431e2ba14 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -10,7 +10,8 @@ import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.risk.result.AggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.device.ForegroundState import io.kotest.matchers.shouldBe @@ -22,7 +23,6 @@ import io.mockk.coVerifySequence import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just -import io.mockk.mockkObject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest @@ -30,9 +30,9 @@ import org.joda.time.Instant import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference class RiskLevelChangeDetectorTest : BaseTest() { - @MockK lateinit var context: Context @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var riskLevelStorage: RiskLevelStorage @@ -41,15 +41,15 @@ class RiskLevelChangeDetectorTest : BaseTest() { @MockK lateinit var riskLevelSettings: RiskLevelSettings @MockK lateinit var notificationHelper: NotificationHelper @MockK lateinit var surveys: Surveys + @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var tracingSettings: TracingSettings @BeforeEach fun setup() { MockKAnnotations.init(this) - mockkObject(LocalData) - - every { LocalData.isUserToBeNotifiedOfLoweredRiskLevel = any() } just Runs - every { LocalData.submissionWasSuccessful() } returns false + every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockFlowPreference(false) + every { submissionSettings.isSubmissionSuccessful } returns false every { foregroundState.isInForeground } returns flowOf(true) every { notificationManagerCompat.areNotificationsEnabled() } returns true every { riskLevelSettings.lastChangeCheckedRiskLevelTimestamp = any() } just Runs @@ -78,7 +78,9 @@ class RiskLevelChangeDetectorTest : BaseTest() { foregroundState = foregroundState, riskLevelSettings = riskLevelSettings, notificationHelper = notificationHelper, - surveys = surveys + surveys = surveys, + submissionSettings = submissionSettings, + tracingSettings = tracingSettings ) @Test @@ -92,7 +94,6 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - LocalData wasNot Called notificationManagerCompat wasNot Called surveys wasNot Called } @@ -115,7 +116,6 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - LocalData wasNot Called notificationManagerCompat wasNot Called surveys wasNot Called } @@ -138,9 +138,8 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - LocalData.submissionWasSuccessful() + submissionSettings.isSubmissionSuccessful foregroundState.isInForeground - LocalData.isUserToBeNotifiedOfLoweredRiskLevel = any() surveys.resetSurvey(Surveys.Type.HIGH_RISK_ENCOUNTER) } } @@ -162,7 +161,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - LocalData.submissionWasSuccessful() + submissionSettings.isSubmissionSuccessful foregroundState.isInForeground surveys wasNot Called } @@ -186,7 +185,6 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - LocalData wasNot Called notificationManagerCompat wasNot Called surveys wasNot Called } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt index 62a8305271e50cc3dd45c4c8014fd28180ec137f..28cf7f0f7f8327649644c8b907bebe7033c87694 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt @@ -13,7 +13,7 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.risk.result.AggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.util.TimeStamper @@ -39,7 +39,6 @@ import org.junit.jupiter.api.assertThrows import testhelpers.BaseTest class RiskLevelTaskTest : BaseTest() { - @MockK lateinit var riskLevels: RiskLevels @MockK lateinit var context: Context @MockK lateinit var enfClient: ENFClient @@ -50,6 +49,7 @@ class RiskLevelTaskTest : BaseTest() { @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var keyCacheRepository: KeyCacheRepository + @MockK lateinit var submissionSettings: SubmissionSettings @MockK lateinit var analyticsExposureWindowCollector: AnalyticsExposureWindowCollector private val arguments: Task.Arguments = object : Task.Arguments {} @@ -59,9 +59,8 @@ class RiskLevelTaskTest : BaseTest() { MockKAnnotations.init(this) mockkObject(TimeVariables) - mockkObject(LocalData) - every { LocalData.isAllowedToSubmitDiagnosisKeys() } returns false + every { submissionSettings.isAllowedToSubmitKeys } returns false every { configData.isDeviceTimeCorrect } returns true every { backgroundModeStatus.isAutoModeEnabled } returns flowOf(true) coEvery { appConfigProvider.getAppConfig() } returns configData @@ -95,6 +94,7 @@ class RiskLevelTaskTest : BaseTest() { appConfigProvider = appConfigProvider, riskLevelStorage = riskLevelStorage, keyCacheRepository = keyCacheRepository, + submissionSettings = submissionSettings, analyticsExposureWindowCollector = analyticsExposureWindowCollector ) @@ -215,7 +215,7 @@ class RiskLevelTaskTest : BaseTest() { coEvery { keyCacheRepository.getAllCachedKeys() } returns listOf(cachedKey) every { backgroundModeStatus.isAutoModeEnabled } returns flowOf(false) every { timeStamper.nowUTC } returns now - every { LocalData.isAllowedToSubmitDiagnosisKeys() } returns true + every { submissionSettings.isAllowedToSubmitKeys } returns true createTask().run(arguments) shouldBe RiskLevelTaskResult( calculatedAt = now, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt index 31d2aaa7c6d0775833f853d5d30f53f923de3dfe..31f62170a700594e6b68beb6e7f3039da3154d4f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt @@ -27,6 +27,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockkObject +import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.emptyFlow @@ -53,6 +54,7 @@ class SubmissionRepositoryTest : BaseTest() { @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var tracingSettings: TracingSettings private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064" @@ -60,6 +62,9 @@ class SubmissionRepositoryTest : BaseTest() { private val testResult = TestResult.PENDING private val registrationData = SubmissionService.RegistrationData(registrationToken, testResult) + private val registrationTokenPreference = mockFlowPreference<String?>(null) + private val resultReceivedTimeStamp = Instant.ofEpochMilli(101010101) + @BeforeEach fun setUp() { MockKAnnotations.init(this) @@ -69,14 +74,12 @@ class SubmissionRepositoryTest : BaseTest() { every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory every { appComponent.errorResetTool } returns encryptionErrorResetTool - mockkObject(BackgroundNoise.Companion) - every { BackgroundNoise.getInstance() } returns backgroundNoise every { backgroundNoise.scheduleDummyPattern() } just Runs - mockkObject(LocalData) - every { LocalData.registrationToken(any()) } just Runs - every { LocalData.devicePairingSuccessfulTimestamp(any()) } just Runs - every { LocalData.initialTestResultReceivedTimestamp() } returns 1L + every { submissionSettings.registrationToken } returns registrationTokenPreference + + every { submissionSettings.devicePairingSuccessfulAt = any() } just Runs + every { submissionSettings.initialTestResultReceivedAt } returns resultReceivedTimeStamp every { submissionSettings.hasGivenConsent } returns mockFlowPreference(false) every { submissionSettings.hasViewedTestResult } returns mockFlowPreference(false) @@ -97,31 +100,41 @@ class SubmissionRepositoryTest : BaseTest() { timeStamper = timeStamper, tekHistoryStorage = tekHistoryStorage, deadmanNotificationScheduler = deadmanNotificationScheduler, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + backgroundNoise = backgroundNoise, + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + tracingSettings = tracingSettings ) @Test fun removeTestFromDeviceSucceeds() = runBlockingTest { val submissionRepository = createInstance(scope = this) - every { LocalData.initialPollingForTestResultTimeStamp(any()) } just Runs - every { LocalData.initialTestResultReceivedTimestamp(any()) } just Runs - every { LocalData.isAllowedToSubmitDiagnosisKeys(any()) } just Runs - every { LocalData.isTestResultAvailableNotificationSent(any()) } just Runs - every { LocalData.numberOfSuccessfulSubmissions(any()) } just Runs + val initialPollingForTestResultTimeStampSlot = slot<Long>() + val isTestResultAvailableNotificationSent = slot<Boolean>() + every { + tracingSettings.initialPollingForTestResultTimeStamp = capture(initialPollingForTestResultTimeStampSlot) + } answers {} + every { + tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) + } answers {} + + every { submissionSettings.initialTestResultReceivedAt = any() } just Runs + every { submissionSettings.isAllowedToSubmitKeys = any() } just Runs + every { submissionSettings.isSubmissionSuccessful = any() } just Runs every { analyticsKeySubmissionCollector.reset() } just Runs submissionRepository.removeTestFromDevice() verify(exactly = 1) { - LocalData.registrationToken(null) - LocalData.devicePairingSuccessfulTimestamp(0L) - LocalData.initialPollingForTestResultTimeStamp(0L) - LocalData.initialTestResultReceivedTimestamp(0L) - LocalData.isAllowedToSubmitDiagnosisKeys(false) - LocalData.isTestResultAvailableNotificationSent(false) - LocalData.numberOfSuccessfulSubmissions(0) + registrationTokenPreference.update(any()) + submissionSettings.devicePairingSuccessfulAt = null + submissionSettings.initialTestResultReceivedAt = null + submissionSettings.isAllowedToSubmitKeys = false + submissionSettings.isSubmissionSuccessful = false } + + initialPollingForTestResultTimeStampSlot.captured shouldBe 0L + isTestResultAvailableNotificationSent.captured shouldBe false } @Test @@ -133,11 +146,13 @@ class SubmissionRepositoryTest : BaseTest() { submissionRepository.asyncRegisterDeviceViaGUID(guid) - verify { - LocalData.devicePairingSuccessfulTimestamp(any()) - LocalData.registrationToken(registrationToken) + registrationTokenPreference.value shouldBe registrationToken + submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() + + verify(exactly = 1) { + registrationTokenPreference.update(any()) + submissionSettings.devicePairingSuccessfulAt = any() backgroundNoise.scheduleDummyPattern() - submissionRepository.updateTestResult(testResult) } } @@ -151,11 +166,13 @@ class SubmissionRepositoryTest : BaseTest() { submissionRepository.asyncRegisterDeviceViaTAN(tan) - coVerify { - LocalData.devicePairingSuccessfulTimestamp(any()) - LocalData.registrationToken(registrationToken) + registrationTokenPreference.value shouldBe registrationToken + submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() + + verify(exactly = 1) { + registrationTokenPreference.update(any()) + submissionSettings.devicePairingSuccessfulAt = any() backgroundNoise.scheduleDummyPattern() - submissionRepository.updateTestResult(testResult) } } @@ -174,8 +191,10 @@ class SubmissionRepositoryTest : BaseTest() { @Test fun `ui state is SUBMITTED_FINAL when submission was done`() = runBlockingTest { - coEvery { LocalData.submissionWasSuccessful() } returns true + every { submissionSettings.isSubmissionSuccessful } returns true + val submissionRepository = createInstance(scope = this) + submissionRepository.refreshDeviceUIState() submissionRepository.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) @@ -183,9 +202,11 @@ class SubmissionRepositoryTest : BaseTest() { @Test fun `ui state is UNPAIRED when no token is present`() = runBlockingTest { - coEvery { LocalData.submissionWasSuccessful() } returns false - coEvery { LocalData.registrationToken() } returns null + every { submissionSettings.isSubmissionSuccessful } returns false + every { submissionSettings.registrationToken } returns mockFlowPreference(null) + val submissionRepository = createInstance(scope = this) + submissionRepository.refreshDeviceUIState() submissionRepository.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) @@ -193,10 +214,12 @@ class SubmissionRepositoryTest : BaseTest() { @Test fun `ui state is PAIRED_POSITIVE when allowed to submit`() = runBlockingTest { - coEvery { LocalData.submissionWasSuccessful() } returns false - coEvery { LocalData.registrationToken() } returns "token" - coEvery { LocalData.isAllowedToSubmitDiagnosisKeys() } returns true + every { submissionSettings.isSubmissionSuccessful } returns false + every { submissionSettings.registrationToken } returns mockFlowPreference("token") + coEvery { submissionSettings.isAllowedToSubmitKeys } returns true + val submissionRepository = createInstance(scope = this) + submissionRepository.refreshDeviceUIState() submissionRepository.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE) @@ -204,40 +227,53 @@ class SubmissionRepositoryTest : BaseTest() { @Test fun `refresh when state is PAIRED_NO_RESULT`() = runBlockingTest { - coEvery { LocalData.submissionWasSuccessful() } returns false - coEvery { LocalData.registrationToken() } returns "token" - coEvery { LocalData.isAllowedToSubmitDiagnosisKeys() } returns false + every { submissionSettings.isSubmissionSuccessful } returns false + every { submissionSettings.registrationToken } returns mockFlowPreference("token") + coEvery { submissionSettings.isAllowedToSubmitKeys } returns false coEvery { submissionService.asyncRequestTestResult(any()) } returns TestResult.PENDING + val submissionRepository = createInstance(scope = this) + submissionRepository.refreshDeviceUIState() submissionRepository.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT) + coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } } @Test fun `refresh when state is UNPAIRED`() = runBlockingTest { - coEvery { LocalData.submissionWasSuccessful() } returns false - coEvery { LocalData.registrationToken() } returns null - coEvery { LocalData.isAllowedToSubmitDiagnosisKeys() } returns false + every { submissionSettings.isSubmissionSuccessful } returns false + every { submissionSettings.registrationToken } returns mockFlowPreference(null) + coEvery { submissionSettings.isAllowedToSubmitKeys } returns false coEvery { submissionService.asyncRequestTestResult(any()) } returns TestResult.PENDING + val submissionRepository = createInstance(scope = this) + submissionRepository.refreshDeviceUIState() submissionRepository.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) - coEvery { LocalData.registrationToken() } returns "token" + + every { submissionSettings.registrationToken } returns mockFlowPreference("token") + submissionRepository.refreshDeviceUIState() + coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } } @Test fun `no refresh when state is SUBMITTED_FINAL`() = runBlockingTest { - coEvery { LocalData.submissionWasSuccessful() } returns true + every { submissionSettings.isSubmissionSuccessful } returns true + val submissionRepository = createInstance(scope = this) + submissionRepository.refreshDeviceUIState() + submissionRepository.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) + submissionRepository.refreshDeviceUIState() + coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index f5e67e1b5f43d10e8739f91da62534ef7b022aaa..f49adc5f87d8e25d5dacaf1dae27cd0ef3396290 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.submission.auto.AutoSubmission @@ -63,6 +62,7 @@ class SubmissionTaskTest : BaseTest() { private lateinit var settingSymptomsPreference: FlowPreference<Symptoms?> + private val registrationToken: FlowPreference<String?> = mockFlowPreference("regtoken") private val settingHasGivenConsent: FlowPreference<Boolean> = mockFlowPreference(true) private val settingAutoSubmissionAttemptsCount: FlowPreference<Int> = mockFlowPreference(0) private val settingAutoSubmissionAttemptsLast: FlowPreference<Instant> = mockFlowPreference(Instant.EPOCH) @@ -73,9 +73,8 @@ class SubmissionTaskTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - mockkObject(LocalData) - every { LocalData.registrationToken() } returns "regtoken" - every { LocalData.numberOfSuccessfulSubmissions(any()) } just Runs + every { submissionSettings.registrationToken } returns registrationToken + every { submissionSettings.isSubmissionSuccessful = any() } just Runs mockkObject(BackgroundWorkScheduler) every { BackgroundWorkScheduler.stopWorkScheduler() } just Runs @@ -133,11 +132,20 @@ class SubmissionTaskTest : BaseTest() { ) coVerifySequence { + submissionSettings.lastSubmissionUserActivityUTC settingLastUserActivityUTC.value + submissionSettings.hasGivenConsent settingHasGivenConsent.value - LocalData.registrationToken() + submissionSettings.autoSubmissionAttemptsCount + submissionSettings.autoSubmissionAttemptsLast + submissionSettings.autoSubmissionAttemptsCount + submissionSettings.autoSubmissionAttemptsLast + submissionSettings.registrationToken + + registrationToken.value tekHistoryStorage.tekData + submissionSettings.symptoms settingSymptomsPreference.value tekHistoryCalculations.transformToKeyHistoryInExternalFormat(listOf(tek), userSymptoms) @@ -156,12 +164,13 @@ class SubmissionTaskTest : BaseTest() { analyticsKeySubmissionCollector.reportSubmittedInBackground() tekHistoryStorage.clear() + submissionSettings.symptoms settingSymptomsPreference.update(match { it.invoke(mockk()) == null }) autoSubmission.updateMode(AutoSubmission.Mode.DISABLED) BackgroundWorkScheduler.stopWorkScheduler() - LocalData.numberOfSuccessfulSubmissions(1) + submissionSettings.isSubmissionSuccessful = true BackgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() @@ -195,7 +204,7 @@ class SubmissionTaskTest : BaseTest() { coVerifySequence { settingHasGivenConsent.value - LocalData.registrationToken() + registrationToken.value tekHistoryStorage.tekData settingSymptomsPreference.value @@ -222,7 +231,7 @@ class SubmissionTaskTest : BaseTest() { @Test fun `task throws if no registration token is available`() = runBlockingTest { - every { LocalData.registrationToken() } returns null + every { submissionSettings.registrationToken } returns mockFlowPreference(null) val task = createTask() shouldThrow<NoRegistrationTokenSetException> { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt index 4cc268ca6d272c0d96c80f8847883504572b549a..7ca361ae4bfbf43bfd339e3262fadbd93f3c097c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsItemProviderTest.kt @@ -18,7 +18,6 @@ import de.rki.coronawarnapp.tracing.ui.details.items.riskdetails.DetailsFailedCa import de.rki.coronawarnapp.tracing.ui.details.items.riskdetails.DetailsIncreasedRiskBox import de.rki.coronawarnapp.tracing.ui.details.items.riskdetails.DetailsLowRiskBox import de.rki.coronawarnapp.tracing.ui.details.items.survey.UserSurveyBox -import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds import io.kotest.matchers.ints.shouldBeGreaterThan import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt index 15decd484cd425591face7df7a9faf9995bed521..dee497b2ccdd3f88fa5a838b3480616fd2940ce5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.tracing.ui.homecards import android.content.Context -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.ui.homecards.NoTest import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider import de.rki.coronawarnapp.util.DeviceUIState @@ -11,7 +11,6 @@ import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.mockkObject import io.mockk.verifySequence import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -22,6 +21,7 @@ import org.junit.jupiter.api.extension.ExtendWith import testhelpers.BaseTest import testhelpers.extensions.CoroutinesTestExtension import testhelpers.extensions.InstantExecutorExtension +import testhelpers.preferences.mockFlowPreference import java.util.Date @ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class) @@ -29,21 +29,24 @@ class SubmissionStateProviderTest : BaseTest() { @MockK lateinit var context: Context @MockK lateinit var submissionRepository: SubmissionRepository + @MockK lateinit var submissionSettings: SubmissionSettings @BeforeEach fun setup() { MockKAnnotations.init(this) - mockkObject(LocalData) every { submissionRepository.hasViewedTestResult } returns flow { emit(true) } every { submissionRepository.deviceUIStateFlow } returns flow { emit(NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>(DeviceUIState.PAIRED_POSITIVE)) } every { submissionRepository.testResultReceivedDateFlow } returns flow { emit(Date()) } - every { LocalData.registrationToken() } returns null + every { submissionSettings.registrationToken } returns mockFlowPreference(null) } - private fun createInstance() = SubmissionStateProvider(submissionRepository) + private fun createInstance() = SubmissionStateProvider( + submissionRepository = submissionRepository, + submissionSettings = submissionSettings + ) @Test fun `state determination, unregistered test`() = runBlockingTest { @@ -54,7 +57,7 @@ class SubmissionStateProviderTest : BaseTest() { submissionRepository.deviceUIStateFlow submissionRepository.hasViewedTestResult submissionRepository.testResultReceivedDateFlow - LocalData.registrationToken() + submissionSettings.registrationToken } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt index 14df18a27d310a3266724880134bfb279cee3f18..07a76044a01c0f3840389387debbd865b0bc9202 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/launcher/LauncherActivityViewModelTest.kt @@ -2,7 +2,7 @@ package de.rki.coronawarnapp.ui.launcher import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.update.UpdateChecker import io.kotest.matchers.shouldBe import io.kotest.matchers.types.instanceOf @@ -26,13 +26,13 @@ class LauncherActivityViewModelTest : BaseTest() { @MockK lateinit var updateChecker: UpdateChecker @MockK lateinit var cwaSettings: CWASettings + @MockK lateinit var onboardingSettings: OnboardingSettings @BeforeEach fun setupFreshViewModel() { MockKAnnotations.init(this) - mockkObject(LocalData) - every { LocalData.isOnboarded() } returns false + every { onboardingSettings.isOnboarded } returns false mockkObject(BuildConfigWrap) every { BuildConfigWrap.VERSION_CODE } returns 10L @@ -43,7 +43,8 @@ class LauncherActivityViewModelTest : BaseTest() { private fun createViewModel() = LauncherActivityViewModel( updateChecker = updateChecker, dispatcherProvider = TestDispatcherProvider(), - cwaSettings = cwaSettings + cwaSettings = cwaSettings, + onboardingSettings = onboardingSettings ) @Test @@ -67,8 +68,8 @@ class LauncherActivityViewModelTest : BaseTest() { @Test fun `onboarding finished`() { - every { LocalData.isOnboarded() } returns true - every { LocalData.isInteroperabilityShownAtLeastOnce } returns true + every { onboardingSettings.isOnboarded } returns true + every { cwaSettings.wasInteroperabilityShownAtLeastOnce } returns true every { cwaSettings.lastChangelogVersion } returns mockFlowPreference(10L) val vm = createViewModel() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/settings/notification/NotificationsSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/settings/notification/NotificationsSettingsTest.kt index 3bd5a7811a0ab9771756341527dd20c07f0f8b6f..7f0c43618e781d12ee792f90b6c2d6d6c5390b47 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/settings/notification/NotificationsSettingsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/settings/notification/NotificationsSettingsTest.kt @@ -1,18 +1,14 @@ package de.rki.coronawarnapp.ui.settings.notification import androidx.core.app.NotificationManagerCompat -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.ui.settings.notifications.NotificationSettings import de.rki.coronawarnapp.util.device.ForegroundState import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations -import io.mockk.Runs import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.mockkObject -import io.mockk.verify import io.mockk.verifySequence import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -20,32 +16,28 @@ import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference class NotificationsSettingsTest : BaseTest() { @MockK lateinit var foregroundState: ForegroundState @MockK lateinit var notificationManagerCompat: NotificationManagerCompat + @MockK lateinit var cwaSettings: CWASettings @BeforeEach fun setup() { MockKAnnotations.init(this) - mockkObject(LocalData) - - every { LocalData.isNotificationsRiskEnabledFlow } returns flow { emit(true) } - every { LocalData.isNotificationsRiskEnabled = any() } just Runs - every { LocalData.isNotificationsRiskEnabled } returns true - - every { LocalData.isNotificationsTestEnabledFlow } returns flow { emit(true) } - every { LocalData.isNotificationsTestEnabled = any() } just Runs - every { LocalData.isNotificationsTestEnabled } returns true + every { cwaSettings.isNotificationsRiskEnabled } returns mockFlowPreference(true) + every { cwaSettings.isNotificationsTestEnabled } returns mockFlowPreference(true) every { notificationManagerCompat.areNotificationsEnabled() } returns true coEvery { foregroundState.isInForeground } returns flow { emit(true) } } private fun createInstance() = NotificationSettings( foregroundState = foregroundState, - notificationManagerCompat = notificationManagerCompat + notificationManagerCompat = notificationManagerCompat, + cwaSettings = cwaSettings ) @Test @@ -72,16 +64,4 @@ class NotificationsSettingsTest : BaseTest() { isNotificationsTestEnabled.first() shouldBe true } } - - @Test - fun toggleNotificationsRiskEnabled() { - createInstance().toggleNotificationsRiskEnabled() - verify { LocalData.isNotificationsRiskEnabled = false } - } - - @Test - fun toggleNotificationsTestEnabled() { - createInstance().toggleNotificationsTestEnabled() - verify { LocalData.isNotificationsTestEnabled = false } - } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt index b3d1b9bae194730731f92604c6d2a97b3d22bfc7..87265f9ce82758aecc07abf52db12f7563382062 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt @@ -2,7 +2,6 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector -import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.service.submission.QRScanResult import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ScanStatus @@ -14,7 +13,6 @@ import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.mockk -import io.mockk.mockkObject import org.junit.Assert import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -25,16 +23,12 @@ import testhelpers.extensions.InstantExecutorExtension @ExtendWith(InstantExecutorExtension::class) class SubmissionQRCodeScanViewModelTest : BaseTest() { - @MockK lateinit var backgroundNoise: BackgroundNoise @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var testResultDataCollector: TestResultDataCollector @BeforeEach fun setUp() { MockKAnnotations.init(this) - - mockkObject(BackgroundNoise.Companion) - every { BackgroundNoise.getInstance() } returns backgroundNoise } private fun createViewModel() = SubmissionQRCodeScanViewModel( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/security/EncryptionResetToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/security/EncryptionResetToolTest.kt deleted file mode 100644 index 574b1a009ce5b50486fd72da511a89e6488463fc..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/security/EncryptionResetToolTest.kt +++ /dev/null @@ -1,339 +0,0 @@ -package de.rki.coronawarnapp.util.security - -import android.content.Context -import androidx.core.content.edit -import de.rki.coronawarnapp.exception.CwaSecurityException -import de.rki.coronawarnapp.util.TimeStamper -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -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.BaseIOTest -import testhelpers.preferences.MockSharedPreferences -import java.io.File -import java.io.IOException -import java.security.GeneralSecurityException -import java.security.KeyException -import java.security.KeyStoreException - -class EncryptionResetToolTest : BaseIOTest() { - - @MockK lateinit var context: Context - @MockK lateinit var timeStamper: TimeStamper - private lateinit var mockPreferences: MockSharedPreferences - - private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) - private val privateFilesDir = File(testDir, "files") - private val encryptedPrefsFile = File(testDir, "shared_prefs/shared_preferences_cwa.xml") - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - every { context.filesDir } returns privateFilesDir - - mockPreferences = MockSharedPreferences() - every { - context.getSharedPreferences( - "encryption_error_reset_tool", - Context.MODE_PRIVATE - ) - } returns mockPreferences - - every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234567890L) - } - - @AfterEach - fun teardown() { - testDir.deleteRecursively() - } - - private fun createInstance() = EncryptionErrorResetTool( - context = context, - timeStamper = timeStamper - ) - - private fun createMockFiles() { - encryptedPrefsFile.apply { - parentFile!!.mkdirs() - createNewFile() - exists() shouldBe true - } - } - - @Test - fun `initialiation is sideeffect free`() { - createMockFiles() - - createInstance() - - encryptedPrefsFile.exists() shouldBe true - mockPreferences.dataMapPeek shouldBe emptyMap() - } - - @Test - fun `reset dialog show flag is writable and persisted`() { - val instance = createInstance() - mockPreferences.dataMapPeek["ea1851.reset.shownotice"] shouldBe null - instance.isResetNoticeToBeShown shouldBe false - - instance.isResetNoticeToBeShown = true - mockPreferences.dataMapPeek["ea1851.reset.shownotice"] shouldBe true - instance.isResetNoticeToBeShown shouldBe true - - createInstance().isResetNoticeToBeShown shouldBe true - - instance.isResetNoticeToBeShown = false - mockPreferences.dataMapPeek["ea1851.reset.shownotice"] shouldBe false - instance.isResetNoticeToBeShown shouldBe false - } - - @Test - fun `reset is not warranted by default`() { - createMockFiles() - - createInstance().tryResetIfNecessary(Exception()) - - encryptedPrefsFile.exists() shouldBe true - } - - /** - Based on https://github.com/corona-warn-app/cwa-app-android/issues/642#issuecomment-650199424 - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: java.lang.SecurityException: Could not decrypt value. decryption failed - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at androidx.security.crypto.EncryptedSharedPreferences.getDecryptedObject(EncryptedSharedPreferences.java:33) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at androidx.security.crypto.EncryptedSharedPreferences.getBoolean(EncryptedSharedPreferences.java:1) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at de.rki.coronawarnapp.update.UpdateChecker.checkForUpdate(UpdateChecker.kt:23) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at de.rki.coronawarnapp.update.UpdateChecker$checkForUpdate$1.invokeSuspend(Unknown Source:11) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:2) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:18) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:809) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:102) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at android.os.Looper.loop(Looper.java:166) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:7377) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:469) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:963) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: Caused by: java.security.GeneralSecurityException: decryption failed - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at com.google.crypto.tink.aead.AeadWrapper$WrappedAead.decrypt(AeadWrapper.java:15) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: at androidx.security.crypto.EncryptedSharedPreferences.getDecryptedObject(EncryptedSharedPreferences.java:5) - 06-23 21:52:51.681 10311 17331 17331 E AndroidRuntime: ... 12 more - */ - @Test - fun `reset is warranted if the first exception after upgrade was a GeneralSecurityException`() { - // We only perform the reset for users who encounter it the first time after the upgrade - createMockFiles() - - createInstance().tryResetIfNecessary( - GeneralSecurityException("decryption failed") - ) shouldBe true - - createInstance().tryResetIfNecessary( - GeneralSecurityException("decryption failed") - ) shouldBe false - - encryptedPrefsFile.exists() shouldBe false - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe 1234567890L - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe true - } - } - - @Test - fun `the previous reset attempt from 1_5_0 is ignored`() { - mockPreferences.edit { putBoolean("ea1851.reset.windowconsumed", true) } - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe null - this["ea1851.reset.windowconsumed"] shouldBe true - this["ea1851.reset.windowconsumed.160"] shouldBe null - this["ea1851.reset.shownotice"] shouldBe null - } - - createMockFiles() - - createInstance().tryResetIfNecessary( - GeneralSecurityException("decryption failed") - ) shouldBe true - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe 1234567890L - this["ea1851.reset.windowconsumed"] shouldBe true - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe true - } - } - - @Test - fun `reset is also warranted if the exception has our desired exception as cause`() { - // We only perform the reset for users who encounter it the first time after the upgrade - createMockFiles() - - createInstance().tryResetIfNecessary( - CwaSecurityException(RuntimeException(GeneralSecurityException("decryption failed"))) - ) shouldBe true - - encryptedPrefsFile.exists() shouldBe false - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe 1234567890L - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe true - } - } - - @Test - fun `nested exception may have the same base exception type, ie GeneralSecurityException`() { - // https://github.com/corona-warn-app/cwa-app-android/issues/642#issuecomment-712188157 - createMockFiles() - - createInstance().tryResetIfNecessary( - KeyException( // subclass of GeneralSecurityException - "Permantly failed to instantiate encrypted preferences", - SecurityException( - "Could not decrypt key. decryption failed", - GeneralSecurityException("decryption failed") - ) - ) - ) shouldBe true - - encryptedPrefsFile.exists() shouldBe false - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe 1234567890L - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe true - } - } - - @Test - fun `exception check does not care about the first exception type`() { - createMockFiles() - - createInstance().tryResetIfNecessary( - CwaSecurityException( - KeyException( // subclass of GeneralSecurityException - "Permantly failed to instantiate encrypted preferences", - SecurityException( - "Could not decrypt key. decryption failed", - GeneralSecurityException("decryption failed") - ) - ) - ) - ) shouldBe true - - encryptedPrefsFile.exists() shouldBe false - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe 1234567890L - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe true - } - } - - @Test - fun `exception check DOES care about the most nested exception`() { - createMockFiles() - - createInstance().tryResetIfNecessary( - CwaSecurityException( - KeyException( // subclass of GeneralSecurityException - "Permantly failed to instantiate encrypted preferences", - SecurityException( - "Could not decrypt key. decryption failed", - GeneralSecurityException( - "decryption failed", - IOException("I am unexpeted") - ) - ) - ) - ) - ) shouldBe false - - encryptedPrefsFile.exists() shouldBe true - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe null - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe null - } - } - - @Test - fun `we want only a specific type of GeneralSecurityException`() { - createMockFiles() - - createInstance().tryResetIfNecessary( - GeneralSecurityException("2020 failed") - ) shouldBe false - - encryptedPrefsFile.exists() shouldBe true - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe null - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe null - } - } - - @Test - fun `reset is not warranted for GeneralSecurityException that happened later`() { - createMockFiles() - - createInstance().tryResetIfNecessary(KeyStoreException()) shouldBe false - - createInstance().tryResetIfNecessary( - GeneralSecurityException("decryption failed") - ) shouldBe false - - encryptedPrefsFile.exists() shouldBe true - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe null - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe null - } - } - - @Test - fun `reset is not warranted if the error fits, but there is no existing preference file`() { - encryptedPrefsFile.exists() shouldBe false - - createInstance().tryResetIfNecessary( - GeneralSecurityException("decryption failed") - ) shouldBe false - - encryptedPrefsFile.exists() shouldBe false - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe null - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe null - } - } - - @Test - fun `the reset is considered failed if the preferences can not be deleted`() { - createMockFiles() - encryptedPrefsFile.delete() - encryptedPrefsFile.mkdir() // Can't delete directories with children via `delete()` - File(encryptedPrefsFile, "prevent deletion").createNewFile() - - createInstance().tryResetIfNecessary( - GeneralSecurityException("decryption failed") - ) shouldBe false - - encryptedPrefsFile.exists() shouldBe true - - mockPreferences.dataMapPeek.apply { - this["ea1851.reset.performedAt"] shouldBe null - this["ea1851.reset.windowconsumed.160"] shouldBe true - this["ea1851.reset.shownotice"] shouldBe null - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/security/SecurityHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/security/SecurityHelperTest.kt deleted file mode 100644 index cb3ce3fdd240f7fd67e74f109957c3b789355709..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/security/SecurityHelperTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package de.rki.coronawarnapp.util.security - -import android.content.SharedPreferences -import de.rki.coronawarnapp.util.di.ApplicationComponent -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.verify -import io.mockk.verifySequence -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import testhelpers.BaseTest - -class SecurityHelperTest : BaseTest() { - @MockK - lateinit var appComponent: ApplicationComponent - - @MockK - lateinit var errorResetTool: EncryptionErrorResetTool - - @MockK - lateinit var preferenceFactory: EncryptedPreferencesFactory - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - every { appComponent.errorResetTool } returns errorResetTool - every { appComponent.encryptedPreferencesFactory } returns preferenceFactory - } - - @Test - fun `error free case is sideeffect free`() { - val sharedPreferences: SharedPreferences = mockk() - every { preferenceFactory.create("shared_preferences_cwa") } returns sharedPreferences - - SecurityHelper.encryptedPreferencesProvider(appComponent) shouldBe sharedPreferences - verify(exactly = 0) { errorResetTool.tryResetIfNecessary(any()) } - } - - @Test - fun `positive reset tool results cause a retry`() { - val ourPreferences: SharedPreferences = mockk() - var ourException: Exception? = null - every { preferenceFactory.create("shared_preferences_cwa") } answers { - if (ourException == null) { - ourException = Exception("99 bugs") - throw ourException!! - } else { - ourPreferences - } - } - every { errorResetTool.tryResetIfNecessary(any()) } returns true - - SecurityHelper.encryptedPreferencesProvider(appComponent) shouldBe ourPreferences - - verifySequence { - preferenceFactory.create(any()) - errorResetTool.tryResetIfNecessary(ourException!!) - preferenceFactory.create(any()) - } - } - - @Test - fun `negative reset tool results rethrow the exception`() { - val ourPreferences: SharedPreferences = mockk() - var ourException: Exception? = null - every { preferenceFactory.create("shared_preferences_cwa") } answers { - if (ourException == null) { - ourException = Exception("99 bugs") - throw ourException!! - } else { - ourPreferences - } - } - every { errorResetTool.tryResetIfNecessary(any()) } returns false - - shouldThrow<Exception> { - SecurityHelper.encryptedPreferencesProvider(appComponent) shouldBe ourPreferences - }.cause shouldBe ourException - - verifySequence { - preferenceFactory.create(any()) - errorResetTool.tryResetIfNecessary(ourException!!) - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt index 9de1965602d2351a0fc09046ce922b777f8d790a..2547fd7543ba824ef0c7cb14acfe1036a2ae4343 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt @@ -9,7 +9,7 @@ import de.rki.coronawarnapp.notification.NotificationConstants import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.service.submission.SubmissionService -import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds import de.rki.coronawarnapp.util.TimeStamper @@ -29,12 +29,14 @@ import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.just import io.mockk.mockkObject +import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Instant import org.junit.Before import org.junit.Test import testhelpers.BaseTest +import testhelpers.preferences.mockFlowPreference class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { @MockK lateinit var context: Context @@ -48,6 +50,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool @MockK lateinit var operation: Operation @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var tracingSettings: TracingSettings @RelaxedMockK lateinit var workerParams: WorkerParameters private val currentInstant = Instant.ofEpochSecond(1611764225) private val registrationToken = "test token" @@ -57,18 +60,17 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { MockKAnnotations.init(this) every { submissionSettings.hasViewedTestResult.value } returns false every { timeStamper.nowUTC } returns currentInstant + every { tracingSettings.initialPollingForTestResultTimeStamp } returns currentInstant.millis + every { tracingSettings.isTestResultAvailableNotificationSent } returns false + every { tracingSettings.initialPollingForTestResultTimeStamp = capture(slot()) } answers {} + every { tracingSettings.isTestResultAvailableNotificationSent = capture(slot()) } answers {} mockkObject(AppInjector) every { AppInjector.component } returns appComponent every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory every { appComponent.errorResetTool } returns encryptionErrorResetTool - mockkObject(LocalData) - every { LocalData.registrationToken() } returns registrationToken - every { LocalData.isTestResultAvailableNotificationSent() } returns false - every { LocalData.initialPollingForTestResultTimeStamp() } returns currentInstant.millis - every { LocalData.initialPollingForTestResultTimeStamp(any()) } just Runs - every { LocalData.isTestResultAvailableNotificationSent(any()) } just Runs + every { submissionSettings.registrationToken } returns mockFlowPreference(registrationToken) mockkObject(BackgroundWorkScheduler) every { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } returns operation @@ -82,19 +84,19 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } - assert(result is ListenableWorker.Result.Success) + result shouldBe ListenableWorker.Result.success() } } @Test fun testStopWorkerWhenNotificationSent() { runBlockingTest { - every { LocalData.isTestResultAvailableNotificationSent() } returns true + every { tracingSettings.isTestResultAvailableNotificationSent } returns true val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } verify(exactly = 1) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } - assert(result is ListenableWorker.Result.Success) + result shouldBe ListenableWorker.Result.success() } } @@ -103,7 +105,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { runBlockingTest { val past = currentInstant - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() + 1).daysToMilliseconds() - every { LocalData.initialPollingForTestResultTimeStamp() } returns past.millis + every { tracingSettings.initialPollingForTestResultTimeStamp } returns past.millis val worker = createWorker() val result = worker.doWork() coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } @@ -114,6 +116,11 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { @Test fun testSendNotificationWhenPositive() { + val isTestResultAvailableNotificationSent = slot<Boolean>() + every { + tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) + } answers {} + runBlockingTest { val testResult = TestResult.POSITIVE coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult @@ -126,19 +133,24 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify { LocalData.isTestResultAvailableNotificationSent(true) } coVerify { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } coVerify { notificationHelper.cancelCurrentNotification( NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID ) } - assert(result is ListenableWorker.Result.Success) + result shouldBe ListenableWorker.Result.success() + isTestResultAvailableNotificationSent.captured shouldBe true } } @Test fun testSendNotificationWhenNegative() { + val isTestResultAvailableNotificationSent = slot<Boolean>() + every { + tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) + } answers {} + runBlockingTest { val testResult = TestResult.NEGATIVE coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult @@ -151,19 +163,24 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify { LocalData.isTestResultAvailableNotificationSent(true) } coVerify { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } coVerify { notificationHelper.cancelCurrentNotification( NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID ) } - assert(result is ListenableWorker.Result.Success) + result shouldBe ListenableWorker.Result.success() + isTestResultAvailableNotificationSent.captured shouldBe true } } @Test fun testSendNotificationWhenInvalid() { + val isTestResultAvailableNotificationSent = slot<Boolean>() + every { + tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) + } answers {} + runBlockingTest { val testResult = TestResult.INVALID coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult @@ -176,14 +193,14 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify { LocalData.isTestResultAvailableNotificationSent(true) } coVerify { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } coVerify { notificationHelper.cancelCurrentNotification( NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID ) } - assert(result is ListenableWorker.Result.Success) + result shouldBe ListenableWorker.Result.success() + isTestResultAvailableNotificationSent.captured shouldBe true } } @@ -201,7 +218,6 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { val worker = createWorker() val result = worker.doWork() coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify(exactly = 0) { LocalData.isTestResultAvailableNotificationSent(true) } coVerify(exactly = 0) { testResultAvailableNotificationService.showTestResultAvailableNotification( testResult @@ -213,7 +229,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { ) } coVerify(exactly = 0) { BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() } - assert(result is ListenableWorker.Result.Success) + result shouldBe ListenableWorker.Result.success() } } @@ -236,6 +252,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { notificationHelper, submissionSettings, submissionService, - timeStamper + timeStamper, + tracingSettings ) } diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt b/Corona-Warn-App/src/testShared/java/testhelpers/preferences/MockFlowPreference.kt similarity index 100% rename from Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt rename to Corona-Warn-App/src/testShared/java/testhelpers/preferences/MockFlowPreference.kt diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt b/Corona-Warn-App/src/testShared/java/testhelpers/preferences/MockSharedPreferences.kt similarity index 100% rename from Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt rename to Corona-Warn-App/src/testShared/java/testhelpers/preferences/MockSharedPreferences.kt