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 new file mode 100644 index 0000000000000000000000000000000000000000..e1131726c36f35eb79041d6058ddae315e85e782 --- /dev/null +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultRiskLevelStorage @Inject constructor( + riskResultDatabaseFactory: RiskResultDatabase.Factory, + riskLevelResultMigrator: RiskLevelResultMigrator +) : BaseRiskLevelStorage(riskResultDatabaseFactory, riskLevelResultMigrator) { + + // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation + // Taken from TimeVariables.MAX_STALE_EXPOSURE_RISK_RANGE + override val storedResultLimit: Int = 2 * 6 + + override suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) { + Timber.d("storeExposureWindows(): NOOP") + // NOOP + } + + override suspend fun deletedOrphanedExposureWindows() { + Timber.d("deletedOrphanedExposureWindows(): NOOP") + // NOOP + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..0ce4d0e7ae795152f84e8fd33fd6e7a210cea4ba --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.risk.storage + +import de.rki.coronawarnapp.risk.RiskLevelResult +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 +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedScanInstances +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import kotlinx.coroutines.flow.firstOrNull +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DefaultRiskLevelStorage @Inject constructor( + riskResultDatabaseFactory: RiskResultDatabase.Factory, + riskLevelResultMigrator: RiskLevelResultMigrator +) : BaseRiskLevelStorage(riskResultDatabaseFactory, riskLevelResultMigrator) { + + // 14 days, 6 times per day + // For testers keep all the results! + override val storedResultLimit: Int = 14 * 6 + + override suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) { + Timber.d("Storing exposure windows for storedResultId=%s", storedResultId) + try { + val startTime = System.currentTimeMillis() + val exposureWindows = result.exposureWindows ?: emptyList() + val windowIds = exposureWindows + .map { it.toPersistedExposureWindow(riskLevelResultId = storedResultId) } + .let { exposureWindowsTables.insertWindows(it) } + + require(windowIds.size == exposureWindows.size) { + Timber.e("Inserted ${windowIds.size}, but wanted ${exposureWindows.size}") + } + + val persistedScanInstances: List<PersistedScanInstance> = windowIds.flatMapIndexed { index, id -> + val scanInstances = exposureWindows[index].scanInstances + scanInstances.toPersistedScanInstances(exposureWindowId = id) + } + exposureWindowsTables.insertScanInstances(persistedScanInstances) + + Timber.d("Storing ExposureWindows took %dms.", (System.currentTimeMillis() - startTime)) + } catch (e: Exception) { + Timber.e(e, "Failed to save exposure windows") + } + } + + override suspend fun deletedOrphanedExposureWindows() { + Timber.d("deletedOrphanedExposureWindows() running...") + val currentRiskResultIds = riskResultsTables.allEntries().firstOrNull()?.map { it.id } ?: emptyList() + + exposureWindowsTables.deleteByRiskResultId(currentRiskResultIds).also { + Timber.d("$it orphaned exposure windows were deleted.") + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt index c03b84f39023d1999f7d0e38b93fa25b1e6d0d10..15968158f9b4d89f681d127bba1cdfc97559bd9e 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt @@ -34,8 +34,8 @@ import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.receiver.ExposureStateUpdateReceiver -import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.TimeVariables +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange import de.rki.coronawarnapp.sharing.ExposureSharingService import de.rki.coronawarnapp.storage.AppDatabase @@ -65,7 +65,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory @Inject lateinit var enfClient: ENFClient - @Inject lateinit var exposureResultStore: ExposureResultStore + @Inject lateinit var riskLevelStorage: RiskLevelStorage private val vm: TestForApiFragmentViewModel by cwaViewModels { viewModelFactory } companion object { @@ -159,7 +159,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), buttonRetrieveExposureSummary.setOnClickListener { vm.launch { - val summary = exposureResultStore.entities.first().exposureWindows.toString() + val summary = riskLevelStorage.exposureWindows.first().toString() withContext(Dispatchers.Main) { showToast(summary) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt index dd93a0e154d8e7ece32be12a0984a78c197c3560..8cbf5d39e7817597eede7b9abf20a9c51a7808fd 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -11,21 +11,20 @@ import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.risk.ExposureResult -import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.RiskLevel import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.AppDatabase import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider +import de.rki.coronawarnapp.ui.tracing.common.tryLatestResultsWithDefaults import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.di.AppContext @@ -52,7 +51,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( private val keyCacheRepository: KeyCacheRepository, private val appConfigProvider: AppConfigProvider, tracingCardStateProvider: TracingCardStateProvider, - private val exposureResultStore: ExposureResultStore, + private val riskLevelStorage: RiskLevelStorage, private val testSettings: TestSettings ) : CWAViewModel( dispatcherProvider = dispatcherProvider @@ -76,19 +75,26 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( .sample(150L) .asLiveData(dispatcherProvider.Default) - val exposureWindowCountString = exposureResultStore - .entities - .map { "Retrieved ${it.exposureWindows.size} Exposure Windows" } + val exposureWindowCountString = riskLevelStorage + .exposureWindows + .map { "Retrieved ${it.size} Exposure Windows" } .asLiveData() - val exposureWindows = exposureResultStore - .entities - .map { if (it.exposureWindows.isEmpty()) "Exposure windows list is empty" else it.exposureWindows.toString() } + val exposureWindows = riskLevelStorage + .exposureWindows + .map { if (it.isEmpty()) "Exposure windows list is empty" else it.toString() } .asLiveData() - val aggregatedRiskResult = exposureResultStore - .entities - .map { if (it.aggregatedRiskResult != null) it.aggregatedRiskResult.toReadableString() else "Aggregated risk result is not available" } + val aggregatedRiskResult = riskLevelStorage + .riskLevelResults + .map { + val latest = it.maxByOrNull { it.calculatedAt } + if (latest?.aggregatedRiskResult != null) { + latest.aggregatedRiskResult?.toReadableString() + } else { + "Aggregated risk result is not available" + } + } .asLiveData() private fun AggregatedRiskResult.toReadableString(): String = StringBuilder() @@ -129,26 +135,24 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( .toString() val additionalRiskCalcInfo = combine( - RiskLevelRepository.riskLevelScore, - RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated, - exposureResultStore.matchedKeyCount, - exposureResultStore.daysSinceLastExposure, + riskLevelStorage.riskLevelResults, LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() - ) { riskLevelScore, - riskLevelScoreLastSuccessfulCalculated, - matchedKeyCount, - daysSinceLastExposure, - lastTimeDiagnosisKeysFromServerFetch -> + ) { riskLevelResults, lastTimeDiagnosisKeysFromServerFetch -> + + val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults() + createAdditionalRiskCalcInfo( - riskLevelScore = riskLevelScore, - riskLevelScoreLastSuccessfulCalculated = riskLevelScoreLastSuccessfulCalculated, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, + latestCalc.calculatedAt, + riskLevelScore = latestCalc.riskLevel.raw, + riskLevelScoreLastSuccessfulCalculated = latestSuccessfulCalc.riskLevel.raw, + matchedKeyCount = latestCalc.matchedKeyCount, + daysSinceLastExposure = latestCalc.daysWithEncounters, lastTimeDiagnosisKeysFromServerFetch = lastTimeDiagnosisKeysFromServerFetch ) }.asLiveData() private suspend fun createAdditionalRiskCalcInfo( + lastTimeRiskLevelCalculation: Instant, riskLevelScore: Int, riskLevelScoreLastSuccessfulCalculated: Int, matchedKeyCount: Int, @@ -162,11 +166,7 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( .appendLine("Last Time Server Fetch: ${lastTimeDiagnosisKeysFromServerFetch?.time?.let { Instant.ofEpochMilli(it) }}") .appendLine("Tracing Duration: ${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days") .appendLine("Tracing Duration in last 14 days: ${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days") - .appendLine( - "Last time risk level calculation ${ - LocalData.lastTimeRiskLevelCalculation()?.let { Instant.ofEpochMilli(it) } - }" - ) + .appendLine("Last time risk level calculation $lastTimeRiskLevelCalculation") .toString() fun retrieveDiagnosisKeys() { @@ -204,10 +204,8 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( // Export File Reset keyCacheRepository.clear() - exposureResultStore.entities.value = ExposureResult(emptyList(), null) + riskLevelStorage.clear() - LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) - LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) LocalData.lastTimeDiagnosisKeysFromServerFetch(null) } catch (e: Exception) { e.report(ExceptionCategory.INTERNAL) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index 941a2b4e1890e8c0f69d4804f7c9f454313a7242..7bdbcd80dbf9c81bb829df92e6371d3a3ac33dbc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -17,6 +17,7 @@ import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver import de.rki.coronawarnapp.exception.reporting.ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL import de.rki.coronawarnapp.notification.NotificationHelper +import de.rki.coronawarnapp.risk.RiskLevelChangeDetector import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.CWADebug @@ -45,6 +46,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var foregroundState: ForegroundState @Inject lateinit var workManager: WorkManager @Inject lateinit var configChangeDetector: ConfigChangeDetector + @Inject lateinit var riskLevelChangeDetector: RiskLevelChangeDetector @Inject lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree @@ -78,6 +80,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { } configChangeDetector.launch() + riskLevelChangeDetector.launch() } private val activityLifecycleCallback = object : ActivityLifecycleCallbacks { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt index 588451c9dec8b5393a6c55e1aaf9fbb00bc2317b..da3440abb8741903b56bf3c87c5f51b674e61a0f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt @@ -1,10 +1,9 @@ package de.rki.coronawarnapp.appconfig import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.risk.RiskLevelData +import de.rki.coronawarnapp.risk.RiskLevelSettings import de.rki.coronawarnapp.risk.RiskLevelTask -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.util.coroutine.AppScope @@ -20,7 +19,8 @@ class ConfigChangeDetector @Inject constructor( private val appConfigProvider: AppConfigProvider, private val taskController: TaskController, @AppScope private val appScope: CoroutineScope, - private val riskLevelData: RiskLevelData + private val riskLevelSettings: RiskLevelSettings, + private val riskLevelStorage: RiskLevelStorage ) { fun launch() { @@ -36,28 +36,20 @@ class ConfigChangeDetector @Inject constructor( } @VisibleForTesting - internal fun check(newIdentifier: String) { - if (riskLevelData.lastUsedConfigIdentifier == null) { + internal suspend fun check(newIdentifier: String) { + if (riskLevelSettings.lastUsedConfigIdentifier == null) { // No need to reset anything if we didn't calculate a risklevel yet. Timber.d("Config changed, but no previous identifier is available.") return } - val oldConfigId = riskLevelData.lastUsedConfigIdentifier + val oldConfigId = riskLevelSettings.lastUsedConfigIdentifier if (newIdentifier != oldConfigId) { Timber.i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.") - RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector")) } else { Timber.v("Config identifier ($oldConfigId) didn't change, NOOP.") } } - - @VisibleForTesting - internal object RiskLevelRepositoryDeferrer { - - fun resetRiskLevel() { - RiskLevelRepository.setRiskLevelScore(RiskLevel.UNDETERMINED) - } - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt index a15a2fa6969f165ac04109a4a55e3022a8bf878a..6cd2b736817058246ad9515cab0c3e09309f776c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt @@ -9,6 +9,7 @@ import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.exception.http.NetworkConnectTimeoutException import de.rki.coronawarnapp.storage.DeviceStorage import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate import de.rki.coronawarnapp.util.TimeStamper @@ -51,7 +52,12 @@ class HourPackageSyncTool @Inject constructor( val keysWereRevoked = revokeCachedKeys(downloadConfig.revokedHourPackages) val missingHours = targetLocations.mapNotNull { - determineMissingHours(it, forceIndexLookup || keysWereRevoked) + try { + determineMissingHours(it, forceIndexLookup || keysWereRevoked) + } catch (e: NetworkConnectTimeoutException) { + Timber.tag(TAG).i("missing hours sync failed due to network timeout") + return SyncResult(successful = false, newPackages = emptyList()) + } } if (missingHours.isEmpty()) { Timber.tag(TAG).i("There were no missing hours.") @@ -142,6 +148,9 @@ class HourPackageSyncTool @Inject constructor( val availableHours = run { val hoursToday = try { keyServer.getHourIndex(location, today) + } catch (e: NetworkConnectTimeoutException) { + Timber.tag(TAG).e(e, "Failed to get today's hour due - not going to delete the cache.") + throw e } catch (e: IOException) { Timber.tag(TAG).e(e, "failed to get today's hour index.") emptyList() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt index fc89eff4627de3bd0fb3069f8937876563fb0341..7f8d7b4ae755482bc1d80ca7a1d4ce85bd243010 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt @@ -3,46 +3,77 @@ package de.rki.coronawarnapp.exception.http import de.rki.coronawarnapp.exception.reporting.ErrorCodes import de.rki.coronawarnapp.exception.reporting.ReportedIOException -open class CwaWebException(val statusCode: Int) : ReportedIOException( - ErrorCodes.CWA_WEB_REQUEST_PROBLEM.code, "error during web request, http status $statusCode" +open class CwaWebException( + val statusCode: Int, + message: String?, + cause: Throwable? = null +) : ReportedIOException( + code = ErrorCodes.CWA_WEB_REQUEST_PROBLEM.code, + message = "Error during web request: code=$statusCode message=$message", + cause = cause ) -open class CwaServerError(statusCode: Int) : CwaWebException(statusCode) { +open class CwaServerError( + statusCode: Int, + message: String?, + cause: Throwable? = null +) : CwaWebException( + statusCode = statusCode, + message = message, + cause = cause +) { init { - if (statusCode !in 500..599) - throw IllegalArgumentException("a server error has to have code 5xx") + if (statusCode !in 500..599) { + throw IllegalArgumentException("Invalid HTTP server error code $statusCode (!= 5xx)") + } } } -open class CwaClientError(statusCode: Int) : CwaWebException(statusCode) { +open class CwaClientError( + statusCode: Int, + message: String?, + cause: Throwable? = null +) : CwaWebException( + statusCode = statusCode, + message = message, + cause = cause +) { init { - if (statusCode !in 400..499) - throw IllegalArgumentException("a client error has to have code 4xx") + if (statusCode !in 400..499) { + throw IllegalArgumentException("Invalid HTTP client error code $statusCode (!= 4xx)") + } } } -open class CwaSuccessResponseWithCodeMismatchNotSupportedError(statusCode: Int) : - CwaWebException(statusCode) - -open class CwaInformationalNotSupportedError(statusCode: Int) : CwaWebException(statusCode) -open class CwaRedirectNotSupportedError(statusCode: Int) : CwaWebException(statusCode) - -class BadRequestException : CwaClientError(400) -class UnauthorizedException : CwaClientError(401) -class ForbiddenException : CwaClientError(403) -class NotFoundException : CwaClientError(404) -class ConflictException : CwaClientError(409) -class GoneException : CwaClientError(410) -class UnsupportedMediaTypeException : CwaClientError(415) -class TooManyRequestsException : CwaClientError(429) - -class InternalServerErrorException : CwaServerError(500) -class NotImplementedException : CwaServerError(501) -class BadGatewayException : CwaServerError(502) -class ServiceUnavailableException : CwaServerError(503) -class GatewayTimeoutException : CwaServerError(504) -class HTTPVersionNotSupported : CwaServerError(505) -class NetworkAuthenticationRequiredException : CwaServerError(511) -class CwaUnknownHostException : CwaServerError(597) -class NetworkReadTimeoutException : CwaServerError(598) -class NetworkConnectTimeoutException : CwaServerError(599) +open class CwaSuccessResponseWithCodeMismatchNotSupportedError(statusCode: Int, message: String?) : + CwaWebException(statusCode, message) + +open class CwaInformationalNotSupportedError(statusCode: Int, message: String?) : CwaWebException(statusCode, message) +open class CwaRedirectNotSupportedError(statusCode: Int, message: String?) : CwaWebException(statusCode, message) + +class BadRequestException(message: String?) : CwaClientError(400, message) +class UnauthorizedException(message: String?) : CwaClientError(401, message) +class ForbiddenException(message: String?) : CwaClientError(403, message) +class NotFoundException(message: String?) : CwaClientError(404, message) +class ConflictException(message: String?) : CwaClientError(409, message) +class GoneException(message: String?) : CwaClientError(410, message) +class UnsupportedMediaTypeException(message: String?) : CwaClientError(415, message) +class TooManyRequestsException(message: String?) : CwaClientError(429, message) + +class InternalServerErrorException(message: String?) : CwaServerError(500, message) +class NotImplementedException(message: String?) : CwaServerError(501, message) +class BadGatewayException(message: String?) : CwaServerError(502, message) +class ServiceUnavailableException(message: String?) : CwaServerError(503, message) +class GatewayTimeoutException(message: String?) : CwaServerError(504, message) +class HTTPVersionNotSupported(message: String?) : CwaServerError(505, message) +class NetworkAuthenticationRequiredException(message: String?) : CwaServerError(511, message) +class CwaUnknownHostException( + message: String? = null, + cause: Throwable? +) : CwaServerError(597, message, cause) + +class NetworkReadTimeoutException(message: String?) : CwaServerError(598, message) +class NetworkConnectTimeoutException( + message: String? = null, + cause: Throwable? = null +) : CwaServerError(599, message, cause) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt index 1061abc7e164af4ff5ac41ec2e63776004ebcce3..40765f7fa89c1dbc9f350737c818cbf97900a35d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt @@ -26,6 +26,7 @@ import de.rki.coronawarnapp.exception.http.UnauthorizedException import de.rki.coronawarnapp.exception.http.UnsupportedMediaTypeException import okhttp3.Interceptor import okhttp3.Response +import timber.log.Timber import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection @@ -34,43 +35,54 @@ class HttpErrorParser : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { try { val response = chain.proceed(chain.request()) + + val message: String? = try { + if (response.isSuccessful) { + null + } else { + response.message + } + } catch (e: Exception) { + Timber.w("Failed to get http error message.") + null + } return when (val code = response.code) { HttpsURLConnection.HTTP_OK -> response HttpsURLConnection.HTTP_CREATED -> response HttpsURLConnection.HTTP_ACCEPTED -> response HttpsURLConnection.HTTP_NO_CONTENT -> response - HttpsURLConnection.HTTP_BAD_REQUEST -> throw BadRequestException() - HttpsURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException() - HttpsURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException() - HttpsURLConnection.HTTP_NOT_FOUND -> throw NotFoundException() - HttpsURLConnection.HTTP_CONFLICT -> throw ConflictException() - HttpsURLConnection.HTTP_GONE -> throw GoneException() - HttpsURLConnection.HTTP_UNSUPPORTED_TYPE -> throw UnsupportedMediaTypeException() - 429 -> throw TooManyRequestsException() - HttpsURLConnection.HTTP_INTERNAL_ERROR -> throw InternalServerErrorException() - HttpsURLConnection.HTTP_NOT_IMPLEMENTED -> throw NotImplementedException() - HttpsURLConnection.HTTP_BAD_GATEWAY -> throw BadGatewayException() - HttpsURLConnection.HTTP_UNAVAILABLE -> throw ServiceUnavailableException() - HttpsURLConnection.HTTP_GATEWAY_TIMEOUT -> throw GatewayTimeoutException() - HttpsURLConnection.HTTP_VERSION -> throw HTTPVersionNotSupported() - 511 -> throw NetworkAuthenticationRequiredException() - 598 -> throw NetworkReadTimeoutException() - 599 -> throw NetworkConnectTimeoutException() + HttpsURLConnection.HTTP_BAD_REQUEST -> throw BadRequestException(message) + HttpsURLConnection.HTTP_UNAUTHORIZED -> throw UnauthorizedException(message) + HttpsURLConnection.HTTP_FORBIDDEN -> throw ForbiddenException(message) + HttpsURLConnection.HTTP_NOT_FOUND -> throw NotFoundException(message) + HttpsURLConnection.HTTP_CONFLICT -> throw ConflictException(message) + HttpsURLConnection.HTTP_GONE -> throw GoneException(message) + HttpsURLConnection.HTTP_UNSUPPORTED_TYPE -> throw UnsupportedMediaTypeException(message) + 429 -> throw TooManyRequestsException(message) + HttpsURLConnection.HTTP_INTERNAL_ERROR -> throw InternalServerErrorException(message) + HttpsURLConnection.HTTP_NOT_IMPLEMENTED -> throw NotImplementedException(message) + HttpsURLConnection.HTTP_BAD_GATEWAY -> throw BadGatewayException(message) + HttpsURLConnection.HTTP_UNAVAILABLE -> throw ServiceUnavailableException(message) + HttpsURLConnection.HTTP_GATEWAY_TIMEOUT -> throw GatewayTimeoutException(message) + HttpsURLConnection.HTTP_VERSION -> throw HTTPVersionNotSupported(message) + 511 -> throw NetworkAuthenticationRequiredException(message) + 598 -> throw NetworkReadTimeoutException(message) + 599 -> throw NetworkConnectTimeoutException(message) else -> { - if (code in 100..199) throw CwaInformationalNotSupportedError(code) + if (code in 100..199) throw CwaInformationalNotSupportedError(code, message) if (code in 200..299) throw CwaSuccessResponseWithCodeMismatchNotSupportedError( - code + code, message ) - if (code in 300..399) throw CwaRedirectNotSupportedError(code) - if (code in 400..499) throw CwaClientError(code) - if (code in 500..599) throw CwaServerError(code) - throw CwaWebException(code) + if (code in 300..399) throw CwaRedirectNotSupportedError(code, message) + if (code in 400..499) throw CwaClientError(code, message) + if (code in 500..599) throw CwaServerError(code, message) + throw CwaWebException(code, message) } } } catch (err: SocketTimeoutException) { - throw NetworkConnectTimeoutException() + throw NetworkConnectTimeoutException(cause = err) } catch (err: UnknownHostException) { - throw CwaUnknownHostException() + throw CwaUnknownHostException(cause = err) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt index f0f0ed5c5ff56d8758407875de879465d2e79560..e383844c36934a71d25d72f588af93f36b11dae9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt @@ -8,8 +8,6 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.risk.ExposureResult -import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest @@ -19,18 +17,12 @@ import timber.log.Timber class ExposureStateUpdateWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, - private val exposureResultStore: ExposureResultStore, - private val enfClient: ENFClient, private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { try { Timber.v("worker to persist exposure summary started") - enfClient.exposureWindows().let { - exposureResultStore.entities.value = ExposureResult(it, null) - Timber.v("exposure summary state updated: $it") - } taskController.submit( DefaultTaskRequest(RiskLevelTask::class, originTag = "ExposureStateUpdateWorker") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt index c2319109f5914784469d885563b9d82fb7511809..d8ef3330e9fda35f2935660f6ce4ffef0094f8c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt @@ -5,16 +5,16 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.flow.shareLatest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.isActive import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -28,24 +28,26 @@ class DefaultTracingStatus @Inject constructor( @AppScope val scope: CoroutineScope ) : TracingStatus { - override val isTracingEnabled: Flow<Boolean> = callbackFlow<Boolean> { + override val isTracingEnabled: Flow<Boolean> = flow { while (true) { try { - send(pollIsEnabled()) - } catch (e: Exception) { - Timber.w(e, "ENF isEnabled failed.") - send(false) - e.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null) - cancel("ENF isEnabled failed", e) + emit(pollIsEnabled()) + delay(POLLING_DELAY_MS) + } catch (e: CancellationException) { + Timber.d("isBackgroundRestricted was cancelled") + break } - if (!isActive) break - delay(POLLING_DELAY_MS) } } .distinctUntilChanged() .onStart { Timber.v("isTracingEnabled FLOW start") } .onEach { Timber.v("isTracingEnabled FLOW emission: %b", it) } - .onCompletion { Timber.v("isTracingEnabled FLOW completed.") } + .onCompletion { if (it == null) Timber.v("isTracingEnabled FLOW completed.") } + .catch { + Timber.w(it, "ENF isEnabled failed.") + it.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null) + emit(false) + } .shareLatest( tag = TAG, scope = scope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt index c642a8a4b7788ec131309bd8704549889abffebc..2da051ce6df105ace8c956cff183d8f82f7cb1b6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/NotificationHelper.kt @@ -189,12 +189,10 @@ object NotificationHelper { * * @param title: String * @param content: String - * @param visibility: Int * @param expandableLongText: Boolean * @param notificationId: NotificationId * @param pendingIntent: PendingIntent */ - fun sendNotification( title: String, content: String, @@ -209,19 +207,6 @@ object NotificationHelper { } } - /** - * Send notification - * Build and send notification with content and visibility. - * Notification is only sent if app is not in foreground. - * - * @param content: String - */ - fun sendNotification(content: String) { - if (!CoronaWarnApplication.isAppInForeground) { - sendNotification("", content, true) - } - } - /** * Log notification build * Log success or failure of creating new notification diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt deleted file mode 100644 index 3b26fc49702912b12b519529d3df6e2eced4c749..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt +++ /dev/null @@ -1,30 +0,0 @@ -package de.rki.coronawarnapp.risk - -import com.google.android.gms.nearby.exposurenotification.ExposureWindow -import de.rki.coronawarnapp.risk.result.AggregatedRiskResult -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ExposureResultStore @Inject constructor() { - - val entities = MutableStateFlow( - ExposureResult( - exposureWindows = emptyList(), - aggregatedRiskResult = null - ) - ) - - internal val internalMatchedKeyCount = MutableStateFlow(0) - val matchedKeyCount: Flow<Int> = internalMatchedKeyCount - - internal val internalDaysSinceLastExposure = MutableStateFlow(0) - val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure -} - -data class ExposureResult( - val exposureWindows: List<ExposureWindow>, - val aggregatedRiskResult: AggregatedRiskResult? -) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt index 05267394d6cabd8aa691caf1ae5c8a8deeba455e..89e6d8a21b0d52d1bc9595da1771db09cabf5f13 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevel.kt @@ -47,38 +47,5 @@ enum class RiskLevel(val raw: Int) { else -> UNDETERMINED } } - - // risk level categories - val UNSUCCESSFUL_RISK_LEVELS = - arrayOf( - UNDETERMINED, - NO_CALCULATION_POSSIBLE_TRACING_OFF, - UNKNOWN_RISK_OUTDATED_RESULTS, - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - ) - private val HIGH_RISK_LEVELS = arrayOf(INCREASED_RISK) - private val LOW_RISK_LEVELS = arrayOf( - UNKNOWN_RISK_INITIAL, - NO_CALCULATION_POSSIBLE_TRACING_OFF, - LOW_LEVEL_RISK, - UNKNOWN_RISK_OUTDATED_RESULTS, - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL, - UNDETERMINED - ) - - /** - * Checks if the RiskLevel has change from a high to low or from low to high - * - * @param previousRiskLevel previously persisted RiskLevel - * @param currentRiskLevel newly calculated RiskLevel - * @return - */ - fun riskLevelChangedBetweenLowAndHigh( - previousRiskLevel: RiskLevel, - currentRiskLevel: RiskLevel - ): Boolean { - return HIGH_RISK_LEVELS.contains(previousRiskLevel) && LOW_RISK_LEVELS.contains(currentRiskLevel) || - LOW_RISK_LEVELS.contains(previousRiskLevel) && HIGH_RISK_LEVELS.contains(currentRiskLevel) - } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt new file mode 100644 index 0000000000000000000000000000000000000000..78c75891b5d926b3f3c58a28f503d02b18eaa94d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -0,0 +1,92 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.core.app.NotificationManagerCompat +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.notification.NotificationHelper +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.ForegroundState +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class RiskLevelChangeDetector @Inject constructor( + @AppContext private val context: Context, + @AppScope private val appScope: CoroutineScope, + private val riskLevelStorage: RiskLevelStorage, + private val notificationManagerCompat: NotificationManagerCompat, + private val foregroundState: ForegroundState +) { + + fun launch() { + Timber.v("Monitoring risk level changes.") + riskLevelStorage.riskLevelResults + .map { results -> + results.sortedBy { it.calculatedAt }.takeLast(2) + } + .filter { it.size == 2 } + .onEach { + Timber.v("Checking for risklevel change.") + check(it) + } + .catch { Timber.e(it, "App config change checks failed.") } + .launchIn(appScope) + } + + private suspend fun check(changedLevels: List<RiskLevelResult>) { + val oldResult = changedLevels.first() + val newResult = changedLevels.last() + + val oldRiskLevel = oldResult.riskLevel + val newRiskLevel = newResult.riskLevel + + Timber.d("last CalculatedS core is ${oldRiskLevel.raw} and Current Risk Level is ${newRiskLevel.raw}") + + if (hasHighLowLevelChanged(oldRiskLevel, newRiskLevel) && !LocalData.submissionWasSuccessful()) { + Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}") + + if (!foregroundState.isInForeground.first()) { + NotificationHelper.sendNotification("", context.getString(R.string.notification_body), true) + } else { + Timber.d("App is in foreground, not sending notifications") + } + + Timber.d("Risk level changed and notification sent. Current Risk level is $newRiskLevel") + } + + if ( + oldRiskLevel.raw == RiskLevelConstants.INCREASED_RISK && + newRiskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK + ) { + LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true + + Timber.d("Risk level changed LocalData is updated. Current Risk level is $newRiskLevel") + } + } + + companion object { + /** + * Checks if the RiskLevel has change from a high to low or from low to high + * + * @param previous previously persisted RiskLevel + * @param current newly calculated RiskLevel + * @return + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun hasHighLowLevelChanged(previous: RiskLevel, current: RiskLevel) = + previous.isIncreasedRisk != current.isIncreasedRisk + + private val RiskLevel.isIncreasedRisk: Boolean + get() = this == RiskLevel.INCREASED_RISK + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..b8baae9d8a01fc587eee2872935987eb6c29ec8f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelResult.kt @@ -0,0 +1,53 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import org.joda.time.Instant + +interface RiskLevelResult { + val riskLevel: RiskLevel + val calculatedAt: Instant + + val aggregatedRiskResult: AggregatedRiskResult? + + /** + * This will only be filled in deviceForTester builds + */ + val exposureWindows: List<ExposureWindow>? + + val wasSuccessfullyCalculated: Boolean + get() = !UNSUCCESSFUL_RISK_LEVELS.contains(riskLevel) + + val isIncreasedRisk: Boolean + get() = aggregatedRiskResult?.isIncreasedRisk() ?: false + + val matchedKeyCount: Int + get() = if (isIncreasedRisk) { + aggregatedRiskResult?.totalMinimumDistinctEncountersWithHighRisk ?: 0 + } else { + aggregatedRiskResult?.totalMinimumDistinctEncountersWithLowRisk ?: 0 + } + + val daysWithEncounters: Int + get() = if (isIncreasedRisk) { + aggregatedRiskResult?.numberOfDaysWithHighRisk ?: 0 + } else { + aggregatedRiskResult?.numberOfDaysWithLowRisk ?: 0 + } + + val lastRiskEncounterAt: Instant? + get() = if (isIncreasedRisk) { + aggregatedRiskResult?.mostRecentDateWithHighRisk + } else { + aggregatedRiskResult?.mostRecentDateWithLowRisk + } + + companion object { + private val UNSUCCESSFUL_RISK_LEVELS = arrayOf( + RiskLevel.UNDETERMINED, + RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF, + RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS, + RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt similarity index 95% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt index 83372c3f5d5d671f21c29ceb1c478e23df6fed79..4af4d0c0517a0ae81111d0e403907d08d1b0fb8c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelSettings.kt @@ -7,7 +7,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class RiskLevelData @Inject constructor( +class RiskLevelSettings @Inject constructor( @AppContext private val context: Context ) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt index 8069454c2b5225e5b157f831777d184519d483d7..8f2cba566887a75b1b1918481ef165c514584722 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt @@ -1,10 +1,6 @@ package de.rki.coronawarnapp.risk import android.content.Context -import androidx.annotation.VisibleForTesting -import androidx.core.app.NotificationManagerCompat -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.R import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig @@ -12,7 +8,6 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.RiskLevelCalculationException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.risk.RiskLevel.INCREASED_RISK import de.rki.coronawarnapp.risk.RiskLevel.LOW_LEVEL_RISK import de.rki.coronawarnapp.risk.RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF @@ -20,8 +15,7 @@ import de.rki.coronawarnapp.risk.RiskLevel.UNDETERMINED import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -40,66 +34,86 @@ import timber.log.Timber import javax.inject.Inject import javax.inject.Provider +@Suppress("ReturnCount") class RiskLevelTask @Inject constructor( private val riskLevels: RiskLevels, @AppContext private val context: Context, private val enfClient: ENFClient, private val timeStamper: TimeStamper, private val backgroundModeStatus: BackgroundModeStatus, - private val riskLevelData: RiskLevelData, + private val riskLevelSettings: RiskLevelSettings, private val appConfigProvider: AppConfigProvider, - private val exposureResultStore: ExposureResultStore -) : Task<DefaultProgress, RiskLevelTask.Result> { + private val riskLevelStorage: RiskLevelStorage +) : Task<DefaultProgress, RiskLevelTaskResult> { private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() override val progress: Flow<DefaultProgress> = internalProgress.asFlow() private var isCanceled = false - override suspend fun run(arguments: Task.Arguments): Result { + @Suppress("LongMethod") + override suspend fun run(arguments: Task.Arguments): RiskLevelTaskResult { try { Timber.d("Running with arguments=%s", arguments) - // If there is no connectivity the transaction will set the last calculated risk level if (!isNetworkEnabled(context)) { - RiskLevelRepository.setLastCalculatedRiskLevelAsCurrent() - return Result(UNDETERMINED) + return RiskLevelTaskResult( + riskLevel = UNDETERMINED, + calculatedAt = timeStamper.nowUTC + ) } if (!enfClient.isTracingEnabled.first()) { - return Result(NO_CALCULATION_POSSIBLE_TRACING_OFF) + return RiskLevelTaskResult( + riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, + calculatedAt = timeStamper.nowUTC + ) } val configData: ConfigData = appConfigProvider.getAppConfig() - return Result( - when { - calculationNotPossibleBecauseOfNoKeys().also { - checkCancel() - } -> UNKNOWN_RISK_INITIAL - - calculationNotPossibleBecauseOfOutdatedResults().also { - checkCancel() - } -> if (backgroundJobsEnabled()) { - UNKNOWN_RISK_OUTDATED_RESULTS - } else { - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - } + return kotlin.run evaluation@{ + if (calculationNotPossibleBecauseOfNoKeys()) { + return@evaluation RiskLevelTaskResult( + riskLevel = UNKNOWN_RISK_INITIAL, + calculatedAt = timeStamper.nowUTC + ) + } - isIncreasedRisk(configData).also { - checkCancel() - } -> INCREASED_RISK + if (calculationNotPossibleBecauseOfOutdatedResults()) { + return@evaluation RiskLevelTaskResult( + riskLevel = when (backgroundJobsEnabled()) { + true -> UNKNOWN_RISK_OUTDATED_RESULTS + false -> UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL + }, + calculatedAt = timeStamper.nowUTC + ) + } + checkCancel() - !isActiveTracingTimeAboveThreshold().also { - checkCancel() - } -> UNKNOWN_RISK_INITIAL + val risklevelResult = calculateRiskLevel(configData) + if (risklevelResult.isIncreasedRisk) { + return@evaluation risklevelResult + } + checkCancel() - else -> LOW_LEVEL_RISK - }.also { - checkCancel() - updateRepository(it, timeStamper.nowUTC.millis) - riskLevelData.lastUsedConfigIdentifier = configData.identifier + if (!isActiveTracingTimeAboveThreshold()) { + return@evaluation RiskLevelTaskResult( + riskLevel = UNKNOWN_RISK_INITIAL, + calculatedAt = timeStamper.nowUTC + ) } - ) + checkCancel() + + return@evaluation risklevelResult + }.also { + checkCancel() + Timber.i("Evaluation finished with %s", it) + + Timber.tag(TAG).d("storeTaskResult(...)") + riskLevelStorage.storeResult(it) + + riskLevelSettings.lastUsedConfigIdentifier = configData.identifier + } } catch (error: Exception) { Timber.tag(TAG).e(error) error.report(ExceptionCategory.EXPOSURENOTIFICATION) @@ -111,6 +125,7 @@ class RiskLevelTask @Inject constructor( } private fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean { + Timber.tag(TAG).d("Evaluating calculationNotPossibleBecauseOfOutdatedResults()") // if the last calculation is longer in the past as the defined threshold we return the stale state val timeSinceLastDiagnosisKeyFetchFromServer = TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer() @@ -119,19 +134,30 @@ class RiskLevelTask @Inject constructor( ) /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */ - return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() > - TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold() + return (timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() > + TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold()).also { + if (it) { + Timber.tag(TAG).i("Calculation was not possible because reults are outdated.") + } else { + Timber.tag(TAG).d("Results are not out dated, continuing evaluation.") + } + } } - private fun calculationNotPossibleBecauseOfNoKeys() = - (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also { + private fun calculationNotPossibleBecauseOfNoKeys(): Boolean { + Timber.tag(TAG).d("Evaluating calculationNotPossibleBecauseOfNoKeys()") + return (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also { if (it) { - Timber.tag(TAG) - .v("No last time diagnosis keys from server fetch timestamp was found") + Timber.tag(TAG).v("No last time diagnosis keys from server fetch timestamp was found") + } else { + Timber.tag(TAG).d("Diagnosis keys from server are available, continuing evaluation.") } } + } private fun isActiveTracingTimeAboveThreshold(): Boolean { + Timber.tag(TAG).d("Evaluating isActiveTracingTimeAboveThreshold()") + val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration() val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours() val durationTracingIsActiveThreshold = TimeVariables.getMinActivatedTracingTime().toLong() @@ -144,85 +170,25 @@ class RiskLevelTask @Inject constructor( } } - private suspend fun isIncreasedRisk(configData: ExposureWindowRiskCalculationConfig): Boolean { + private suspend fun calculateRiskLevel(configData: ExposureWindowRiskCalculationConfig): RiskLevelTaskResult { + Timber.tag(TAG).d("Evaluating isIncreasedRisk(...)") val exposureWindows = enfClient.exposureWindows() - return riskLevels.determineRisk(configData, exposureWindows).apply { - // TODO This should be solved differently, by saving a more specialised result object - if (isIncreasedRisk()) { - exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithHighRisk - exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithHighRisk + return riskLevels.determineRisk(configData, exposureWindows).let { + Timber.tag(TAG).d("Evaluated increased risk: %s", it) + if (it.isIncreasedRisk()) { + Timber.tag(TAG).i("Risk is increased!") } else { - exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithLowRisk - exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithLowRisk - } - exposureResultStore.entities.value = ExposureResult(exposureWindows, this) - }.isIncreasedRisk() - } - - private fun updateRepository(riskLevel: RiskLevel, time: Long) { - val rollbackItems = mutableListOf<RollbackItem>() - try { - Timber.tag(TAG).v("Update the risk level with $riskLevel") - val lastCalculatedRiskLevelScoreForRollback = RiskLevelRepository.getLastCalculatedScore() - updateRiskLevelScore(riskLevel) - rollbackItems.add { - updateRiskLevelScore(lastCalculatedRiskLevelScoreForRollback) - } - - // risk level calculation date update - val lastCalculatedRiskLevelDate = LocalData.lastTimeRiskLevelCalculation() - LocalData.lastTimeRiskLevelCalculation(time) - rollbackItems.add { - LocalData.lastTimeRiskLevelCalculation(lastCalculatedRiskLevelDate) - } - } catch (error: Exception) { - Timber.tag(TAG).e(error, "Updating the RiskLevelRepository failed.") - - try { - Timber.tag(TAG).d("Initiate Rollback") - for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke() - } catch (rollbackException: Exception) { - Timber.tag(TAG).e(rollbackException, "RiskLevelRepository rollback failed.") + Timber.tag(TAG).d("Risk is not increased, continuing evaluating.") } - throw error - } - } - - /** - * Updates the Risk Level Score in the repository with the calculated Risk Level - * - * @param riskLevel - */ - @VisibleForTesting - internal fun updateRiskLevelScore(riskLevel: RiskLevel) { - val lastCalculatedScore = RiskLevelRepository.getLastCalculatedScore() - Timber.d("last CalculatedS core is ${lastCalculatedScore.raw} and Current Risk Level is ${riskLevel.raw}") - - if (RiskLevel.riskLevelChangedBetweenLowAndHigh(lastCalculatedScore, riskLevel) && - !LocalData.submissionWasSuccessful() - ) { - Timber.d( - "Notification Permission = ${ - NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled() - }" + RiskLevelTaskResult( + riskLevel = if (it.isIncreasedRisk()) INCREASED_RISK else LOW_LEVEL_RISK, + aggregatedRiskResult = it, + exposureWindows = exposureWindows, + calculatedAt = timeStamper.nowUTC ) - - NotificationHelper.sendNotification( - CoronaWarnApplication.getAppContext().getString(R.string.notification_body) - ) - - Timber.d("Risk level changed and notification sent. Current Risk level is ${riskLevel.raw}") } - if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK && - riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK - ) { - LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true - - Timber.d("Risk level changed LocalData is updated. Current Risk level is ${riskLevel.raw}") - } - RiskLevelRepository.setRiskLevelScore(riskLevel) } private fun checkCancel() { @@ -245,12 +211,6 @@ class RiskLevelTask @Inject constructor( isCanceled = true } - class Result(val riskLevel: RiskLevel) : Task.Result { - override fun toString(): String { - return "Result(riskLevel=${riskLevel.name})" - } - } - data class Config( // TODO unit-test that not > 9 min override val executionTimeout: Duration = Duration.standardMinutes(8), @@ -262,10 +222,10 @@ class RiskLevelTask @Inject constructor( class Factory @Inject constructor( private val taskByDagger: Provider<RiskLevelTask> - ) : TaskFactory<DefaultProgress, Result> { + ) : TaskFactory<DefaultProgress, RiskLevelTaskResult> { override suspend fun createConfig(): TaskFactory.Config = Config() - override val taskProvider: () -> Task<DefaultProgress, Result> = { + override val taskProvider: () -> Task<DefaultProgress, RiskLevelTaskResult> = { taskByDagger.get() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..f930cf13468683bd8334080e3a3b3c69d747c377 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTaskResult.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.task.Task +import org.joda.time.Instant + +data class RiskLevelTaskResult( + override val riskLevel: RiskLevel, + override val calculatedAt: Instant, + override val aggregatedRiskResult: AggregatedRiskResult? = null, + override val exposureWindows: List<ExposureWindow>? = null +) : Task.Result, RiskLevelResult diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt index ca97d2dc37ee251a74321e42c620dc0b49c90a2c..ecf7818a7796c39a206ba779e00e0b7118e46d71 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskModule.kt @@ -1,26 +1,34 @@ package de.rki.coronawarnapp.risk -import dagger.Binds import dagger.Module +import dagger.Provides import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskFactory import de.rki.coronawarnapp.task.TaskTypeKey import javax.inject.Singleton @Module -abstract class RiskModule { +class RiskModule { - @Binds + @Provides @IntoMap @TaskTypeKey(RiskLevelTask::class) - abstract fun riskLevelTaskFactory( + fun riskLevelTaskFactory( factory: RiskLevelTask.Factory - ): TaskFactory<out Task.Progress, out Task.Result> + ): TaskFactory<out Task.Progress, out Task.Result> = factory - @Binds + @Provides @Singleton - abstract fun bindRiskLevelCalculation( + fun bindRiskLevelCalculation( riskLevelCalculation: DefaultRiskLevels - ): RiskLevels + ): RiskLevels = riskLevelCalculation + + @Provides + @Singleton + fun riskLevelStorage( + storage: DefaultRiskLevelStorage + ): RiskLevelStorage = storage } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt index 07595cd56e098af5055339c2ce3cbfcbf07ed19b..1a6d0c6ccd2d06f0925464843cb924b45acfe241 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt @@ -15,4 +15,6 @@ data class AggregatedRiskResult( ) { fun isIncreasedRisk(): Boolean = totalRiskLevel == ProtoRiskLevel.HIGH + + fun isLowRisk(): Boolean = totalRiskLevel == ProtoRiskLevel.LOW } 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 new file mode 100644 index 0000000000000000000000000000000000000000..4c5c45727b0ed71ed0ffca4db488d85ae60dc637 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -0,0 +1,100 @@ +package de.rki.coronawarnapp.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedRiskResult +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.joda.time.Instant +import timber.log.Timber + +abstract class BaseRiskLevelStorage constructor( + private val riskResultDatabaseFactory: RiskResultDatabase.Factory, + private val riskLevelResultMigrator: RiskLevelResultMigrator +) : RiskLevelStorage { + + private val database by lazy { riskResultDatabaseFactory.create() } + internal val riskResultsTables by lazy { database.riskResults() } + internal val exposureWindowsTables by lazy { database.exposureWindows() } + + abstract val storedResultLimit: Int + + override val exposureWindows: Flow<List<ExposureWindow>> = exposureWindowsTables.allEntries().map { windows -> + windows.map { it.toExposureWindow() } + } + + final override val riskLevelResults: Flow<List<RiskLevelResult>> = riskResultsTables.allEntries() + .map { latestResults -> + latestResults.map { it.toRiskResult() } + } + .map { results -> + if (results.isEmpty()) { + riskLevelResultMigrator.getLegacyResults() + } else { + results + } + } + + override val lastRiskLevelResult: Flow<RiskLevelResult> = riskLevelResults.map { results -> + results.maxByOrNull { it.calculatedAt } ?: INITIAL_RESULT + } + + override suspend fun storeResult(result: RiskLevelResult) { + Timber.d("Storing result (exposureWindows.size=%s)", result.exposureWindows?.size) + + val storedResultId = try { + val startTime = System.currentTimeMillis() + + val resultToPersist = result.toPersistedRiskResult() + riskResultsTables.insertEntry(resultToPersist).also { + Timber.d("Storing RiskLevelResult took %dms.", (System.currentTimeMillis() - startTime)) + } + + resultToPersist.id + } catch (e: Exception) { + Timber.e(e, "Failed to store latest result: %s", result) + throw e + } + + try { + Timber.d("Cleaning up old results.") + + riskResultsTables.deleteOldest(storedResultLimit).also { + Timber.d("$it old results were deleted.") + } + } catch (e: Exception) { + Timber.e(e, "Failed to clean up old results.") + throw e + } + + Timber.d("Storing exposure windows.") + storeExposureWindows(storedResultId = storedResultId, result) + + Timber.d("Deleting orphaned exposure windows.") + deletedOrphanedExposureWindows() + } + + internal abstract suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) + + internal abstract suspend fun deletedOrphanedExposureWindows() + + override suspend fun clear() { + Timber.w("clear() - Clearing stored riskleve/exposure-detection results.") + database.clearAllTables() + } + + companion object { + private val INITIAL_RESULT = object : RiskLevelResult { + override val riskLevel: RiskLevel = RiskLevel.LOW_LEVEL_RISK + override val calculatedAt: Instant = Instant.EPOCH + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt new file mode 100644 index 0000000000000000000000000000000000000000..6a261f4480e4b020daaa8ec34542df56d7a969e1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevelResult +import kotlinx.coroutines.flow.Flow + +interface RiskLevelStorage { + + val exposureWindows: Flow<List<ExposureWindow>> + + val riskLevelResults: Flow<List<RiskLevelResult>> + + val lastRiskLevelResult: Flow<RiskLevelResult> + + suspend fun storeResult(result: RiskLevelResult) + + suspend fun clear() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt new file mode 100644 index 0000000000000000000000000000000000000000..f9f1252fa4931ed3014424929357d46e2cc7f656 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskResultDatabase.kt @@ -0,0 +1,87 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import android.content.Context +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.util.database.CommonConverters +import de.rki.coronawarnapp.util.di.AppContext +import kotlinx.coroutines.flow.Flow +import timber.log.Timber +import javax.inject.Inject + +@Suppress("MaxLineLength") +@Database( + entities = [ + PersistedRiskLevelResultDao::class, + PersistedExposureWindowDao::class, + PersistedExposureWindowDao.PersistedScanInstance::class + ], + version = 1, + exportSchema = true +) +@TypeConverters( + CommonConverters::class, + PersistedRiskLevelResultDao.Converter::class, + PersistedRiskLevelResultDao.PersistedAggregatedRiskResult.Converter::class +) +abstract class RiskResultDatabase : RoomDatabase() { + + abstract fun riskResults(): RiskResultsDao + + abstract fun exposureWindows(): ExposureWindowsDao + + @Dao + interface RiskResultsDao { + @Query("SELECT * FROM riskresults") + fun allEntries(): Flow<List<PersistedRiskLevelResultDao>> + + @Insert(onConflict = OnConflictStrategy.ABORT) + suspend fun insertEntry(riskResultDao: PersistedRiskLevelResultDao) + + @Query( + "DELETE FROM riskresults where id NOT IN (SELECT id from riskresults ORDER BY calculatedAt DESC LIMIT :keep)" + ) + suspend fun deleteOldest(keep: Int): Int + } + + @Dao + interface ExposureWindowsDao { + @Query("SELECT * FROM exposurewindows") + fun allEntries(): Flow<List<PersistedExposureWindowDaoWrapper>> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertWindows(exposureWindows: List<PersistedExposureWindowDao>): List<Long> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertScanInstances(scanInstances: List<PersistedExposureWindowDao.PersistedScanInstance>) + + @Query( + "DELETE FROM exposurewindows where riskLevelResultId NOT IN (:riskResultIds)" + ) + suspend fun deleteByRiskResultId(riskResultIds: List<String>): Int + } + + class Factory @Inject constructor(@AppContext private val context: Context) { + + fun create(): RiskResultDatabase { + Timber.d("Instantiating risk result database.") + return Room + .databaseBuilder(context, RiskResultDatabase::class.java, DATABASE_NAME) + .fallbackToDestructiveMigrationFrom() + .build() + } + } + + companion object { + private const val DATABASE_NAME = "riskresults.db" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9f15fcb579bd514218e3e541f1f6531b040655d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskLevelResultDao.kt @@ -0,0 +1,73 @@ +package de.rki.coronawarnapp.risk.storage.internal.riskresults + +import androidx.room.ColumnInfo +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping +import org.joda.time.Instant + +@Entity(tableName = "riskresults") +data class PersistedRiskLevelResultDao( + @PrimaryKey @ColumnInfo(name = "id") val id: String, + @ColumnInfo(name = "riskLevel") val riskLevel: RiskLevel, + @ColumnInfo(name = "calculatedAt") val calculatedAt: Instant, + @Embedded val aggregatedRiskResult: PersistedAggregatedRiskResult? +) { + + fun toRiskResult() = RiskLevelTaskResult( + riskLevel = riskLevel, + calculatedAt = calculatedAt, + aggregatedRiskResult = aggregatedRiskResult?.toAggregatedRiskResult() + ) + + data class PersistedAggregatedRiskResult( + @ColumnInfo(name = "totalRiskLevel") + val totalRiskLevel: NormalizedTimeToRiskLevelMapping.RiskLevel, + @ColumnInfo(name = "totalMinimumDistinctEncountersWithLowRisk") + val totalMinimumDistinctEncountersWithLowRisk: Int, + @ColumnInfo(name = "totalMinimumDistinctEncountersWithHighRisk") + val totalMinimumDistinctEncountersWithHighRisk: Int, + @ColumnInfo(name = "mostRecentDateWithLowRisk") + val mostRecentDateWithLowRisk: Instant?, + @ColumnInfo(name = "mostRecentDateWithHighRisk") + val mostRecentDateWithHighRisk: Instant?, + @ColumnInfo(name = "numberOfDaysWithLowRisk") + val numberOfDaysWithLowRisk: Int, + @ColumnInfo(name = "numberOfDaysWithHighRisk") + val numberOfDaysWithHighRisk: Int + ) { + + fun toAggregatedRiskResult() = AggregatedRiskResult( + totalRiskLevel = totalRiskLevel, + totalMinimumDistinctEncountersWithLowRisk = totalMinimumDistinctEncountersWithLowRisk, + totalMinimumDistinctEncountersWithHighRisk = totalMinimumDistinctEncountersWithHighRisk, + mostRecentDateWithLowRisk = mostRecentDateWithLowRisk, + mostRecentDateWithHighRisk = mostRecentDateWithHighRisk, + numberOfDaysWithLowRisk = numberOfDaysWithLowRisk, + numberOfDaysWithHighRisk = numberOfDaysWithHighRisk + ) + + class Converter { + @TypeConverter + fun toType(value: Int?): NormalizedTimeToRiskLevelMapping.RiskLevel? = + value?.let { NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber(value) } + + @TypeConverter + fun fromType(type: NormalizedTimeToRiskLevelMapping.RiskLevel?): Int? = type?.number + } + } + + class Converter { + @TypeConverter + fun toType(value: Int?): RiskLevel? = + value?.let { RiskLevel.values().single { it.raw == value } } + + @TypeConverter + fun fromType(type: RiskLevel?): Int? = type?.raw + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskResultDaoExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskResultDaoExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..4fbf56ecaabaf13f9741b48448b21adb1c86b30b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/riskresults/PersistedRiskResultDaoExtensions.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.risk.storage.internal.riskresults + +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import java.util.UUID + +fun RiskLevelResult.toPersistedRiskResult( + id: String = UUID.randomUUID().toString() +) = PersistedRiskLevelResultDao( + id = id, + riskLevel = riskLevel, + calculatedAt = calculatedAt, + aggregatedRiskResult = aggregatedRiskResult?.toPersistedAggregatedRiskResult() +) + +fun AggregatedRiskResult.toPersistedAggregatedRiskResult() = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = totalRiskLevel, + totalMinimumDistinctEncountersWithLowRisk = totalMinimumDistinctEncountersWithLowRisk, + totalMinimumDistinctEncountersWithHighRisk = totalMinimumDistinctEncountersWithHighRisk, + mostRecentDateWithLowRisk = mostRecentDateWithLowRisk, + mostRecentDateWithHighRisk = mostRecentDateWithHighRisk, + numberOfDaysWithLowRisk = numberOfDaysWithLowRisk, + numberOfDaysWithHighRisk = numberOfDaysWithHighRisk +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDao.kt new file mode 100644 index 0000000000000000000000000000000000000000..c82caf8dd075bc00a6d6a8dc492b8e53d62ce814 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDao.kt @@ -0,0 +1,39 @@ +package de.rki.coronawarnapp.risk.storage.internal.windows + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.ForeignKey.CASCADE +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity(tableName = "exposurewindows") +data class PersistedExposureWindowDao( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "riskLevelResultId") val riskLevelResultId: String, + @ColumnInfo(name = "dateMillisSinceEpoch") val dateMillisSinceEpoch: Long, + @ColumnInfo(name = "calibrationConfidence") val calibrationConfidence: Int, + @ColumnInfo(name = "infectiousness") val infectiousness: Int, + @ColumnInfo(name = "reportType") val reportType: Int +) { + + @Entity( + tableName = "scaninstances", + foreignKeys = [ + ForeignKey( + onDelete = CASCADE, + entity = PersistedExposureWindowDao::class, + parentColumns = ["id"], + childColumns = ["exposureWindowId"] + ) + ], + indices = [Index("exposureWindowId")] + ) + data class PersistedScanInstance( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0, + @ColumnInfo(name = "exposureWindowId") val exposureWindowId: Long, + @ColumnInfo(name = "minAttenuationDb") val minAttenuationDb: Int, + @ColumnInfo(name = "secondsSinceLastScan") val secondsSinceLastScan: Int, + @ColumnInfo(name = "typicalAttenuationDb") val typicalAttenuationDb: Int + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d43fe68d27727125aeb684c8c507aa95a0c5a0d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoExtensions.kt @@ -0,0 +1,29 @@ +package de.rki.coronawarnapp.risk.storage.internal.windows + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance + +fun ExposureWindow.toPersistedExposureWindow( + riskLevelResultId: String +) = PersistedExposureWindowDao( + riskLevelResultId = riskLevelResultId, + dateMillisSinceEpoch = this.dateMillisSinceEpoch, + calibrationConfidence = this.calibrationConfidence, + infectiousness = this.infectiousness, + reportType = this.reportType +) + +fun List<ExposureWindow>.toPersistedExposureWindows( + riskLevelResultId: String +) = this.map { it.toPersistedExposureWindow(riskLevelResultId) } + +fun ScanInstance.toPersistedScanInstance(exposureWindowId: Long) = PersistedExposureWindowDao.PersistedScanInstance( + exposureWindowId = exposureWindowId, + minAttenuationDb = minAttenuationDb, + secondsSinceLastScan = secondsSinceLastScan, + typicalAttenuationDb = typicalAttenuationDb +) + +fun List<ScanInstance>.toPersistedScanInstances( + exposureWindowId: Long +) = this.map { it.toPersistedScanInstance(exposureWindowId) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoWrapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..b3b8fb13dd6f83a18275de716ade67841292aa95 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/windows/PersistedExposureWindowDaoWrapper.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.risk.storage.internal.windows + +import androidx.room.Embedded +import androidx.room.Relation +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance + +/** + * Helper class for Room @Relation + */ +data class PersistedExposureWindowDaoWrapper( + @Embedded + val exposureWindowDao: PersistedExposureWindowDao, + @Relation(parentColumn = "id", entityColumn = "exposureWindowId") + val scanInstances: List<PersistedExposureWindowDao.PersistedScanInstance> +) { + fun toExposureWindow(): ExposureWindow = + ExposureWindow.Builder().apply { + setDateMillisSinceEpoch(exposureWindowDao.dateMillisSinceEpoch) + setCalibrationConfidence(exposureWindowDao.calibrationConfidence) + setInfectiousness(exposureWindowDao.infectiousness) + setReportType(exposureWindowDao.reportType) + setScanInstances(scanInstances.map { it.toScanInstance() }) + }.build() + + private fun PersistedExposureWindowDao.PersistedScanInstance.toScanInstance(): ScanInstance = ScanInstance.Builder() + .apply { + setMinAttenuationDb(minAttenuationDb) + setSecondsSinceLastScan(secondsSinceLastScan) + setTypicalAttenuationDb(typicalAttenuationDb) + }.build() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt new file mode 100644 index 0000000000000000000000000000000000000000..0e39b13cb57d73e36bb764f7f6b2b2691ac66a3a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigrator.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.risk.storage.legacy + +import android.content.SharedPreferences +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import dagger.Lazy +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.storage.EncryptedPreferences +import de.rki.coronawarnapp.util.TimeStamper +import org.joda.time.Instant +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +/** + * TODO Remove this in the future + * Once a significant portion of the user base has already been running 1.8.x, + * this class can be removed to reduce access to the EncryptedPreferences. + */ +@Singleton +class RiskLevelResultMigrator @Inject constructor( + @EncryptedPreferences encryptedPreferences: Lazy<SharedPreferences>, + private val timeStamper: TimeStamper +) { + + private val prefs by lazy { encryptedPreferences.get() } + + private fun lastTimeRiskLevelCalculation(): Instant? { + prefs.getLong("preference_timestamp_risk_level_calculation", -1L).also { + return if (it < 0) null else Instant.ofEpochMilli(it) + } + } + + private fun lastCalculatedRiskLevel(): RiskLevel? { + val rawRiskLevel = prefs.getInt("preference_risk_level_score", -1) + return if (rawRiskLevel != -1) RiskLevel.forValue(rawRiskLevel) else null + } + + private fun lastSuccessfullyCalculatedRiskLevel(): RiskLevel? { + val rawRiskLevel = prefs.getInt("preference_risk_level_score_successful", -1) + return if (rawRiskLevel != -1) RiskLevel.forValue(rawRiskLevel) else null + } + + fun getLegacyResults(): List<RiskLevelResult> = try { + val legacyResults = mutableListOf<RiskLevelResult>() + lastCalculatedRiskLevel()?.let { + legacyResults.add( + LegacyResult( + riskLevel = it, + calculatedAt = lastTimeRiskLevelCalculation() ?: timeStamper.nowUTC + ) + ) + } + + lastSuccessfullyCalculatedRiskLevel()?.let { + legacyResults.add(LegacyResult(riskLevel = it, calculatedAt = timeStamper.nowUTC)) + } + + legacyResults + } catch (e: Exception) { + Timber.e(e, "Failed to parse legacy risklevel data.") + emptyList() + } + + data class LegacyResult( + override val riskLevel: RiskLevel, + override val calculatedAt: Instant + ) : RiskLevelResult { + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt new file mode 100644 index 0000000000000000000000000000000000000000..63733ba49fa6471e67ab037f2835dbcf677a8381 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/EncryptedPreferences.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.storage + +import javax.inject.Qualifier + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.RUNTIME) +annotation class EncryptedPreferences diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt index fd533993e2ecbee697a3ec5fc5c2215ce056b7c1..326f0579081966e92e4e4aeaf4a0394e81fe2974 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt @@ -4,7 +4,6 @@ import android.content.SharedPreferences import androidx.core.content.edit import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.risk.RiskLevel import de.rki.coronawarnapp.util.preferences.createFlowPreference import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance import kotlinx.coroutines.flow.Flow @@ -261,78 +260,6 @@ object LocalData { ) } - /**************************************************** - * RISK LEVEL - ****************************************************/ - - /** - * Gets the last calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @return - */ - fun lastCalculatedRiskLevel(): RiskLevel { - val rawRiskLevel = getSharedPreferenceInstance().getInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score), - RiskLevel.UNDETERMINED.raw - ) - return RiskLevel.forValue(rawRiskLevel) - } - - /** - * Sets the last calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @param rawRiskLevel - */ - fun lastCalculatedRiskLevel(rawRiskLevel: Int) = - getSharedPreferenceInstance().edit(true) { - putInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score), - rawRiskLevel - ) - } - - /** - * Gets the last successfully calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @return - */ - fun lastSuccessfullyCalculatedRiskLevel(): RiskLevel { - val rawRiskLevel = getSharedPreferenceInstance().getInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score_successful), - RiskLevel.UNDETERMINED.raw - ) - return RiskLevel.forValue(rawRiskLevel) - } - - /** - * Sets the last calculated risk level - * from the EncryptedSharedPrefs - * - * @see RiskLevelRepository - * - * @param rawRiskLevel - */ - fun lastSuccessfullyCalculatedRiskLevel(rawRiskLevel: Int) = - getSharedPreferenceInstance().edit(true) { - putInt( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_risk_level_score_successful), - rawRiskLevel - ) - } - /** * Gets the boolean if the user has seen the explanation dialog for the * risk level tracing days @@ -401,37 +328,6 @@ object LocalData { fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) = lastTimeDiagnosisKeysFetchedFlowPref.update { value?.time ?: 0L } - /** - * Gets the last time of successful risk level calculation as long - * from the EncryptedSharedPrefs - * - * @return Long - */ - fun lastTimeRiskLevelCalculation(): Long? { - val time = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_risk_level_calculation), - 0L - ) - return Date(time).time - } - - /** - * Sets the last time of successful risk level calculation as long - * from the EncryptedSharedPrefs - * - * @param value timestamp as Long - */ - fun lastTimeRiskLevelCalculation(value: Long?) { - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_risk_level_calculation), - value ?: 0L - ) - } - } - /**************************************************** * SETTINGS DATA ****************************************************/ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt deleted file mode 100644 index 62db1f8e70df71d7e2a1866c33ff8e6cfdb85d4d..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt +++ /dev/null @@ -1,106 +0,0 @@ -package de.rki.coronawarnapp.storage - -import de.rki.coronawarnapp.risk.RiskLevel -import de.rki.coronawarnapp.risk.RiskLevelConstants -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -object RiskLevelRepository { - - private val internalRisklevelScore = MutableStateFlow(getLastSuccessfullyCalculatedScore().raw) - val riskLevelScore: Flow<Int> = internalRisklevelScore - - private val internalRiskLevelScoreLastSuccessfulCalculated = - MutableStateFlow(LocalData.lastSuccessfullyCalculatedRiskLevel().raw) - val riskLevelScoreLastSuccessfulCalculated: Flow<Int> = - internalRiskLevelScoreLastSuccessfulCalculated - - /** - * Set the new calculated [RiskLevel] - * Calculation happens in the [de.rki.coronawarnapp.transaction.RiskLevelTransaction] - * - * @see de.rki.coronawarnapp.transaction.RiskLevelTransaction - * @see de.rki.coronawarnapp.risk.RiskLevels - * - * @param riskLevel - */ - fun setRiskLevelScore(riskLevel: RiskLevel) { - val rawRiskLevel = riskLevel.raw - internalRisklevelScore.value = rawRiskLevel - - setLastCalculatedScore(rawRiskLevel) - setLastSuccessfullyCalculatedScore(riskLevel) - } - - /** - * Resets the data in the [RiskLevelRepository] - * - * @see de.rki.coronawarnapp.util.DataReset - * - */ - fun reset() { - internalRisklevelScore.value = RiskLevelConstants.UNKNOWN_RISK_INITIAL - } - - /** - * Set the current risk level from the last calculated risk level. - * This is necessary if the app has no connectivity and the risk level transaction - * fails. - * - * @see de.rki.coronawarnapp.transaction.RiskLevelTransaction - * - */ - fun setLastCalculatedRiskLevelAsCurrent() { - var lastRiskLevelScore = getLastCalculatedScore() - if (lastRiskLevelScore == RiskLevel.UNDETERMINED) { - lastRiskLevelScore = RiskLevel.UNKNOWN_RISK_INITIAL - } - internalRisklevelScore.value = lastRiskLevelScore.raw - } - - /** - * Get the last calculated RiskLevel - * - * @return - */ - fun getLastCalculatedScore(): RiskLevel = LocalData.lastCalculatedRiskLevel() - - /** - * Set the last calculated RiskLevel - * - * @param rawRiskLevel - */ - private fun setLastCalculatedScore(rawRiskLevel: Int) = - LocalData.lastCalculatedRiskLevel(rawRiskLevel) - - /** - * Get the last successfully calculated [RiskLevel] - * - * @see RiskLevel - * - * @return - */ - fun getLastSuccessfullyCalculatedScore(): RiskLevel = - LocalData.lastSuccessfullyCalculatedRiskLevel() - - /** - * Refreshes repository variable with local data - * - */ - fun refreshLastSuccessfullyCalculatedScore() { - internalRiskLevelScoreLastSuccessfulCalculated.value = - getLastSuccessfullyCalculatedScore().raw - } - - /** - * Set the last successfully calculated [RiskLevel] - * - * @param riskLevel - */ - private fun setLastSuccessfullyCalculatedScore(riskLevel: RiskLevel) { - if (!RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(riskLevel)) { - LocalData.lastSuccessfullyCalculatedRiskLevel(riskLevel.raw) - internalRiskLevelScoreLastSuccessfulCalculated.value = riskLevel.raw - } - } -} 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 75be4b8cf6503bb73bdf1a9dbda5c92e1fad2206..34c4f5fc3ef7226cac825e8d0b3d4f45d9b6169f 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 @@ -172,10 +172,6 @@ class TracingRepository @Inject constructor( } } - fun refreshLastSuccessfullyCalculatedScore() { - RiskLevelRepository.refreshLastSuccessfullyCalculatedScore() - } - companion object { private val TAG: String? = TracingRepository::class.simpleName } 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 99d389d7d608dc1a9491aaa1e5c444c344da490a..60fe10547b02b591e589071454249ac763a71f46 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 @@ -106,7 +106,6 @@ class HomeFragmentViewModel @AssistedInject constructor( tracingRepository.refreshRiskLevel() tracingRepository.refreshActiveTracingDaysInRetentionPeriod() TimerHelper.checkManualKeyRetrievalTimer() - tracingRepository.refreshLastSuccessfullyCalculatedScore() } fun tracingExplanationWasShown() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt index ceb286194df6b309f4b4138964e573721ddb3bc1..69e50181fe5307301d8eae1526bea80d61772583 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardState.kt @@ -10,18 +10,21 @@ import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.ui.tracing.common.BaseTracingState +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate +import org.joda.time.Instant +import org.joda.time.format.DateTimeFormat import java.util.Date data class TracingCardState( override val tracingStatus: GeneralTracingStatus.Status, override val riskLevelScore: Int, override val tracingProgress: TracingProgress, - override val lastRiskLevelScoreCalculated: Int, - override val matchedKeyCount: Int, - override val daysSinceLastExposure: Int, - override val activeTracingDaysInRetentionPeriod: Long, - override val lastTimeDiagnosisKeysFetched: Date?, - override val isBackgroundJobEnabled: Boolean, + val lastRiskLevelScoreCalculated: Int, + val daysWithEncounters: Int, + val lastEncounterAt: Instant?, + val activeTracingDaysInRetentionPeriod: Long, + val lastTimeDiagnosisKeysFetched: Date?, + val isBackgroundJobEnabled: Boolean, override val isManualKeyRetrievalEnabled: Boolean, override val manualKeyRetrievalTime: Long, override val showDetails: Boolean = false @@ -92,27 +95,26 @@ data class TracingCardState( */ fun getRiskContactBody(c: Context): String { val resources = c.resources - val contacts = matchedKeyCount return when (riskLevelScore) { RiskLevelConstants.INCREASED_RISK -> { - if (matchedKeyCount == 0) { - c.getString(R.string.risk_card_body_contact) + if (daysWithEncounters == 0) { + c.getString(R.string.risk_card_high_risk_no_encounters_body) } else { resources.getQuantityString( - R.plurals.risk_card_body_contact_value_high_risk, - contacts, - contacts + R.plurals.risk_card_high_risk_encounter_days_body, + daysWithEncounters, + daysWithEncounters ) } } RiskLevelConstants.LOW_LEVEL_RISK -> { - if (matchedKeyCount == 0) { - c.getString(R.string.risk_card_body_contact) + if (daysWithEncounters == 0) { + c.getString(R.string.risk_card_low_risk_no_encounters_body) } else { resources.getQuantityString( - R.plurals.risk_card_body_contact_value, - contacts, - contacts + R.plurals.risk_card_low_risk_encounter_days_body, + daysWithEncounters, + daysWithEncounters ) } } @@ -136,18 +138,11 @@ data class TracingCardState( * only in the special case of increased risk as a positive contact is a * prerequisite for increased risk */ - fun getRiskContactLast(c: Context): String { - val resources = c.resources - val days = daysSinceLastExposure - return if (riskLevelScore == RiskLevelConstants.INCREASED_RISK) { - resources.getQuantityString( - R.plurals.risk_card_increased_risk_body_contact_last, - days, - days - ) - } else { - "" - } + fun getRiskContactLast(c: Context): String = if (riskLevelScore == RiskLevelConstants.INCREASED_RISK) { + val formattedDate = lastEncounterAt?.toLocalDate()?.toString(DateTimeFormat.mediumDate()) + c.getString(R.string.risk_card_high_risk_most_recent_body, formattedDate) + } else { + "" } /** @@ -211,7 +206,7 @@ data class TracingCardState( c.getString(R.string.risk_card_body_not_yet_fetched) } } - return when (riskLevelScore) { + return when (riskLevelScore) { RiskLevelConstants.LOW_LEVEL_RISK, RiskLevelConstants.INCREASED_RISK -> { if (lastTimeDiagnosisKeysFetched != null) { @@ -302,14 +297,14 @@ data class TracingCardState( fun getRiskInfoContainerBackgroundTint(c: Context): ColorStateList { return if (tracingStatus != GeneralTracingStatus.Status.TRACING_INACTIVE) { - when (riskLevelScore) { - RiskLevelConstants.INCREASED_RISK -> R.color.card_increased - RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> R.color.card_outdated - RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> R.color.card_no_calculation - RiskLevelConstants.LOW_LEVEL_RISK -> R.color.card_low - else -> R.color.card_unknown - }.let { c.getColorStateList(it) } - } else { + when (riskLevelScore) { + RiskLevelConstants.INCREASED_RISK -> R.color.card_increased + RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS -> R.color.card_outdated + RiskLevelConstants.NO_CALCULATION_POSSIBLE_TRACING_OFF -> R.color.card_no_calculation + RiskLevelConstants.LOW_LEVEL_RISK -> R.color.card_low + else -> R.color.card_unknown + }.let { c.getColorStateList(it) } + } else { return c.getColorStateList(R.color.card_no_calculation) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt index 80db0f28aba95ec7aa073aa399fdaf769fdf3f4a..93c77e2289c91f7b097421624289832053ad05ab 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.ui.tracing.card import dagger.Reusable -import de.rki.coronawarnapp.risk.ExposureResultStore -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.SettingsRepository import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus +import de.rki.coronawarnapp.ui.tracing.common.tryLatestResultsWithDefaults import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.flow.combine import kotlinx.coroutines.flow.Flow @@ -21,28 +21,18 @@ class TracingCardStateProvider @Inject constructor( backgroundModeStatus: BackgroundModeStatus, settingsRepository: SettingsRepository, tracingRepository: TracingRepository, - exposureResultStore: ExposureResultStore + riskLevelStorage: RiskLevelStorage ) { - // TODO Refactor these singletons away val state: Flow<TracingCardState> = combine( tracingStatus.generalStatus.onEach { Timber.v("tracingStatus: $it") }, - RiskLevelRepository.riskLevelScore.onEach { - Timber.v("riskLevelScore: $it") - }, - RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated.onEach { - Timber.v("riskLevelScoreLastSuccessfulCalculated: $it") - }, tracingRepository.tracingProgress.onEach { Timber.v("tracingProgress: $it") }, - exposureResultStore.matchedKeyCount.onEach { - Timber.v("matchedKeyCount: $it") - }, - exposureResultStore.daysSinceLastExposure.onEach { - Timber.v("daysSinceLastExposure: $it") + riskLevelStorage.riskLevelResults.onEach { + Timber.v("riskLevelResults: $it") }, tracingRepository.activeTracingDaysInRetentionPeriod.onEach { Timber.v("activeTracingDaysInRetentionPeriod: $it") @@ -60,25 +50,24 @@ class TracingCardStateProvider @Inject constructor( Timber.v("manualKeyRetrievalTimeFlow: $it") } ) { status, - riskLevelScore, - riskLevelScoreLastSuccessfulCalculated, tracingProgress, - matchedKeyCount, - daysSinceLastExposure, + riskLevelResults, activeTracingDaysInRetentionPeriod, lastTimeDiagnosisKeysFetched, isBackgroundJobEnabled, isManualKeyRetrievalEnabled, manualKeyRetrievalTime -> + val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults() + TracingCardState( tracingStatus = status, - riskLevelScore = riskLevelScore, + riskLevelScore = latestCalc.riskLevel.raw, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelScoreLastSuccessfulCalculated, + lastRiskLevelScoreCalculated = latestSuccessfulCalc.riskLevel.raw, lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, + daysWithEncounters = latestCalc.daysWithEncounters, + lastEncounterAt = latestCalc.lastRiskEncounterAt, activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, isBackgroundJobEnabled = isBackgroundJobEnabled, isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt index a132f0cd0f96f0d316e2d5502ce4c3958463e900..f562b2a250efbfec9e06786417c450a95a36ae75 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingState.kt @@ -6,18 +6,11 @@ import de.rki.coronawarnapp.risk.RiskLevelConstants import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHMS -import java.util.Date abstract class BaseTracingState { abstract val tracingStatus: GeneralTracingStatus.Status abstract val riskLevelScore: Int abstract val tracingProgress: TracingProgress - abstract val lastRiskLevelScoreCalculated: Int - abstract val matchedKeyCount: Int - abstract val daysSinceLastExposure: Int - abstract val activeTracingDaysInRetentionPeriod: Long - abstract val lastTimeDiagnosisKeysFetched: Date? - abstract val isBackgroundJobEnabled: Boolean abstract val showDetails: Boolean // Only true for riskdetailsfragment abstract val isManualKeyRetrievalEnabled: Boolean abstract val manualKeyRetrievalTime: Long diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..d190ff869e2e50f709ca3d1021f9c9471a01a504 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensions.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.ui.tracing.common + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import org.joda.time.Instant + +fun List<RiskLevelResult>.tryLatestResultsWithDefaults(): DisplayableRiskResults { + val latestCalculation = this.maxByOrNull { it.calculatedAt } + ?: InitialLowLevelRiskLevelResult + + val lastSuccessfullyCalculated = this.filter { it.wasSuccessfullyCalculated } + .maxByOrNull { it.calculatedAt } ?: UndeterminedRiskLevelResult + + return DisplayableRiskResults( + lastCalculated = latestCalculation, + lastSuccessfullyCalculated = lastSuccessfullyCalculated + ) +} + +data class DisplayableRiskResults( + val lastCalculated: RiskLevelResult, + val lastSuccessfullyCalculated: RiskLevelResult +) + +private object InitialLowLevelRiskLevelResult : RiskLevelResult { + override val riskLevel: RiskLevel = RiskLevel.LOW_LEVEL_RISK + override val calculatedAt: Instant = Instant.now() + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} + +private object UndeterminedRiskLevelResult : RiskLevelResult { + override val riskLevel: RiskLevel = RiskLevel.UNDETERMINED + override val calculatedAt: Instant = Instant.EPOCH + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt index 7db5aa19ca115756fded302a59c5a9d75eebb66c..c167733575c82a39e46f2f9188b53f1b4472d0fc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsState.kt @@ -6,22 +6,19 @@ import de.rki.coronawarnapp.risk.RiskLevelConstants import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.ui.tracing.common.BaseTracingState -import java.util.Date data class TracingDetailsState( override val tracingStatus: GeneralTracingStatus.Status, override val riskLevelScore: Int, override val tracingProgress: TracingProgress, - override val lastRiskLevelScoreCalculated: Int, - override val matchedKeyCount: Int, - override val daysSinceLastExposure: Int, - override val activeTracingDaysInRetentionPeriod: Long, - override val lastTimeDiagnosisKeysFetched: Date?, - override val isBackgroundJobEnabled: Boolean, + val matchedKeyCount: Int, + val activeTracingDaysInRetentionPeriod: Long, + val isBackgroundJobEnabled: Boolean, override val isManualKeyRetrievalEnabled: Boolean, override val manualKeyRetrievalTime: Long, val isInformationBodyNoticeVisible: Boolean, - val isAdditionalInformationVisible: Boolean + val isAdditionalInformationVisible: Boolean, + val daysSinceLastExposure: Int ) : BaseTracingState() { override val showDetails: Boolean = true diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt index 388bca23b501d24bda54b33b85110df0bceb0826..c24de33521b3711f1c10cf2e6e8e4afde713b101 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.ui.tracing.details import dagger.Reusable -import de.rki.coronawarnapp.risk.ExposureResultStore -import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.SettingsRepository import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus +import de.rki.coronawarnapp.ui.tracing.common.tryLatestResultsWithDefaults import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.flow.combine import kotlinx.coroutines.flow.Flow @@ -22,50 +22,41 @@ class TracingDetailsStateProvider @Inject constructor( backgroundModeStatus: BackgroundModeStatus, settingsRepository: SettingsRepository, tracingRepository: TracingRepository, - exposureResultStore: ExposureResultStore + riskLevelStorage: RiskLevelStorage ) { - // TODO Refactore these singletons away val state: Flow<TracingDetailsState> = combine( tracingStatus.generalStatus, - RiskLevelRepository.riskLevelScore, - RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated, tracingRepository.tracingProgress, - exposureResultStore.matchedKeyCount, - exposureResultStore.daysSinceLastExposure, + riskLevelStorage.riskLevelResults, tracingRepository.activeTracingDaysInRetentionPeriod, - tracingRepository.lastTimeDiagnosisKeysFetched, backgroundModeStatus.isAutoModeEnabled, settingsRepository.isManualKeyRetrievalEnabledFlow, settingsRepository.manualKeyRetrievalTimeFlow ) { status, - riskLevelScore, - riskLevelScoreLastSuccessfulCalculated, tracingProgress, - matchedKeyCount, - daysSinceLastExposure, activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched, + riskLevelResults, + activeTracingDaysInRetentionPeriod, isBackgroundJobEnabled, isManualKeyRetrievalEnabled, manualKeyRetrievalTime -> + val (latestCalc, latestSuccessfulCalc) = riskLevelResults.tryLatestResultsWithDefaults() + val isAdditionalInformationVisible = riskDetailPresenter.isAdditionalInfoVisible( - riskLevelScore, matchedKeyCount + latestCalc.riskLevel.raw, latestCalc.matchedKeyCount + ) + val isInformationBodyNoticeVisible = riskDetailPresenter.isInformationBodyNoticeVisible( + latestCalc.riskLevel.raw ) - val isInformationBodyNoticeVisible = - riskDetailPresenter.isInformationBodyNoticeVisible( - riskLevelScore - ) TracingDetailsState( tracingStatus = status, - riskLevelScore = riskLevelScore, + riskLevelScore = latestCalc.riskLevel.raw, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelScoreLastSuccessfulCalculated, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, + matchedKeyCount = latestCalc.matchedKeyCount, + daysSinceLastExposure = latestCalc.daysWithEncounters, activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, isBackgroundJobEnabled = isBackgroundJobEnabled, isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, manualKeyRetrievalTime = manualKeyRetrievalTime, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt index 2e26c1991f2aae2889e89542223fe0116dd341ed..838637192d741986c7b2e407c2b666f590421b76 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt @@ -4,13 +4,13 @@ import android.content.Context import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.flow.shareLatest +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.isActive +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -21,41 +21,41 @@ class BackgroundModeStatus @Inject constructor( @AppScope private val appScope: CoroutineScope ) { - val isBackgroundRestricted: Flow<Boolean?> = callbackFlow<Boolean> { + val isBackgroundRestricted: Flow<Boolean> = flow { while (true) { try { - send(pollIsBackgroundRestricted()) - } catch (e: Exception) { - Timber.w(e, "isBackgroundRestricted failed.") - cancel("isBackgroundRestricted failed", e) + emit(pollIsBackgroundRestricted()) + delay(POLLING_DELAY_MS) + } catch (e: CancellationException) { + Timber.d("isBackgroundRestricted was cancelled") + break } - - if (!isActive) break - - delay(POLLING_DELAY_MS) } } .distinctUntilChanged() + .onCompletion { + if (it != null) Timber.w(it, "isBackgroundRestricted failed.") + } .shareLatest( tag = "isBackgroundRestricted", scope = appScope ) - val isAutoModeEnabled: Flow<Boolean> = callbackFlow<Boolean> { + val isAutoModeEnabled: Flow<Boolean> = flow { while (true) { try { - send(pollIsAutoMode()) - } catch (e: Exception) { - Timber.w(e, "autoModeEnabled failed.") - cancel("autoModeEnabled failed", e) + emit(pollIsAutoMode()) + delay(POLLING_DELAY_MS) + } catch (e: CancellationException) { + Timber.d("isAutoModeEnabled was cancelled") + break } - - if (!isActive) break - - delay(POLLING_DELAY_MS) } } .distinctUntilChanged() + .onCompletion { + if (it != null) Timber.w(it, "autoModeEnabled failed.") + } .shareLatest( tag = "autoModeEnabled", scope = appScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt index 195bd3551aae9a2ba6a3d5306e2faeb64bcbb52c..9e35e4947e08bdbe8615982962b94fee70d786fb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt @@ -17,6 +17,9 @@ object CWADebug { if (isDeviceForTestersBuild) { fileLogger = FileLogger(application) } + + Timber.i("CWA version: %s (%s)", BuildConfig.VERSION_CODE, BuildConfig.GIT_COMMIT_SHORT_HASH) + Timber.i("CWA flavor: %s (%s)", BuildConfig.FLAVOR, BuildConfig.BUILD_TYPE) } val isDebugBuildOrMode: Boolean diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index fd1064bbfef209742a3e763e9ecd386543311e80..cfd87b55762e01eae7698ced66028aa8f1a838e3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -27,7 +27,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.storage.AppDatabase import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.util.di.AppContext @@ -65,8 +64,7 @@ class DataReset @Inject constructor( LocalData.clear() // Shared Preferences Reset SecurityHelper.resetSharedPrefs() - // Reset the current risk level stored in LiveData - RiskLevelRepository.reset() + // Reset the current states stored in LiveData SubmissionRepository.reset() keyCacheRepository.clear() @@ -74,6 +72,7 @@ class DataReset @Inject constructor( interoperabilityRepository.clear() exposureDetectionTracker.clear() keyPackageSyncSettings.clear() + Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt index 84bd21ad68562197d35c5e136283ccd22ef73f7f..c3f50042c4c57701603af3c29fe10bf2782b6604 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLoggerTree.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.util.debug import android.annotation.SuppressLint import android.util.Log +import org.joda.time.Instant import timber.log.Timber import java.io.File import java.io.FileOutputStream @@ -55,7 +56,7 @@ class FileLoggerTree(private val logFile: File) : Timber.DebugTree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { logWriter?.let { try { - it.write("${System.currentTimeMillis()} ${priorityToString(priority)}/$tag: $message\n") + it.write("${Instant.now()} ${priorityToString(priority)}/$tag: $message\n") it.flush() } catch (e: IOException) { Timber.tag(TAG).e(e) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt index 6fdc8c4a0a4137d21f1efb3535cd7cab66cf6cc4..c8a9be0e7fbe0196a2c7780f9689400595a1a420 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/AndroidModule.kt @@ -3,11 +3,14 @@ package de.rki.coronawarnapp.util.di import android.app.Application import android.bluetooth.BluetoothAdapter import android.content.Context +import android.content.SharedPreferences import androidx.core.app.NotificationManagerCompat import androidx.work.WorkManager import dagger.Module import dagger.Provides import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.storage.EncryptedPreferences +import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.worker.WorkManagerProvider import javax.inject.Singleton @@ -38,4 +41,9 @@ class AndroidModule { fun workManager( workManagerProvider: WorkManagerProvider ): WorkManager = workManagerProvider.workManager + + @EncryptedPreferences + @Provides + @Singleton + fun encryptedPreferences(): SharedPreferences = SecurityHelper.globalEncryptedSharedPreferencesInstance } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt index 947b6d55c29ea2894a9050b31a16069d2440c3f3..d5dcfbc2a0e34b162d0c26c98ce38d24efa479ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt @@ -42,6 +42,86 @@ fun <T : Any> Flow<T>.shareLatest( ) .filterNotNull() +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8 + ) +} + +@Suppress("UNCHECKED_CAST", "LongParameterList") +inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, R> combine( + flow: Flow<T1>, + flow2: Flow<T2>, + flow3: Flow<T3>, + flow4: Flow<T4>, + flow5: Flow<T5>, + flow6: Flow<T6>, + flow7: Flow<T7>, + flow8: Flow<T8>, + flow9: Flow<T9>, + flow10: Flow<T10>, + crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8, T9, T10) -> R +): Flow<R> = combine( + flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8, flow9, flow10 +) { args: Array<*> -> + transform( + args[0] as T1, + args[1] as T2, + args[2] as T3, + args[3] as T4, + args[4] as T5, + args[5] as T6, + args[6] as T7, + args[7] as T8, + args[8] as T9, + args[9] as T10 + ) +} + @Suppress("UNCHECKED_CAST", "LongParameterList") inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R> combine( flow: Flow<T1>, diff --git a/Corona-Warn-App/src/main/res/values-bg/strings.xml b/Corona-Warn-App/src/main/res/values-bg/strings.xml index 469ac6b4b49c3c138f8703f8e2d5db12bedf9726..5d8c2cdd3f2a8dd8817fba4c35c0072fb244b634 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"До момента нÑма излагане на риÑк"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s излагане на ниÑък риÑк"</item> - <item quantity="other">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - <item quantity="zero">"До момента нÑма излагане на ниÑък риÑк"</item> - <item quantity="two">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - <item quantity="few">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - <item quantity="many">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° ниÑък риÑк"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s излагане на риÑк"</item> - <item quantity="other">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - <item quantity="zero">"До момента нÑма излагане на риÑк"</item> - <item quantity="two">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - <item quantity="few">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - <item quantity="many">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк беше активно през %1$s от изминалите 14 дни."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -167,15 +141,6 @@ <string name="risk_card_low_risk_headline">"ÐиÑък риÑк"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Повишен риÑк"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s ден от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="other">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="zero">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="two">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="few">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - <item quantity="many">"%1$s дни от поÑÐ»ÐµÐ´Ð½Ð¸Ñ ÐºÐ¾Ð½Ñ‚Ð°ÐºÑ‚"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"ÐеизвеÑтен риÑк"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index d024cca935d97319b2a2836ee1d2c7a278b7cd78..266c4eea2ab2589f4e135eb24424b6161cd2c9a1 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -35,12 +35,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -126,26 +120,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Bisher keine Risiko-Begegnungen"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s Begegnung mit niedrigem Risiko"</item> - <item quantity="other">"%1$s Begegnungen mit niedrigem Risiko"</item> - <item quantity="zero">"Bisher keine Begegnungen mit niedrigem Risiko"</item> - <item quantity="two">"%1$s Begegnungen mit niedrigem Risiko"</item> - <item quantity="few">"%1$s Begegnungen mit niedrigem Risiko"</item> - <item quantity="many">"%1$s Begegnungen mit niedrigem Risiko"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s Risiko-Begegnung"</item> - <item quantity="other">"%1$s Risiko-Begegnungen"</item> - <item quantity="zero">"Bisher keine Risiko-Begegnungen"</item> - <item quantity="two">"%1$s Risiko-Begegnungen"</item> - <item quantity="few">"%1$s Risiko-Begegnungen"</item> - <item quantity="many">"%1$s Risiko-Begegnungen"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Risiko-Ermittlung war für %1$s der letzten 14 Tage aktiv"</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -168,15 +142,6 @@ <string name="risk_card_low_risk_headline">"Niedriges Risiko"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Erhöhtes Risiko"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s Tag seit der letzten Begegnung"</item> - <item quantity="other">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="zero">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="two">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="few">"%1$s Tage seit der letzten Begegnung"</item> - <item quantity="many">"%1$s Tage seit der letzten Begegnung"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"Unbekanntes Risiko"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml index a5e01cdeff99cac3ee50b5509b42354aebefa547..334936e23b415c5c293b7a60a70c3484701be007 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"No exposure up to now"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s exposure with low risk"</item> - <item quantity="other">"%1$s exposures with low risk"</item> - <item quantity="zero">"No exposure with low risk so far"</item> - <item quantity="two">"%1$s exposures with low risk"</item> - <item quantity="few">"%1$s exposures with low risk"</item> - <item quantity="many">"%1$s exposures with low risk"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s exposure"</item> - <item quantity="other">"%1$s exposures"</item> - <item quantity="zero">"No exposure up to now"</item> - <item quantity="two">"%1$s exposures"</item> - <item quantity="few">"%1$s exposures"</item> - <item quantity="many">"%1$s exposures"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Exposure logging was active for %1$s of the past 14 days."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -167,15 +141,6 @@ <string name="risk_card_low_risk_headline">"Low Risk"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Increased Risk"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s day since the last encounter"</item> - <item quantity="other">"%1$s days since the last encounter"</item> - <item quantity="zero">"%1$s days since the last encounter"</item> - <item quantity="two">"%1$s days since the last encounter"</item> - <item quantity="few">"%1$s days since the last encounter"</item> - <item quantity="many">"%1$s days since the last encounter"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"Unknown Risk"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> diff --git a/Corona-Warn-App/src/main/res/values-pl/strings.xml b/Corona-Warn-App/src/main/res/values-pl/strings.xml index eded35c4be1a262535dce655302b0fba532e50e5..045ea94fbec30cea390a9df274344d47c14639cc 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Brak narażenia do tej pory"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s narażenie z niskim ryzykiem"</item> - <item quantity="other">"%1$s narażenia z niskim ryzykiem"</item> - <item quantity="zero">"Brak narażenia z niskim ryzykiem do tej pory"</item> - <item quantity="two">"%1$s narażeÅ„ z niskim ryzykiem"</item> - <item quantity="few">"%1$s narażenia z niskim ryzykiem"</item> - <item quantity="many">"%1$s narażeÅ„ z niskim ryzykiem"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s narażenie"</item> - <item quantity="other">"%1$s narażenia"</item> - <item quantity="zero">"Brak narażenia do tej pory"</item> - <item quantity="two">"%1$s narażenia"</item> - <item quantity="few">"%1$s narażenia"</item> - <item quantity="many">"%1$s narażeÅ„"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Rejestrowanie narażenia byÅ‚o aktywne przez %1$s z ostatnich 14 dni."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -167,15 +141,6 @@ <string name="risk_card_low_risk_headline">"Niskie ryzyko"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Podwyższone ryzyko"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s dzieÅ„ od ostatniego kontaktu"</item> - <item quantity="other">"%1$s dnia od ostatniego kontaktu"</item> - <item quantity="zero">"%1$s dni od ostatniego kontaktu"</item> - <item quantity="two">"%1$s dni od ostatniego kontaktu"</item> - <item quantity="few">"%1$s dni od ostatniego kontaktu"</item> - <item quantity="many">"%1$s dni od ostatniego kontaktu"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"Ryzyko nieznane"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> diff --git a/Corona-Warn-App/src/main/res/values-ro/strings.xml b/Corona-Warn-App/src/main/res/values-ro/strings.xml index 0c5a9e4f4e0e8f0252b4a82e4df7ddcaa07dac65..c9195cae2f84eed65972d422311ac86927bbeb32 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Nicio expunere până acum"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s expunere cu risc redus"</item> - <item quantity="other">"%1$s de expuneri cu risc redus"</item> - <item quantity="zero">"Nicio expunere cu risc redus până acum"</item> - <item quantity="two">"%1$s expuneri cu risc redus"</item> - <item quantity="few">"%1$s expuneri cu risc redus"</item> - <item quantity="many">"%1$s expuneri cu risc redus"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s expunere"</item> - <item quantity="other">"%1$s de expuneri"</item> - <item quantity="zero">"Nicio expunere până acum"</item> - <item quantity="two">"%1$s expuneri"</item> - <item quantity="few">"%1$s expuneri"</item> - <item quantity="many">"%1$s expuneri"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"ÃŽn ultimele 14 zile, înregistrarea în jurnal a expunerilor a fost activă timp de %1$s zile."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -167,15 +141,6 @@ <string name="risk_card_low_risk_headline">"Risc redus"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Risc crescut"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s zi de la ultima întâlnire"</item> - <item quantity="other">"%1$s de zile de la ultima întâlnire"</item> - <item quantity="zero">"%1$s zile de la ultima întâlnire"</item> - <item quantity="two">"%1$s zile de la ultima întâlnire"</item> - <item quantity="few">"%1$s zile de la ultima întâlnire"</item> - <item quantity="many">"%1$s zile de la ultima întâlnire"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"Risc necunoscut"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> diff --git a/Corona-Warn-App/src/main/res/values-tr/strings.xml b/Corona-Warn-App/src/main/res/values-tr/strings.xml index 78dc5a455f852fb5ba62c532be47921025386790..99da3bf37b913883e1be1ddd8d0d9f838e3ac9b9 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -34,12 +34,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -125,26 +119,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"Åžu ana dek hiçbir maruz kalma yok"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="other">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="zero">"Åžu ana dek hiçbir düşük riskli maruz kalma yok"</item> - <item quantity="two">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="few">"%1$s kez düşük riskli maruz kalma"</item> - <item quantity="many">"%1$s kez düşük riskli maruz kalma"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s maruz kalma"</item> - <item quantity="other">"%1$s maruz kalma"</item> - <item quantity="zero">"Åžu ana dek hiçbir maruz kalma yok"</item> - <item quantity="two">"%1$s maruz kalma"</item> - <item quantity="few">"%1$s maruz kalma"</item> - <item quantity="many">"%1$s maruz kalma"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Maruz kalma günlüğü son 14 günde %1$s gün etkindi."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -167,15 +141,6 @@ <string name="risk_card_low_risk_headline">"Düşük Risk"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Daha Yüksek Risk"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="other">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="zero">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="two">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="few">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - <item quantity="many">"son karşılaÅŸmanın üzerinden %1$s gün geçti"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"Bilinmeyen Risk"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index f40fe15af2be5b2b4de248d79c2275640f69ad12..24720e30883db8e325bda5a21b3adf3f1eec6e08 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -35,12 +35,6 @@ <!-- NOTR --> <string name="preference_initial_result_received_time"><xliff:g id="preference">"preference_initial_result_received_time"</xliff:g></string> <!-- NOTR --> - <string name="preference_risk_level_score"><xliff:g id="preference">"preference_risk_level_score"</xliff:g></string> - <!-- NOTR --> - <string name="preference_risk_level_score_successful"><xliff:g id="preference">"preference_risk_level_score_successful"</xliff:g></string> - <!-- NOTR --> - <string name="preference_timestamp_risk_level_calculation"><xliff:g id="preference">"preference_timestamp_risk_level_calculation"</xliff:g></string> - <!-- NOTR --> <string name="preference_test_guid"><xliff:g id="preference">"preference_test_guid"</xliff:g></string> <!-- NOTR --> <string name="preference_is_allowed_to_submit_diagnosis_keys"><xliff:g id="preference">"preference_is_allowed_to_submit_diagnosis_keys"</xliff:g></string> @@ -130,26 +124,6 @@ Risk Card ###################################### --> - <!-- XTXT: risk card - no contact yet --> - <string name="risk_card_body_contact">"No exposure up to now"</string> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value"> - <item quantity="one">"%1$s exposure with low risk"</item> - <item quantity="other">"%1$s exposures with low risk"</item> - <item quantity="zero">"No exposure with low risk so far"</item> - <item quantity="two">"%1$s exposures with low risk"</item> - <item quantity="few">"%1$s exposures with low risk"</item> - <item quantity="many">"%1$s exposures with low risk"</item> - </plurals> - <!-- XTXT: risk card - number of contacts for one or more --> - <plurals name="risk_card_body_contact_value_high_risk"> - <item quantity="one">"%1$s exposure"</item> - <item quantity="other">"%1$s exposures"</item> - <item quantity="zero">"No exposure up to now"</item> - <item quantity="two">"%1$s exposures"</item> - <item quantity="few">"%1$s exposures"</item> - <item quantity="many">"%1$s exposures"</item> - </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> <string name="risk_card_body_saved_days">"Exposure logging was active for %1$s of the past 14 days."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> @@ -172,15 +146,6 @@ <string name="risk_card_low_risk_headline">"Low Risk"</string> <!-- XHED: risk card - increased risk headline --> <string name="risk_card_increased_risk_headline">"Increased Risk"</string> - <!-- XTXT: risk card - increased risk days since last contact --> - <plurals name="risk_card_increased_risk_body_contact_last"> - <item quantity="one">"%1$s day since the last encounter"</item> - <item quantity="other">"%1$s days since the last encounter"</item> - <item quantity="zero">"%1$s days since the last encounter"</item> - <item quantity="two">"%1$s days since the last encounter"</item> - <item quantity="few">"%1$s days since the last encounter"</item> - <item quantity="many">"%1$s days since the last encounter"</item> - </plurals> <!-- XHED: risk card - unknown risk headline --> <string name="risk_card_unknown_risk_headline">"Unknown Risk"</string> <!-- XTXT: risk card - tracing isn't active long enough, so a new risk level can't be calculated --> @@ -226,7 +191,7 @@ <item quantity="many" /> </plurals> <!-- XTXT: risk card - High risk state - Most recent date with high risk --> - <string name="risk_card_high_risk_most_recent_body" /> + <string name="risk_card_high_risk_most_recent_body">"Last contact at %1$s"</string> <!-- #################################### Risk Card - Progress diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt index e06b40b3ed38a7e8d3a194f27ad2a003749bd293..d8199e64261e221a682a98b6c2e65ffecadf4055 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt @@ -1,16 +1,17 @@ package de.rki.coronawarnapp.appconfig -import de.rki.coronawarnapp.risk.RiskLevelData +import de.rki.coronawarnapp.risk.RiskLevelSettings +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import io.mockk.MockKAnnotations import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.verify -import io.mockk.verifySequence import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestCoroutineScope import org.junit.jupiter.api.BeforeEach @@ -21,7 +22,8 @@ class ConfigChangeDetectorTest : BaseTest() { @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var taskController: TaskController - @MockK lateinit var riskLevelData: RiskLevelData + @MockK lateinit var riskLevelSettings: RiskLevelSettings + @MockK lateinit var riskLevelStorage: RiskLevelStorage private val currentConfigFake = MutableStateFlow(mockConfigId("initial")) @@ -29,11 +31,9 @@ class ConfigChangeDetectorTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - mockkObject(ConfigChangeDetector.RiskLevelRepositoryDeferrer) - every { ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() } just Runs - every { taskController.submit(any()) } just Runs every { appConfigProvider.currentConfig } returns currentConfigFake + coEvery { riskLevelStorage.clear() } just Runs } private fun mockConfigId(id: String): ConfigData { @@ -46,58 +46,59 @@ class ConfigChangeDetectorTest : BaseTest() { appConfigProvider = appConfigProvider, taskController = taskController, appScope = TestCoroutineScope(), - riskLevelData = riskLevelData + riskLevelSettings = riskLevelSettings, + riskLevelStorage = riskLevelStorage ) @Test fun `new identifier without previous one is ignored`() { - every { riskLevelData.lastUsedConfigIdentifier } returns null + every { riskLevelSettings.lastUsedConfigIdentifier } returns null createInstance().launch() - verify(exactly = 0) { + coVerify(exactly = 0) { taskController.submit(any()) - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() } } @Test fun `new identifier results in new risk level calculation`() { - every { riskLevelData.lastUsedConfigIdentifier } returns "I'm a new identifier" + every { riskLevelSettings.lastUsedConfigIdentifier } returns "I'm a new identifier" createInstance().launch() - verifySequence { - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + coVerifySequence { + riskLevelStorage.clear() taskController.submit(any()) } } @Test fun `same idetifier results in no op`() { - every { riskLevelData.lastUsedConfigIdentifier } returns "initial" + every { riskLevelSettings.lastUsedConfigIdentifier } returns "initial" createInstance().launch() - verify(exactly = 0) { + coVerify(exactly = 0) { taskController.submit(any()) - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() } } @Test fun `new emissions keep triggering the check`() { - every { riskLevelData.lastUsedConfigIdentifier } returns "initial" + every { riskLevelSettings.lastUsedConfigIdentifier } returns "initial" createInstance().launch() currentConfigFake.value = mockConfigId("Straw") currentConfigFake.value = mockConfigId("berry") - verifySequence { - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + coVerifySequence { + riskLevelStorage.clear() taskController.submit(any()) - ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + riskLevelStorage.clear() taskController.submit(any()) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt index f6dae8e55b255f6e8cb3d71bd3c8a7577cb88466..5e50eeaedd865915c26891ecd384e711160b6a7e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.appconfig.mapping.RevokedKeyPackage import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type +import de.rki.coronawarnapp.exception.http.NetworkConnectTimeoutException import io.kotest.matchers.shouldBe import io.mockk.coEvery import io.mockk.coVerify @@ -258,4 +259,18 @@ class HourPackageSyncToolTest : CommonSyncToolTest() { coVerify(exactly = 0) { keyServer.getHourIndex("EUR".loc, "2020-01-04".day) } } + + @Test + fun `network connection time out does not clear the cache and returns an unsuccessful result`() = runBlockingTest { + coEvery { keyServer.getHourIndex(any(), any()) } throws NetworkConnectTimeoutException() + + val instance = createInstance() + instance.syncMissingHourPackages(listOf("EUR".loc), false) shouldBe BaseKeyPackageSyncTool.SyncResult( + successful = false, + newPackages = emptyList() + ) + + coVerify(exactly = 1) { keyServer.getHourIndex("EUR".loc, "2020-01-04".day) } + coVerify(exactly = 0) { keyCache.delete(any()) } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt index d484333fe3dfab9d173bb2e716572ff21f19a9e6..3092fb5d3457283c591e17720f3efba28f1f880e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt @@ -100,7 +100,6 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { } } - @Test fun `quota is just monitored`() { coEvery { submissionQuota.consumeQuota(any()) } returns false diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2f8b7d350b15ce23ac86587e6364698a01d96976 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -0,0 +1,174 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import androidx.core.app.NotificationManagerCompat +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevel.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskLevel.LOW_LEVEL_RISK +import de.rki.coronawarnapp.risk.RiskLevel.UNDETERMINED +import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.ForegroundState +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockkObject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RiskLevelChangeDetectorTest : BaseTest() { + + @MockK lateinit var context: Context + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var riskLevelStorage: RiskLevelStorage + @MockK lateinit var notificationManagerCompat: NotificationManagerCompat + @MockK lateinit var foregroundState: ForegroundState + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkObject(LocalData) + + every { LocalData.isUserToBeNotifiedOfLoweredRiskLevel = any() } just Runs + every { LocalData.submissionWasSuccessful() } returns false + every { foregroundState.isInForeground } returns flowOf(true) + every { notificationManagerCompat.areNotificationsEnabled() } returns true + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createRiskLevel( + riskLevel: RiskLevel, + calculatedAt: Instant = Instant.EPOCH + ): RiskLevelResult = object : RiskLevelResult { + override val riskLevel: RiskLevel = riskLevel + override val calculatedAt: Instant = calculatedAt + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + private fun createInstance(scope: CoroutineScope) = RiskLevelChangeDetector( + context = context, + appScope = scope, + riskLevelStorage = riskLevelStorage, + notificationManagerCompat = notificationManagerCompat, + foregroundState = foregroundState + ) + + @Test + fun `nothing happens if there is only one result yet`() { + every { riskLevelStorage.riskLevelResults } returns flowOf(listOf(createRiskLevel(LOW_LEVEL_RISK))) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData wasNot Called + notificationManagerCompat wasNot Called + } + } + } + + @Test + fun `no risklevel change, nothing should happen`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(LOW_LEVEL_RISK), + createRiskLevel(LOW_LEVEL_RISK) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData wasNot Called + notificationManagerCompat wasNot Called + } + } + } + + @Test + fun `risklevel went from HIGH to LOW`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData.submissionWasSuccessful() + foregroundState.isInForeground + LocalData.isUserToBeNotifiedOfLoweredRiskLevel = any() + } + } + } + + @Test + fun `risklevel went from LOW to HIGH`() { + every { riskLevelStorage.riskLevelResults } returns flowOf( + listOf( + createRiskLevel(INCREASED_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevel(LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH) + ) + ) + + runBlockingTest { + val instance = createInstance(scope = this) + instance.launch() + + advanceUntilIdle() + + coVerifySequence { + LocalData.submissionWasSuccessful() + foregroundState.isInForeground + } + } + } + + @Test + fun `evaluate risk level change detection function`() { + RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, INCREASED_RISK) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(UNKNOWN_RISK_INITIAL, LOW_LEVEL_RISK) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(UNKNOWN_RISK_INITIAL, INCREASED_RISK) shouldBe true + RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, UNKNOWN_RISK_INITIAL) shouldBe true + RiskLevelChangeDetector.hasHighLowLevelChanged(UNDETERMINED, UNKNOWN_RISK_INITIAL) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(UNDETERMINED, INCREASED_RISK) shouldBe true + RiskLevelChangeDetector.hasHighLowLevelChanged(UNKNOWN_RISK_INITIAL, UNDETERMINED) shouldBe false + RiskLevelChangeDetector.hasHighLowLevelChanged(INCREASED_RISK, UNDETERMINED) shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..a5871486aa59588946febedac5d0ef71e47083cc --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelResultTest.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.junit.Test +import testhelpers.BaseTest + +class RiskLevelResultTest : BaseTest() { + + private fun createRiskLevel(riskLevel: RiskLevel): RiskLevelResult = object : RiskLevelResult { + override val riskLevel: RiskLevel = riskLevel + override val calculatedAt: Instant = Instant.EPOCH + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + @Test + fun testUnsuccessfulRistLevels() { + createRiskLevel(RiskLevel.UNDETERMINED).wasSuccessfullyCalculated shouldBe false + createRiskLevel(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF).wasSuccessfullyCalculated shouldBe false + createRiskLevel(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS).wasSuccessfullyCalculated shouldBe false + + createRiskLevel(RiskLevel.UNKNOWN_RISK_INITIAL).wasSuccessfullyCalculated shouldBe true + createRiskLevel(RiskLevel.LOW_LEVEL_RISK).wasSuccessfullyCalculated shouldBe true + createRiskLevel(RiskLevel.INCREASED_RISK).wasSuccessfullyCalculated shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelSettingsTest.kt similarity index 91% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelSettingsTest.kt index 41a2b35177a6eab0a08331f6337a9ce6f991554d..36b3bd07bdc414de3745f27683def088c5f42cfa 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelSettingsTest.kt @@ -10,7 +10,7 @@ import org.junit.jupiter.api.Test import testhelpers.BaseTest import testhelpers.preferences.MockSharedPreferences -class RiskLevelDataTest : BaseTest() { +class RiskLevelSettingsTest : BaseTest() { @MockK lateinit var context: Context lateinit var preferences: MockSharedPreferences @@ -22,7 +22,7 @@ class RiskLevelDataTest : BaseTest() { every { context.getSharedPreferences("risklevel_localdata", Context.MODE_PRIVATE) } returns preferences } - fun createInstance() = RiskLevelData(context = context) + fun createInstance() = RiskLevelSettings(context = context) @Test fun `update last used config identifier`() { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt index f174600cfd181bc4936dbe935c9195c830be8c82..df3dbcab8f7dfa56e2efe7aeee5c50c814680479 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt @@ -7,6 +7,7 @@ import android.net.NetworkCapabilities import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.TimeStamper @@ -32,10 +33,10 @@ class RiskLevelTaskTest : BaseTest() { @MockK lateinit var enfClient: ENFClient @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var backgroundModeStatus: BackgroundModeStatus - @MockK lateinit var riskLevelData: RiskLevelData + @MockK lateinit var riskLevelSettings: RiskLevelSettings @MockK lateinit var configData: ConfigData @MockK lateinit var appConfigProvider: AppConfigProvider - @MockK lateinit var exposureResultStore: ExposureResultStore + @MockK lateinit var riskLevelStorage: RiskLevelStorage private val arguments: Task.Arguments = object : Task.Arguments {} @@ -45,9 +46,9 @@ class RiskLevelTaskTest : BaseTest() { enfClient = enfClient, timeStamper = timeStamper, backgroundModeStatus = backgroundModeStatus, - riskLevelData = riskLevelData, + riskLevelSettings = riskLevelSettings, appConfigProvider = appConfigProvider, - exposureResultStore = exposureResultStore + riskLevelStorage = riskLevelStorage ) @BeforeEach @@ -71,7 +72,7 @@ class RiskLevelTaskTest : BaseTest() { every { enfClient.isTracingEnabled } returns flowOf(true) every { timeStamper.nowUTC } returns Instant.EPOCH - every { riskLevelData.lastUsedConfigIdentifier = any() } just Runs + every { riskLevelSettings.lastUsedConfigIdentifier = any() } just Runs } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt index 7d4c1d7ba462d10b7c381124a96c9a39ee2bfdfd..74290d95b7338ebfb6ad2cc584ed9a9a9d667994 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTest.kt @@ -1,9 +1,7 @@ package de.rki.coronawarnapp.risk import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals -import org.junit.Assert.assertTrue import org.junit.Test class RiskLevelTest { @@ -44,87 +42,4 @@ class RiskLevelTest { assertNotEquals(RiskLevel.forValue(RiskLevelConstants.INCREASED_RISK), RiskLevel.UNDETERMINED) assertNotEquals(RiskLevel.forValue(RiskLevelConstants.UNKNOWN_RISK_OUTDATED_RESULTS), RiskLevel.UNDETERMINED) } - - @Test - fun testUnsuccessfulRistLevels() { - assertTrue(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.UNDETERMINED)) - assertTrue(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF)) - assertTrue(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS)) - - assertFalse(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.UNKNOWN_RISK_INITIAL)) - assertFalse(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.LOW_LEVEL_RISK)) - assertFalse(RiskLevel.UNSUCCESSFUL_RISK_LEVELS.contains(RiskLevel.INCREASED_RISK)) - } - - @Test - fun testRiskLevelChangedFromHighToHigh() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.INCREASED_RISK, - RiskLevel.INCREASED_RISK - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromLowToLow() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNKNOWN_RISK_INITIAL, - RiskLevel.LOW_LEVEL_RISK - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromLowToHigh() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNKNOWN_RISK_INITIAL, - RiskLevel.INCREASED_RISK - ) - assertTrue(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromHighToLow() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.INCREASED_RISK, - RiskLevel.UNKNOWN_RISK_INITIAL - ) - assertTrue(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromUndeterminedToLow() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNDETERMINED, - RiskLevel.UNKNOWN_RISK_INITIAL - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromUndeterminedToHigh() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNDETERMINED, - RiskLevel.INCREASED_RISK - ) - assertTrue(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromLowToUndetermined() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.UNKNOWN_RISK_INITIAL, - RiskLevel.UNDETERMINED - ) - assertFalse(riskLevelHasChanged) - } - - @Test - fun testRiskLevelChangedFromHighToUndetermined() { - val riskLevelHasChanged = RiskLevel.riskLevelChangedBetweenLowAndHigh( - RiskLevel.INCREASED_RISK, - RiskLevel.UNDETERMINED - ) - assertTrue(riskLevelHasChanged) - } } 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 new file mode 100644 index 0000000000000000000000000000000000000000..033cae2147a1e19fc25e3c78bd41a240a25ce216 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -0,0 +1,229 @@ +package de.rki.coronawarnapp.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.Factory +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResultsDao +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao.PersistedAggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao +import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDaoWrapper +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class BaseRiskLevelStorageTest : BaseTest() { + + @MockK lateinit var databaseFactory: Factory + @MockK lateinit var database: RiskResultDatabase + @MockK lateinit var riskResultTables: RiskResultsDao + @MockK lateinit var exposureWindowTables: ExposureWindowsDao + @MockK lateinit var riskLevelResultMigrator: RiskLevelResultMigrator + + private val testRiskLevelResultDao = PersistedRiskLevelResultDao( + id = "riskresult-id", + riskLevel = RiskLevel.INCREASED_RISK, + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + + private val testRisklevelResult = RiskLevelTaskResult( + riskLevel = RiskLevel.INCREASED_RISK, + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = AggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + exposureWindows = null + ) + + private val testExposureWindowDaoWrapper = PersistedExposureWindowDaoWrapper( + exposureWindowDao = PersistedExposureWindowDao( + id = 1, + riskLevelResultId = "riskresult-id", + dateMillisSinceEpoch = 123L, + calibrationConfidence = 1, + infectiousness = 2, + reportType = 3 + ), + scanInstances = listOf( + PersistedExposureWindowDao.PersistedScanInstance( + exposureWindowId = 1, + minAttenuationDb = 10, + secondsSinceLastScan = 20, + typicalAttenuationDb = 30 + ) + ) + ) + private val testExposureWindow = ExposureWindow.Builder().apply { + setDateMillisSinceEpoch(123L) + setCalibrationConfidence(1) + setInfectiousness(2) + setReportType(3) + ScanInstance.Builder().apply { + setMinAttenuationDb(10) + setSecondsSinceLastScan(20) + setTypicalAttenuationDb(30) + }.build().let { setScanInstances(listOf(it)) } + }.build() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { databaseFactory.create() } returns database + every { database.riskResults() } returns riskResultTables + every { database.exposureWindows() } returns exposureWindowTables + every { database.clearAllTables() } just Runs + + every { riskLevelResultMigrator.getLegacyResults() } returns emptyList() + + every { riskResultTables.allEntries() } returns emptyFlow() + coEvery { riskResultTables.insertEntry(any()) } just Runs + coEvery { riskResultTables.deleteOldest(any()) } returns 7 + + every { exposureWindowTables.allEntries() } returns emptyFlow() + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createInstance( + storedResultLimit: Int = 10, + onStoreExposureWindows: (String, RiskLevelResult) -> Unit = { id, result -> }, + onDeletedOrphanedExposureWindows: () -> Unit = {} + ) = object : BaseRiskLevelStorage( + riskResultDatabaseFactory = databaseFactory, + riskLevelResultMigrator = riskLevelResultMigrator + ) { + override val storedResultLimit: Int = storedResultLimit + + override suspend fun storeExposureWindows(storedResultId: String, result: RiskLevelResult) { + onStoreExposureWindows(storedResultId, result) + } + + override suspend fun deletedOrphanedExposureWindows() { + onDeletedOrphanedExposureWindows() + } + } + + @Test + fun `exposureWindows are returned from database and mapped`() { + every { exposureWindowTables.allEntries() } returns flowOf(listOf(testExposureWindowDaoWrapper)) + + runBlockingTest { + val instance = createInstance() + instance.exposureWindows.first() shouldBe listOf(testExposureWindow) + } + } + + @Test + fun `riskLevelResults are returned from database and mapped`() { + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + + runBlockingTest { + val instance = createInstance() + instance.riskLevelResults.first() shouldBe listOf(testRisklevelResult) + + verify { riskLevelResultMigrator wasNot Called } + } + } + + @Test + fun `if no risk level results are available we try to get legacy results`() { + every { riskLevelResultMigrator.getLegacyResults() } returns listOf(mockk(), mockk()) + every { riskResultTables.allEntries() } returns flowOf(emptyList()) + + runBlockingTest { + val instance = createInstance() + instance.riskLevelResults.first().size shouldBe 2 + + verify { riskLevelResultMigrator.getLegacyResults() } + } + } + + @Test + fun `errors when storing risklevel result are rethrown`() = runBlockingTest { + coEvery { riskResultTables.insertEntry(any()) } throws IllegalStateException("No body expects the...") + val instance = createInstance() + shouldThrow<java.lang.IllegalStateException> { + instance.storeResult(testRisklevelResult) + } + } + + @Test + fun `errors when storing exposure window results are thrown`() = runBlockingTest { + val instance = createInstance(onStoreExposureWindows = { _, _ -> throw IllegalStateException("Surprise!") }) + shouldThrow<IllegalStateException> { + instance.storeResult(testRisklevelResult) + } + } + + @Test + fun `storeResult works`() = runBlockingTest { + val mockStoreWindows: (String, RiskLevelResult) -> Unit = spyk() + val mockDeleteOrphanedWindows: () -> Unit = spyk() + + val instance = createInstance( + onStoreExposureWindows = mockStoreWindows, + onDeletedOrphanedExposureWindows = mockDeleteOrphanedWindows + ) + instance.storeResult(testRisklevelResult) + + coVerify { + riskResultTables.insertEntry(any()) + riskResultTables.deleteOldest(instance.storedResultLimit) + mockStoreWindows.invoke(any(), testRisklevelResult) + mockDeleteOrphanedWindows.invoke() + } + } + + @Test + fun `clear works`() = runBlockingTest { + createInstance().clear() + verify { database.clearAllTables() } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedExposureWindowDaoTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedExposureWindowDaoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..61aee1225fd3b8dc7af92644e708f52415c6220b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedExposureWindowDaoTest.kt @@ -0,0 +1,41 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow +import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedScanInstance +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PersistedExposureWindowDaoTest : BaseTest() { + + @Test + fun `mapping is correct`() { + val window: ExposureWindow = mockk() + every { window.calibrationConfidence } returns 0 + every { window.dateMillisSinceEpoch } returns 849628347458723L + every { window.infectiousness } returns 2 + every { window.reportType } returns 2 + window.toPersistedExposureWindow("RESULT_ID").apply { + riskLevelResultId shouldBe "RESULT_ID" + dateMillisSinceEpoch shouldBe 849628347458723L + calibrationConfidence shouldBe 0 + infectiousness shouldBe 2 + reportType shouldBe 2 + } + + val scanInstance: ScanInstance = mockk() + every { scanInstance.minAttenuationDb } returns 30 + every { scanInstance.secondsSinceLastScan } returns 300 + every { scanInstance.typicalAttenuationDb } returns 25 + scanInstance.toPersistedScanInstance(5000L).apply { + exposureWindowId shouldBe 5000 + minAttenuationDb shouldBe 30 + typicalAttenuationDb shouldBe 25 + secondsSinceLastScan shouldBe 300 + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..155c9e81ba5a4db2683f8585bf9d3cd01a2e13ee --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/PersistedRiskResultDaoTest.kt @@ -0,0 +1,46 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PersistedRiskResultDaoTest : BaseTest() { + @Test + fun `mapping is correct`() { + PersistedRiskLevelResultDao( + id = "", + riskLevel = RiskLevel.LOW_LEVEL_RISK, + calculatedAt = Instant.ofEpochMilli(931161601L), + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, + totalMinimumDistinctEncountersWithLowRisk = 89, + totalMinimumDistinctEncountersWithHighRisk = 59, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(852191241L), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(790335113L), + numberOfDaysWithLowRisk = 52, + numberOfDaysWithHighRisk = 81 + ) + ).toRiskResult().apply { + riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK + calculatedAt.millis shouldBe 931161601L + exposureWindows shouldBe null + aggregatedRiskResult shouldNotBe null + aggregatedRiskResult?.apply { + totalRiskLevel shouldBe RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW + totalMinimumDistinctEncountersWithLowRisk shouldBe 89 + totalMinimumDistinctEncountersWithHighRisk shouldBe 59 + mostRecentDateWithLowRisk shouldNotBe null + mostRecentDateWithLowRisk?.millis shouldBe 852191241L + mostRecentDateWithHighRisk shouldNotBe null + mostRecentDateWithHighRisk?.millis shouldBe 790335113L + numberOfDaysWithLowRisk shouldBe 52 + numberOfDaysWithHighRisk shouldBe 81 + } + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..832d0e8c332837c7d93883c529fed885ce9995c0 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/legacy/RiskLevelResultMigratorTest.kt @@ -0,0 +1,124 @@ +package de.rki.coronawarnapp.risk.storage.legacy + +import androidx.core.content.edit +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.MockSharedPreferences + +class RiskLevelResultMigratorTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + private val mockPreferences = MockSharedPreferences() + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.EPOCH.plus(1337) + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + fun createInstance() = RiskLevelResultMigrator( + timeStamper = timeStamper, + encryptedPreferences = { mockPreferences } + ) + + @Test + fun `normal case with full values`() { + mockPreferences.edit { + putInt("preference_risk_level_score", RiskLevel.INCREASED_RISK.raw) + putInt("preference_risk_level_score_successful", RiskLevel.LOW_LEVEL_RISK.raw) + putLong("preference_timestamp_risk_level_calculation", 1234567890L) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults[0].apply { + riskLevel shouldBe RiskLevel.INCREASED_RISK + calculatedAt shouldBe Instant.ofEpochMilli(1234567890L) + } + legacyResults[1].apply { + riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + } + } + + @Test + fun `empty list if no previous data was available`() { + mockPreferences.dataMapPeek.isEmpty() shouldBe true + createInstance().getLegacyResults() shouldBe emptyList() + } + + @Test + fun `if no timestamp is available we use the current time`() { + mockPreferences.edit { + putInt("preference_risk_level_score", RiskLevel.INCREASED_RISK.raw) + putInt("preference_risk_level_score_successful", RiskLevel.LOW_LEVEL_RISK.raw) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults[0].apply { + riskLevel shouldBe RiskLevel.INCREASED_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + legacyResults[1].apply { + riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + } + } + + @Test + fun `last successful is null`() { + mockPreferences.edit { + putInt("preference_risk_level_score_successful", RiskLevel.INCREASED_RISK.raw) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults.size shouldBe 1 + legacyResults.first().apply { + riskLevel shouldBe RiskLevel.INCREASED_RISK + calculatedAt shouldBe Instant.EPOCH.plus(1337) + } + } + } + + @Test + fun `last successfully calculated is null`() { + mockPreferences.edit { + putInt("preference_risk_level_score", RiskLevel.INCREASED_RISK.raw) + putLong("preference_timestamp_risk_level_calculation", 1234567890L) + } + createInstance().apply { + val legacyResults = getLegacyResults() + legacyResults.size shouldBe 1 + legacyResults.first().apply { + riskLevel shouldBe RiskLevel.INCREASED_RISK + calculatedAt shouldBe Instant.ofEpochMilli(1234567890L) + } + } + } + + @Test + fun `exceptions are handled gracefully`() { + mockPreferences.edit { + putInt("preference_risk_level_score", RiskLevel.INCREASED_RISK.raw) + } + every { timeStamper.nowUTC } throws Exception("Surprise!") + createInstance().getLegacyResults() shouldBe emptyList() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt index ba00e34847588437cdfc964648d3a06e011fe2e7..1d6bd137da9f06e25732fbe70724562c0646d7b6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateTest.kt @@ -17,6 +17,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify import io.mockk.verifySequence +import org.joda.time.Instant import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -42,8 +43,8 @@ class TracingCardStateTest : BaseTest() { riskLevel: Int = 0, tracingProgress: TracingProgress = TracingProgress.Idle, riskLevelLastSuccessfulCalculation: Int = 0, - matchedKeyCount: Int = 0, - daysSinceLastExposure: Int = 0, + daysWithEncounters: Int = 0, + lastEncounterAt: Instant? = null, activeTracingDaysInRetentionPeriod: Long = 0, lastTimeDiagnosisKeysFetched: Date? = mockk(), isBackgroundJobEnabled: Boolean = false, @@ -54,8 +55,8 @@ class TracingCardStateTest : BaseTest() { riskLevelScore = riskLevel, tracingProgress = tracingProgress, lastRiskLevelScoreCalculated = riskLevelLastSuccessfulCalculation, - matchedKeyCount = matchedKeyCount, - daysSinceLastExposure = daysSinceLastExposure, + daysWithEncounters = daysWithEncounters, + lastEncounterAt = lastEncounterAt, activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, isBackgroundJobEnabled = isBackgroundJobEnabled, @@ -336,88 +337,44 @@ class TracingCardStateTest : BaseTest() { @Test fun `risk contact body is affected by risklevel`() { - createInstance( - riskLevel = INCREASED_RISK, - matchedKeyCount = 0 - ).apply { - getRiskContactBody(context) - verify { context.getString(R.string.risk_card_body_contact) } - } - - createInstance( - riskLevel = INCREASED_RISK, - matchedKeyCount = 2 - ).apply { - getRiskContactBody(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_body_contact_value_high_risk, - 2, - 2 - ) - } - } - - createInstance( - riskLevel = LOW_LEVEL_RISK, - matchedKeyCount = 0 - ).apply { - getRiskContactBody(context) - verify { context.getString(R.string.risk_card_body_contact) } - } - - createInstance( - riskLevel = LOW_LEVEL_RISK, - matchedKeyCount = 2 - ).apply { - getRiskContactBody(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_body_contact_value, - 2, - 2 - ) - } - } - createInstance( riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - matchedKeyCount = 0 + daysWithEncounters = 0 ).apply { getRiskContactBody(context) shouldBe "" } createInstance( riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - matchedKeyCount = 0 + daysWithEncounters = 0 ).apply { getRiskContactBody(context) shouldBe "" } createInstance( riskLevel = UNKNOWN_RISK_INITIAL, - matchedKeyCount = 0 + daysWithEncounters = 0 ).apply { getRiskContactBody(context) shouldBe "" } createInstance( riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - matchedKeyCount = 2 + daysWithEncounters = 2 ).apply { getRiskContactBody(context) shouldBe "" } createInstance( riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - matchedKeyCount = 2 + daysWithEncounters = 2 ).apply { getRiskContactBody(context) shouldBe "" } createInstance( riskLevel = UNKNOWN_RISK_INITIAL, - matchedKeyCount = 2 + daysWithEncounters = 2 ).apply { getRiskContactBody(context) shouldBe "" } @@ -454,57 +411,25 @@ class TracingCardStateTest : BaseTest() { @Test fun `last risk contact text formatting`() { createInstance( - riskLevel = INCREASED_RISK, - daysSinceLastExposure = 2 - ).apply { - getRiskContactLast(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_increased_risk_body_contact_last, - 2, - 2 - ) - } - } - - createInstance( - riskLevel = INCREASED_RISK, - daysSinceLastExposure = 0 - ).apply { - getRiskContactLast(context) - verify { - context.resources.getQuantityString( - R.plurals.risk_card_increased_risk_body_contact_last, - 0, - 0 - ) - } - } - - createInstance( - riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS, - daysSinceLastExposure = 2 + riskLevel = UNKNOWN_RISK_OUTDATED_RESULTS ).apply { getRiskContactLast(context) shouldBe "" } createInstance( - riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF, - daysSinceLastExposure = 2 + riskLevel = NO_CALCULATION_POSSIBLE_TRACING_OFF ).apply { getRiskContactLast(context) shouldBe "" } createInstance( - riskLevel = LOW_LEVEL_RISK, - daysSinceLastExposure = 2 + riskLevel = LOW_LEVEL_RISK ).apply { getRiskContactLast(context) shouldBe "" } createInstance( - riskLevel = UNKNOWN_RISK_INITIAL, - daysSinceLastExposure = 2 + riskLevel = UNKNOWN_RISK_INITIAL ).apply { getRiskContactLast(context) shouldBe "" } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt index 75687a7d8950be735359262e26f93162fbbd1a87..d5e6090b8b42814db11bf29edf1bbc5f899e1b77 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/BaseTracingStateTest.kt @@ -59,12 +59,6 @@ class BaseTracingStateTest : BaseTest() { override val tracingStatus: GeneralTracingStatus.Status = tracingStatus override val riskLevelScore: Int = riskLevelScore override val tracingProgress: TracingProgress = tracingProgress - override val lastRiskLevelScoreCalculated: Int = riskLevelLastSuccessfulCalculation - override val matchedKeyCount: Int = matchedKeyCount - override val daysSinceLastExposure: Int = daysSinceLastExposure - override val activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod - override val lastTimeDiagnosisKeysFetched: Date? = lastTimeDiagnosisKeysFetched - override val isBackgroundJobEnabled: Boolean = isBackgroundJobEnabled override val showDetails: Boolean = showDetails override val isManualKeyRetrievalEnabled: Boolean = isManualKeyRetrievalEnabled override val manualKeyRetrievalTime: Long = manualKeyRetrievalTime diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..77208efddd71c37f580c89709c0bb2dd043e0417 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/common/RiskLevelExtensionsTest.kt @@ -0,0 +1,93 @@ +package de.rki.coronawarnapp.ui.tracing.common + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import io.kotest.matchers.longs.shouldBeInRange +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RiskLevelExtensionsTest : BaseTest() { + + private fun createRiskLevel( + riskLevel: RiskLevel, + calculatedAt: Instant + ): RiskLevelResult = object : RiskLevelResult { + override val riskLevel: RiskLevel = riskLevel + override val calculatedAt: Instant = calculatedAt + override val aggregatedRiskResult: AggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + @Test + fun `getLastestAndLastSuccessful on empty results`() { + val emptyResults = emptyList<RiskLevelResult>() + + emptyResults.tryLatestResultsWithDefaults().apply { + lastCalculated.apply { + riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK + val now = Instant.now().millis + calculatedAt.millis shouldBeInRange ((now - 60 * 1000L)..now + 60 * 1000L) + } + lastSuccessfullyCalculated.apply { + riskLevel shouldBe RiskLevel.UNDETERMINED + } + } + } + + @Test + fun `getLastestAndLastSuccessful last calculation was successful`() { + val results = listOf( + createRiskLevel(RiskLevel.INCREASED_RISK, calculatedAt = Instant.EPOCH), + createRiskLevel(RiskLevel.LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH.plus(1)) + ) + + results.tryLatestResultsWithDefaults().apply { + lastCalculated.riskLevel shouldBe lastSuccessfullyCalculated.riskLevel + lastCalculated.calculatedAt shouldBe lastSuccessfullyCalculated.calculatedAt + + lastCalculated.riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK + lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1) + } + } + + @Test + fun `getLastestAndLastSuccessful last calculation was not successful`() { + val results = listOf( + createRiskLevel(RiskLevel.INCREASED_RISK, calculatedAt = Instant.EPOCH), + createRiskLevel(RiskLevel.LOW_LEVEL_RISK, calculatedAt = Instant.EPOCH.plus(1)), + createRiskLevel(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF, calculatedAt = Instant.EPOCH.plus(2)) + ) + + results.tryLatestResultsWithDefaults().apply { + lastCalculated.riskLevel shouldBe RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF + lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(2) + + lastSuccessfullyCalculated.riskLevel shouldBe RiskLevel.LOW_LEVEL_RISK + lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH.plus(1) + } + } + + @Test + fun `getLastestAndLastSuccessful no successful calculations yet`() { + val results = listOf( + createRiskLevel(RiskLevel.UNDETERMINED, calculatedAt = Instant.EPOCH.plus(10)), + createRiskLevel(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL, calculatedAt = Instant.EPOCH.plus(11)), + createRiskLevel(RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS, calculatedAt = Instant.EPOCH.plus(12)), + createRiskLevel(RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF, calculatedAt = Instant.EPOCH.plus(13)) + ) + + results.tryLatestResultsWithDefaults().apply { + lastCalculated.riskLevel shouldBe RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF + lastCalculated.calculatedAt shouldBe Instant.EPOCH.plus(13) + + lastSuccessfullyCalculated.riskLevel shouldBe RiskLevel.UNDETERMINED + lastSuccessfullyCalculated.calculatedAt shouldBe Instant.EPOCH + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt index 93154d75926eaf91c65b80950faeac0735a74072..46f3bf4433ae4684a960c3d1e56c751120f96df2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateTest.kt @@ -17,7 +17,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import java.util.Date class TracingDetailsStateTest : BaseTest() { @@ -40,11 +39,9 @@ class TracingDetailsStateTest : BaseTest() { tracingStatus: GeneralTracingStatus.Status = mockk(), riskLevelScore: Int = 0, tracingProgress: TracingProgress = TracingProgress.Idle, - riskLevelLastSuccessfulCalculation: Int = 0, matchedKeyCount: Int = 3, daysSinceLastExposure: Int = 2, activeTracingDaysInRetentionPeriod: Long = 0, - lastTimeDiagnosisKeysFetched: Date? = mockk(), isBackgroundJobEnabled: Boolean = false, isManualKeyRetrievalEnabled: Boolean = false, manualKeyRetrievalTime: Long = 0L, @@ -54,11 +51,9 @@ class TracingDetailsStateTest : BaseTest() { tracingStatus = tracingStatus, riskLevelScore = riskLevelScore, tracingProgress = tracingProgress, - lastRiskLevelScoreCalculated = riskLevelLastSuccessfulCalculation, matchedKeyCount = matchedKeyCount, daysSinceLastExposure = daysSinceLastExposure, activeTracingDaysInRetentionPeriod = activeTracingDaysInRetentionPeriod, - lastTimeDiagnosisKeysFetched = lastTimeDiagnosisKeysFetched, isBackgroundJobEnabled = isBackgroundJobEnabled, isManualKeyRetrievalEnabled = isManualKeyRetrievalEnabled, manualKeyRetrievalTime = manualKeyRetrievalTime, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt index 88976e539bab83ed2fb7aa482ee3f35e26557652..385d00586af931d1260de2b514a4cd0c2801ac29 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt @@ -8,7 +8,7 @@ import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationSender import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.playbook.Playbook -import de.rki.coronawarnapp.risk.ExposureResultStore +import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.di.AssistedInjectModule import io.github.classgraph.ClassGraph @@ -95,5 +95,5 @@ class MockProvider { fun enfClient(): ENFClient = mockk() @Provides - fun exposureSummaryRepository(): ExposureResultStore = mockk() + fun exposureSummaryRepository(): RiskLevelStorage = mockk() } 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 new file mode 100644 index 0000000000000000000000000000000000000000..a739176bea2011d5e042eb2e17916cf241da7021 --- /dev/null +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -0,0 +1,122 @@ +package de.rki.coronawarnapp.test.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +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.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DefaultRiskLevelStorageTest : BaseTest() { + + @MockK lateinit var databaseFactory: RiskResultDatabase.Factory + @MockK lateinit var database: RiskResultDatabase + @MockK lateinit var riskResultTables: RiskResultDatabase.RiskResultsDao + @MockK lateinit var exposureWindowTables: RiskResultDatabase.ExposureWindowsDao + @MockK lateinit var riskLevelResultMigrator: RiskLevelResultMigrator + + private val testRiskLevelResultDao = PersistedRiskLevelResultDao( + id = "riskresult-id", + riskLevel = RiskLevel.INCREASED_RISK, + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + private val testRisklevelResult = RiskLevelTaskResult( + riskLevel = RiskLevel.INCREASED_RISK, + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = AggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + exposureWindows = listOf( + ExposureWindow.Builder().build(), + ExposureWindow.Builder().build() + ) + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { databaseFactory.create() } returns database + every { database.riskResults() } returns riskResultTables + every { database.exposureWindows() } returns exposureWindowTables + every { database.clearAllTables() } just Runs + + every { riskLevelResultMigrator.getLegacyResults() } returns emptyList() + + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + coEvery { riskResultTables.insertEntry(any()) } just Runs + coEvery { riskResultTables.deleteOldest(any()) } returns 7 + + every { exposureWindowTables.allEntries() } returns emptyFlow() + coEvery { exposureWindowTables.insertWindows(any()) } returns listOf(111L, 222L) + coEvery { exposureWindowTables.insertScanInstances(any()) } just Runs + coEvery { exposureWindowTables.deleteByRiskResultId(any()) } returns 1 + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createInstance() = DefaultRiskLevelStorage( + riskResultDatabaseFactory = databaseFactory, + riskLevelResultMigrator = riskLevelResultMigrator + ) + + @Test + fun `stored item limit for deviceForTesters`() { + createInstance().storedResultLimit shouldBe 2 * 6 + } + + @Test + fun `we are NOT storing or cleaning up exposure windows`() = runBlockingTest { + val instance = createInstance() + instance.storeResult(testRisklevelResult) + + coVerify { + riskResultTables.insertEntry(any()) + riskResultTables.deleteOldest(instance.storedResultLimit) + } + + coVerify(exactly = 0) { + exposureWindowTables.insertWindows(any()) + exposureWindowTables.insertScanInstances(any()) + exposureWindowTables.deleteByRiskResultId(listOf("riskresult-id")) + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..f2036adfd34846ddb9e2b22eaca6ec3d0f86ba30 --- /dev/null +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -0,0 +1,120 @@ +package de.rki.coronawarnapp.test.risk.storage + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelTaskResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase +import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao +import de.rki.coronawarnapp.risk.storage.legacy.RiskLevelResultMigrator +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +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.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class DefaultRiskLevelStorageTest : BaseTest() { + + @MockK lateinit var databaseFactory: RiskResultDatabase.Factory + @MockK lateinit var database: RiskResultDatabase + @MockK lateinit var riskResultTables: RiskResultDatabase.RiskResultsDao + @MockK lateinit var exposureWindowTables: RiskResultDatabase.ExposureWindowsDao + @MockK lateinit var riskLevelResultMigrator: RiskLevelResultMigrator + + private val testRiskLevelResultDao = PersistedRiskLevelResultDao( + id = "riskresult-id", + riskLevel = RiskLevel.INCREASED_RISK, + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = PersistedRiskLevelResultDao.PersistedAggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ) + ) + private val testRisklevelResult = RiskLevelTaskResult( + riskLevel = RiskLevel.INCREASED_RISK, + calculatedAt = Instant.ofEpochMilli(9999L), + aggregatedRiskResult = AggregatedRiskResult( + totalRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH, + totalMinimumDistinctEncountersWithLowRisk = 1, + totalMinimumDistinctEncountersWithHighRisk = 2, + mostRecentDateWithLowRisk = Instant.ofEpochMilli(3), + mostRecentDateWithHighRisk = Instant.ofEpochMilli(4), + numberOfDaysWithLowRisk = 5, + numberOfDaysWithHighRisk = 6 + ), + exposureWindows = listOf( + ExposureWindow.Builder().build(), + ExposureWindow.Builder().build() + ) + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { databaseFactory.create() } returns database + every { database.riskResults() } returns riskResultTables + every { database.exposureWindows() } returns exposureWindowTables + every { database.clearAllTables() } just Runs + + every { riskLevelResultMigrator.getLegacyResults() } returns emptyList() + + every { riskResultTables.allEntries() } returns flowOf(listOf(testRiskLevelResultDao)) + coEvery { riskResultTables.insertEntry(any()) } just Runs + coEvery { riskResultTables.deleteOldest(any()) } returns 7 + + every { exposureWindowTables.allEntries() } returns emptyFlow() + coEvery { exposureWindowTables.insertWindows(any()) } returns listOf(111L, 222L) + coEvery { exposureWindowTables.insertScanInstances(any()) } just Runs + coEvery { exposureWindowTables.deleteByRiskResultId(any()) } returns 1 + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + private fun createInstance() = DefaultRiskLevelStorage( + riskResultDatabaseFactory = databaseFactory, + riskLevelResultMigrator = riskLevelResultMigrator + ) + + @Test + fun `stored item limit for deviceForTesters`() { + createInstance().storedResultLimit shouldBe 14 * 6 + } + + @Test + fun `we are storing and cleaning up exposure windows`() = runBlockingTest { + val instance = createInstance() + instance.storeResult(testRisklevelResult) + + coVerify { + riskResultTables.insertEntry(any()) + riskResultTables.deleteOldest(instance.storedResultLimit) + + exposureWindowTables.insertWindows(any()) + exposureWindowTables.insertScanInstances(any()) + exposureWindowTables.deleteByRiskResultId(listOf("riskresult-id")) + } + } +}