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 4016e191ea1d74a6b97295354813b4d5ece1896a..6856b0f41124fe28851d80b913a5e61e3b017c3c 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -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()