diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt index 76a5bb2f119b9ec2556b0d9e9c6d52291b407cb8..6771827fa9cab7f2cc67ea55cdb6a0c537a6f3ec 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt @@ -2,13 +2,15 @@ package de.rki.coronawarnapp.test import android.content.Context import android.text.format.Formatter -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionFinished +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionStarted +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadFinished +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadStarted import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.task.submitAndListen import de.rki.coronawarnapp.util.di.AppInjector import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map @@ -39,7 +41,7 @@ class RiskLevelAndKeyRetrievalBenchmark( var resultInfo = StringBuilder() .append( "MEASUREMENT Running for Countries:\n " + - "${countries.joinToString(", ")}\n\n" + "${countries.joinToString(", ")}\n\n" ) .append("Result: \n\n") .append("#\t Combined \t Download \t Sub \t Risk \t File # \t F. size\n") @@ -56,20 +58,16 @@ class RiskLevelAndKeyRetrievalBenchmark( var keyFilesSize: Long = -1 var apiSubmissionDuration: Long = -1 - try { - measureDiagnosticKeyRetrieval( - label = "#$index", - countries = countries, - downloadFinished = { duration, keyCount, totalFileSize -> - keyFileCount = keyCount - keyFileDownloadDuration = duration - keyFilesSize = totalFileSize - }, apiSubmissionFinished = { duration -> - apiSubmissionDuration = duration - }) - } catch (e: TransactionException) { - keyRetrievalError = e.message.toString() - } + measureDiagnosticKeyRetrieval( + label = "#$index", + countries = countries, + downloadFinished = { duration, keyCount, totalFileSize -> + keyFileCount = keyCount + keyFileDownloadDuration = duration + keyFilesSize = totalFileSize + }, apiSubmissionFinished = { duration -> + apiSubmissionDuration = duration + }) var calculationDuration: Long = -1 var calculationError = "" @@ -137,32 +135,27 @@ class RiskLevelAndKeyRetrievalBenchmark( var keyFileDownloadStart: Long = -1 var apiSubmissionStarted: Long = -1 - try { - RetrieveDiagnosisKeysTransaction.onKeyFilesDownloadStarted = { - Timber.v("MEASURE [Diagnostic Key Files] $label started") - keyFileDownloadStart = System.currentTimeMillis() - } - - RetrieveDiagnosisKeysTransaction.onKeyFilesDownloadFinished = { count, size -> - Timber.v("MEASURE [Diagnostic Key Files] $label finished") - val duration = System.currentTimeMillis() - keyFileDownloadStart - downloadFinished(duration, count, size) - } - - RetrieveDiagnosisKeysTransaction.onApiSubmissionStarted = { - apiSubmissionStarted = System.currentTimeMillis() - } - - RetrieveDiagnosisKeysTransaction.onApiSubmissionFinished = { - val duration = System.currentTimeMillis() - apiSubmissionStarted - apiSubmissionFinished(duration) + AppInjector.component.taskController.submitAndListen( + DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(countries)) + ).collect { progress: Task.Progress -> + when (progress) { + is KeyFilesDownloadStarted -> { + Timber.v("MEASURE [Diagnostic Key Files] $label started") + keyFileDownloadStart = System.currentTimeMillis() + } + is KeyFilesDownloadFinished -> { + Timber.v("MEASURE [Diagnostic Key Files] $label finished") + val duration = System.currentTimeMillis() - keyFileDownloadStart + downloadFinished(duration, progress.keyCount, progress.fileSize) + } + is ApiSubmissionStarted -> { + apiSubmissionStarted = System.currentTimeMillis() + } + is ApiSubmissionFinished -> { + val duration = System.currentTimeMillis() - apiSubmissionStarted + apiSubmissionFinished(duration) + } } - - // start diagnostic key transaction - RetrieveDiagnosisKeysTransaction.start(countries) - } catch (e: TransactionException) { - e.report(ExceptionCategory.INTERNAL) - throw e } } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt index add8047c5e6ceb4138286da92f5ebf02ec1bf4d0..d0a85e7f0f7f54678a5258bb921b8bca8595726b 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -9,6 +9,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureInformation import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.appconfig.RiskCalculationConfig +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report @@ -24,7 +25,7 @@ import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider import de.rki.coronawarnapp.util.KeyFileHelper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -76,13 +77,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( } fun retrieveDiagnosisKeys() { - viewModelScope.launch { - try { - RetrieveDiagnosisKeysTransaction.start() - calculateRiskLevel() - } catch (e: Exception) { - e.report(ExceptionCategory.INTERNAL) - } + launch { + taskController.submitBlocking( + DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments()) + ) + calculateRiskLevel() } } @@ -174,9 +173,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" + "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" + "Attenuation Durations: [${ - exposureSummary.attenuationDurationsInMinutes?.get( - 0 - ) + exposureSummary.attenuationDurationsInMinutes?.get( + 0 + ) }," + "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," + "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" + diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DownloadDiagnosisKeysTaskModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DownloadDiagnosisKeysTaskModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..626496d130889ccf34e081534774ccd3d5f421c6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/DownloadDiagnosisKeysTaskModule.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.diagnosiskeys + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.task.Task +import de.rki.coronawarnapp.task.TaskFactory +import de.rki.coronawarnapp.task.TaskTypeKey + +@Module +abstract class DownloadDiagnosisKeysTaskModule { + + @Binds + @IntoMap + @TaskTypeKey(DownloadDiagnosisKeysTask::class) + abstract fun downloadDiagnosisKeysTaskFactory( + factory: DownloadDiagnosisKeysTask.Factory + ): TaskFactory<out Task.Progress, out Task.Result> +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..552dc370b440abca3609f95419dc10db966cac08 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt @@ -0,0 +1,232 @@ +package de.rki.coronawarnapp.diagnosiskeys.download + +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.risk.RollbackItem +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.task.Task +import de.rki.coronawarnapp.task.TaskCancellationException +import de.rki.coronawarnapp.task.TaskFactory +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.ui.toLazyString +import de.rki.coronawarnapp.worker.BackgroundWorkHelper +import kotlinx.coroutines.channels.ConflatedBroadcastChannel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import org.joda.time.DateTime +import org.joda.time.DateTimeZone +import org.joda.time.Duration +import timber.log.Timber +import java.io.File +import java.util.Date +import java.util.UUID +import javax.inject.Inject +import javax.inject.Provider + +class DownloadDiagnosisKeysTask @Inject constructor( + private val enfClient: ENFClient, + private val environmentSetup: EnvironmentSetup, + private val appConfigProvider: AppConfigProvider, + private val keyFileDownloader: KeyFileDownloader, + private val timeStamper: TimeStamper +) : Task<DownloadDiagnosisKeysTask.Progress, Task.Result> { + + private val internalProgress = ConflatedBroadcastChannel<Progress>() + override val progress: Flow<Progress> = internalProgress.asFlow() + + private var isCanceled = false + + override suspend fun run(arguments: Task.Arguments): Task.Result { + val rollbackItems = mutableListOf<RollbackItem>() + try { + Timber.d("Running with arguments=%s", arguments) + arguments as Arguments + + if (arguments.withConstraints) { + if (!noKeysFetchedToday()) return object : Task.Result {} + } + + /** + * Handles the case when the ENClient got disabled but the Task is still scheduled + * in a background job. Also it acts as a failure catch in case the orchestration code did + * not check in before. + */ + if (!InternalExposureNotificationClient.asyncIsEnabled()) { + Timber.tag(TAG).w("EN is not enabled, skipping RetrieveDiagnosisKeys") + return object : Task.Result {} + } + + checkCancel() + val currentDate = Date(timeStamper.nowUTC.millis) + Timber.tag(TAG).d("Using $currentDate as current date in task.") + + /**************************************************** + * RETRIEVE TOKEN + ****************************************************/ + val token = retrieveToken(rollbackItems) + checkCancel() + + // RETRIEVE RISK SCORE PARAMETERS + val exposureConfiguration = appConfigProvider.getAppConfig().exposureDetectionConfiguration + + internalProgress.send(Progress.ApiSubmissionStarted) + internalProgress.send(Progress.KeyFilesDownloadStarted) + + val requestedCountries = arguments.requestedCountries + val availableKeyFiles = getAvailableKeyFiles(requestedCountries) + checkCancel() + + val totalFileSize = availableKeyFiles.fold(0L, { acc, file -> + file.length() + acc + }) + + internalProgress.send( + Progress.KeyFilesDownloadFinished( + availableKeyFiles.size, + totalFileSize + ) + ) + + Timber.tag(TAG).d("Attempting submission to ENF") + val isSubmissionSuccessful = enfClient.provideDiagnosisKeys( + keyFiles = availableKeyFiles, + configuration = exposureConfiguration, + token = token + ) + Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", isSubmissionSuccessful, token) + + internalProgress.send(Progress.ApiSubmissionFinished) + checkCancel() + + if (isSubmissionSuccessful) { + saveTimestamp(currentDate, rollbackItems) + } + + return object : Task.Result {} + } catch (error: Exception) { + Timber.tag(TAG).e(error) + + rollback(rollbackItems) + + throw error + } finally { + Timber.i("Finished (isCanceled=$isCanceled).") + internalProgress.close() + } + } + + private fun saveTimestamp( + currentDate: Date, + rollbackItems: MutableList<RollbackItem> + ) { + val lastFetchDateForRollback = LocalData.lastTimeDiagnosisKeysFromServerFetch() + rollbackItems.add { + LocalData.lastTimeDiagnosisKeysFromServerFetch(lastFetchDateForRollback) + } + Timber.tag(TAG).d("dateUpdate(currentDate=%s)", currentDate) + LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate) + } + + private fun retrieveToken(rollbackItems: MutableList<RollbackItem>): String { + val googleAPITokenForRollback = LocalData.googleApiToken() + rollbackItems.add { + LocalData.googleApiToken(googleAPITokenForRollback) + } + return UUID.randomUUID().toString().also { + LocalData.googleApiToken(it) + } + } + + private fun noKeysFetchedToday(): Boolean { + val currentDate = DateTime(timeStamper.nowUTC, DateTimeZone.UTC) + val lastFetch = DateTime( + LocalData.lastTimeDiagnosisKeysFromServerFetch(), + DateTimeZone.UTC + ) + return (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || + currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()).also { + if (it) { + Timber.tag(TAG) + .d("No keys fetched today yet (last=%s, now=%s)", lastFetch, currentDate) + BackgroundWorkHelper.sendDebugNotification( + "Start Task", + "No keys fetched today yet \n${DateTime.now()}\nUTC: $currentDate" + ) + } + } + } + + private fun rollback(rollbackItems: MutableList<RollbackItem>) { + try { + Timber.tag(TAG).d("Initiate Rollback") + for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke() + } catch (rollbackException: Exception) { + Timber.tag(TAG).e(rollbackException, "Rollback failed.") + } + } + + private suspend fun getAvailableKeyFiles(requestedCountries: List<String>?): List<File> { + val availableKeyFiles = + keyFileDownloader.asyncFetchKeyFiles(if (environmentSetup.useEuropeKeyPackageFiles) { + listOf("EUR") + } else { + requestedCountries + ?: appConfigProvider.getAppConfig().supportedCountries + }.map { LocationCode(it) }) + + if (availableKeyFiles.isEmpty()) { + Timber.tag(TAG).w("No keyfiles were available!") + } + return availableKeyFiles + } + + private fun checkCancel() { + if (isCanceled) throw TaskCancellationException() + } + + override suspend fun cancel() { + Timber.w("cancel() called.") + isCanceled = true + } + + sealed class Progress : Task.Progress { + object ApiSubmissionStarted : Progress() + object ApiSubmissionFinished : Progress() + + object KeyFilesDownloadStarted : Progress() + data class KeyFilesDownloadFinished(val keyCount: Int, val fileSize: Long) : Progress() + + override val primaryMessage = this::class.java.simpleName.toLazyString() + } + + class Arguments( + val requestedCountries: List<String>? = null, + val withConstraints: Boolean = false + ) : Task.Arguments + + data class Config( + @Suppress("MagicNumber") + override val executionTimeout: Duration = Duration.standardMinutes(8), // TODO unit-test that not > 9 min + + override val collisionBehavior: TaskFactory.Config.CollisionBehavior = + TaskFactory.Config.CollisionBehavior.ENQUEUE + + ) : TaskFactory.Config + + class Factory @Inject constructor( + private val taskByDagger: Provider<DownloadDiagnosisKeysTask> + ) : TaskFactory<Progress, Task.Result> { + + override val config: TaskFactory.Config = Config() + override val taskProvider: () -> Task<Progress, Task.Result> = { + taskByDagger.get() + } + } + + companion object { + private val TAG: String? = DownloadDiagnosisKeysTask::class.simpleName + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt index cfd3f3f408fbc76bc5138c4498e8245be8cebfc4..0961774da36df652d96f19c151f3332eb883f233 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.service.submission -import de.rki.coronawarnapp.exception.NoGUIDOrTANSetException import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.verification.server.VerificationKeyType @@ -16,41 +16,34 @@ object SubmissionService { private val playbook: Playbook get() = AppInjector.component.playbook - suspend fun asyncRegisterDevice() { - val testGUID = LocalData.testGUID() - val testTAN = LocalData.teletan() + private val timeStamper: TimeStamper + get() = TimeStamper() - when { - testGUID != null -> asyncRegisterDeviceViaGUID(testGUID) - testTAN != null -> asyncRegisterDeviceViaTAN(testTAN) - else -> throw NoGUIDOrTANSetException() - } - LocalData.devicePairingSuccessfulTimestamp(System.currentTimeMillis()) - BackgroundNoise.getInstance().scheduleDummyPattern() - } - - private suspend fun asyncRegisterDeviceViaGUID(guid: String) { + suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { val (registrationToken, testResult) = playbook.initialRegistration( guid, VerificationKeyType.GUID ) - LocalData.registrationToken(registrationToken) deleteTestGUID() SubmissionRepository.updateTestResult(testResult) + LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) + BackgroundNoise.getInstance().scheduleDummyPattern() + return testResult } - private suspend fun asyncRegisterDeviceViaTAN(tan: String) { + suspend fun asyncRegisterDeviceViaTAN(tan: String) { val (registrationToken, testResult) = playbook.initialRegistration( tan, VerificationKeyType.TELETAN ) - LocalData.registrationToken(registrationToken) deleteTeleTAN() SubmissionRepository.updateTestResult(testResult) + LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) + BackgroundNoise.getInstance().scheduleDummyPattern() } suspend fun asyncRequestTestResult(): TestResult { 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 b52252c0819b0bdd2bf6ce1355cd8785825a2fa5..86baa1239d015ffc5bddc26f18034b7cd98e9523 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 @@ -1,8 +1,8 @@ package de.rki.coronawarnapp.storage -import de.rki.coronawarnapp.CoronaWarnApplication +import android.content.Context +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient @@ -11,11 +11,12 @@ import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPe import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskInfo import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.tracing.TracingProgress -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -35,11 +36,11 @@ import javax.inject.Singleton * * @see LocalData * @see InternalExposureNotificationClient - * @see RetrieveDiagnosisKeysTransaction * @see RiskLevelRepository */ @Singleton class TracingRepository @Inject constructor( + @AppContext private val context: Context, @AppScope private val scope: CoroutineScope, private val taskController: TaskController, enfClient: ENFClient @@ -88,18 +89,18 @@ class TracingRepository @Inject constructor( * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a * successful go through from the RetrievelDiagnosisKeysTransaction. * - * @see RetrieveDiagnosisKeysTransaction * @see RiskLevelRepository */ fun refreshDiagnosisKeys() { scope.launch { retrievingDiagnosisKeys.value = true - try { - RetrieveDiagnosisKeysTransaction.start() - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) - } catch (e: Exception) { - e.report(ExceptionCategory.EXPOSURENOTIFICATION) - } + taskController.submitBlocking( + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments() + ) + ) + taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) refreshLastTimeDiagnosisKeysFetchedDate() retrievingDiagnosisKeys.value = false TimerHelper.startManualKeyRetrievalTimer() @@ -121,59 +122,53 @@ class TracingRepository @Inject constructor( /** * Launches the RetrieveDiagnosisKeysTransaction and RiskLevelTransaction in the viewModel scope * - * @see RiskLevelTransaction * @see RiskLevelRepository */ // TODO temp place, this needs to go somewhere better fun refreshRiskLevel() { - scope.launch { - try { - // get the current date and the date the diagnosis keys were fetched the last time - val currentDate = DateTime(Instant.now(), DateTimeZone.UTC) - val lastFetch = DateTime( - LocalData.lastTimeDiagnosisKeysFromServerFetch(), - DateTimeZone.UTC - ) + // get the current date and the date the diagnosis keys were fetched the last time + val currentDate = DateTime(Instant.now(), DateTimeZone.UTC) + val lastFetch = DateTime( + LocalData.lastTimeDiagnosisKeysFromServerFetch(), + DateTimeZone.UTC + ) - // check if the keys were not already retrieved today - val keysWereNotRetrievedToday = - LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || - currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay() - - // check if the network is enabled to make the server fetch - val isNetworkEnabled = - ConnectivityHelper.isNetworkEnabled(CoronaWarnApplication.getAppContext()) - - // only fetch the diagnosis keys if background jobs are enabled, so that in manual - // model the keys are only fetched on button press of the user - val isBackgroundJobEnabled = - ConnectivityHelper.autoModeEnabled(CoronaWarnApplication.getAppContext()) - - Timber.tag(TAG) - .v("Keys were not retrieved today $keysWereNotRetrievedToday") - Timber.tag(TAG).v("Network is enabled $isNetworkEnabled") - Timber.tag(TAG) - .v("Background jobs are enabled $isBackgroundJobEnabled") - - if (keysWereNotRetrievedToday && isNetworkEnabled && isBackgroundJobEnabled) { - // TODO shouldn't access this directly - retrievingDiagnosisKeys.value = true - - // start the fetching and submitting of the diagnosis keys - RetrieveDiagnosisKeysTransaction.start() - refreshLastTimeDiagnosisKeysFetchedDate() - TimerHelper.checkManualKeyRetrievalTimer() - } - } catch (e: TransactionException) { - e.cause?.report(ExceptionCategory.INTERNAL) - } catch (e: Exception) { - e.report(ExceptionCategory.INTERNAL) - } + // check if the keys were not already retrieved today + val keysWereNotRetrievedToday = + LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || + currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay() - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) + // check if the network is enabled to make the server fetch + val isNetworkEnabled = ConnectivityHelper.isNetworkEnabled(context) + + // only fetch the diagnosis keys if background jobs are enabled, so that in manual + // model the keys are only fetched on button press of the user + val isBackgroundJobEnabled = ConnectivityHelper.autoModeEnabled(context) + + Timber.tag(TAG).v("Keys were not retrieved today $keysWereNotRetrievedToday") + Timber.tag(TAG).v("Network is enabled $isNetworkEnabled") + Timber.tag(TAG).v("Background jobs are enabled $isBackgroundJobEnabled") + + if (keysWereNotRetrievedToday && isNetworkEnabled && isBackgroundJobEnabled) { // TODO shouldn't access this directly - retrievingDiagnosisKeys.value = false + retrievingDiagnosisKeys.value = true + + // start the fetching and submitting of the diagnosis keys + scope.launch { + taskController.submitBlocking( + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments() + ) + ) + refreshLastTimeDiagnosisKeysFetchedDate() + TimerHelper.checkManualKeyRetrievalTimer() + + taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) + // TODO shouldn't access this directly + retrievingDiagnosisKeys.value = false + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskControllerExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskControllerExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..2166ace118dc16417307a1f2ec6775de7656e69e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskControllerExtensions.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.task + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapMerge +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import timber.log.Timber + +suspend fun TaskController.submitBlocking(ourRequest: TaskRequest) = + tasks.flatMapMerge { it.asFlow() }.map { it.taskState }.onStart { + submit(ourRequest) + Timber.v("submitBlocking(request=%s) waiting for result...", ourRequest) + }.first { + it.request.id == ourRequest.id && it.isFinished + }.also { + Timber.v("submitBlocking(request=%s) continuing with result %s", ourRequest, it) + } + +suspend fun TaskController.submitAndListen(ourRequest: TaskRequest): Flow<Task.Progress> { + submit(ourRequest) + Timber.v("submitAndListen(request=%s) waiting for progress flow...", ourRequest) + + return tasks.flatMapMerge { it.asFlow() }.first { + it.taskState.request.id == ourRequest.id + }.progress.also { + Timber.v("submitAndListen(request=%s) continuing with flow %s", ourRequest, it) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt deleted file mode 100644 index 5d118d7e7e252f580cd9d87ebd0dfb642f9364e7..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisInjectionHelper.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.transaction - -import de.rki.coronawarnapp.environment.EnvironmentSetup -import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.util.GoogleAPIVersion -import javax.inject.Inject -import javax.inject.Singleton - -// TODO Remove once we have refactored the transaction and it's no longer a singleton -@Singleton -data class RetrieveDiagnosisInjectionHelper @Inject constructor( - val transactionScope: TransactionCoroutineScope, - val googleAPIVersion: GoogleAPIVersion, - val cwaEnfClient: ENFClient, - val environmentSetup: EnvironmentSetup -) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt deleted file mode 100644 index 493bb0c9e41d74b398d64b5c83012a72da296d5f..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransaction.kt +++ /dev/null @@ -1,342 +0,0 @@ -/****************************************************************************** - * Corona-Warn-App * - * * - * SAP SE and all other contributors / * - * copyright owners license this file to you under the Apache * - * License, Version 2.0 (the "License"); you may not use this * - * file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ******************************************************************************/ - -package de.rki.coronawarnapp.transaction - -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration -import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader -import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode -import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository -import de.rki.coronawarnapp.environment.EnvironmentSetup -import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.API_SUBMISSION -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.CLOSE -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.FETCH_DATE_UPDATE -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.FILES_FROM_WEB_REQUESTS -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.RETRIEVE_RISK_SCORE_PARAMS -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.SETUP -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.RetrieveDiagnosisKeysTransactionState.TOKEN -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.rollback -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction.start -import de.rki.coronawarnapp.util.CWADebug -import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.worker.BackgroundWorkHelper -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Instant -import timber.log.Timber -import java.io.File -import java.util.Date -import java.util.UUID -import java.util.concurrent.atomic.AtomicReference - -/** - * The RetrieveDiagnosisKeysTransaction is used to define an atomic Transaction for Key Retrieval. Its states allow an - * isolated work area that can recover from failures and keep a consistent key state even through an - * unclear, potentially dangerous state within the transaction itself. It is guaranteed that the keys used in the - * transaction will be successfully retrieved from the Google API and accepted by the server and that the transaction - * has completed its work after completing the [start] coroutine. - * - * It has to be noted that this is not a real, but a transient transaction that does not have an explicit commit stage. - * As such we do not define an execution plan, but rather commit in each transaction state and do a [rollback] based - * on given state. (given that we do not operate on a database layer but on a business logic requiring states to be run - * asynchronously and in a distributed context) - * - * There is currently a simple rollback behavior defined for this transaction. This means that persisted files up until - * that point are deleted. Also, The last fetch date will be rolled back on best effort base and the Google API token - * reset if necessary. - * - * The Transaction undergoes multiple States: - * 1. [SETUP] - * 2. [TOKEN] - * 3. [RETRIEVE_RISK_SCORE_PARAMS] - * 4. [FILES_FROM_WEB_REQUESTS] - * 5. [API_SUBMISSION] - * 6. [FETCH_DATE_UPDATE] - * 7. [CLOSE] - * - * This transaction is special in terms of concurrent entry-calls (e.g. calling the transaction again before it closes and - * releases its internal mutex. The transaction will not queue up like a normal mutex, but instead completely omit the last - * execution. Execution Privilege is First In. - * - * @see Transaction - * - * @throws de.rki.coronawarnapp.exception.TransactionException An Exception thrown when an error occurs during Transaction Execution - * @throws de.rki.coronawarnapp.exception.RollbackException An Exception thrown when an error occurs during Rollback of the Transaction - */ -object RetrieveDiagnosisKeysTransaction : Transaction() { - - override val TAG: String? = RetrieveDiagnosisKeysTransaction::class.simpleName - - /** possible transaction states */ - private enum class RetrieveDiagnosisKeysTransactionState : - TransactionState { - /** Initial Setup of the Transaction and Transaction ID Generation and Date Lock */ - SETUP, - - /** Initialisation of the identifying token used during the entire transaction */ - TOKEN, - - /** Retrieval of Risk Score Parameters used for the Key Submission to the Google API */ - RETRIEVE_RISK_SCORE_PARAMS, - - /** Retrieval of actual Key Files based on the URLs */ - FILES_FROM_WEB_REQUESTS, - - /** Submission of parsed KeyFiles into the Google API */ - API_SUBMISSION, - - /** Update of the Fetch Date to reflect a complete Transaction State */ - FETCH_DATE_UPDATE, - - /** Transaction Closure */ - CLOSE - } - - /** atomic reference for the rollback value for the last fetch date */ - private val lastFetchDateForRollback = AtomicReference<Date>() - - /** atomic reference for the rollback value for the google api */ - private val googleAPITokenForRollback = AtomicReference<String>() - - /** atomic reference for the rollback value for created files during the transaction */ - private val exportFilesForRollback = AtomicReference<List<File>>() - - private val transactionScope: TransactionCoroutineScope by lazy { - AppInjector.component.transRetrieveKeysInjection.transactionScope - } - private val keyCacheRepository: KeyCacheRepository by lazy { - AppInjector.component.keyCacheRepository - } - private val keyFileDownloader: KeyFileDownloader by lazy { - AppInjector.component.keyFileDownloader - } - - var onApiSubmissionStarted: (() -> Unit)? = null - var onApiSubmissionFinished: (() -> Unit)? = null - - var onKeyFilesDownloadStarted: (() -> Unit)? = null - var onKeyFilesDownloadFinished: ((keyCount: Int, fileSize: Long) -> Unit)? = null - - private val enfClient: ENFClient - get() = AppInjector.component.transRetrieveKeysInjection.cwaEnfClient - - private val environmentSetup: EnvironmentSetup - get() = AppInjector.component.transRetrieveKeysInjection.environmentSetup - - suspend fun startWithConstraints() { - val currentDate = DateTime(Instant.now(), DateTimeZone.UTC) - val lastFetch = DateTime( - LocalData.lastTimeDiagnosisKeysFromServerFetch(), - DateTimeZone.UTC - ) - if (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || - currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay() - ) { - Timber.tag(TAG).d("No keys fetched today yet (last=%s, now=%s)", lastFetch, currentDate) - BackgroundWorkHelper.sendDebugNotification( - "Start RetrieveDiagnosisKeysTransaction", - "No keys fetched today yet \n${DateTime.now()}\nUTC: $currentDate" - ) - start() - } - } - - /** initiates the transaction. This suspend function guarantees a successful transaction once completed. - * @param requestedCountries defines which countries (country codes) should be used. If not filled the - * country codes will be loaded from the ApplicationConfigurationService - */ - suspend fun start( - requestedCountries: List<String>? = null - ) = lockAndExecute(unique = true, scope = transactionScope) { - - /** - * Handles the case when the ENClient got disabled but the Transaction is still scheduled - * in a background job. Also it acts as a failure catch in case the orchestration code did - * not check in before. - */ - if (!InternalExposureNotificationClient.asyncIsEnabled()) { - Timber.tag(TAG).w("EN is not enabled, skipping RetrieveDiagnosisKeys") - executeClose() - return@lockAndExecute - } - /**************************************************** - * INIT TRANSACTION - ****************************************************/ - val currentDate = executeSetup() - - /**************************************************** - * RETRIEVE TOKEN - ****************************************************/ - val token = executeToken() - - // RETRIEVE RISK SCORE PARAMETERS - val exposureConfiguration = executeRetrieveRiskScoreParams() - - val countries = if (environmentSetup.useEuropeKeyPackageFiles) { - listOf("EUR") - } else { - requestedCountries - ?: AppInjector.component.appConfigProvider.getAppConfig().supportedCountries - } - invokeSubmissionStartedInDebugOrBuildMode() - - val availableKeyFiles = executeFetchKeyFilesFromServer(countries) - - if (availableKeyFiles.isEmpty()) { - Timber.tag(TAG).w("No keyfiles were available!") - } - - if (CWADebug.isDebugBuildOrMode) { - val totalFileSize = availableKeyFiles.fold(0L, { acc, file -> - file.length() + acc - }) - - onKeyFilesDownloadFinished?.invoke(availableKeyFiles.size, totalFileSize) - onKeyFilesDownloadFinished = null - invokeSubmissionStartedInDebugOrBuildMode() - } - - val isSubmissionSuccessful = executeAPISubmission( - exportFiles = availableKeyFiles, - exposureConfiguration = exposureConfiguration, - token = token - ) - - invokeSubmissionFinishedInDebugOrBuildMode() - - if (isSubmissionSuccessful) executeFetchDateUpdate(currentDate) - - executeClose() - } - - private fun invokeSubmissionStartedInDebugOrBuildMode() { - if (CWADebug.isDebugBuildOrMode) { - onApiSubmissionStarted?.invoke() - onApiSubmissionStarted = null - } - } - - private fun invokeSubmissionFinishedInDebugOrBuildMode() { - if (CWADebug.isDebugBuildOrMode) { - onApiSubmissionFinished?.invoke() - onApiSubmissionFinished = null - } - } - - override suspend fun rollback() { - super.rollback() - try { - if (SETUP.isInStateStack()) { - rollbackSetup() - } - if (TOKEN.isInStateStack()) { - rollbackToken() - } - } catch (e: Exception) { - // We handle every exception through a RollbackException to make sure that a single EntryPoint - // is available for the caller. - handleRollbackError(e) - } - } - - private fun rollbackSetup() { - Timber.tag(TAG).v("rollback $SETUP") - LocalData.lastTimeDiagnosisKeysFromServerFetch(lastFetchDateForRollback.get()) - } - - private fun rollbackToken() { - Timber.tag(TAG).v("rollback $TOKEN") - LocalData.googleApiToken(googleAPITokenForRollback.get()) - } - - /** - * Executes the INIT Transaction State - */ - private suspend fun executeSetup() = executeState(SETUP) { - lastFetchDateForRollback.set(LocalData.lastTimeDiagnosisKeysFromServerFetch()) - val currentDate = Date(System.currentTimeMillis()) - Timber.tag(TAG).d("using $currentDate as current date in Transaction.") - currentDate - } - - /** - * Executes the TOKEN Transaction State - */ - private suspend fun executeToken() = executeState(TOKEN) { - googleAPITokenForRollback.set(LocalData.googleApiToken()) - val tempToken = UUID.randomUUID().toString() - LocalData.googleApiToken(tempToken) - return@executeState tempToken - } - - /** - * Executes the RETRIEVE_RISK_CORE_PARAMS Transaction State - */ - private suspend fun executeRetrieveRiskScoreParams() = - executeState(RETRIEVE_RISK_SCORE_PARAMS) { - AppInjector.component.appConfigProvider.getAppConfig().exposureDetectionConfiguration - } - - /** - * Executes the WEB_REQUESTS Transaction State - */ - private suspend fun executeFetchKeyFilesFromServer( - countries: List<String> - ) = executeState(FILES_FROM_WEB_REQUESTS) { - val locationCodes = countries.map { LocationCode(it) } - keyFileDownloader.asyncFetchKeyFiles(locationCodes) - } - - private suspend fun executeAPISubmission( - token: String, - exportFiles: Collection<File>, - exposureConfiguration: ExposureConfiguration? - ): Boolean = executeState(API_SUBMISSION) { - Timber.tag(TAG).d("Attempting submission to ENF") - val success = enfClient.provideDiagnosisKeys( - keyFiles = exportFiles, - configuration = exposureConfiguration, - token = token - ) - Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", success, token) - return@executeState success - } - - /** - * Executes the FETCH_DATE_UPDATE Transaction State - */ - private suspend fun executeFetchDateUpdate( - currentDate: Date - ) = executeState(FETCH_DATE_UPDATE) { - Timber.tag(TAG).d("executeFetchDateUpdate(currentDate=%s)", currentDate) - LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate) - } - - /** - * Executes the CLOSE Transaction State - */ - private suspend fun executeClose() = executeState(CLOSE) { - exportFilesForRollback.set(null) - lastFetchDateForRollback.set(null) - googleAPITokenForRollback.set(null) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt deleted file mode 100644 index e206e1a2a8b7148c6ae6306fb392e9556571a456..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/Transaction.kt +++ /dev/null @@ -1,289 +0,0 @@ -/****************************************************************************** - * Corona-Warn-App * - * * - * SAP SE and all other contributors / * - * copyright owners license this file to you under the Apache * - * License, Version 2.0 (the "License"); you may not use this * - * file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, * - * software distributed under the License is distributed on an * - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * - * KIND, either express or implied. See the License for the * - * specific language governing permissions and limitations * - * under the License. * - ******************************************************************************/ - -package de.rki.coronawarnapp.transaction - -import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.exception.RollbackException -import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.transaction.Transaction.InternalTransactionStates.INIT -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import kotlinx.coroutines.withTimeout -import timber.log.Timber -import java.util.UUID -import java.util.concurrent.atomic.AtomicReference -import kotlin.coroutines.CoroutineContext -import kotlin.system.measureTimeMillis - -/** - * The Transaction is used to define an internal process that can go through various states. - * It contains a mutex that is used to reference the current coroutine context and also a thread-safe - * Transaction ID that can be used to identify a transaction instance in the entire system. - * - * The Transaction uses an internal State Handling that is defined so that error cases can be caught by state - * - * @throws TransactionException An Exception thrown when an error occurs during Transaction Execution - * @throws RollbackException An Exception might get thrown if rollback behavior is implemented - */ -abstract class Transaction { - - @Suppress("VariableNaming", "PropertyName") // Done as the Convention is TAG for every class - abstract val TAG: String? - - /** - * This is the State Stack that is used inside the Transaction. It is an atomic reference held only by the - * internal transaction and cannot be interacted with directly to ensure atomic operation. - * - * It is modified by executing a state. It will contain the latest state after execution of a state. - * - * @see executeState - * @see resetExecutedStateStack - * @see getExecutedStates - * - * @see finalizeState - * @see setState - * @see currentTransactionState - * @see isInStateStack - */ - private val executedStatesStack: AtomicReference<MutableList<TransactionState>> = - AtomicReference(ArrayList()) - - /** - * Finalizes a state by adding the state to the executedStatesStack - */ - private fun finalizeState() = executedStatesStack.get().add(currentTransactionState.get()) - - /** - * Sets the transaction state and logs the state change. - * - * @param state the new transaction state - */ - private fun setState(state: TransactionState) = - currentTransactionState.set(state) - .also { - Timber.tag(TAG).d("$transactionId - STATE CHANGE: ${currentTransactionState.get()}") - } - - /** - * The atomic Transaction ID that should be set during Transaction Start. Used to identify execution context and errors. - */ - protected val transactionId = AtomicReference<UUID>() - - /** - * The mutual exclusion lock used to handle the lock during the execution across contexts. - */ - private val internalMutualExclusionLock = Mutex() - - /** - * The atomic Transaction State that should be set during Transaction steps. - * It should be updated by the implementing Transaction. - */ - private val currentTransactionState = AtomicReference<TransactionState>() - - /** - * Checks if a given Transaction State is in the current Stack Trace. - * - * @see executedStatesStack - */ - protected fun TransactionState.isInStateStack() = executedStatesStack.get().contains(this) - - /** - * Checks for all already executed states from the state stack. - * - * @return list of all executed states - * @see executedStatesStack - */ - private fun getExecutedStates() = executedStatesStack.get().toList() - - /** - * Resets the state stack. this method needs to be called after successful transaction execution in order to - * not contain any states from a previous transaction - * - * @see executedStatesStack - */ - private fun resetExecutedStateStack() = executedStatesStack.get().clear() - - /** - * Executes a given state and sets it as the active State, then executing the coroutine that should - * be called for this state, and then finalizing the state by adding the state to the executedStatesStack. - * - * Should an error occur during the state execution, an exception can take a look at the currently executed state - * as well as the transaction ID to refer to the concrete Error case. - * - * @param T The generic Return Type used for typing the state return value. - * @param context The context used to spawn the coroutine in - * @param state The state that should be executed and added to the state stack. - * @param block Any function containing the actual Execution Code for that state - * @return The return value of the state, useful for piping to a wrapper or a lock without a message bus or actor - */ - private suspend fun <T> executeState( - context: CoroutineContext, - state: TransactionState, - block: suspend CoroutineScope.() -> T - ): T = withContext(context) { - setState(state) - val result = block.invoke(this) - finalizeState() - return@withContext result - } - - /** - * Convenience method to call for a state execution with the Default Dispatcher. For more details, refer to - * the more detailed executeState that this call wraps around. - * - * @see executeState - * @param T The generic Return Type used for typing the state return value. - * @param state The state that should be executed and added to the state stack. - * @param block Any function containing the actual Execution Code for that state - * @return The return value of the state, useful for piping to a wrapper or a lock without a message bus or actor - */ - protected suspend fun <T> executeState( - state: TransactionState, - block: suspend CoroutineScope.() -> T - ): T = - executeState(Dispatchers.Default, state, block) - - /** - * Attempts to go into the internal lock context (mutual exclusion coroutine) and executes the given suspending - * function. Standard Logging is executed to inform about the transaction status. - * The Lock will run under the Timeout defined under TRANSACTION_TIMEOUT_MS. If the coroutine executed during this - * transaction does not returned within the specified timeout, an error will be thrown. - * - * After invoking the suspending function, the internal state stack will be reset for the next execution. - * - * Inside the given function one should execute executeState() as this will set the Transaction State accordingly - * and allow for atomic rollbacks. - * - * In an error scenario, during the handling of the transaction error, a rollback will be executed on best-effort basis. - * - * @param unique Executes the transaction as Unique. This results in the next execution being omitted in case of a race towards the lock. - * @param block the suspending function that should be used to execute the transaction. - * @param timeout the timeout for the transcation (in milliseconds) - * @throws TransactionException the exception that wraps around any error that occurs inside the lock. - * - * @see executeState - * @see executedStatesStack - */ - suspend fun lockAndExecute( - unique: Boolean = false, - scope: CoroutineScope, - timeout: Long = TimeVariables.getTransactionTimeout(), - block: suspend CoroutineScope.() -> Unit - ) { - - if (unique && internalMutualExclusionLock.isLocked) { - Timber.tag(TAG).w( - "TRANSACTION WITH ID %s ALREADY RUNNING (%s) AS UNIQUE, SKIPPING EXECUTION.", - transactionId, currentTransactionState - ) - return - } - - val deferred = scope.async { - internalMutualExclusionLock.withLock { - executeState(INIT) { transactionId.set(UUID.randomUUID()) } - - val duration = measureTimeMillis { - withTimeout(timeout) { - block.invoke(this) - } - } - - Timber.tag(TAG).i( - "TRANSACTION %s COMPLETED (%d) in %d ms, STATES EXECUTED: %s", - transactionId, System.currentTimeMillis(), duration, getExecutedStates() - ) - - resetExecutedStateStack() - } - } - - withContext(scope.coroutineContext) { - try { - deferred.await() - } catch (e: Exception) { - handleTransactionError(e) - } - } - } - - private enum class InternalTransactionStates : TransactionState { - INIT - } - - /** - * Handles the Transaction Error by performing a rollback, resetting the state stack for consistency and then - * throwing a Transaction Exception with the given error as cause - * - * @throws TransactionException an error containing the cause of the transaction failure as cause, if provided - * - * @param error the error that lead to an error case in the transaction that cannot be handled inside the - * transaction but has to be caught from the exception caller - */ - protected open suspend fun handleTransactionError(error: Throwable): Nothing { - val wrap = TransactionException( - transactionId.get(), - currentTransactionState.toString(), - error - ) - Timber.tag(TAG).e(wrap) - - rollback() - resetExecutedStateStack() - - throw wrap - } - - /** - * Initiates rollback based on the atomic rollback value references inside the transaction and the current stack. - * It is called during the Handling of Transaction Errors and should call handleRollbackError on an error case. - * - * Atomic references towards potential rollback targets need to be kept in the concrete Transaction Implementation, - * as this can differ on a case by case basis. Nevertheless, rollback will always be executed, no matter if an - * override is provided or not - * - * @throws RollbackException throws a rollback exception when handleRollbackError() is called - */ - protected open suspend fun rollback() { - if (BuildConfig.DEBUG) Timber.tag(TAG).d("Initiate Rollback") - } - - /** - * Handles the Rollback Error by throwing a RollbackException with the given error as cause - * - * @throws TransactionException an error containing the cause of the rollback failure as cause, if provided - * - * @param error the error that lead to an error case in the rollback - */ - protected open fun handleRollbackError(error: Throwable?): Nothing { - val wrap = RollbackException( - transactionId.get(), - currentTransactionState.toString(), - error - ) - Timber.tag(TAG).e(wrap) - throw wrap - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionState.kt deleted file mode 100644 index da1f2c072d18f5b9270bda3ba33f6a914c4b81bb..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/transaction/TransactionState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.rki.coronawarnapp.transaction - -/** - * An Interface used by Transactions to define different states during execution. - * - * @see Transaction - */ -interface TransactionState diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt index 1ecb64ba822eb0f531f8cf37d4fc769e464f1925..b9b618c3088a2d80f62d9ac752b85bb911b9a17e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt @@ -21,7 +21,6 @@ import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.CameraPermissionHelper import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject -import de.rki.coronawarnapp.util.observeEvent import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy @@ -32,7 +31,8 @@ import javax.inject.Inject /** * A simple [Fragment] subclass. */ -class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_code_scan), AutoInject { +class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_code_scan), + AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory private val viewModel: SubmissionQRCodeScanViewModel by cwaViewModels { viewModelFactory } @@ -58,22 +58,29 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co binding.submissionQrCodeScanViewfinderView.setCameraPreview(binding.submissionQrCodeScanPreview) - viewModel.scanStatus.observeEvent(viewLifecycleOwner) { - if (ScanStatus.SUCCESS == it) { - viewModel.doDeviceRegistration() - } - + viewModel.scanStatusValue.observe2(this) { if (ScanStatus.INVALID == it) { showInvalidScanDialog() } } + viewModel.showRedeemedTokenWarning.observe2(this) { + val dialog = DialogHelper.DialogInstance( + requireActivity(), + R.string.submission_error_dialog_web_tan_redeemed_title, + R.string.submission_error_dialog_web_tan_redeemed_body, + R.string.submission_error_dialog_web_tan_redeemed_button_positive + ) + + DialogHelper.showDialog(dialog) + goBack() + } + viewModel.registrationState.observe2(this) { binding.submissionQrCodeScanSpinner.visibility = when (it) { ApiRequestState.STARTED -> View.VISIBLE else -> View.GONE } - if (ApiRequestState.SUCCESS == it) { doNavigate( SubmissionQRCodeScanFragmentDirections @@ -98,7 +105,7 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co private fun startDecode() { binding.submissionQrCodeScanPreview.decodeSingle { - viewModel.validateAndStoreTestGUID(it.text) + viewModel.validateTestGUID(it.text) } } @@ -165,13 +172,14 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co ) { // if permission was denied if (requestCode == REQUEST_CAMERA_PERMISSION_CODE && - (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED)) { - if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { - showCameraPermissionRationaleDialog() - } else { - // user permanently denied access to the camera - showCameraPermissionDeniedDialog() - } + (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_DENIED) + ) { + if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showCameraPermissionRationaleDialog() + } else { + // user permanently denied access to the camera + showCameraPermissionDeniedDialog() + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt index b4330c73b3d8596cfcd44f708d814e6e9e786ede..ec4ade17f1b2408ab3fcefd0249b13ec191af7a8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan -import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.exception.ExceptionCategory @@ -9,38 +8,41 @@ import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.service.submission.QRScanResult import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents -import de.rki.coronawarnapp.util.Event +import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import timber.log.Timber -class SubmissionQRCodeScanViewModel @AssistedInject constructor() : CWAViewModel() { - +class SubmissionQRCodeScanViewModel @AssistedInject constructor() : + CWAViewModel() { val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() - private val _scanStatus = MutableLiveData(Event(ScanStatus.STARTED)) + val showRedeemedTokenWarning = SingleLiveEvent<Unit>() + val scanStatusValue = SingleLiveEvent<ScanStatus>() - val scanStatus: LiveData<Event<ScanStatus>> = _scanStatus + open class InvalidQRCodeException : Exception("error in qr code") - fun validateAndStoreTestGUID(rawResult: String) { + fun validateTestGUID(rawResult: String) { val scanResult = QRScanResult(rawResult) if (scanResult.isValid) { - SubmissionService.storeTestGUID(scanResult.guid!!) - _scanStatus.value = Event(ScanStatus.SUCCESS) + scanStatusValue.postValue(ScanStatus.SUCCESS) + doDeviceRegistration(scanResult) } else { - _scanStatus.value = Event(ScanStatus.INVALID) + scanStatusValue.postValue(ScanStatus.INVALID) } } val registrationState = MutableLiveData(ApiRequestState.IDLE) val registrationError = SingleLiveEvent<CwaWebException>() - fun doDeviceRegistration() = launch { + private fun doDeviceRegistration(scanResult: QRScanResult) = launch { try { registrationState.postValue(ApiRequestState.STARTED) - SubmissionService.asyncRegisterDevice() + checkTestResult(SubmissionService.asyncRegisterDeviceViaGUID(scanResult.guid!!)) registrationState.postValue(ApiRequestState.SUCCESS) } catch (err: CwaWebException) { registrationState.postValue(ApiRequestState.FAILED) @@ -52,12 +54,34 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor() : CWAViewModel err.report(ExceptionCategory.INTERNAL) } registrationState.postValue(ApiRequestState.FAILED) + } catch (err: InvalidQRCodeException) { + registrationState.postValue(ApiRequestState.FAILED) + deregisterTestFromDevice() + showRedeemedTokenWarning.postValue(Unit) } catch (err: Exception) { registrationState.postValue(ApiRequestState.FAILED) err.report(ExceptionCategory.INTERNAL) } } + private fun checkTestResult(testResult: TestResult) { + if (testResult == TestResult.REDEEMED) { + throw InvalidQRCodeException() + } + } + + private fun deregisterTestFromDevice() { + launch { + Timber.d("deregisterTestFromDevice()") + SubmissionService.deleteTestGUID() + SubmissionService.deleteRegistrationToken() + LocalData.isAllowedToSubmitDiagnosisKeys(false) + LocalData.initialTestResultReceivedTimestamp(0L) + + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) + } + } + fun onBackPressed() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRInfo) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt index b2fcfe37df70cdd766efa70050c84b350cba7336..c947e09a91c7507945441f918e49abe8306814a2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt @@ -51,7 +51,7 @@ class SubmissionTanViewModel @AssistedInject constructor( launch { try { registrationState.postValue(ApiRequestState.STARTED) - SubmissionService.asyncRegisterDevice() + SubmissionService.asyncRegisterDeviceViaTAN(teletan.value) registrationState.postValue(ApiRequestState.SUCCESS) } catch (err: CwaWebException) { registrationState.postValue(ApiRequestState.FAILED) 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 93ad141a6257c0f04084b90cae08a3463761fcd6..00d62d17067f43ef080520518ba36bf2d4ee5d8f 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 @@ -5,8 +5,11 @@ import android.net.wifi.WifiManager import android.os.PowerManager import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.lifecycleScope +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.worker.BackgroundWorkHelper import de.rki.coronawarnapp.worker.BackgroundWorkScheduler @@ -18,7 +21,8 @@ import javax.inject.Singleton @Singleton class WatchdogService @Inject constructor( - @AppContext private val context: Context + @AppContext private val context: Context, + private val taskController: TaskController ) { private val powerManager by lazy { @@ -41,16 +45,20 @@ class WatchdogService @Inject constructor( val wakeLock = createWakeLock() // A wifi lock to wake up the wifi connection in case the device is dozing val wifiLock = createWifiLock() - try { - BackgroundWorkHelper.sendDebugNotification( - "Automatic mode is on", "Check if we have downloaded keys already today" + BackgroundWorkHelper.sendDebugNotification( + "Automatic mode is on", "Check if we have downloaded keys already today" + ) + val state = taskController.submitBlocking( + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments(null, true) ) - RetrieveDiagnosisKeysTransaction.startWithConstraints() - } catch (e: Exception) { + ) + if (state.isFailed) { BackgroundWorkHelper.sendDebugNotification( "RetrieveDiagnosisKeysTransaction failed", - (e.localizedMessage - ?: "Unknown exception occurred in onCreate") + "\n\n" + (e.cause + (state.error?.localizedMessage + ?: "Unknown exception occurred in onCreate") + "\n\n" + (state.error?.cause ?: "Cause is unknown").toString() ) // retry the key retrieval in case of an error with a scheduled work 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 a866c2998e38b8cf77fd4b8e1af84e9fd5b05456..71c8f4f7a8770fc2cee5a1d104f469741ba72b1b 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 @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.BugReporter import de.rki.coronawarnapp.bugreporting.BugReportingModule import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule +import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.environment.EnvironmentModule @@ -28,7 +29,6 @@ import de.rki.coronawarnapp.submission.SubmissionTaskModule import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.internal.TaskModule import de.rki.coronawarnapp.test.DeviceForTestersModule -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisInjectionHelper import de.rki.coronawarnapp.ui.ActivityBinder import de.rki.coronawarnapp.util.ConnectivityHelperInjection import de.rki.coronawarnapp.util.UtilModule @@ -63,6 +63,7 @@ import javax.inject.Singleton AppConfigModule::class, SubmissionModule::class, SubmissionTaskModule::class, + DownloadDiagnosisKeysTaskModule::class, VerificationModule::class, PlaybookModule::class, TaskModule::class, @@ -74,9 +75,6 @@ import javax.inject.Singleton ) interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { - // TODO Remove once Singletons are gone - val transRetrieveKeysInjection: RetrieveDiagnosisInjectionHelper - val connectivityHelperInjection: ConnectivityHelperInjection val settingsRepository: SettingsRepository diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt index a1f13d193a075b644393c367c7419f89beb67f23..4fd37b49fb823679d637553e239d24b52361c3f0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/ui/LazyString.kt @@ -13,3 +13,7 @@ data class CachedString(val provider: (Context) -> String) : LazyString { cached = it } } + +fun String.toLazyString() = object : LazyString { + override fun get(context: Context) = this@toLazyString +} 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 51ca8ea55de10fe99bcc1a1abe0ba87a9932df66..59631fb84cbdf88ac69cf8dd916eb2412224fb68 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 @@ -178,10 +178,10 @@ object BackgroundWorkScheduler { /** * Schedule background noise one time work * - * @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK + * @see WorkType.BACKGROUND_NOISE_ONE_TIME_WORK */ fun scheduleBackgroundNoiseOneTimeWork() { - WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start() + WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.start() } /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt index 275708013c4ed13dd391f2e78babab668dece30f..ef5f51077634e914630f793378753b17336e4432 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt @@ -5,7 +5,10 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction +import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory import timber.log.Timber @@ -17,15 +20,14 @@ import timber.log.Timber */ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( @Assisted val context: Context, - @Assisted workerParams: WorkerParameters + @Assisted workerParams: WorkerParameters, + private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { /** * Work execution * * @return Result - * - * @see RetrieveDiagnosisKeysTransaction */ override suspend fun doWork(): Result { Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") @@ -35,15 +37,18 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( ) var result = Result.success() - try { - RetrieveDiagnosisKeysTransaction.startWithConstraints() - } catch (e: Exception) { + taskController.submitBlocking( + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments(null, true) + ) + ).error?.also { error: Throwable -> Timber.w( - e, "$id: Error during RetrieveDiagnosisKeysTransaction.startWithConstraints()." + error, "$id: Error during startWithConstraints()." ) if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { - Timber.w(e, "$id: Retry attempts exceeded.") + Timber.w(error, "$id: Retry attempts exceeded.") BackgroundWorkHelper.sendDebugNotification( "KeyOneTime Executing: Failure", @@ -52,7 +57,7 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( return Result.failure() } else { - Timber.d(e, "$id: Retrying.") + Timber.d(error, "$id: Retrying.") result = Result.retry() } } 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 47597832966fcef9f641b812858c7f2ec4cafb3f..6856b0f41124fe28851d80b913a5e61e3b017c3c 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -288,7 +288,7 @@ <!-- XHED: App overview subtitle for glossary contact --> <string name="main_overview_subtitle_glossary_contact">"Risiko-Begegnung"</string> <!-- YTXT: App overview body for glossary contact --> - <string name="main_overview_body_glossary_contact">"Begegnung mit einer infizierten Person, die ihr positives Testergebnis über die CWA mit anderen geteilt hat. Eine Begegnung muss bestimmte Kriterien hinsichtlich Dauer, Abstand und vermuteter Infektiosität der anderen Person erfüllen, um als Risiko-Begegnung eingestuft zu werden."</string> + <string name="main_overview_body_glossary_contact">"Begegnung mit einer infizierten Person, die ihr positives Testergebnis über die App mit anderen geteilt hat. Eine Begegnung muss bestimmte Kriterien hinsichtlich Dauer, Abstand und vermuteter Infektiosität der anderen Person erfüllen, um als Risiko-Begegnung eingestuft zu werden."</string> <!-- XHED: App overview subtitle for glossary notifications --> <string name="main_overview_subtitle_glossary_notification">"Risiko-Benachrichtigung"</string> <!-- YTXT: App overview body for glossary notifications --> @@ -799,9 +799,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"Zurück"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Test fehlerhaft"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"QR-Code nicht mehr gültig"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"Es gab ein Problem bei der Auswertung Ihres Tests. Ihr QR Code ist bereits abgelaufen."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"Ihr Test liegt länger als 21 Tage zurück und kann nicht mehr in der App registriert werden. Bitte stellen Sie bei zukünftigen Tests sicher, dass Sie den QR-Code scannen, sobald er Ihnen vorliegt."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt index e94de291f5bca96a1adefb5d57eade3d92760167..5669835c338bbe4d15683b6a62bcff189e66ed36 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt @@ -50,7 +50,7 @@ class ScanResultTest { @Test fun containsInvalidGUID() { - //extra slashes should be invalid. + // extra slashes should be invalid. buildQRCodeCases("HTTPS:///LOCALHOST/?", guidUpperCase, false) buildQRCodeCases("HTTPS://LOCALHOST//?", guidUpperCase, false) buildQRCodeCases("HTTPS://LOCALHOST///?", guidUpperCase, false) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt index 553c5c3a6c4de1c6550e402b2220e54d1ffa824f..5645c8f4921f046181893fb317d88e457de7b114 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.service.submission -import de.rki.coronawarnapp.exception.NoGUIDOrTANSetException import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.playbook.Playbook @@ -66,13 +65,6 @@ class SubmissionServiceTest { clearAllMocks() } - @Test - fun registerDeviceWithoutTANOrGUIDFails(): Unit = runBlocking { - shouldThrow<NoGUIDOrTANSetException> { - SubmissionService.asyncRegisterDevice() - } - } - @Test fun registrationWithGUIDSucceeds() { every { LocalData.testGUID() } returns guid @@ -89,7 +81,7 @@ class SubmissionServiceTest { every { backgroundNoise.scheduleDummyPattern() } just Runs runBlocking { - SubmissionService.asyncRegisterDevice() + SubmissionService.asyncRegisterDeviceViaGUID(guid) } verify(exactly = 1) { @@ -117,7 +109,7 @@ class SubmissionServiceTest { every { backgroundNoise.scheduleDummyPattern() } just Runs runBlocking { - SubmissionService.asyncRegisterDevice() + SubmissionService.asyncRegisterDeviceViaTAN(guid) } verify(exactly = 1) { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt deleted file mode 100644 index 4cbe2bda9a570500b6b6d56b6b23d3bcf139fc19..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/RetrieveDiagnosisKeysTransactionTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -package de.rki.coronawarnapp.transaction - -import de.rki.coronawarnapp.appconfig.AppConfigProvider -import de.rki.coronawarnapp.appconfig.ConfigData -import de.rki.coronawarnapp.environment.EnvironmentSetup -import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.util.GoogleAPIVersion -import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.util.di.ApplicationComponent -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.clearAllMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.mockk -import io.mockk.mockkObject -import kotlinx.coroutines.runBlocking -import org.joda.time.LocalDate -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import java.io.File -import java.nio.file.Paths -import java.util.Date -import java.util.UUID - -/** - * RetrieveDiagnosisKeysTransaction test. - */ -class RetrieveDiagnosisKeysTransactionTest { - - @MockK lateinit var mockEnfClient: ENFClient - @MockK lateinit var environmentSetup: EnvironmentSetup - @MockK lateinit var configProvider: AppConfigProvider - @MockK lateinit var configData: ConfigData - - @BeforeEach - fun setUp() { - MockKAnnotations.init(this) - - mockkObject(AppInjector) - val appComponent = mockk<ApplicationComponent>().apply { - every { transRetrieveKeysInjection } returns RetrieveDiagnosisInjectionHelper( - TransactionCoroutineScope(), - GoogleAPIVersion(), - mockEnfClient, - environmentSetup - ) - every { appConfigProvider } returns configProvider - } - - coEvery { configProvider.getAppConfig() } returns configData - every { configData.supportedCountries } returns emptyList() - every { configData.exposureDetectionConfiguration } returns mockk() - - every { AppInjector.component } returns appComponent - - mockkObject(InternalExposureNotificationClient) - mockkObject(RetrieveDiagnosisKeysTransaction) - mockkObject(LocalData) - - coEvery { InternalExposureNotificationClient.asyncIsEnabled() } returns true - - every { LocalData.googleApiToken(any()) } just Runs - every { LocalData.lastTimeDiagnosisKeysFromServerFetch() } returns Date() - every { LocalData.lastTimeDiagnosisKeysFromServerFetch(any()) } just Runs - every { LocalData.googleApiToken() } returns UUID.randomUUID().toString() - - every { environmentSetup.useEuropeKeyPackageFiles } returns false - } - - @AfterEach - fun cleanUp() { - clearAllMocks() - } - - @Test - fun `unsuccessful ENF submission`() { - coEvery { mockEnfClient.provideDiagnosisKeys(any(), any(), any()) } returns false - val requestedCountries = listOf("DE") - coEvery { - RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"]( - requestedCountries - ) - } returns listOf<File>() - - runBlocking { - RetrieveDiagnosisKeysTransaction.start(requestedCountries) - } - - coVerifyOrder { - RetrieveDiagnosisKeysTransaction["executeSetup"]() - RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]() - RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"]( - requestedCountries - ) - } - coVerify(exactly = 0) { - RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>()) - } - } - - @Test - fun `successful submission`() { - val file = Paths.get("src", "test", "resources", "keys.bin").toFile() - coEvery { mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any()) } returns true - val requestedCountries = listOf("DE") - - coEvery { - RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"]( - requestedCountries - ) - } returns listOf(file) - - runBlocking { - RetrieveDiagnosisKeysTransaction.start(requestedCountries) - } - - coVerifyOrder { - RetrieveDiagnosisKeysTransaction["executeSetup"]() - RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]() - RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"]( - requestedCountries - ) - mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any()) - RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>()) - } - } - - @Test - fun `successful submission with EUR`() { - every { environmentSetup.useEuropeKeyPackageFiles } returns true - val file = Paths.get("src", "test", "resources", "keys.bin").toFile() - coEvery { mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any()) } returns true - val requestedCountries = listOf("EUR") - - coEvery { - RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"]( - requestedCountries - ) - } returns listOf(file) - - runBlocking { - RetrieveDiagnosisKeysTransaction.start(requestedCountries) - } - - coVerifyOrder { - RetrieveDiagnosisKeysTransaction["executeSetup"]() - RetrieveDiagnosisKeysTransaction["executeRetrieveRiskScoreParams"]() - RetrieveDiagnosisKeysTransaction["executeFetchKeyFilesFromServer"]( - requestedCountries - ) - mockEnfClient.provideDiagnosisKeys(listOf(file), any(), any()) - RetrieveDiagnosisKeysTransaction["executeFetchDateUpdate"](any<Date>()) - } - } - - @Test - fun `conversion from date to localdate`() { - LocalDate.fromDateFields(Date(0)) shouldBe LocalDate.parse("1970-01-01") - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt deleted file mode 100644 index 5ba62a995d754feedd5966522f036871c2f0da84..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/transaction/TransactionTest.kt +++ /dev/null @@ -1,113 +0,0 @@ -package de.rki.coronawarnapp.transaction - -import de.rki.coronawarnapp.exception.RollbackException -import de.rki.coronawarnapp.exception.TransactionException -import de.rki.coronawarnapp.risk.TimeVariables -import io.kotest.assertions.throwables.shouldThrow -import io.kotest.matchers.should -import io.kotest.matchers.types.beInstanceOf -import io.mockk.clearAllMocks -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockkObject -import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.delay -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.TestCoroutineScope -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import testhelpers.BaseTest -import java.io.IOException - -@ExperimentalCoroutinesApi -class TransactionTest : BaseTest() { - - @BeforeEach - fun setup() { - mockkObject(TimeVariables) - } - - @AfterEach - fun tearDown() { - clearAllMocks() - } - - @Suppress("UNREACHABLE_CODE") - private class TestTransaction( - val errorOnRollBack: Exception? = null - ) : Transaction() { - override val TAG: String = "TestTag" - - public override suspend fun rollback() { - errorOnRollBack?.let { handleRollbackError(it) } - super.rollback() - } - - public override suspend fun handleTransactionError(error: Throwable): Nothing { - return super.handleTransactionError(error) - } - - public override fun handleRollbackError(error: Throwable?): Nothing { - return super.handleRollbackError(error) - } - } - - @Test - fun `transaction error handler is called`() { - val testScope = TestCoroutineScope() - val testTransaction = spyk(TestTransaction()) - shouldThrow<TransactionException> { - runBlocking { - testTransaction.lockAndExecute(scope = testScope) { - throw IOException() - } - } - } - - coVerify { testTransaction.handleTransactionError(any()) } - coVerify { testTransaction.rollback() } - } - - @Test - fun `rollback error handler is called`() { - val testScope = TestCoroutineScope() - val testTransaction = spyk( - TestTransaction( - errorOnRollBack = IllegalAccessException() - ) - ) - shouldThrow<RollbackException> { - runBlocking { - testTransaction.lockAndExecute(scope = testScope) { - throw IOException() - } - } - } - - coVerify { testTransaction.handleTransactionError(ofType<IOException>()) } - coVerify { testTransaction.rollback() } - coVerify { testTransaction.handleRollbackError(ofType<IllegalAccessException>()) } - } - - @Test - fun `transactions can timeout`() { - /** - * TODO use runBlockingTest & advanceTime, which currently does not work - * https://github.com/Kotlin/kotlinx.coroutines/issues/1204 - */ - every { TimeVariables.getTransactionTimeout() } returns 0L - - val testTransaction = TestTransaction() - val exception = shouldThrow<TransactionException> { - runBlocking { - testTransaction.lockAndExecute(scope = this) { - delay(TimeVariables.getTransactionTimeout()) - } - } - } - exception.cause should beInstanceOf<TimeoutCancellationException>() - } -} 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 b518ae31a0ba845187e3889b415605b4507da462..05edd8a920a6bdf01c7796b68d25d38d59c05243 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 @@ -40,15 +40,17 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { val viewModel = createViewModel() // start - viewModel.scanStatus.value!!.getContent() shouldBe ScanStatus.STARTED + viewModel.scanStatusValue.value = ScanStatus.STARTED + + viewModel.scanStatusValue.value shouldBe ScanStatus.STARTED // valid guid val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" - viewModel.validateAndStoreTestGUID("https://localhost/?$guid") - viewModel.scanStatus.value?.getContent().let { Assert.assertEquals(ScanStatus.SUCCESS, it) } + viewModel.validateTestGUID("https://localhost/?$guid") + viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.SUCCESS, it.value) } // invalid guid - viewModel.validateAndStoreTestGUID("https://no-guid-here") - viewModel.scanStatus.value?.getContent().let { Assert.assertEquals(ScanStatus.INVALID, it) } + viewModel.validateTestGUID("https://no-guid-here") + viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.INVALID, it.value) } } } diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt index e626c3e92891f7570e0a386fd23749db6e8a44cb..0413a8515b6db83ef95f76efb6e8af70b46238e7 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt @@ -7,7 +7,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.risk.RiskLevels import de.rki.coronawarnapp.task.TaskController -import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -15,12 +14,10 @@ import io.mockk.Runs import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -47,9 +44,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - mockkObject(RetrieveDiagnosisKeysTransaction) - coEvery { RetrieveDiagnosisKeysTransaction.start() } returns Unit - coEvery { keyCacheRepository.clear() } returns Unit every { enfClient.internalClient } returns exposureNotificationClient every { tracingCardStateProvider.state } returns flowOf(mockk()) @@ -74,28 +68,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() { taskController = taskController ) - @Test - fun `action retrieveDiagnosisKeys, retieves diagnosis keys and calls risklevel calculation`() { - val vm = createViewModel() - - vm.retrieveDiagnosisKeys() - - coVerifyOrder { - RetrieveDiagnosisKeysTransaction.start() - taskController.submit(any()) - } - } - - @Test - fun `action calculateRiskLevel, calls risklevel calculation`() { - val vm = createViewModel() - - vm.calculateRiskLevel() - - coVerify(exactly = 1) { taskController.submit(any()) } - coVerify(exactly = 0) { RetrieveDiagnosisKeysTransaction.start() } - } - @Test fun `action clearDiagnosisKeys calls the keyCacheRepo`() { val vm = createViewModel()