diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index f8ac9f20dbc3afc5118bb6e262863d392401a67c..92111156ce7451c297cd779ec6333f70f9d2ee0d 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk.storage import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.CoroutineScope @@ -13,8 +14,14 @@ import javax.inject.Singleton class DefaultRiskLevelStorage @Inject constructor( riskResultDatabaseFactory: RiskResultDatabase.Factory, presenceTracingRiskRepository: PresenceTracingRiskRepository, - @AppScope scope: CoroutineScope -) : BaseRiskLevelStorage(riskResultDatabaseFactory, presenceTracingRiskRepository, scope) { + @AppScope scope: CoroutineScope, + riskCombinator: RiskCombinator, +) : BaseRiskLevelStorage( + riskResultDatabaseFactory, + presenceTracingRiskRepository, + scope, + riskCombinator +) { // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation // Taken from TimeVariables.MAX_STALE_EXPOSURE_RISK_RANGE diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 7fbb56a67d86ed6d29371dec56f158905536068e..23a0450fef17a9260ddcd2b06328d3501d15b7c8 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk.storage import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow @@ -17,8 +18,14 @@ import javax.inject.Singleton class DefaultRiskLevelStorage @Inject constructor( riskResultDatabaseFactory: RiskResultDatabase.Factory, presenceTracingRiskRepository: PresenceTracingRiskRepository, - @AppScope val scope: CoroutineScope -) : BaseRiskLevelStorage(riskResultDatabaseFactory, presenceTracingRiskRepository, scope) { + @AppScope val scope: CoroutineScope, + riskCombinator: RiskCombinator, +) : BaseRiskLevelStorage( + riskResultDatabaseFactory, + presenceTracingRiskRepository, + scope, + riskCombinator, +) { // 14 days, 6 times per day // For testers keep all the results! diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt index 1e2a96561f7506bb77d98c6e253feececd26b436..304aa75d3599ce225c70b2eaddd3ed199d84e596 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt @@ -1,8 +1,7 @@ package de.rki.coronawarnapp.risk import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult -import de.rki.coronawarnapp.risk.storage.combine -import de.rki.coronawarnapp.risk.storage.max +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.Instant import org.joda.time.LocalDate @@ -18,7 +17,7 @@ data class CombinedEwPtRiskLevelResult( ) { val riskState: RiskState by lazy { - combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) + RiskCombinator.combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) } val wasSuccessfullyCalculated: Boolean by lazy { @@ -71,3 +70,13 @@ data class LastCombinedRiskResults( val lastCalculated: CombinedEwPtRiskLevelResult, val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult ) + +internal fun max(left: Instant, right: Instant): Instant { + return Instant.ofEpochMilli(kotlin.math.max(left.millis, right.millis)) +} + +internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? { + if (left == null) return right + if (right == null) return left + return if (left.isAfter(right)) left else right +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index af277a073144c8f1a9aa075768c14e2c18a4013c..0582cc3c9579abb25f40c3ecb13a79bbd617f2c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -1,7 +1,5 @@ package de.rki.coronawarnapp.risk.storage -import androidx.annotation.VisibleForTesting -import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk @@ -11,10 +9,8 @@ import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.LastCombinedRiskResults -import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.mapToRiskState -import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedAggregatedRiskPerDateResult @@ -26,17 +22,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import org.joda.time.Instant -import org.joda.time.LocalDate import timber.log.Timber -import java.lang.reflect.Modifier.PRIVATE -import kotlin.math.max import de.rki.coronawarnapp.util.flow.combine as flowCombine abstract class BaseRiskLevelStorage constructor( private val riskResultDatabaseFactory: RiskResultDatabase.Factory, private val presenceTracingRiskRepository: PresenceTracingRiskRepository, - scope: CoroutineScope + scope: CoroutineScope, + private val riskCombinator: RiskCombinator, ) : RiskLevelStorage { private val database by lazy { riskResultDatabaseFactory.create() } @@ -179,7 +172,7 @@ abstract class BaseRiskLevelStorage constructor( ptDayRiskStates, ewDayRiskStates ) { ptRiskList, ewRiskList -> - combineRisk(ptRiskList, ewRiskList) + riskCombinator.combineRisk(ptRiskList, ewRiskList) } override val latestAndLastSuccessfulEwRiskLevelResult: Flow<List<EwRiskLevelResult>> = riskResultsTables @@ -197,12 +190,14 @@ abstract class BaseRiskLevelStorage constructor( presenceTracingRiskRepository.allEntries() ) { ewRiskLevelResults, ptRiskLevelResults -> - val combinedResults = combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + val combinedResults = riskCombinator.combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) .sortedByDescending { it.calculatedAt } LastCombinedRiskResults( - lastCalculated = combinedResults.firstOrNull() ?: currentCombinedLowRisk, - lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: initialCombined + lastCalculated = combinedResults.firstOrNull() ?: riskCombinator.latestCombinedResult, + lastSuccessfullyCalculated = combinedResults.find { + it.wasSuccessfullyCalculated + } ?: riskCombinator.initialCombinedResult ) } @@ -217,7 +212,7 @@ abstract class BaseRiskLevelStorage constructor( latestEwRiskLevelResults, latestPtRiskLevelResults ) { ewRiskLevelResults, ptRiskLevelResults -> - combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + riskCombinator.combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) .sortedByDescending { it.calculatedAt } .take(2) } @@ -236,105 +231,3 @@ abstract class BaseRiskLevelStorage constructor( private const val TAG = "RiskLevelStorage" } } - -@VisibleForTesting(otherwise = PRIVATE) -internal fun combineRisk( - ptRiskList: List<PresenceTracingDayRisk>, - exposureWindowDayRiskList: List<ExposureWindowDayRisk> -): List<CombinedEwPtDayRisk> { - val allDates = ptRiskList.map { it.localDateUtc }.plus(exposureWindowDayRiskList.map { it.localDateUtc }).distinct() - return allDates.map { date -> - val ptRisk = ptRiskList.find { it.localDateUtc == date } - val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date } - CombinedEwPtDayRisk( - date, - combine( - ptRisk?.riskState, - ewRisk?.riskLevel?.mapToRiskState() - ) - ) - } -} - -internal fun combine(vararg states: RiskState?): RiskState { - if (states.any { it == RiskState.CALCULATION_FAILED }) return RiskState.CALCULATION_FAILED - if (states.any { it == RiskState.INCREASED_RISK }) return RiskState.INCREASED_RISK - - require(states.filterNotNull().all { it == RiskState.LOW_RISK }) - - return RiskState.LOW_RISK -} - -internal fun max(left: Instant, right: Instant): Instant { - return Instant.ofEpochMilli(max(left.millis, right.millis)) -} - -internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? { - if (left == null) return right - if (right == null) return left - return if (left.isAfter(right)) left - else right -} - -@VisibleForTesting(otherwise = PRIVATE) -internal fun combineEwPtRiskLevelResults( - ptRiskResults: List<PtRiskLevelResult>, - ewRiskResults: List<EwRiskLevelResult> -): List<CombinedEwPtRiskLevelResult> { - val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct() - val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt } - val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt } - return allDates.map { date -> - val ptRisk = sortedPtResults.find { it.calculatedAt <= date } ?: ptInitialRiskLevelResult - val ewRisk = sortedEwResults.find { it.calculatedAt <= date } ?: EwInitialRiskLevelResult - CombinedEwPtRiskLevelResult( - ptRisk, - ewRisk - ) - } -} - -private object EwInitialRiskLevelResult : EwRiskLevelResult { - override val calculatedAt: Instant = Instant.EPOCH - override val riskState: RiskState = RiskState.CALCULATION_FAILED - override val failureReason: EwRiskLevelResult.FailureReason? = null - override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null - override val exposureWindows: List<ExposureWindow>? = null - override val matchedKeyCount: Int = 0 - override val daysWithEncounters: Int = 0 -} - -private val ptInitialRiskLevelResult: PtRiskLevelResult by lazy { - PtRiskLevelResult( - calculatedAt = Instant.EPOCH, - riskState = RiskState.CALCULATION_FAILED - ) -} - -private val ewCurrentLowRiskLevelResult - get() = object : EwRiskLevelResult { - override val calculatedAt: Instant = Instant.now() - override val riskState: RiskState = RiskState.LOW_RISK - override val failureReason: EwRiskLevelResult.FailureReason? = null - override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null - override val exposureWindows: List<ExposureWindow>? = null - override val matchedKeyCount: Int = 0 - override val daysWithEncounters: Int = 0 - } - -private val ptCurrentLowRiskLevelResult: PtRiskLevelResult - get() = PtRiskLevelResult( - calculatedAt = Instant.now(), - riskState = RiskState.LOW_RISK - ) - -private val initialCombined = CombinedEwPtRiskLevelResult( - ptInitialRiskLevelResult, - EwInitialRiskLevelResult -) - -private val currentCombinedLowRisk: CombinedEwPtRiskLevelResult - get() = CombinedEwPtRiskLevelResult( - ptCurrentLowRiskLevelResult, - ewCurrentLowRiskLevelResult - ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt new file mode 100644 index 0000000000000000000000000000000000000000..13aa62d91979693e113b43d430cfd340c44aa13b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt @@ -0,0 +1,117 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import dagger.Reusable +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult +import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.mapToRiskState +import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult +import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk +import de.rki.coronawarnapp.util.TimeStamper +import org.joda.time.Instant +import javax.inject.Inject + +@Reusable +class RiskCombinator @Inject constructor( + private val timeStamper: TimeStamper +) { + + private val initialEWRiskLevelResult = object : EwRiskLevelResult { + override val calculatedAt: Instant = Instant.EPOCH + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + private val initialPTRiskLevelResult: PtRiskLevelResult = PtRiskLevelResult( + calculatedAt = Instant.EPOCH, + riskState = RiskState.LOW_RISK + ) + + internal val initialCombinedResult = CombinedEwPtRiskLevelResult( + ptRiskLevelResult = initialPTRiskLevelResult, + ewRiskLevelResult = initialEWRiskLevelResult + ) + + private val ewCurrentLowRiskLevelResult + get() = object : EwRiskLevelResult { + override val calculatedAt: Instant = timeStamper.nowUTC + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + private val ptCurrentLowRiskLevelResult: PtRiskLevelResult + get() = PtRiskLevelResult( + calculatedAt = timeStamper.nowUTC, + riskState = RiskState.LOW_RISK + ) + + internal val latestCombinedResult: CombinedEwPtRiskLevelResult + get() = CombinedEwPtRiskLevelResult( + ptCurrentLowRiskLevelResult, + ewCurrentLowRiskLevelResult + ) + + internal fun combineEwPtRiskLevelResults( + ptRiskResults: List<PtRiskLevelResult>, + ewRiskResults: List<EwRiskLevelResult> + ): List<CombinedEwPtRiskLevelResult> { + val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct() + val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt } + val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt } + return allDates.map { date -> + val ptRisk = sortedPtResults.find { + it.calculatedAt <= date + } ?: initialPTRiskLevelResult + val ewRisk = sortedEwResults.find { + it.calculatedAt <= date + } ?: initialEWRiskLevelResult + + CombinedEwPtRiskLevelResult( + ptRiskLevelResult = ptRisk, + ewRiskLevelResult = ewRisk + ) + } + } + + internal fun combineRisk( + ptRiskList: List<PresenceTracingDayRisk>, + exposureWindowDayRiskList: List<ExposureWindowDayRisk> + ): List<CombinedEwPtDayRisk> { + val allDates = + ptRiskList.map { it.localDateUtc }.plus(exposureWindowDayRiskList.map { it.localDateUtc }).distinct() + return allDates.map { date -> + val ptRisk = ptRiskList.find { it.localDateUtc == date } + val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date } + CombinedEwPtDayRisk( + localDate = date, + riskState = combine( + ptRisk?.riskState, + ewRisk?.riskLevel?.mapToRiskState() + ) + ) + } + } + + companion object { + fun combine(vararg states: RiskState?): RiskState { + if (states.any { it == RiskState.CALCULATION_FAILED }) return RiskState.CALCULATION_FAILED + if (states.any { it == RiskState.INCREASED_RISK }) return RiskState.INCREASED_RISK + + require(states.filterNotNull().all { it == RiskState.LOW_RISK }) + + return RiskState.LOW_RISK + } + } +} 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 6f9fdeed6e2231cc1f4f7bba4c3ea874a405d299..b69db70bb6c049c1119e8f3313ca2157039a49c2 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 @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.lastSubmission import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTaskProgress import de.rki.coronawarnapp.risk.RiskLevelTask +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskInfo import de.rki.coronawarnapp.task.common.DefaultTaskRequest @@ -45,7 +46,8 @@ class TracingRepository @Inject constructor( enfClient: ENFClient, private val timeStamper: TimeStamper, private val exposureDetectionTracker: ExposureDetectionTracker, - private val backgroundModeStatus: BackgroundModeStatus + private val backgroundModeStatus: BackgroundModeStatus, + private val riskWorkScheduler: RiskWorkScheduler, ) { @SuppressLint("BinaryOperationInTimber") @@ -91,29 +93,17 @@ class TracingRepository @Inject constructor( it.taskState.isActive && it.taskState.request.type == RiskLevelTask::class } - /** - * Refresh the diagnosis keys. For that isRefreshing is set to true which is displayed in the ui. - * Afterwards the RetrieveDiagnosisKeysTransaction and the RiskLevelTransaction are started. - * Regardless of whether the transactions where successful or not the - * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a - * successful go through from the RetrievelDiagnosisKeysTransaction. - */ - fun refreshDiagnosisKeys() { - scope.launch { - taskController.submitBlocking( - DefaultTaskRequest( - DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments(), - originTag = "TracingRepository.refreshDiagnosisKeys()" - ) - ) - taskController.submit( - DefaultTaskRequest( - RiskLevelTask::class, - originTag = "TracingRepository.refreshDiagnosisKeys()" - ) + fun refreshRiskResult() = scope.launch { + Timber.tag(TAG).d("refreshRiskResults()") + + riskWorkScheduler.runRiskTasksNow() + + taskController.submit( + DefaultTaskRequest( + RiskLevelTask::class, + originTag = "TracingRepository.refreshRiskResult()" ) - } + ) } /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt index 5aa675aae2f4b9182fce26433476fb2427f97a6c..55e4b09d7ab90a2ba39ba30e9feec61482065b57 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt @@ -30,19 +30,19 @@ class TracingStateProvider @AssistedInject constructor( ) { val state: Flow<TracingState> = combine( tracingStatus.generalStatus.onEach { - Timber.v("tracingStatus: $it") + Timber.tag(TAG).v("tracingStatus: $it") }, tracingRepository.tracingProgress.onEach { - Timber.v("tracingProgress: $it") + Timber.tag(TAG).v("tracingProgress: $it") }, riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.onEach { - Timber.v("riskLevelResults: $it") + Timber.tag(TAG).v("riskLevelResults: $it") }, exposureDetectionTracker.latestSubmission().onEach { - Timber.v("latestSubmission: $it") + Timber.tag(TAG).v("latestSubmission: $it") }, backgroundModeStatus.isAutoModeEnabled.onEach { - Timber.v("isAutoModeEnabled: $it") + Timber.tag(TAG).v("isAutoModeEnabled: $it") } ) { tracingStatus, tracingProgress, @@ -88,12 +88,16 @@ class TracingStateProvider @AssistedInject constructor( ) } } - .onStart { Timber.v("TracingStateProvider FLOW start") } - .onEach { Timber.d("TracingStateProvider FLOW emission: %s", it) } - .onCompletion { Timber.v("TracingStateProvider FLOW completed.") } + .onStart { Timber.tag(TAG).v("TracingStateProvider FLOW start") } + .onEach { Timber.tag(TAG).d("TracingStateProvider FLOW emission: %s", it) } + .onCompletion { Timber.tag(TAG).v("TracingStateProvider FLOW completed.") } @AssistedFactory interface Factory { fun create(isDetailsMode: Boolean): TracingStateProvider } + + companion object { + const val TAG = "TracingStateProvider" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt index dd52cdbc939d76e6c95e680b4982fe33ebd93500..1fbb2d1f2d414fbf604956c3d9ade6e13fa774e3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt @@ -104,7 +104,7 @@ class TracingDetailsFragmentViewModel @AssistedInject constructor( } fun updateRiskDetails() { - tracingRepository.refreshDiagnosisKeys() + tracingRepository.refreshRiskResult() } fun onItemClicked(item: DetailsItem) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index c795ab4d5dd99665eded47666d7f0fb7cc73eee1..3e0d93c81d701e3875a42efb2e13c0ad5e4990c5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -146,21 +146,21 @@ class HomeFragmentViewModel @AssistedInject constructor( onCardClick = { routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) }, - onUpdateClick = { refreshDiagnosisKeys() } + onUpdateClick = { refreshRiskResult() } ) is IncreasedRisk -> IncreasedRiskCard.Item( state = tracingState, onCardClick = { routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) }, - onUpdateClick = { refreshDiagnosisKeys() } + onUpdateClick = { refreshRiskResult() } ) is TracingFailed -> TracingFailedCard.Item( state = tracingState, onCardClick = { routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) }, - onRetryClick = { refreshDiagnosisKeys() } + onRetryClick = { refreshRiskResult() } ) } }.distinctUntilChanged() @@ -268,7 +268,7 @@ class HomeFragmentViewModel @AssistedInject constructor( fun reenableRiskCalculation() { deregisterWarningAccepted() deadmanNotificationScheduler.schedulePeriodic() - refreshDiagnosisKeys() + refreshRiskResult() } // TODO only lazy to keep tests going which would break because of LocalData access @@ -303,8 +303,8 @@ class HomeFragmentViewModel @AssistedInject constructor( } } - private fun refreshDiagnosisKeys() { - tracingRepository.refreshDiagnosisKeys() + private fun refreshRiskResult() { + tracingRepository.refreshRiskResult() } fun deregisterWarningAccepted() { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt index fdff4062dbc0aefef65a35dc69770f9fb3462280..4ad2bbd28da2b03920fd7d3a785ae6229519c7fd 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -13,6 +13,7 @@ import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testPersistedAggreg import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRiskLevelResultDao import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResult import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResultWithAggregatedRiskPerDateResult +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.AggregatedRiskPerDateResultDao import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao @@ -21,6 +22,7 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResults import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -53,11 +55,19 @@ class BaseRiskLevelStorageTest : BaseTest() { @MockK lateinit var exposureWindowTables: ExposureWindowsDao @MockK lateinit var aggregatedRiskPerDateResultDao: AggregatedRiskPerDateResultDao @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository + @MockK lateinit var timeStamper: TimeStamper + + private lateinit var riskCombinator: RiskCombinator @BeforeEach fun setup() { MockKAnnotations.init(this) + every { timeStamper.nowUTC } returns Instant.parse("2021-01-01T12:00:00.000Z") + riskCombinator = RiskCombinator( + timeStamper = timeStamper + ) + every { databaseFactory.create() } returns database every { database.riskResults() } returns riskResultTables every { database.exposureWindows() } returns exposureWindowTables @@ -91,7 +101,8 @@ class BaseRiskLevelStorageTest : BaseTest() { ) = object : BaseRiskLevelStorage( scope = scope, riskResultDatabaseFactory = databaseFactory, - presenceTracingRiskRepository = presenceTracingRiskRepository + presenceTracingRiskRepository = presenceTracingRiskRepository, + riskCombinator = riskCombinator, ) { override val storedResultLimit: Int = storedResultLimit @@ -256,7 +267,7 @@ class BaseRiskLevelStorageTest : BaseTest() { // result from the combination with initial ew low risk result riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(400L) - riskLevelResults[1].riskState shouldBe RiskState.CALCULATION_FAILED + riskLevelResults[1].riskState shouldBe RiskState.LOW_RISK verify { riskResultTables.latestEntries(2) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt similarity index 63% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt index 9e1122826a32e6f43db61015975e11d0421c3a17..3d23b8d37e02bafdd04f5ae0f76d094417f07845 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt @@ -1,45 +1,81 @@ -package de.rki.coronawarnapp.risk.storage +package de.rki.coronawarnapp.risk.storage.internal import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK import org.joda.time.Instant import org.joda.time.LocalDate +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import testhelpers.BaseTest -class CombineRiskTest { +class RiskCombinatorTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234567890) + } + + private fun createInstance() = RiskCombinator( + timeStamper = timeStamper + ) + + @Test + fun `Initial results`() { + createInstance().initialCombinedResult.apply { + riskState shouldBe LOW_RISK + } + } + + @Test + fun `Fallback results on empty data`() { + createInstance().latestCombinedResult.apply { + riskState shouldBe LOW_RISK + } + } @Test fun `combineRisk works`() { val ptRisk0 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 19), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptRisk1 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 20), - riskState = RiskState.INCREASED_RISK + riskState = INCREASED_RISK ) val ptRisk2 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 21), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptRisk3 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 22), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ptRisk4 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 23), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptRisk5 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 24), - riskState = RiskState.INCREASED_RISK + riskState = INCREASED_RISK ) val ewRisk0 = ExposureWindowDayRisk( @@ -81,33 +117,33 @@ class CombineRiskTest { val ptDayRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk0, ptRisk1, ptRisk2, ptRisk3, ptRisk4, ptRisk5) val ewDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk0, ewRisk1, ewRisk2, ewRisk3, ewRisk4, ewRisk5) - val result = combineRisk(ptDayRiskList, ewDayRiskList) + val result = createInstance().combineRisk(ptDayRiskList, ewDayRiskList) result.size shouldBe 7 result.single { it.localDate == LocalDate(2021, 3, 15) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 19) - }.riskState shouldBe RiskState.LOW_RISK + }.riskState shouldBe LOW_RISK result.single { it.localDate == LocalDate(2021, 3, 20) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 21) - }.riskState shouldBe RiskState.LOW_RISK + }.riskState shouldBe LOW_RISK result.single { it.localDate == LocalDate(2021, 3, 22) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 22) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 23) - }.riskState shouldBe RiskState.INCREASED_RISK + }.riskState shouldBe INCREASED_RISK result.single { it.localDate == LocalDate(2021, 3, 24) - }.riskState shouldBe RiskState.INCREASED_RISK + }.riskState shouldBe INCREASED_RISK } @Test @@ -116,69 +152,73 @@ class CombineRiskTest { val ptResult = PtRiskLevelResult( calculatedAt = startInstant.plus(1000L), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptResult2 = PtRiskLevelResult( calculatedAt = startInstant.plus(3000L), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptResult3 = PtRiskLevelResult( calculatedAt = startInstant.plus(6000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ptResult4 = PtRiskLevelResult( calculatedAt = startInstant.plus(7000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ptResults = listOf(ptResult, ptResult2, ptResult4, ptResult3) val ewResult = createEwRiskLevelResult( calculatedAt = startInstant.plus(2000L), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ewResult2 = createEwRiskLevelResult( calculatedAt = startInstant.plus(4000L), - riskState = RiskState.INCREASED_RISK + riskState = INCREASED_RISK ) val ewResult3 = createEwRiskLevelResult( calculatedAt = startInstant.plus(5000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ewResult4 = createEwRiskLevelResult( calculatedAt = startInstant.plus(8000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ewResults = listOf(ewResult, ewResult4, ewResult2, ewResult3) - val result = combineEwPtRiskLevelResults(ptResults, ewResults).sortedByDescending { it.calculatedAt } + val result = createInstance().combineEwPtRiskLevelResults( + ptRiskResults = ptResults, + ewRiskResults = ewResults + ).sortedByDescending { it.calculatedAt } + result.size shouldBe 8 - result[0].riskState shouldBe RiskState.CALCULATION_FAILED + result[0].riskState shouldBe CALCULATION_FAILED result[0].calculatedAt shouldBe startInstant.plus(8000L) - result[1].riskState shouldBe RiskState.CALCULATION_FAILED + result[1].riskState shouldBe CALCULATION_FAILED result[1].calculatedAt shouldBe startInstant.plus(7000L) - result[2].riskState shouldBe RiskState.CALCULATION_FAILED + result[2].riskState shouldBe CALCULATION_FAILED result[2].calculatedAt shouldBe startInstant.plus(6000L) - result[3].riskState shouldBe RiskState.CALCULATION_FAILED + result[3].riskState shouldBe CALCULATION_FAILED result[3].calculatedAt shouldBe startInstant.plus(5000L) - result[4].riskState shouldBe RiskState.INCREASED_RISK + result[4].riskState shouldBe INCREASED_RISK result[4].calculatedAt shouldBe startInstant.plus(4000L) - result[5].riskState shouldBe RiskState.LOW_RISK + result[5].riskState shouldBe LOW_RISK result[5].calculatedAt shouldBe startInstant.plus(3000L) - result[6].riskState shouldBe RiskState.LOW_RISK + result[6].riskState shouldBe LOW_RISK result[6].calculatedAt shouldBe startInstant.plus(2000L) - result[7].riskState shouldBe RiskState.CALCULATION_FAILED + result[7].riskState shouldBe LOW_RISK result[7].calculatedAt shouldBe startInstant.plus(1000L) } @Test fun `max RiskState works`() { - combine(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - combine(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK - combine(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED - combine(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - combine(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.CALCULATION_FAILED - combine(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK - combine(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.CALCULATION_FAILED - combine(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED + RiskCombinator.combine(INCREASED_RISK, INCREASED_RISK) shouldBe INCREASED_RISK + RiskCombinator.combine(INCREASED_RISK, LOW_RISK) shouldBe INCREASED_RISK + RiskCombinator.combine(INCREASED_RISK, CALCULATION_FAILED) shouldBe CALCULATION_FAILED + RiskCombinator.combine(LOW_RISK, INCREASED_RISK) shouldBe INCREASED_RISK + RiskCombinator.combine(CALCULATION_FAILED, INCREASED_RISK) shouldBe CALCULATION_FAILED + RiskCombinator.combine(LOW_RISK, LOW_RISK) shouldBe LOW_RISK + RiskCombinator.combine(CALCULATION_FAILED, LOW_RISK) shouldBe CALCULATION_FAILED + RiskCombinator.combine(CALCULATION_FAILED, CALCULATION_FAILED) shouldBe CALCULATION_FAILED } } diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 274a5e5f941e266f6db258ecf57cad71bd51e806..673ac7b96c843f1c2ed9909a24b0a68e7d383ff9 100644 --- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -5,9 +5,11 @@ import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepo import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -97,7 +99,8 @@ class DefaultRiskLevelStorageTest : BaseTest() { ) = DefaultRiskLevelStorage( scope = scope, riskResultDatabaseFactory = databaseFactory, - presenceTracingRiskRepository = presenceTracingRiskRepository + presenceTracingRiskRepository = presenceTracingRiskRepository, + riskCombinator = RiskCombinator(TimeStamper()), ) @Test diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 72eae29914ebfe630caea23002104922563ca4dd..5502ead593df03320642893e077e6a6201216d88 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -5,9 +5,11 @@ import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepo import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -94,7 +96,8 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() { private fun createInstance() = DefaultRiskLevelStorage( scope = TestCoroutineScope(), riskResultDatabaseFactory = databaseFactory, - presenceTracingRiskRepository = presenceTracingRiskRepository + presenceTracingRiskRepository = presenceTracingRiskRepository, + riskCombinator = RiskCombinator(TimeStamper()) ) @Test