Skip to content
Snippets Groups Projects
Unverified Commit c0c2fa30 authored by chris-cwa's avatar chris-cwa Committed by GitHub
Browse files

Converted RetrieveDiagnosisKeysTransaction to Task (Exposureapp-2908) (#1545)


* converted RetrieveDiagnosisKeysTransaction to Task

* cherry picked and solved access to the new task

* fixed background workers

* fixed background workers

* detekt ktlint unit test

* ktlint

* check cancel

* fixed flow

* fixed flow

* fixed flow

* fixed typo

* Make klint happy.

* Little refactoring to address PR comments (DI instead of static access).

Co-authored-by: default avatarharambasicluka <64483219+harambasicluka@users.noreply.github.com>
Co-authored-by: default avatarMatthias Urhahn <matthias.urhahn@sap.com>
parent f8284112
No related branches found
No related tags found
No related merge requests found
Showing
with 422 additions and 1103 deletions
...@@ -2,13 +2,15 @@ package de.rki.coronawarnapp.test ...@@ -2,13 +2,15 @@ package de.rki.coronawarnapp.test
import android.content.Context import android.content.Context
import android.text.format.Formatter import android.text.format.Formatter
import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionFinished
import de.rki.coronawarnapp.exception.reporting.report 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.risk.RiskLevelTask
import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.Task
import de.rki.coronawarnapp.task.common.DefaultTaskRequest 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 de.rki.coronawarnapp.util.di.AppInjector
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
...@@ -39,7 +41,7 @@ class RiskLevelAndKeyRetrievalBenchmark( ...@@ -39,7 +41,7 @@ class RiskLevelAndKeyRetrievalBenchmark(
var resultInfo = StringBuilder() var resultInfo = StringBuilder()
.append( .append(
"MEASUREMENT Running for Countries:\n " + "MEASUREMENT Running for Countries:\n " +
"${countries.joinToString(", ")}\n\n" "${countries.joinToString(", ")}\n\n"
) )
.append("Result: \n\n") .append("Result: \n\n")
.append("#\t Combined \t Download \t Sub \t Risk \t File # \t F. size\n") .append("#\t Combined \t Download \t Sub \t Risk \t File # \t F. size\n")
...@@ -56,20 +58,16 @@ class RiskLevelAndKeyRetrievalBenchmark( ...@@ -56,20 +58,16 @@ class RiskLevelAndKeyRetrievalBenchmark(
var keyFilesSize: Long = -1 var keyFilesSize: Long = -1
var apiSubmissionDuration: Long = -1 var apiSubmissionDuration: Long = -1
try { measureDiagnosticKeyRetrieval(
measureDiagnosticKeyRetrieval( label = "#$index",
label = "#$index", countries = countries,
countries = countries, downloadFinished = { duration, keyCount, totalFileSize ->
downloadFinished = { duration, keyCount, totalFileSize -> keyFileCount = keyCount
keyFileCount = keyCount keyFileDownloadDuration = duration
keyFileDownloadDuration = duration keyFilesSize = totalFileSize
keyFilesSize = totalFileSize }, apiSubmissionFinished = { duration ->
}, apiSubmissionFinished = { duration -> apiSubmissionDuration = duration
apiSubmissionDuration = duration })
})
} catch (e: TransactionException) {
keyRetrievalError = e.message.toString()
}
var calculationDuration: Long = -1 var calculationDuration: Long = -1
var calculationError = "" var calculationError = ""
...@@ -137,32 +135,27 @@ class RiskLevelAndKeyRetrievalBenchmark( ...@@ -137,32 +135,27 @@ class RiskLevelAndKeyRetrievalBenchmark(
var keyFileDownloadStart: Long = -1 var keyFileDownloadStart: Long = -1
var apiSubmissionStarted: Long = -1 var apiSubmissionStarted: Long = -1
try { AppInjector.component.taskController.submitAndListen(
RetrieveDiagnosisKeysTransaction.onKeyFilesDownloadStarted = { DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(countries))
Timber.v("MEASURE [Diagnostic Key Files] $label started") ).collect { progress: Task.Progress ->
keyFileDownloadStart = System.currentTimeMillis() when (progress) {
} is KeyFilesDownloadStarted -> {
Timber.v("MEASURE [Diagnostic Key Files] $label started")
RetrieveDiagnosisKeysTransaction.onKeyFilesDownloadFinished = { count, size -> keyFileDownloadStart = System.currentTimeMillis()
Timber.v("MEASURE [Diagnostic Key Files] $label finished") }
val duration = System.currentTimeMillis() - keyFileDownloadStart is KeyFilesDownloadFinished -> {
downloadFinished(duration, count, size) Timber.v("MEASURE [Diagnostic Key Files] $label finished")
} val duration = System.currentTimeMillis() - keyFileDownloadStart
downloadFinished(duration, progress.keyCount, progress.fileSize)
RetrieveDiagnosisKeysTransaction.onApiSubmissionStarted = { }
apiSubmissionStarted = System.currentTimeMillis() is ApiSubmissionStarted -> {
} apiSubmissionStarted = System.currentTimeMillis()
}
RetrieveDiagnosisKeysTransaction.onApiSubmissionFinished = { is ApiSubmissionFinished -> {
val duration = System.currentTimeMillis() - apiSubmissionStarted val duration = System.currentTimeMillis() - apiSubmissionStarted
apiSubmissionFinished(duration) apiSubmissionFinished(duration)
}
} }
// start diagnostic key transaction
RetrieveDiagnosisKeysTransaction.start(countries)
} catch (e: TransactionException) {
e.report(ExceptionCategory.INTERNAL)
throw e
} }
} }
} }
......
...@@ -9,6 +9,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureInformation ...@@ -9,6 +9,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureInformation
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import de.rki.coronawarnapp.appconfig.RiskCalculationConfig import de.rki.coronawarnapp.appconfig.RiskCalculationConfig
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.exception.reporting.report
...@@ -24,7 +25,7 @@ import de.rki.coronawarnapp.storage.LocalData ...@@ -24,7 +25,7 @@ import de.rki.coronawarnapp.storage.LocalData
import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.RiskLevelRepository
import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskController
import de.rki.coronawarnapp.task.common.DefaultTaskRequest 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.ui.tracing.card.TracingCardStateProvider
import de.rki.coronawarnapp.util.KeyFileHelper import de.rki.coronawarnapp.util.KeyFileHelper
import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
...@@ -76,13 +77,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( ...@@ -76,13 +77,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
} }
fun retrieveDiagnosisKeys() { fun retrieveDiagnosisKeys() {
viewModelScope.launch { launch {
try { taskController.submitBlocking(
RetrieveDiagnosisKeysTransaction.start() DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments())
calculateRiskLevel() )
} catch (e: Exception) { calculateRiskLevel()
e.report(ExceptionCategory.INTERNAL)
}
} }
} }
...@@ -174,9 +173,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( ...@@ -174,9 +173,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor(
"Matched Key Count: ${exposureSummary.matchedKeyCount}\n" + "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" +
"Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" + "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" +
"Attenuation Durations: [${ "Attenuation Durations: [${
exposureSummary.attenuationDurationsInMinutes?.get( exposureSummary.attenuationDurationsInMinutes?.get(
0 0
) )
}," + }," +
"${exposureSummary.attenuationDurationsInMinutes?.get(1)}," + "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," +
"${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" + "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" +
......
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>
}
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
}
}
package de.rki.coronawarnapp.storage 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.ExceptionCategory
import de.rki.coronawarnapp.exception.TransactionException
import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.exception.reporting.report
import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.ENFClient
import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient
...@@ -11,11 +11,12 @@ import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPe ...@@ -11,11 +11,12 @@ import de.rki.coronawarnapp.risk.TimeVariables.getActiveTracingDaysInRetentionPe
import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskController
import de.rki.coronawarnapp.task.TaskInfo import de.rki.coronawarnapp.task.TaskInfo
import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.common.DefaultTaskRequest
import de.rki.coronawarnapp.task.submitBlocking
import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.timer.TimerHelper
import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.tracing.TracingProgress
import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
import de.rki.coronawarnapp.util.ConnectivityHelper import de.rki.coronawarnapp.util.ConnectivityHelper
import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.AppScope
import de.rki.coronawarnapp.util.di.AppContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
...@@ -35,11 +36,11 @@ import javax.inject.Singleton ...@@ -35,11 +36,11 @@ import javax.inject.Singleton
* *
* @see LocalData * @see LocalData
* @see InternalExposureNotificationClient * @see InternalExposureNotificationClient
* @see RetrieveDiagnosisKeysTransaction
* @see RiskLevelRepository * @see RiskLevelRepository
*/ */
@Singleton @Singleton
class TracingRepository @Inject constructor( class TracingRepository @Inject constructor(
@AppContext private val context: Context,
@AppScope private val scope: CoroutineScope, @AppScope private val scope: CoroutineScope,
private val taskController: TaskController, private val taskController: TaskController,
enfClient: ENFClient enfClient: ENFClient
...@@ -88,18 +89,18 @@ class TracingRepository @Inject constructor( ...@@ -88,18 +89,18 @@ class TracingRepository @Inject constructor(
* lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a
* successful go through from the RetrievelDiagnosisKeysTransaction. * successful go through from the RetrievelDiagnosisKeysTransaction.
* *
* @see RetrieveDiagnosisKeysTransaction
* @see RiskLevelRepository * @see RiskLevelRepository
*/ */
fun refreshDiagnosisKeys() { fun refreshDiagnosisKeys() {
scope.launch { scope.launch {
retrievingDiagnosisKeys.value = true retrievingDiagnosisKeys.value = true
try { taskController.submitBlocking(
RetrieveDiagnosisKeysTransaction.start() DefaultTaskRequest(
taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) DownloadDiagnosisKeysTask::class,
} catch (e: Exception) { DownloadDiagnosisKeysTask.Arguments()
e.report(ExceptionCategory.EXPOSURENOTIFICATION) )
} )
taskController.submit(DefaultTaskRequest(RiskLevelTask::class))
refreshLastTimeDiagnosisKeysFetchedDate() refreshLastTimeDiagnosisKeysFetchedDate()
retrievingDiagnosisKeys.value = false retrievingDiagnosisKeys.value = false
TimerHelper.startManualKeyRetrievalTimer() TimerHelper.startManualKeyRetrievalTimer()
...@@ -121,59 +122,53 @@ class TracingRepository @Inject constructor( ...@@ -121,59 +122,53 @@ class TracingRepository @Inject constructor(
/** /**
* Launches the RetrieveDiagnosisKeysTransaction and RiskLevelTransaction in the viewModel scope * Launches the RetrieveDiagnosisKeysTransaction and RiskLevelTransaction in the viewModel scope
* *
* @see RiskLevelTransaction
* @see RiskLevelRepository * @see RiskLevelRepository
*/ */
// TODO temp place, this needs to go somewhere better // TODO temp place, this needs to go somewhere better
fun refreshRiskLevel() { fun refreshRiskLevel() {
scope.launch {
try {
// get the current date and the date the diagnosis keys were fetched the last time // get the current date and the date the diagnosis keys were fetched the last time
val currentDate = DateTime(Instant.now(), DateTimeZone.UTC) val currentDate = DateTime(Instant.now(), DateTimeZone.UTC)
val lastFetch = DateTime( val lastFetch = DateTime(
LocalData.lastTimeDiagnosisKeysFromServerFetch(), LocalData.lastTimeDiagnosisKeysFromServerFetch(),
DateTimeZone.UTC DateTimeZone.UTC
) )
// check if the keys were not already retrieved today // check if the keys were not already retrieved today
val keysWereNotRetrievedToday = val keysWereNotRetrievedToday =
LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || LocalData.lastTimeDiagnosisKeysFromServerFetch() == null ||
currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay() 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)
}
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 // 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
}
} }
} }
......
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)
}
}
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
)
/******************************************************************************
* 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)
}
}
/******************************************************************************
* 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
}
}
package de.rki.coronawarnapp.transaction
/**
* An Interface used by Transactions to define different states during execution.
*
* @see Transaction
*/
interface TransactionState
...@@ -5,8 +5,11 @@ import android.net.wifi.WifiManager ...@@ -5,8 +5,11 @@ import android.net.wifi.WifiManager
import android.os.PowerManager import android.os.PowerManager
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask
import de.rki.coronawarnapp.storage.LocalData 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.util.di.AppContext
import de.rki.coronawarnapp.worker.BackgroundWorkHelper import de.rki.coronawarnapp.worker.BackgroundWorkHelper
import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import de.rki.coronawarnapp.worker.BackgroundWorkScheduler
...@@ -18,7 +21,8 @@ import javax.inject.Singleton ...@@ -18,7 +21,8 @@ import javax.inject.Singleton
@Singleton @Singleton
class WatchdogService @Inject constructor( class WatchdogService @Inject constructor(
@AppContext private val context: Context @AppContext private val context: Context,
private val taskController: TaskController
) { ) {
private val powerManager by lazy { private val powerManager by lazy {
...@@ -41,16 +45,20 @@ class WatchdogService @Inject constructor( ...@@ -41,16 +45,20 @@ class WatchdogService @Inject constructor(
val wakeLock = createWakeLock() val wakeLock = createWakeLock()
// A wifi lock to wake up the wifi connection in case the device is dozing // A wifi lock to wake up the wifi connection in case the device is dozing
val wifiLock = createWifiLock() val wifiLock = createWifiLock()
try { BackgroundWorkHelper.sendDebugNotification(
BackgroundWorkHelper.sendDebugNotification( "Automatic mode is on", "Check if we have downloaded keys already today"
"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( BackgroundWorkHelper.sendDebugNotification(
"RetrieveDiagnosisKeysTransaction failed", "RetrieveDiagnosisKeysTransaction failed",
(e.localizedMessage (state.error?.localizedMessage
?: "Unknown exception occurred in onCreate") + "\n\n" + (e.cause ?: "Unknown exception occurred in onCreate") + "\n\n" + (state.error?.cause
?: "Cause is unknown").toString() ?: "Cause is unknown").toString()
) )
// retry the key retrieval in case of an error with a scheduled work // retry the key retrieval in case of an error with a scheduled work
......
...@@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider ...@@ -10,6 +10,7 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider
import de.rki.coronawarnapp.bugreporting.BugReporter import de.rki.coronawarnapp.bugreporting.BugReporter
import de.rki.coronawarnapp.bugreporting.BugReportingModule import de.rki.coronawarnapp.bugreporting.BugReportingModule
import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule
import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule
import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader import de.rki.coronawarnapp.diagnosiskeys.download.KeyFileDownloader
import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
import de.rki.coronawarnapp.environment.EnvironmentModule import de.rki.coronawarnapp.environment.EnvironmentModule
...@@ -28,7 +29,6 @@ import de.rki.coronawarnapp.submission.SubmissionTaskModule ...@@ -28,7 +29,6 @@ import de.rki.coronawarnapp.submission.SubmissionTaskModule
import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskController
import de.rki.coronawarnapp.task.internal.TaskModule import de.rki.coronawarnapp.task.internal.TaskModule
import de.rki.coronawarnapp.test.DeviceForTestersModule import de.rki.coronawarnapp.test.DeviceForTestersModule
import de.rki.coronawarnapp.transaction.RetrieveDiagnosisInjectionHelper
import de.rki.coronawarnapp.ui.ActivityBinder import de.rki.coronawarnapp.ui.ActivityBinder
import de.rki.coronawarnapp.util.ConnectivityHelperInjection import de.rki.coronawarnapp.util.ConnectivityHelperInjection
import de.rki.coronawarnapp.util.UtilModule import de.rki.coronawarnapp.util.UtilModule
...@@ -63,6 +63,7 @@ import javax.inject.Singleton ...@@ -63,6 +63,7 @@ import javax.inject.Singleton
AppConfigModule::class, AppConfigModule::class,
SubmissionModule::class, SubmissionModule::class,
SubmissionTaskModule::class, SubmissionTaskModule::class,
DownloadDiagnosisKeysTaskModule::class,
VerificationModule::class, VerificationModule::class,
PlaybookModule::class, PlaybookModule::class,
TaskModule::class, TaskModule::class,
...@@ -74,9 +75,6 @@ import javax.inject.Singleton ...@@ -74,9 +75,6 @@ import javax.inject.Singleton
) )
interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> {
// TODO Remove once Singletons are gone
val transRetrieveKeysInjection: RetrieveDiagnosisInjectionHelper
val connectivityHelperInjection: ConnectivityHelperInjection val connectivityHelperInjection: ConnectivityHelperInjection
val settingsRepository: SettingsRepository val settingsRepository: SettingsRepository
......
...@@ -13,3 +13,7 @@ data class CachedString(val provider: (Context) -> String) : LazyString { ...@@ -13,3 +13,7 @@ data class CachedString(val provider: (Context) -> String) : LazyString {
cached = it cached = it
} }
} }
fun String.toLazyString() = object : LazyString {
override fun get(context: Context) = this@toLazyString
}
...@@ -178,10 +178,10 @@ object BackgroundWorkScheduler { ...@@ -178,10 +178,10 @@ object BackgroundWorkScheduler {
/** /**
* Schedule background noise one time work * Schedule background noise one time work
* *
* @see WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK * @see WorkType.BACKGROUND_NOISE_ONE_TIME_WORK
*/ */
fun scheduleBackgroundNoiseOneTimeWork() { fun scheduleBackgroundNoiseOneTimeWork() {
WorkType.DIAGNOSIS_KEY_BACKGROUND_ONE_TIME_WORK.start() WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.start()
} }
/** /**
......
...@@ -5,7 +5,10 @@ import androidx.work.CoroutineWorker ...@@ -5,7 +5,10 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject 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 de.rki.coronawarnapp.util.worker.InjectedWorkerFactory
import timber.log.Timber import timber.log.Timber
...@@ -17,15 +20,14 @@ import timber.log.Timber ...@@ -17,15 +20,14 @@ import timber.log.Timber
*/ */
class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
@Assisted val context: Context, @Assisted val context: Context,
@Assisted workerParams: WorkerParameters @Assisted workerParams: WorkerParameters,
private val taskController: TaskController
) : CoroutineWorker(context, workerParams) { ) : CoroutineWorker(context, workerParams) {
/** /**
* Work execution * Work execution
* *
* @return Result * @return Result
*
* @see RetrieveDiagnosisKeysTransaction
*/ */
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") Timber.d("$id: doWork() started. Run attempt: $runAttemptCount")
...@@ -35,15 +37,18 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( ...@@ -35,15 +37,18 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
) )
var result = Result.success() var result = Result.success()
try { taskController.submitBlocking(
RetrieveDiagnosisKeysTransaction.startWithConstraints() DefaultTaskRequest(
} catch (e: Exception) { DownloadDiagnosisKeysTask::class,
DownloadDiagnosisKeysTask.Arguments(null, true)
)
).error?.also { error: Throwable ->
Timber.w( Timber.w(
e, "$id: Error during RetrieveDiagnosisKeysTransaction.startWithConstraints()." error, "$id: Error during startWithConstraints()."
) )
if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) {
Timber.w(e, "$id: Retry attempts exceeded.") Timber.w(error, "$id: Retry attempts exceeded.")
BackgroundWorkHelper.sendDebugNotification( BackgroundWorkHelper.sendDebugNotification(
"KeyOneTime Executing: Failure", "KeyOneTime Executing: Failure",
...@@ -52,7 +57,7 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( ...@@ -52,7 +57,7 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor(
return Result.failure() return Result.failure()
} else { } else {
Timber.d(e, "$id: Retrying.") Timber.d(error, "$id: Retrying.")
result = Result.retry() result = Result.retry()
} }
} }
......
...@@ -50,7 +50,7 @@ class ScanResultTest { ...@@ -50,7 +50,7 @@ class ScanResultTest {
@Test @Test
fun containsInvalidGUID() { 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) buildQRCodeCases("HTTPS://LOCALHOST//?", guidUpperCase, false)
buildQRCodeCases("HTTPS://LOCALHOST///?", guidUpperCase, false) buildQRCodeCases("HTTPS://LOCALHOST///?", guidUpperCase, false)
......
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")
}
}
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>()
}
}
...@@ -42,7 +42,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { ...@@ -42,7 +42,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() {
// start // start
viewModel.scanStatusValue.value = ScanStatus.STARTED viewModel.scanStatusValue.value = ScanStatus.STARTED
viewModel.scanStatusValue.value shouldBe ScanStatus.STARTED viewModel.scanStatusValue.value shouldBe ScanStatus.STARTED
// valid guid // valid guid
val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
......
...@@ -7,7 +7,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository ...@@ -7,7 +7,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository
import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.ENFClient
import de.rki.coronawarnapp.risk.RiskLevels import de.rki.coronawarnapp.risk.RiskLevels
import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskController
import de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction
import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
...@@ -15,12 +14,10 @@ import io.mockk.Runs ...@@ -15,12 +14,10 @@ import io.mockk.Runs
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.coVerifyOrder
import io.mockk.every import io.mockk.every
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkObject
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
...@@ -47,9 +44,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() { ...@@ -47,9 +44,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() {
fun setup() { fun setup() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
mockkObject(RetrieveDiagnosisKeysTransaction)
coEvery { RetrieveDiagnosisKeysTransaction.start() } returns Unit
coEvery { keyCacheRepository.clear() } returns Unit coEvery { keyCacheRepository.clear() } returns Unit
every { enfClient.internalClient } returns exposureNotificationClient every { enfClient.internalClient } returns exposureNotificationClient
every { tracingCardStateProvider.state } returns flowOf(mockk()) every { tracingCardStateProvider.state } returns flowOf(mockk())
...@@ -74,28 +68,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() { ...@@ -74,28 +68,6 @@ class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() {
taskController = taskController 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 @Test
fun `action clearDiagnosisKeys calls the keyCacheRepo`() { fun `action clearDiagnosisKeys calls the keyCacheRepo`() {
val vm = createViewModel() val vm = createViewModel()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment