diff --git a/.reuse/dep5 b/.reuse/dep5 index b1088ff6d57f1f228fdbe25522ebe3b70c80941e..ae4ac319fb88ee2f0c1dc565ae79e4385f673f2d 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -50,4 +50,8 @@ License: Apache-2.0 Files: Corona-Warn-App/libs/play-services-nearby-exposurenotification-18.0.3.aar Copyright: Copyright 2020 Google LLC +License: Apache-2.0 + +Files: Corona-Warn-App/src/main/res/font/roboto.ttf +Copyright: 2011 Google Inc. License: Apache-2.0 \ No newline at end of file diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index f8ac9f20dbc3afc5118bb6e262863d392401a67c..92111156ce7451c297cd779ec6333f70f9d2ee0d 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk.storage import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.util.coroutine.AppScope import kotlinx.coroutines.CoroutineScope @@ -13,8 +14,14 @@ import javax.inject.Singleton class DefaultRiskLevelStorage @Inject constructor( riskResultDatabaseFactory: RiskResultDatabase.Factory, presenceTracingRiskRepository: PresenceTracingRiskRepository, - @AppScope scope: CoroutineScope -) : BaseRiskLevelStorage(riskResultDatabaseFactory, presenceTracingRiskRepository, scope) { + @AppScope scope: CoroutineScope, + riskCombinator: RiskCombinator, +) : BaseRiskLevelStorage( + riskResultDatabaseFactory, + presenceTracingRiskRepository, + scope, + riskCombinator +) { // 2 days, 6 times per day, data is considered stale after 48 hours with risk calculation // Taken from TimeVariables.MAX_STALE_EXPOSURE_RISK_RANGE diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 7fbb56a67d86ed6d29371dec56f158905536068e..23a0450fef17a9260ddcd2b06328d3501d15b7c8 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.risk.storage import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.windows.PersistedExposureWindowDao.PersistedScanInstance import de.rki.coronawarnapp.risk.storage.internal.windows.toPersistedExposureWindow @@ -17,8 +18,14 @@ import javax.inject.Singleton class DefaultRiskLevelStorage @Inject constructor( riskResultDatabaseFactory: RiskResultDatabase.Factory, presenceTracingRiskRepository: PresenceTracingRiskRepository, - @AppScope val scope: CoroutineScope -) : BaseRiskLevelStorage(riskResultDatabaseFactory, presenceTracingRiskRepository, scope) { + @AppScope val scope: CoroutineScope, + riskCombinator: RiskCombinator, +) : BaseRiskLevelStorage( + riskResultDatabaseFactory, + presenceTracingRiskRepository, + scope, + riskCombinator, +) { // 14 days, 6 times per day // For testers keep all the results! diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt index 7e0e674bcee1e3f48cb962d9a1f286e9ef20f8ba..dfb0d221f958bb44cadceaff6600824147254b68 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestViewModel.kt @@ -68,7 +68,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor( taskRunTime.postValue(duration) val warningPackages = traceWarningRepository.allMetaData.first() - val overlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() + val overlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first() val lastResult = presenceTracingRiskRepository.latestEntries(1).first().singleOrNull() val infoText = when { @@ -99,7 +99,7 @@ class PresenceTracingTestViewModel @AssistedInject constructor( riskCalculationRuntime.postValue(it) }, { - val checkInWarningOverlaps = presenceTracingRiskRepository.checkInWarningOverlaps.first() + val checkInWarningOverlaps = presenceTracingRiskRepository.overlapsOfLast14DaysPlusToday.first() val normalizedTimePerCheckInDayList = presenceTracingRiskCalculator.calculateNormalizedTime(checkInWarningOverlaps) val riskStates = diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml index 590b0a48c28bc6f1771a2a1063778e0c93b67f41..f6b5098993b199f056ddc6741d156a879e71a93d 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_qr_code_poster.xml @@ -97,6 +97,7 @@ <TextView android:id="@+id/info_text_view" + style="@style/PosterTextStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:maxLines="2" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..abd313609ebdd09320116af2a9a86fdeafb125c9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifier.kt @@ -0,0 +1,76 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import androidx.annotation.StringRes +import dagger.Reusable +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import javax.inject.Inject + +@Reusable +class TraceLocationVerifier @Inject constructor() { + @Suppress("ReturnCount") + fun verifyTraceLocation(protoQrCodePayload: TraceLocationOuterClass.QRCodePayload): VerificationResult { + val traceLocation = protoQrCodePayload.traceLocation() + + if (traceLocation.description.isEmpty()) { + return VerificationResult.Invalid.Description + } + + if (traceLocation.description.length > QR_CODE_DESCRIPTION_MAX_LENGTH) { + return VerificationResult.Invalid.Description + } + + if (traceLocation.description.lines().size > 1) { + return VerificationResult.Invalid.Description + } + + if (traceLocation.address.isEmpty()) { + return VerificationResult.Invalid.Address + } + + if (traceLocation.address.length > QR_CODE_ADDRESS_MAX_LENGTH) { + return VerificationResult.Invalid.Address + } + + if (traceLocation.address.lines().size > 1) { + return VerificationResult.Invalid.Address + } + + // If both are 0 do nothing else check start is smaller than end or return error + if (!( + protoQrCodePayload.locationData.startTimestamp == 0L && + protoQrCodePayload.locationData.endTimestamp == 0L + ) + ) { + if (protoQrCodePayload.locationData.startTimestamp > protoQrCodePayload.locationData.endTimestamp) { + return VerificationResult.Invalid.StartEndTime + } + } + + if (traceLocation.cryptographicSeed.size != CROWD_NOTIFIER_CRYPTO_SEED_LENGTH) { + return VerificationResult.Invalid.CryptographicSeed + } + + return VerificationResult.Valid( + VerifiedTraceLocation(protoQrCodePayload) + ) + } + + sealed class VerificationResult { + data class Valid(val verifiedTraceLocation: VerifiedTraceLocation) : VerificationResult() + + sealed class Invalid(@StringRes val errorTextRes: Int) : VerificationResult() { + object Description : Invalid(R.string.trace_location_checkins_qr_code_invalid_description) + object Address : Invalid(R.string.trace_location_checkins_qr_code_invalid_address) + object StartEndTime : Invalid(R.string.trace_location_checkins_qr_code_invalid_times) + object CryptographicSeed : + Invalid(R.string.trace_location_checkins_qr_code_invalid_cryptographic_seed) + } + } + + companion object { + private const val CROWD_NOTIFIER_CRYPTO_SEED_LENGTH = 16 + private const val QR_CODE_DESCRIPTION_MAX_LENGTH = 100 + private const val QR_CODE_ADDRESS_MAX_LENGTH = 100 + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt index e553c983471c6c5c523868b2bf95a80a760bf070..ed1f0b0c0eb042105bb16987f3e1f907a6b68528 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreator.kt @@ -12,7 +12,6 @@ import de.rki.coronawarnapp.eventregistration.checkins.split.splitByMidnightUTC import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import org.joda.time.Duration import org.joda.time.Seconds import org.joda.time.format.DateTimeFormat @@ -29,8 +28,7 @@ class ContactJournalCheckInEntryCreator @Inject constructor( Timber.d("Creating journal entry for %s", checkIn) // 1. Create location if missing - val location: ContactDiaryLocation = diaryRepository.locations.first() - .find { it.traceLocationID == checkIn.traceLocationId } ?: checkIn.toLocation() + val location = checkIn.createLocationIfMissing() // 2. Split CheckIn by Midnight UTC val splitCheckIns = checkIn.splitByMidnightUTC() @@ -42,17 +40,24 @@ class ContactJournalCheckInEntryCreator @Inject constructor( .forEach { diaryRepository.addLocationVisit(it) } } - private suspend fun CheckIn.toLocation(): ContactDiaryLocation { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + suspend fun CheckIn.createLocationIfMissing(): ContactDiaryLocation = diaryRepository.locations.first() + .find { it.traceLocationID == traceLocationId } ?: createLocationEntry() + + private suspend fun CheckIn.createLocationEntry(): ContactDiaryLocation { + Timber.d("Creating new contact diary location from %s", this) val location = DefaultContactDiaryLocation( - locationName = locationName(), + locationName = toLocationName(), traceLocationID = traceLocationId ) - Timber.d("Created new location %s and adding it to contact journal db", location) - return diaryRepository.addLocation(location) // Get location from db cause we need the id autogenerated by db + + // Get location from db cause we need the id autogenerated by db + return diaryRepository.addLocation(location) + .also { Timber.d("Created %s and added it to contact journal db", it) } } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - fun CheckIn.locationName(): String { + fun CheckIn.toLocationName(): String { val nameParts = mutableListOf(description, address) if (traceLocationStart != null && traceLocationEnd != null) { @@ -66,7 +71,10 @@ class ContactJournalCheckInEntryCreator @Inject constructor( return nameParts.joinToString(separator = ", ") } - private fun CheckIn.toLocationVisit(location: ContactDiaryLocation): ContactDiaryLocationVisit { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun CheckIn.toLocationVisit(location: ContactDiaryLocation): ContactDiaryLocationVisit { + // Duration column is set by calculating the time difference in minutes between Check-in StartDate + // and Check-in EndDate and rounding it to the closest 15-minute duration // Use Seconds for more precision val durationInMinutes = Seconds.secondsBetween(checkInStart, checkInEnd).seconds / 60.0 val duration = (durationInMinutes / 15).roundToLong() * 15 @@ -75,17 +83,18 @@ class ContactJournalCheckInEntryCreator @Inject constructor( contactDiaryLocation = location, duration = Duration.standardMinutes(duration), checkInID = id - ) + ).also { Timber.d("Created %s for %s", it, this) } } - private suspend fun List<CheckIn>.createMissingLocationVisits(location: ContactDiaryLocation): + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + suspend fun List<CheckIn>.createMissingLocationVisits(location: ContactDiaryLocation): List<ContactDiaryLocationVisit> { Timber.d( "createMissingLocationVisits(location=%s) for %s", location, this.joinToString(prefix = System.lineSeparator(), separator = System.lineSeparator()) ) - val existingLocationVisits = diaryRepository.locationVisits.firstOrNull() ?: emptyList() + val existingLocationVisits = diaryRepository.locationVisits.first() // Existing location visits shall not be updated, so just drop them return filter { existingLocationVisits.none { visit -> @@ -96,7 +105,7 @@ class ContactJournalCheckInEntryCreator @Inject constructor( .map { it.toLocationVisit(location) } .also { Timber.d( - "Created locations visits: %s", + "Created location visits: %s", it.joinToString(prefix = System.lineSeparator(), separator = System.lineSeparator()) ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt index 541496f3bac86227cc1fe7204d8289961871def4..bc9bb66393317c88172eb6889ec8021d322aaab3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/PtRiskLevelResult.kt @@ -7,44 +7,43 @@ import org.joda.time.Instant import org.joda.time.LocalDate /** - * @param presenceTracingDayRisk Only available for the last calculation, if successful, otherwise null - * @param checkInWarningOverlaps Only available for the last calculation, if successful, otherwise null + * @param presenceTracingDayRisk Only available for the latest calculation, otherwise null + * @param checkInWarningOverlaps Only available for the latest calculation, otherwise null */ data class PtRiskLevelResult( val calculatedAt: Instant, val riskState: RiskState, - val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null, + private val presenceTracingDayRisk: List<PresenceTracingDayRisk>? = null, private val checkInWarningOverlaps: List<CheckInWarningOverlap>? = null, ) { - val wasSuccessfullyCalculated: Boolean - get() = riskState != RiskState.CALCULATION_FAILED + val wasSuccessfullyCalculated: Boolean by lazy { + riskState != RiskState.CALCULATION_FAILED + } - val numberOfDaysWithHighRisk: Int - get() = presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0 + val numberOfDaysWithHighRisk: Int by lazy { + presenceTracingDayRisk?.count { it.riskState == RiskState.INCREASED_RISK } ?: 0 + } - val numberOfDaysWithLowRisk: Int - get() = presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0 + val numberOfDaysWithLowRisk: Int by lazy { + presenceTracingDayRisk?.count { it.riskState == RiskState.LOW_RISK } ?: 0 + } - val mostRecentDateWithHighRisk: LocalDate? - get() = presenceTracingDayRisk + val mostRecentDateWithHighRisk: LocalDate? by lazy { + presenceTracingDayRisk ?.filter { it.riskState == RiskState.INCREASED_RISK } ?.maxByOrNull { it.localDateUtc } ?.localDateUtc + } - val mostRecentDateWithLowRisk: LocalDate? - get() = presenceTracingDayRisk + val mostRecentDateWithLowRisk: LocalDate? by lazy { + presenceTracingDayRisk ?.filter { it.riskState == RiskState.LOW_RISK } ?.maxByOrNull { it.localDateUtc } ?.localDateUtc + } - val daysWithEncounters: Int - get() = when (riskState) { - RiskState.INCREASED_RISK -> numberOfDaysWithHighRisk - RiskState.LOW_RISK -> numberOfDaysWithLowRisk - else -> 0 - } - - val checkInOverlapCount: Int - get() = checkInWarningOverlaps?.size ?: 0 + val checkInOverlapCount: Int by lazy { + checkInWarningOverlaps?.size ?: 0 + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt index 568273ff125bcc1716ec8bc64d9ead1a201bc339..a2f9f0098e99fa5077a23dd43be7f48e11e76246 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculator.kt @@ -38,18 +38,19 @@ class PresenceTracingRiskCalculator @Inject constructor( } } - suspend fun calculateAggregatedRiskPerDay(list: List<CheckInNormalizedTime>): - List<PresenceTracingDayRisk> { - return list.groupBy { it.localDateUtc }.map { - val normalizedTimePerDate = it.value.sumByDouble { - it.normalizedTime - } - PresenceTracingDayRisk( - localDateUtc = it.key, - riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate) - ) + suspend fun calculateDayRisk( + list: List<CheckInNormalizedTime> + ): List<PresenceTracingDayRisk> { + return list.groupBy { it.localDateUtc }.map { + val normalizedTimePerDate = it.value.sumByDouble { + it.normalizedTime } + PresenceTracingDayRisk( + localDateUtc = it.key, + riskState = presenceTracingRiskMapper.lookupRiskStatePerDay(normalizedTimePerDate) + ) } + } suspend fun calculateTotalRisk(list: List<CheckInNormalizedTime>): RiskState { if (list.isEmpty()) return RiskState.LOW_RISK diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt index 9dbccbc3e1b0c6c2a191cccde25f7eff674f2d82..1b356f35b51531d19136a1098fd3706e5b1193d8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt @@ -46,27 +46,16 @@ class PresenceTracingRiskRepository @Inject constructor( database.presenceTracingRiskLevelResultDao() } - private val matchesOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches() - .map { timeIntervalMatchEntities -> - timeIntervalMatchEntities - .map { it.toCheckInWarningOverlap() } - .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } - } - - val checkInWarningOverlaps: Flow<List<CheckInWarningOverlap>> = - traceTimeIntervalMatchDao.allMatches().map { matchEntities -> - matchEntities.map { - it.toCheckInWarningOverlap() - } - } + val overlapsOfLast14DaysPlusToday = traceTimeIntervalMatchDao.allMatches().map { entities -> + entities + .map { it.toCheckInWarningOverlap() } + .filter { it.localDateUtc.isAfter(fifteenDaysAgo.toLocalDateUtc()) } + } - private val normalizedTimeOfLast14DaysPlusToday = matchesOfLast14DaysPlusToday.map { + private val normalizedTimeOfLast14DaysPlusToday = overlapsOfLast14DaysPlusToday.map { presenceTracingRiskCalculator.calculateNormalizedTime(it) } - private val fifteenDaysAgo: Instant - get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) - val traceLocationCheckInRiskStates: Flow<List<TraceLocationCheckInRisk>> = normalizedTimeOfLast14DaysPlusToday.map { presenceTracingRiskCalculator.calculateCheckInRiskPerDay(it) @@ -74,7 +63,7 @@ class PresenceTracingRiskRepository @Inject constructor( val presenceTracingDayRisk: Flow<List<PresenceTracingDayRisk>> = normalizedTimeOfLast14DaysPlusToday.map { - presenceTracingRiskCalculator.calculateAggregatedRiskPerDay(it) + presenceTracingRiskCalculator.calculateDayRisk(it) } /** @@ -120,39 +109,23 @@ class PresenceTracingRiskRepository @Inject constructor( } fun latestEntries(limit: Int) = riskLevelResultDao.latestEntries(limit).map { list -> - var lastSuccessfulFound = false - list.sortedByDescending { - it.calculatedAtMillis - } - .map { entity -> - if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { - lastSuccessfulFound = true - // add risk per day to the last successful result - entity.toRiskLevelResult( - presenceTracingDayRisks = presenceTracingDayRisk.first(), - checkInWarningOverlaps = checkInWarningOverlaps.first(), - ) - } else { - entity.toRiskLevelResult( - presenceTracingDayRisks = null, - checkInWarningOverlaps = null, - ) - } - } + list.sortAndComplementLatestResult() } fun allEntries() = riskLevelResultDao.allEntries().map { list -> - var lastSuccessfulFound = false - list.sortedByDescending { + list.sortAndComplementLatestResult() + } + + private suspend fun List<PresenceTracingRiskLevelResultEntity>.sortAndComplementLatestResult() = + sortedByDescending { it.calculatedAtMillis } - .map { entity -> - if (!lastSuccessfulFound && entity.riskState != RiskState.CALCULATION_FAILED) { - lastSuccessfulFound = true - // add risk per day to the last successful result + .mapIndexed { index, entity -> + if (index == 0) { + // add risk per day to the latest result entity.toRiskLevelResult( presenceTracingDayRisks = presenceTracingDayRisk.first(), - checkInWarningOverlaps = checkInWarningOverlaps.first(), + checkInWarningOverlaps = overlapsOfLast14DaysPlusToday.first(), ) } else { entity.toRiskLevelResult( @@ -161,13 +134,15 @@ class PresenceTracingRiskRepository @Inject constructor( ) } } - } private fun addResult(result: PtRiskLevelResult) { Timber.i("Saving risk calculation from ${result.calculatedAt} with result ${result.riskState}.") riskLevelResultDao.insert(result.toRiskLevelEntity()) } + private val fifteenDaysAgo: Instant + get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) + suspend fun clearAllTables() { traceTimeIntervalMatchDao.deleteAll() riskLevelResultDao.deleteAll() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt index 1e2a96561f7506bb77d98c6e253feececd26b436..304aa75d3599ce225c70b2eaddd3ed199d84e596 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/CombinedEwPtRisk.kt @@ -1,8 +1,7 @@ package de.rki.coronawarnapp.risk import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult -import de.rki.coronawarnapp.risk.storage.combine -import de.rki.coronawarnapp.risk.storage.max +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.Instant import org.joda.time.LocalDate @@ -18,7 +17,7 @@ data class CombinedEwPtRiskLevelResult( ) { val riskState: RiskState by lazy { - combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) + RiskCombinator.combine(ptRiskLevelResult.riskState, ewRiskLevelResult.riskState) } val wasSuccessfullyCalculated: Boolean by lazy { @@ -71,3 +70,13 @@ data class LastCombinedRiskResults( val lastCalculated: CombinedEwPtRiskLevelResult, val lastSuccessfullyCalculated: CombinedEwPtRiskLevelResult ) + +internal fun max(left: Instant, right: Instant): Instant { + return Instant.ofEpochMilli(kotlin.math.max(left.millis, right.millis)) +} + +internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? { + if (left == null) return right + if (right == null) return left + return if (left.isAfter(right)) left else right +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index af277a073144c8f1a9aa075768c14e2c18a4013c..0582cc3c9579abb25f40c3ecb13a79bbd617f2c9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -1,7 +1,5 @@ package de.rki.coronawarnapp.risk.storage -import androidx.annotation.VisibleForTesting -import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk @@ -11,10 +9,8 @@ import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.LastCombinedRiskResults -import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.risk.mapToRiskState -import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.risk.storage.internal.riskresults.toPersistedAggregatedRiskPerDateResult @@ -26,17 +22,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import org.joda.time.Instant -import org.joda.time.LocalDate import timber.log.Timber -import java.lang.reflect.Modifier.PRIVATE -import kotlin.math.max import de.rki.coronawarnapp.util.flow.combine as flowCombine abstract class BaseRiskLevelStorage constructor( private val riskResultDatabaseFactory: RiskResultDatabase.Factory, private val presenceTracingRiskRepository: PresenceTracingRiskRepository, - scope: CoroutineScope + scope: CoroutineScope, + private val riskCombinator: RiskCombinator, ) : RiskLevelStorage { private val database by lazy { riskResultDatabaseFactory.create() } @@ -179,7 +172,7 @@ abstract class BaseRiskLevelStorage constructor( ptDayRiskStates, ewDayRiskStates ) { ptRiskList, ewRiskList -> - combineRisk(ptRiskList, ewRiskList) + riskCombinator.combineRisk(ptRiskList, ewRiskList) } override val latestAndLastSuccessfulEwRiskLevelResult: Flow<List<EwRiskLevelResult>> = riskResultsTables @@ -197,12 +190,14 @@ abstract class BaseRiskLevelStorage constructor( presenceTracingRiskRepository.allEntries() ) { ewRiskLevelResults, ptRiskLevelResults -> - val combinedResults = combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + val combinedResults = riskCombinator.combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) .sortedByDescending { it.calculatedAt } LastCombinedRiskResults( - lastCalculated = combinedResults.firstOrNull() ?: currentCombinedLowRisk, - lastSuccessfullyCalculated = combinedResults.find { it.wasSuccessfullyCalculated } ?: initialCombined + lastCalculated = combinedResults.firstOrNull() ?: riskCombinator.latestCombinedResult, + lastSuccessfullyCalculated = combinedResults.find { + it.wasSuccessfullyCalculated + } ?: riskCombinator.initialCombinedResult ) } @@ -217,7 +212,7 @@ abstract class BaseRiskLevelStorage constructor( latestEwRiskLevelResults, latestPtRiskLevelResults ) { ewRiskLevelResults, ptRiskLevelResults -> - combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) + riskCombinator.combineEwPtRiskLevelResults(ptRiskLevelResults, ewRiskLevelResults) .sortedByDescending { it.calculatedAt } .take(2) } @@ -236,105 +231,3 @@ abstract class BaseRiskLevelStorage constructor( private const val TAG = "RiskLevelStorage" } } - -@VisibleForTesting(otherwise = PRIVATE) -internal fun combineRisk( - ptRiskList: List<PresenceTracingDayRisk>, - exposureWindowDayRiskList: List<ExposureWindowDayRisk> -): List<CombinedEwPtDayRisk> { - val allDates = ptRiskList.map { it.localDateUtc }.plus(exposureWindowDayRiskList.map { it.localDateUtc }).distinct() - return allDates.map { date -> - val ptRisk = ptRiskList.find { it.localDateUtc == date } - val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date } - CombinedEwPtDayRisk( - date, - combine( - ptRisk?.riskState, - ewRisk?.riskLevel?.mapToRiskState() - ) - ) - } -} - -internal fun combine(vararg states: RiskState?): RiskState { - if (states.any { it == RiskState.CALCULATION_FAILED }) return RiskState.CALCULATION_FAILED - if (states.any { it == RiskState.INCREASED_RISK }) return RiskState.INCREASED_RISK - - require(states.filterNotNull().all { it == RiskState.LOW_RISK }) - - return RiskState.LOW_RISK -} - -internal fun max(left: Instant, right: Instant): Instant { - return Instant.ofEpochMilli(max(left.millis, right.millis)) -} - -internal fun max(left: LocalDate?, right: LocalDate?): LocalDate? { - if (left == null) return right - if (right == null) return left - return if (left.isAfter(right)) left - else right -} - -@VisibleForTesting(otherwise = PRIVATE) -internal fun combineEwPtRiskLevelResults( - ptRiskResults: List<PtRiskLevelResult>, - ewRiskResults: List<EwRiskLevelResult> -): List<CombinedEwPtRiskLevelResult> { - val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct() - val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt } - val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt } - return allDates.map { date -> - val ptRisk = sortedPtResults.find { it.calculatedAt <= date } ?: ptInitialRiskLevelResult - val ewRisk = sortedEwResults.find { it.calculatedAt <= date } ?: EwInitialRiskLevelResult - CombinedEwPtRiskLevelResult( - ptRisk, - ewRisk - ) - } -} - -private object EwInitialRiskLevelResult : EwRiskLevelResult { - override val calculatedAt: Instant = Instant.EPOCH - override val riskState: RiskState = RiskState.CALCULATION_FAILED - override val failureReason: EwRiskLevelResult.FailureReason? = null - override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null - override val exposureWindows: List<ExposureWindow>? = null - override val matchedKeyCount: Int = 0 - override val daysWithEncounters: Int = 0 -} - -private val ptInitialRiskLevelResult: PtRiskLevelResult by lazy { - PtRiskLevelResult( - calculatedAt = Instant.EPOCH, - riskState = RiskState.CALCULATION_FAILED - ) -} - -private val ewCurrentLowRiskLevelResult - get() = object : EwRiskLevelResult { - override val calculatedAt: Instant = Instant.now() - override val riskState: RiskState = RiskState.LOW_RISK - override val failureReason: EwRiskLevelResult.FailureReason? = null - override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null - override val exposureWindows: List<ExposureWindow>? = null - override val matchedKeyCount: Int = 0 - override val daysWithEncounters: Int = 0 - } - -private val ptCurrentLowRiskLevelResult: PtRiskLevelResult - get() = PtRiskLevelResult( - calculatedAt = Instant.now(), - riskState = RiskState.LOW_RISK - ) - -private val initialCombined = CombinedEwPtRiskLevelResult( - ptInitialRiskLevelResult, - EwInitialRiskLevelResult -) - -private val currentCombinedLowRisk: CombinedEwPtRiskLevelResult - get() = CombinedEwPtRiskLevelResult( - ptCurrentLowRiskLevelResult, - ewCurrentLowRiskLevelResult - ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt new file mode 100644 index 0000000000000000000000000000000000000000..13aa62d91979693e113b43d430cfd340c44aa13b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinator.kt @@ -0,0 +1,117 @@ +package de.rki.coronawarnapp.risk.storage.internal + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import dagger.Reusable +import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtDayRisk +import de.rki.coronawarnapp.risk.CombinedEwPtRiskLevelResult +import de.rki.coronawarnapp.risk.EwRiskLevelResult +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.mapToRiskState +import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult +import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk +import de.rki.coronawarnapp.util.TimeStamper +import org.joda.time.Instant +import javax.inject.Inject + +@Reusable +class RiskCombinator @Inject constructor( + private val timeStamper: TimeStamper +) { + + private val initialEWRiskLevelResult = object : EwRiskLevelResult { + override val calculatedAt: Instant = Instant.EPOCH + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + private val initialPTRiskLevelResult: PtRiskLevelResult = PtRiskLevelResult( + calculatedAt = Instant.EPOCH, + riskState = RiskState.LOW_RISK + ) + + internal val initialCombinedResult = CombinedEwPtRiskLevelResult( + ptRiskLevelResult = initialPTRiskLevelResult, + ewRiskLevelResult = initialEWRiskLevelResult + ) + + private val ewCurrentLowRiskLevelResult + get() = object : EwRiskLevelResult { + override val calculatedAt: Instant = timeStamper.nowUTC + override val riskState: RiskState = RiskState.LOW_RISK + override val failureReason: EwRiskLevelResult.FailureReason? = null + override val ewAggregatedRiskResult: EwAggregatedRiskResult? = null + override val exposureWindows: List<ExposureWindow>? = null + override val matchedKeyCount: Int = 0 + override val daysWithEncounters: Int = 0 + } + + private val ptCurrentLowRiskLevelResult: PtRiskLevelResult + get() = PtRiskLevelResult( + calculatedAt = timeStamper.nowUTC, + riskState = RiskState.LOW_RISK + ) + + internal val latestCombinedResult: CombinedEwPtRiskLevelResult + get() = CombinedEwPtRiskLevelResult( + ptCurrentLowRiskLevelResult, + ewCurrentLowRiskLevelResult + ) + + internal fun combineEwPtRiskLevelResults( + ptRiskResults: List<PtRiskLevelResult>, + ewRiskResults: List<EwRiskLevelResult> + ): List<CombinedEwPtRiskLevelResult> { + val allDates = ptRiskResults.map { it.calculatedAt }.plus(ewRiskResults.map { it.calculatedAt }).distinct() + val sortedPtResults = ptRiskResults.sortedByDescending { it.calculatedAt } + val sortedEwResults = ewRiskResults.sortedByDescending { it.calculatedAt } + return allDates.map { date -> + val ptRisk = sortedPtResults.find { + it.calculatedAt <= date + } ?: initialPTRiskLevelResult + val ewRisk = sortedEwResults.find { + it.calculatedAt <= date + } ?: initialEWRiskLevelResult + + CombinedEwPtRiskLevelResult( + ptRiskLevelResult = ptRisk, + ewRiskLevelResult = ewRisk + ) + } + } + + internal fun combineRisk( + ptRiskList: List<PresenceTracingDayRisk>, + exposureWindowDayRiskList: List<ExposureWindowDayRisk> + ): List<CombinedEwPtDayRisk> { + val allDates = + ptRiskList.map { it.localDateUtc }.plus(exposureWindowDayRiskList.map { it.localDateUtc }).distinct() + return allDates.map { date -> + val ptRisk = ptRiskList.find { it.localDateUtc == date } + val ewRisk = exposureWindowDayRiskList.find { it.localDateUtc == date } + CombinedEwPtDayRisk( + localDate = date, + riskState = combine( + ptRisk?.riskState, + ewRisk?.riskLevel?.mapToRiskState() + ) + ) + } + } + + companion object { + fun combine(vararg states: RiskState?): RiskState { + if (states.any { it == RiskState.CALCULATION_FAILED }) return RiskState.CALCULATION_FAILED + if (states.any { it == RiskState.INCREASED_RISK }) return RiskState.INCREASED_RISK + + require(states.filterNotNull().all { it == RiskState.LOW_RISK }) + + return RiskState.LOW_RISK + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt index 6f9fdeed6e2231cc1f4f7bba4c3ea874a405d299..b69db70bb6c049c1119e8f3313ca2157039a49c2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.lastSubmission import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTaskProgress import de.rki.coronawarnapp.risk.RiskLevelTask +import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.TaskInfo import de.rki.coronawarnapp.task.common.DefaultTaskRequest @@ -45,7 +46,8 @@ class TracingRepository @Inject constructor( enfClient: ENFClient, private val timeStamper: TimeStamper, private val exposureDetectionTracker: ExposureDetectionTracker, - private val backgroundModeStatus: BackgroundModeStatus + private val backgroundModeStatus: BackgroundModeStatus, + private val riskWorkScheduler: RiskWorkScheduler, ) { @SuppressLint("BinaryOperationInTimber") @@ -91,29 +93,17 @@ class TracingRepository @Inject constructor( it.taskState.isActive && it.taskState.request.type == RiskLevelTask::class } - /** - * Refresh the diagnosis keys. For that isRefreshing is set to true which is displayed in the ui. - * Afterwards the RetrieveDiagnosisKeysTransaction and the RiskLevelTransaction are started. - * Regardless of whether the transactions where successful or not the - * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a - * successful go through from the RetrievelDiagnosisKeysTransaction. - */ - fun refreshDiagnosisKeys() { - scope.launch { - taskController.submitBlocking( - DefaultTaskRequest( - DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments(), - originTag = "TracingRepository.refreshDiagnosisKeys()" - ) - ) - taskController.submit( - DefaultTaskRequest( - RiskLevelTask::class, - originTag = "TracingRepository.refreshDiagnosisKeys()" - ) + fun refreshRiskResult() = scope.launch { + Timber.tag(TAG).d("refreshRiskResults()") + + riskWorkScheduler.runRiskTasksNow() + + taskController.submit( + DefaultTaskRequest( + RiskLevelTask::class, + originTag = "TracingRepository.refreshRiskResult()" ) - } + ) } /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt index bc815e3040b8a41cc7b389d7baf59781f43efea8..179d8edbde1c58dcc8672b20c32f3a2ba9de286d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/internal/TaskModule.kt @@ -3,15 +3,10 @@ package de.rki.coronawarnapp.task.internal import dagger.Module import dagger.Provides import de.rki.coronawarnapp.task.TaskCoroutineScope -import de.rki.coronawarnapp.task.example.QueueingTaskModule import kotlinx.coroutines.CoroutineScope import javax.inject.Singleton -@Module( - includes = [ - QueueingTaskModule::class - ] -) +@Module class TaskModule { @Provides diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt index 5aa675aae2f4b9182fce26433476fb2427f97a6c..55e4b09d7ab90a2ba39ba30e9feec61482065b57 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/states/TracingStateProvider.kt @@ -30,19 +30,19 @@ class TracingStateProvider @AssistedInject constructor( ) { val state: Flow<TracingState> = combine( tracingStatus.generalStatus.onEach { - Timber.v("tracingStatus: $it") + Timber.tag(TAG).v("tracingStatus: $it") }, tracingRepository.tracingProgress.onEach { - Timber.v("tracingProgress: $it") + Timber.tag(TAG).v("tracingProgress: $it") }, riskLevelStorage.latestAndLastSuccessfulCombinedEwPtRiskLevelResult.onEach { - Timber.v("riskLevelResults: $it") + Timber.tag(TAG).v("riskLevelResults: $it") }, exposureDetectionTracker.latestSubmission().onEach { - Timber.v("latestSubmission: $it") + Timber.tag(TAG).v("latestSubmission: $it") }, backgroundModeStatus.isAutoModeEnabled.onEach { - Timber.v("isAutoModeEnabled: $it") + Timber.tag(TAG).v("isAutoModeEnabled: $it") } ) { tracingStatus, tracingProgress, @@ -88,12 +88,16 @@ class TracingStateProvider @AssistedInject constructor( ) } } - .onStart { Timber.v("TracingStateProvider FLOW start") } - .onEach { Timber.d("TracingStateProvider FLOW emission: %s", it) } - .onCompletion { Timber.v("TracingStateProvider FLOW completed.") } + .onStart { Timber.tag(TAG).v("TracingStateProvider FLOW start") } + .onEach { Timber.tag(TAG).d("TracingStateProvider FLOW emission: %s", it) } + .onCompletion { Timber.tag(TAG).v("TracingStateProvider FLOW completed.") } @AssistedFactory interface Factory { fun create(isDetailsMode: Boolean): TracingStateProvider } + + companion object { + const val TAG = "TracingStateProvider" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt index dd52cdbc939d76e6c95e680b4982fe33ebd93500..1fbb2d1f2d414fbf604956c3d9ade6e13fa774e3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/tracing/ui/details/TracingDetailsFragmentViewModel.kt @@ -104,7 +104,7 @@ class TracingDetailsFragmentViewModel @AssistedInject constructor( } fun updateRiskDetails() { - tracingRepository.refreshDiagnosisKeys() + tracingRepository.refreshRiskResult() } fun onItemClicked(item: DetailsItem) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt index 5af35e5be9608b289d8430ec27353589a67d2d08..a8c0aba5198cd1c7f7bfa98a90e34dea8566ce5c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInEvent.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.ui.eventregistration.attendee.checkins +import androidx.annotation.StringRes import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation @@ -11,6 +12,10 @@ sealed class CheckInEvent { data class ConfirmCheckIn(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent() + data class InvalidQrCode(@StringRes val errorTextRes: Int) : CheckInEvent() + + data class ConfirmCheckInWithoutHistory(val verifiedTraceLocation: VerifiedTraceLocation) : CheckInEvent() + data class EditCheckIn(val checkInId: Long, val position: Int) : CheckInEvent() data class ConfirmSwipeItem(val checkIn: CheckIn, val position: Int) : CheckInEvent() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt index 4a99d53591edb92a549e77939ecd19b82604adb6..d198e93cc2b4ac82228cc1215d73cec574e7fa7f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsFragment.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.Toast +import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.core.net.toUri @@ -16,6 +17,7 @@ import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.DefaultItemAnimator +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.transition.Hold import com.google.android.material.transition.MaterialSharedAxis import de.rki.coronawarnapp.BuildConfig @@ -38,7 +40,6 @@ import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted import timber.log.Timber -import java.lang.Exception import java.net.URLEncoder import javax.inject.Inject @@ -53,7 +54,8 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag factory as CheckInsViewModel.Factory factory.create( savedState = savedState, - deepLink = navArgs.uri + deepLink = navArgs.uri, + cleanHistory = navArgs.cleanHistory ) } ) @@ -97,6 +99,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag ) } + is CheckInEvent.InvalidQrCode -> showInvalidQrCodeInformation(event.errorTextRes) + + is CheckInEvent.ConfirmCheckInWithoutHistory -> doNavigate( + CheckInsFragmentDirections.actionCheckInsFragmentToConfirmCheckInFragmentCleanHistory( + verifiedTraceLocation = event.verifiedTraceLocation + ) + ) + is CheckInEvent.ConfirmSwipeItem -> showRemovalConfirmation(event.checkIn, event.position) is CheckInEvent.ConfirmRemoveItem -> showRemovalConfirmation(event.checkIn, null) @@ -125,6 +135,13 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } } + private fun showInvalidQrCodeInformation(@StringRes errorTextRes: Int) = + MaterialAlertDialogBuilder(requireContext()).apply { + setTitle(R.string.errors_generic_headline) + setMessage(errorTextRes) + setPositiveButton(R.string.errors_generic_button_positive) { _, _ -> } + }.show() + private fun updateViews(items: List<CheckInsItem>) { checkInsAdapter.update(items) binding.apply { @@ -232,14 +249,14 @@ class CheckInsFragment : Fragment(R.layout.trace_location_attendee_checkins_frag } companion object { - fun createCheckInUri(rootUri: String): Uri { + fun createCheckInUri(rootUri: String, cleanHistory: Boolean = false): Uri { val encodedUrl = try { URLEncoder.encode(rootUri, Charsets.UTF_8.name()) } catch (e: Exception) { Timber.d(e, "URL Encoding failed url($rootUri)") rootUri // Pass original } - return "coronawarnapp://check-ins/$encodedUrl".toUri() + return "coronawarnapp://check-ins/$encodedUrl/?cleanHistory=$cleanHistory".toUri() } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt index cad63ea577b336f62629b952f6668ac15b133842..94c98114e6d18bbff7bd7932ba116ef723bb7684 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModel.kt @@ -9,7 +9,7 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser -import de.rki.coronawarnapp.eventregistration.checkins.qrcode.VerifiedTraceLocation +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationVerifier import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler @@ -28,15 +28,18 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import timber.log.Timber +@Suppress("LongParameterList") class CheckInsViewModel @AssistedInject constructor( @Assisted private val savedState: SavedStateHandle, @Assisted private val deepLink: String?, + @Assisted private val cleanHistory: Boolean, dispatcherProvider: DispatcherProvider, @AppScope private val appScope: CoroutineScope, private val qrCodeUriParser: QRCodeUriParser, private val checkInsRepository: CheckInRepository, private val checkOutHandler: CheckOutHandler, - private val cameraPermissionProvider: CameraPermissionProvider + private val cameraPermissionProvider: CameraPermissionProvider, + private val traceLocationVerifier: TraceLocationVerifier ) : CWAViewModel(dispatcherProvider) { val events = SingleLiveEvent<CheckInEvent>() @@ -142,8 +145,16 @@ class CheckInsViewModel @AssistedInject constructor( try { Timber.i("uri: $uri") val qrCodePayload = qrCodeUriParser.getQrCodePayload(uri) - val verifiedTraceLocation = VerifiedTraceLocation(qrCodePayload) - events.postValue(CheckInEvent.ConfirmCheckIn(verifiedTraceLocation)) + when (val verifyResult = traceLocationVerifier.verifyTraceLocation(qrCodePayload)) { + is TraceLocationVerifier.VerificationResult.Valid -> events.postValue( + if (cleanHistory) + CheckInEvent.ConfirmCheckInWithoutHistory(verifyResult.verifiedTraceLocation) + else + CheckInEvent.ConfirmCheckIn(verifyResult.verifiedTraceLocation) + ) + is TraceLocationVerifier.VerificationResult.Invalid -> + events.postValue(CheckInEvent.InvalidQrCode(verifyResult.errorTextRes)) + } } catch (e: Exception) { Timber.d(e, "TraceLocation verification failed") e.report(ExceptionCategory.INTERNAL) @@ -162,7 +173,8 @@ class CheckInsViewModel @AssistedInject constructor( interface Factory : CWAViewModelFactory<CheckInsViewModel> { fun create( savedState: SavedStateHandle, - deepLink: String? + deepLink: String?, + cleanHistory: Boolean ): CheckInsViewModel } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt index dce82fc756c047ceedd73a1533318fcc2c203892..619de76483d144439940fa7665fc86d1959f31fa 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/ActiveCheckInVH.kt @@ -81,7 +81,6 @@ class ActiveCheckInVH(parent: ViewGroup) : checkoutAction.setOnClickListener { curItem.onCheckout(curItem.checkin) } itemView.apply { - setOnClickListener { curItem.onCardClicked(curItem.checkin, adapterPosition) } transitionName = item.checkin.id.toString() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt index 1a9eb4cd3eb753544367c454dbdc177f2c9f16c3..3c3cd2f0c9b6a16b76f0cdd46feb621e293573e1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/onboarding/CheckInOnboardingFragment.kt @@ -36,7 +36,12 @@ class CheckInOnboardingFragment : Fragment(R.layout.fragment_trace_location_onbo super.onViewCreated(view, savedInstanceState) if (viewModel.isOnboardingComplete && args.uri != null) { - doNavigate(CheckInOnboardingFragmentDirections.actionCheckInOnboardingFragmentToCheckInsFragment(args.uri)) + doNavigate( + CheckInOnboardingFragmentDirections.actionCheckInOnboardingFragmentToCheckInsFragment( + args.uri, + args.cleanHistory + ) + ) } with(binding) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt index 32fccb915730437caa226c162b27f14acfd248d6..6cf9139837b6ba6c194dcd9128985c4652de5de4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/organizer/list/TraceLocationsFragment.kt @@ -116,7 +116,7 @@ class TraceLocationsFragment : Fragment(R.layout.trace_location_organizer_trace_ is TraceLocationEvent.SelfCheckIn -> { findNavController().navigate( - CheckInsFragment.createCheckInUri(it.traceLocation.locationUrl), + CheckInsFragment.createCheckInUri(it.traceLocation.locationUrl, true), NavOptions.Builder() .setPopUpTo(R.id.checkInsFragment, true) .build() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index c795ab4d5dd99665eded47666d7f0fb7cc73eee1..3e0d93c81d701e3875a42efb2e13c0ad5e4990c5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -146,21 +146,21 @@ class HomeFragmentViewModel @AssistedInject constructor( onCardClick = { routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) }, - onUpdateClick = { refreshDiagnosisKeys() } + onUpdateClick = { refreshRiskResult() } ) is IncreasedRisk -> IncreasedRiskCard.Item( state = tracingState, onCardClick = { routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) }, - onUpdateClick = { refreshDiagnosisKeys() } + onUpdateClick = { refreshRiskResult() } ) is TracingFailed -> TracingFailedCard.Item( state = tracingState, onCardClick = { routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToRiskDetailsFragment()) }, - onRetryClick = { refreshDiagnosisKeys() } + onRetryClick = { refreshRiskResult() } ) } }.distinctUntilChanged() @@ -268,7 +268,7 @@ class HomeFragmentViewModel @AssistedInject constructor( fun reenableRiskCalculation() { deregisterWarningAccepted() deadmanNotificationScheduler.schedulePeriodic() - refreshDiagnosisKeys() + refreshRiskResult() } // TODO only lazy to keep tests going which would break because of LocalData access @@ -303,8 +303,8 @@ class HomeFragmentViewModel @AssistedInject constructor( } } - private fun refreshDiagnosisKeys() { - tracingRepository.refreshDiagnosisKeys() + private fun refreshRiskResult() { + tracingRepository.refreshRiskResult() } fun deregisterWarningAccepted() { diff --git a/Corona-Warn-App/src/main/res/font/roboto.ttf b/Corona-Warn-App/src/main/res/font/roboto.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2c97eeadffe1a34bd67d3ff1c3887fd53e22c2ca Binary files /dev/null and b/Corona-Warn-App/src/main/res/font/roboto.ttf differ diff --git a/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml index f2b6d1d139a858278c8c7abcbd497482a39e5a76..903ac942915ebfe2fdb1d7142222ef0b8db30926 100644 --- a/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/qr_code_poster_fragment.xml @@ -93,6 +93,7 @@ <TextView android:id="@+id/info_text_view" + style="@style/PosterTextStyle" android:layout_width="0dp" android:layout_height="wrap_content" android:maxLines="2" diff --git a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml index bbf7abe775c1afe85f8994a4eba64e519d054a59..bf5f0ba98e89a4ec1d90780dbb2337213e3bb056 100644 --- a/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/trace_location_attendee_nav_graph.xml @@ -9,7 +9,7 @@ android:name="de.rki.coronawarnapp.ui.eventregistration.attendee.onboarding.CheckInOnboardingFragment" android:label="CheckInOnboardingFragment" tools:layout="@layout/fragment_trace_location_onboarding"> - <deepLink app:uri="coronawarnapp://check-ins/{uri}" /> + <deepLink app:uri="coronawarnapp://check-ins/{uri}/?cleanHistory={cleanHistory}" /> <action android:id="@+id/action_checkInOnboardingFragment_to_checkInsFragment" app:destination="@id/checkInsFragment" @@ -22,6 +22,10 @@ android:name="showBottomNav" android:defaultValue="true" app:argType="boolean" /> + <argument + android:name="cleanHistory" + android:defaultValue="false" + app:argType="boolean" /> <argument android:name="uri" android:defaultValue="@null" @@ -68,11 +72,20 @@ <action android:id="@+id/action_checkInsFragment_to_confirmCheckInFragment" app:destination="@id/confirmCheckInFragment" /> + <action + android:id="@+id/action_checkInsFragment_to_confirmCheckInFragment_cleanHistory" + app:popUpTo="@id/checkInsFragment" + app:popUpToInclusive="true" + app:destination="@id/confirmCheckInFragment" /> <argument android:name="uri" android:defaultValue="@null" app:argType="string" app:nullable="true" /> + <argument + android:name="cleanHistory" + android:defaultValue="false" + app:argType="boolean" /> <action android:id="@+id/action_checkInsFragment_to_editCheckInFragment" app:destination="@id/editCheckInFragment" /> diff --git a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml index 732c3f2c57841116a81ebae569f4bdc3f7defcef..758b6ca6e8c166dd4540c89b14c42010f5ffb433 100644 --- a/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/event_registration_strings.xml @@ -259,4 +259,14 @@ <!-- XBUT: Organizer Flow : Title for save as template button --> <string name="trace_location_event_detail_save_as_template_button">"Als Vorlage verwenden"</string> + <!-- Qr Code Validation Messages --> + <!-- XTXT: My check-ins: qr code validation wrong description field --> + <string name="trace_location_checkins_qr_code_invalid_description">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_DESCRIPTION)."</string> + <!-- XTXT: My check-ins: qr code validation wrong address field --> + <string name="trace_location_checkins_qr_code_invalid_address">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_ADDRESS)."</string> + <!-- XTXT: My check-ins: qr code validation wrong start end time field --> + <string name="trace_location_checkins_qr_code_invalid_times">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_TIMESTAMPS)."</string> + <!-- XTXT: My check-ins: qr code validation wrong cryptographic seed field --> + <string name="trace_location_checkins_qr_code_invalid_cryptographic_seed">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_CRYPTO_SEED)."</string> + </resources> diff --git a/Corona-Warn-App/src/main/res/values/event_registration_strings.xml b/Corona-Warn-App/src/main/res/values/event_registration_strings.xml index a326943cfb63867f792db7ac14a3789e7bac4326..9896181c86dbe91f97783d12c58501d797371a41 100644 --- a/Corona-Warn-App/src/main/res/values/event_registration_strings.xml +++ b/Corona-Warn-App/src/main/res/values/event_registration_strings.xml @@ -43,7 +43,7 @@ <!-- XTXT: My check-ins card: Active event, checkin information, automatic checkout info --> <string name="trace_location_checkins_card_automatic_checkout_info">"%1$s - check out automatically after %2$s."</string> <!-- XTXT: My check-ins card: Active event, checkin information, automatic checkout info --> - <string name="trace_location_checkins_card_automatic_checkout_info_format">"%1$s 2$s - check out automatically after %3$s."</string> + <string name="trace_location_checkins_card_automatic_checkout_info_format">"%1$s %2$s - check out automatically after %3$s."</string> <!-- XHED: Title of the category list screen of the event creation --> <string name="tracelocation_organizer_category_title">"Create QR Code"</string> @@ -257,4 +257,14 @@ <!-- XBUT: Organizer Flow : Title for save as template button --> <string name="trace_location_event_detail_save_as_template_button">"Use as Template"</string> + <!-- Qr Code Validation Messages --> + <!-- XTXT: My check-ins: qr code validation wrong description field --> + <string name="trace_location_checkins_qr_code_invalid_description">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_DESCRIPTION)."</string> + <!-- XTXT: My check-ins: qr code validation wrong address field --> + <string name="trace_location_checkins_qr_code_invalid_address">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_ADDRESS)."</string> + <!-- XTXT: My check-ins: qr code validation wrong start end time field --> + <string name="trace_location_checkins_qr_code_invalid_times">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_TIMESTAMPS)."</string> + <!-- XTXT: My check-ins: qr code validation wrong cryptographic seed field --> + <string name="trace_location_checkins_qr_code_invalid_cryptographic_seed">"Ungültiger QR-Code verhindert das Einchecken. Die verantwortliche Stelle muss einen neuen QR-Code generieren (Fehlercode INVALID_CRYPTO_SEED)."</string> + </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index 0c94671196cdb387ebe89320c1e148377757b725..ecacc05a638093304b440e6405031fdf834af0ea 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -525,4 +525,8 @@ <item name="android:background">@drawable/circle_ripple</item> <item name="android:src">@drawable/ic_baseline_more_vert_24</item> </style> + + <style name="PosterTextStyle"> + <item name="android:fontFamily">@font/roboto</item> + </style> </resources> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..6d8df6b978065dd082d5a9d2825af00e11df9a88 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/InvalidQRCodeProvider.kt @@ -0,0 +1,148 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class InvalidQRCodeProvider : ArgumentsProvider { + private fun baseValidQrCodeBuilder(): TraceLocationOuterClass.QRCodePayload.Builder = + TraceLocationOuterClass.QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + TraceLocationOuterClass.CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + .build() + ) + + private fun baseValidLocationData(): TraceLocationOuterClass.TraceLocation.Builder = + TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setStartTimestamp(2687991) + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.StartEndTime + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setStartTimestamp(2687991) + .setEndTimestamp(2387991) + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.StartEndTime + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setDescription("") + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Description + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .clearDescription() + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Description + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setDescription((0..101).joinToString { "a" }) + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Description + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setDescription("A \n B") + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Description + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setAddress("") + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Address + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .clearAddress() + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Address + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setAddress((0..101).joinToString { "a" }) + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Address + ), + Arguments.of( + baseValidQrCodeBuilder() + .setLocationData( + baseValidLocationData() + .setAddress("A \n B") + .build() + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.Address + ), + Arguments.of( + baseValidQrCodeBuilder() + .setCrowdNotifierData( + TraceLocationOuterClass.CrowdNotifierData.newBuilder() + .setCryptographicSeed("WNlQ==".decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ).build(), + TraceLocationVerifier.VerificationResult.Invalid.CryptographicSeed + ) + ) + } + + companion object { + const val CRYPTOGRAPHIC_SEED = "zveDikIfwAXWqI6h4dWNlQ==" + const val PUB_KEY = + "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" + + "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifierTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..1bc7865431213f233211fd4eecaa641f8cc16586 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/TraceLocationVerifierTest.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import io.kotest.matchers.shouldBe +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ArgumentsSource +import testhelpers.BaseTest + +class TraceLocationVerifierTest : BaseTest() { + fun createInstance() = TraceLocationVerifier() + + @ParameterizedTest + @ArgumentsSource(ValidQRCodeProvider::class) + fun `Valid QR Codes`( + protoQrCodePayload: TraceLocationOuterClass.QRCodePayload + ) { + val validationResult = createInstance().verifyTraceLocation(protoQrCodePayload) + + (validationResult is TraceLocationVerifier.VerificationResult.Valid) shouldBe true + } + + @ParameterizedTest + @ArgumentsSource(InvalidQRCodeProvider::class) + fun `Invalid QR Codes`( + protoQrCodePayload: TraceLocationOuterClass.QRCodePayload, + expectedFailure: TraceLocationVerifier.VerificationResult.Invalid + ) { + val validationResult = createInstance().verifyTraceLocation(protoQrCodePayload) + + validationResult shouldBe expectedFailure + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidQRCodeProvider.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidQRCodeProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..bd08d284659adf109ec01c0b29834815631f01d1 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/qrcode/ValidQRCodeProvider.kt @@ -0,0 +1,104 @@ +package de.rki.coronawarnapp.eventregistration.checkins.qrcode + +import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.util.toProtoByteString +import okio.ByteString.Companion.decodeBase64 +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.ArgumentsProvider +import java.util.stream.Stream + +class ValidQRCodeProvider : ArgumentsProvider { + override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> { + return Stream.of( + Arguments.of( + TraceLocationOuterClass.QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + TraceLocationOuterClass.CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setStartTimestamp(2687955) + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build() + ), + Arguments.of( + TraceLocationOuterClass.QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + TraceLocationOuterClass.CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setStartTimestamp(2687991) + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build() + ), + Arguments.of( + TraceLocationOuterClass.QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + TraceLocationOuterClass.CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQAg==".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription("My Birthday Party") + .setAddress("at my place") + .setEndTimestamp(2687991) + .setVersion(1) + .build() + ) + .build() + ), + Arguments.of( + TraceLocationOuterClass.QRCodePayload.newBuilder() + .setVersion(1) + .setCrowdNotifierData( + TraceLocationOuterClass.CrowdNotifierData.newBuilder() + .setCryptographicSeed(CRYPTOGRAPHIC_SEED.decodeBase64()!!.toProtoByteString()) + .setPublicKey(PUB_KEY.decodeBase64()!!.toProtoByteString()) + .setVersion(1) + ) + .setVendorData("CAEQARgK".decodeBase64()!!.toProtoByteString()) + .setLocationData( + TraceLocationOuterClass.TraceLocation.newBuilder() + .setDescription("Icecream Shop") + .setAddress("Main Street 1") + .setVersion(1) + .build() + ) + .build() + ) + ) + } + + companion object { + const val CRYPTOGRAPHIC_SEED = "zveDikIfwAXWqI6h4dWNlQ==" + const val PUB_KEY = + "OMTa6eYSiaDv8lW13xdYEvGHOZ1EYTiFSxt51HEoPCD7CNnvCUiIYPhax1MpkN0UfNClCm9ZWYy0JH01CDVD9" + + "eq+voxQ1EcFJQkEIujVwoCNK0MNGuDK1ayjGxeDc4UD" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt index 07007ff99eeed66a36d4c651f91d3a51c8904be0..2c593184e9dc277b021427063294662dbe1a2a51 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/CheckOutHandlerTest.kt @@ -97,7 +97,7 @@ class CheckOutHandlerTest : BaseTest() { checkOut(42) } - updatedCheckIn?.createJournalEntry shouldBe true + updatedCheckIn!!.createJournalEntry shouldBe true coVerify(exactly = 1) { contactJournalCheckInEntryCreator.createEntry(any()) @@ -110,7 +110,7 @@ class CheckOutHandlerTest : BaseTest() { checkOut(43) } - updatedCheckIn?.createJournalEntry shouldBe false + updatedCheckIn!!.createJournalEntry shouldBe false coVerify(exactly = 0) { contactJournalCheckInEntryCreator.createEntry(any()) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt index 58834e6c7b70de6ac2f0e5a7452baabbd0c29e86..0726f259a2e346abaf851c492c5ccc0e4fe2d9fd 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/checkout/ContactJournalCheckInEntryCreatorTest.kt @@ -1,8 +1,12 @@ package de.rki.coronawarnapp.presencetracing.checkins.checkout import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation +import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone +import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -12,8 +16,12 @@ import io.mockk.just import io.mockk.runs import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.decodeBase64 import okio.ByteString.Companion.encode +import org.joda.time.Days import org.joda.time.Instant +import org.joda.time.Minutes +import org.joda.time.format.DateTimeFormat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -24,7 +32,7 @@ class ContactJournalCheckInEntryCreatorTest : BaseTest() { private val testCheckIn = CheckIn( id = 42L, - traceLocationId = "traceLocationId1".encode(), + traceLocationId = "traceLocationId1".decodeBase64()!!, version = 1, type = 1, description = "Restaurant", @@ -42,10 +50,20 @@ class ContactJournalCheckInEntryCreatorTest : BaseTest() { private val testLocation = DefaultContactDiaryLocation( locationId = 123L, - locationName = "${testCheckIn.description}, ${testCheckIn.address}, ${testCheckIn.traceLocationStart} - ${testCheckIn.traceLocationEnd}", + locationName = "${testCheckIn.description}, ${testCheckIn.address}, ${testCheckIn.traceLocationStart?.toPrettyDate()} - ${testCheckIn.traceLocationEnd?.toPrettyDate()}", traceLocationID = testCheckIn.traceLocationId ) + private val testLocationVisit = DefaultContactDiaryLocationVisit( + id = 0, + date = testCheckIn.checkInStart.toLocalDateUtc(), + contactDiaryLocation = testLocation, + checkInID = testCheckIn.id, + duration = Minutes.minutes(60).toStandardDuration() + ) + + private fun Instant.toPrettyDate(): String = toUserTimeZone().toString(DateTimeFormat.shortDateTime()) + @BeforeEach fun setup() { MockKAnnotations.init(this) @@ -68,20 +86,136 @@ class ContactJournalCheckInEntryCreatorTest : BaseTest() { every { contactDiaryRepo.locations } returns flowOf(emptyList()) andThen flowOf(listOf(testLocation)) // Repo returns an empty list for the first call, so location is missing and a new location should be created and added - val instance = createInstance() - instance.createEntry(testCheckIn) + createInstance().apply { + testCheckIn.createLocationIfMissing() + + coVerify(exactly = 1) { + contactDiaryRepo.addLocation(any()) + } + + // Location with trace location id already exists, so that location will be used + testCheckIn.createLocationIfMissing() + testCheckIn.createLocationIfMissing() + testCheckIn.createLocationIfMissing() + testCheckIn.createLocationIfMissing() + + coVerify(exactly = 1) { + contactDiaryRepo.addLocation(any()) + } + + testCheckIn.copy(traceLocationId = "traceLocationId2".decodeBase64()!!).createLocationIfMissing() - coVerify(exactly = 1) { - contactDiaryRepo.addLocation(any()) + coVerify(exactly = 2) { + contactDiaryRepo.addLocation(any()) + } } + } - // Location with trace location id already exists, so that location will be used - instance.createEntry(testCheckIn) - instance.createEntry(testCheckIn) - instance.createEntry(testCheckIn) + @Test + fun `Location name concatenates description, address and if both are set trace location start and end date`() { + val testCheckInNoTraceLocationStartDate = testCheckIn.copy(traceLocationStart = null) + val testCheckInNoTraceLocationEndDate = testCheckIn.copy(traceLocationEnd = null) + val testCheckInNoTraceLocationStartAndEndDate = + testCheckIn.copy(traceLocationStart = null, traceLocationEnd = null) + + createInstance().apply { + testCheckIn.validateLocationName(testCheckIn.toLocationName()) + testCheckInNoTraceLocationStartDate.validateLocationName(testCheckInNoTraceLocationStartDate.toLocationName()) + testCheckInNoTraceLocationEndDate.validateLocationName(testCheckInNoTraceLocationEndDate.toLocationName()) + testCheckInNoTraceLocationStartAndEndDate.validateLocationName(testCheckInNoTraceLocationStartAndEndDate.toLocationName()) + } + } + + private fun CheckIn.validateLocationName(nameToValidate: String) { + nameToValidate shouldBe when (traceLocationStart != null && traceLocationEnd != null) { + true -> "$description, $address, ${traceLocationStart?.toPrettyDate()} - ${traceLocationEnd?.toPrettyDate()}" + else -> "$description, $address" + } + } - coVerify(exactly = 1) { - contactDiaryRepo.addLocation(any()) + @Test + fun `CheckIn to ContactDiaryLocationVisit is correct`() { + createInstance().apply { + testCheckIn.toLocationVisit(testLocation).also { + it.checkInID shouldBe testCheckIn.id + it.date shouldBe testCheckIn.checkInStart.toLocalDateUtc() + it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(60) + it.contactDiaryLocation shouldBe testLocation + } + } + } + + @Test + fun `CheckIn to ContactDiaryLocationVisit duration mapping is correct`() { + createInstance().apply { + // Rounds duration to closest 15 minutes + testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T23:07:29+01:00")).toLocationVisit(testLocation) + .also { + it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(60) + } + + testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T23:07:30+01:00")).toLocationVisit(testLocation) + .also { + it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(75) + } + + testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T22:52:30+01:00")).toLocationVisit(testLocation) + .also { + it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(60) + } + + testCheckIn.copy(checkInEnd = Instant.parse("2021-03-04T22:52:29+01:00")).toLocationVisit(testLocation) + .also { + it.duration!!.toStandardMinutes() shouldBe Minutes.minutes(45) + } + } + } + + @Test + fun `Creates location visits if missing`() = runBlockingTest { + every { contactDiaryRepo.locationVisits } returns flowOf(emptyList()) andThen flowOf(listOf(testLocationVisit)) + + createInstance().apply { + val checkins = mutableListOf(testCheckIn) + + checkins.createMissingLocationVisits(testLocation).also { + it[0] shouldBe testLocationVisit + } + + checkins.createMissingLocationVisits(testLocation).also { + it.isEmpty() shouldBe true + } + + // Create check in for next day which should also create a visit for the next day + val testCheckInNextDay = testCheckIn.copy( + checkInStart = testCheckIn.checkInStart.plus(Days.ONE.toStandardDuration()), + checkInEnd = testCheckIn.checkInEnd.plus(Days.ONE.toStandardDuration()) + ) + checkins.add(testCheckInNextDay) + + checkins.createMissingLocationVisits(testLocation).also { + it.size shouldBe 1 // and not 2 + it[0] shouldBe testLocationVisit.copy(date = testLocationVisit.date.plusDays(1)) + } + } + } + + @Test + fun `Creates 1 location and 2 visits for split check in`() = runBlockingTest { + val splitCheckIn = testCheckIn.copy( + checkInStart = Instant.parse("2021-03-04T22:00+01:00"), + checkInEnd = Instant.parse("2021-03-05T02:00+01:00") + ) + createInstance().apply { + createEntry(splitCheckIn) + + coVerify(exactly = 1) { + contactDiaryRepo.addLocation(any()) + } + + coVerify(exactly = 2) { + contactDiaryRepo.addLocationVisit(any()) + } } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt index f59cd16228473db4a9530fa15857c09c758b048a..b0bfef2136687cb4c6cd327fe96b42c47814a1e3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskCalculatorTest.kt @@ -103,7 +103,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() { ) runBlockingTest { - val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime)) + val result = createInstance().calculateDayRisk(listOf(normTime)) result.size shouldBe 1 result[0].riskState shouldBe RiskState.CALCULATION_FAILED } @@ -133,7 +133,7 @@ class PresenceTracingRiskCalculatorTest : BaseTest() { ) runBlockingTest { - val result = createInstance().calculateAggregatedRiskPerDay(listOf(normTime, normTime2, normTime3)) + val result = createInstance().calculateDayRisk(listOf(normTime, normTime2, normTime3)) result.size shouldBe 2 result.find { it.localDateUtc == localDate }!!.riskState shouldBe RiskState.INCREASED_RISK result.find { it.localDateUtc == localDate2 }!!.riskState shouldBe RiskState.LOW_RISK diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab4464a2e41681105bb35eda245bdb5a662ba7dd --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepositoryTest.kt @@ -0,0 +1,255 @@ +package de.rki.coronawarnapp.presencetracing.risk.storage + +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInNormalizedTime +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInRiskPerDay +import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningOverlap +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.Runs +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.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Days +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PresenceTracingRiskRepositoryTest : BaseTest() { + + @MockK lateinit var presenceTracingRiskCalculator: PresenceTracingRiskCalculator + @MockK lateinit var databaseFactory: PresenceTracingRiskDatabase.Factory + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var traceTimeIntervalMatchDao: TraceTimeIntervalMatchDao + @MockK lateinit var riskLevelResultDao: PresenceTracingRiskLevelResultDao + @MockK lateinit var database: PresenceTracingRiskDatabase + + private val now = Instant.ofEpochMilli(9999999) + private val fifteenDaysAgo = now.minus(Days.days(15).toStandardDuration()) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { timeStamper.nowUTC } returns now + + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(emptyList()) + coEvery { traceTimeIntervalMatchDao.insert(any()) } just Runs + coEvery { traceTimeIntervalMatchDao.deleteMatchesForPackage(any()) } just Runs + coEvery { traceTimeIntervalMatchDao.deleteAll() } just Runs + coEvery { traceTimeIntervalMatchDao.deleteOlderThan(any()) } just Runs + + every { riskLevelResultDao.insert(any()) } just Runs + coEvery { riskLevelResultDao.deleteOlderThan(any()) } just Runs + + coEvery { databaseFactory.create() } returns database + every { database.traceTimeIntervalMatchDao() } returns traceTimeIntervalMatchDao + every { database.presenceTracingRiskLevelResultDao() } returns riskLevelResultDao + + coEvery { presenceTracingRiskCalculator.calculateNormalizedTime(any()) } returns listOf() + coEvery { presenceTracingRiskCalculator.calculateTotalRisk(any()) } returns RiskState.LOW_RISK + } + + @Test + fun `overlapsOfLast14DaysPlusToday works`() { + val entity = TraceTimeIntervalMatchEntity( + checkInId = 1L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = fifteenDaysAgo.minus(100000).millis, + endTimeMillis = fifteenDaysAgo.millis + ) + val entity2 = TraceTimeIntervalMatchEntity( + checkInId = 2L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.minus(80000).millis + ) + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(entity, entity2)) + runBlockingTest { + val overlaps = createInstance().overlapsOfLast14DaysPlusToday.first() + overlaps.size shouldBe 1 + overlaps[0].checkInId shouldBe 2L + } + } + + @Test + fun `traceLocationCheckInRiskStates works`() { + val entity2 = TraceTimeIntervalMatchEntity( + checkInId = 2L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.minus(80000).millis + ) + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(entity2)) + val time = CheckInNormalizedTime( + checkInId = 2L, + localDateUtc = now.minus(100000).toLocalDateUtc(), + normalizedTime = 20.0 + ) + val riskPerDay = CheckInRiskPerDay( + checkInId = 2L, + localDateUtc = now.minus(100000).toLocalDateUtc(), + riskState = RiskState.LOW_RISK + ) + coEvery { presenceTracingRiskCalculator.calculateNormalizedTime(listOf(entity2.toCheckInWarningOverlap())) } returns listOf( + time + ) + coEvery { presenceTracingRiskCalculator.calculateCheckInRiskPerDay(listOf(time)) } returns listOf(riskPerDay) + runBlockingTest { + val riskStates = createInstance().traceLocationCheckInRiskStates.first() + riskStates.size shouldBe 1 + riskStates[0].checkInId shouldBe 2L + riskStates[0].riskState shouldBe RiskState.LOW_RISK + } + } + + @Test + fun `presenceTracingDayRisk works`() { + val dayRisk = PresenceTracingDayRisk( + localDateUtc = now.minus(100000).toLocalDateUtc(), + riskState = RiskState.LOW_RISK + ) + coEvery { presenceTracingRiskCalculator.calculateDayRisk(any()) } returns listOf(dayRisk) + runBlockingTest { + val risks = createInstance().presenceTracingDayRisk.first() + risks.size shouldBe 1 + risks[0].riskState shouldBe RiskState.LOW_RISK + } + } + + @Test + fun `latestEntries works`() { + val resultEntity = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.minus(100000).millis, + riskState = RiskState.LOW_RISK + ) + val resultEntity2 = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.minus(10000).millis, + riskState = RiskState.LOW_RISK + ) + coEvery { riskLevelResultDao.latestEntries(2) } returns flowOf(listOf(resultEntity, resultEntity2)) + val matchEntity = TraceTimeIntervalMatchEntity( + checkInId = 1L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.millis + ) + val matchEntity2 = TraceTimeIntervalMatchEntity( + checkInId = 2L, + traceWarningPackageId = "traceWarningPackageId", + transmissionRiskLevel = 1, + startTimeMillis = now.minus(100000).millis, + endTimeMillis = now.minus(80000).millis + ) + every { traceTimeIntervalMatchDao.allMatches() } returns flowOf(listOf(matchEntity, matchEntity2)) + val dayRisk = PresenceTracingDayRisk( + localDateUtc = now.minus(100000).toLocalDateUtc(), + riskState = RiskState.LOW_RISK + ) + coEvery { presenceTracingRiskCalculator.calculateDayRisk(any()) } returns listOf(dayRisk) + runBlockingTest { + val latest = createInstance().latestEntries(2).first() + latest.size shouldBe 2 + latest[0].calculatedAt shouldBe now.minus(10000) + latest[0].checkInOverlapCount shouldBe 2 + latest[1].calculatedAt shouldBe now.minus(100000) + latest[1].checkInOverlapCount shouldBe 0 + } + } + + @Test + fun `deleteStaleData works`() { + runBlockingTest { + createInstance().deleteStaleData() + coVerify { + traceTimeIntervalMatchDao.deleteOlderThan(fifteenDaysAgo.millis) + riskLevelResultDao.deleteOlderThan(fifteenDaysAgo.millis) + } + } + } + + @Test + fun `deleteAllMatches works`() { + runBlockingTest { + createInstance().deleteAllMatches() + coVerify { traceTimeIntervalMatchDao.deleteAll() } + } + } + + @Test + fun `report successful calculation works`() { + val traceWarningPackageId = "traceWarningPackageId" + val overlap = CheckInWarningOverlap( + checkInId = 1L, + transmissionRiskLevel = 1, + traceWarningPackageId = traceWarningPackageId, + startTime = Instant.ofEpochMilli(9991000), + endTime = Instant.ofEpochMilli(9997000) + ) + + val result = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.millis, + riskState = RiskState.LOW_RISK + ) + runBlockingTest { + createInstance().reportCalculation( + successful = true, + overlaps = listOf(overlap) + ) + + coVerify { + traceTimeIntervalMatchDao.deleteMatchesForPackage(traceWarningPackageId) + traceTimeIntervalMatchDao.insert(listOf(overlap.toTraceTimeIntervalMatchEntity())) + riskLevelResultDao.insert(result) + } + } + } + + @Test + fun `report failed calculation works`() { + val traceWarningPackageId = "traceWarningPackageId" + val overlap = CheckInWarningOverlap( + checkInId = 1L, + transmissionRiskLevel = 1, + traceWarningPackageId = traceWarningPackageId, + startTime = Instant.ofEpochMilli(9991000), + endTime = Instant.ofEpochMilli(9997000) + ) + + val result = PresenceTracingRiskLevelResultEntity( + calculatedAtMillis = now.millis, + riskState = RiskState.CALCULATION_FAILED + ) + runBlockingTest { + createInstance().reportCalculation( + successful = false, + overlaps = listOf(overlap) + ) + + coVerify { + traceTimeIntervalMatchDao.deleteMatchesForPackage(traceWarningPackageId) + traceTimeIntervalMatchDao.insert(any()) + riskLevelResultDao.insert(result) + } + } + } + + private fun createInstance() = PresenceTracingRiskRepository( + presenceTracingRiskCalculator, + databaseFactory, + timeStamper + ) +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt index fdff4062dbc0aefef65a35dc69770f9fb3462280..4ad2bbd28da2b03920fd7d3a785ae6229519c7fd 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorageTest.kt @@ -13,6 +13,7 @@ import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testPersistedAggreg import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRiskLevelResultDao import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResult import de.rki.coronawarnapp.risk.storage.RiskStorageTestData.testRisklevelResultWithAggregatedRiskPerDateResult +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.AggregatedRiskPerDateResultDao import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.ExposureWindowsDao @@ -21,6 +22,7 @@ import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase.RiskResults import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -53,11 +55,19 @@ class BaseRiskLevelStorageTest : BaseTest() { @MockK lateinit var exposureWindowTables: ExposureWindowsDao @MockK lateinit var aggregatedRiskPerDateResultDao: AggregatedRiskPerDateResultDao @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository + @MockK lateinit var timeStamper: TimeStamper + + private lateinit var riskCombinator: RiskCombinator @BeforeEach fun setup() { MockKAnnotations.init(this) + every { timeStamper.nowUTC } returns Instant.parse("2021-01-01T12:00:00.000Z") + riskCombinator = RiskCombinator( + timeStamper = timeStamper + ) + every { databaseFactory.create() } returns database every { database.riskResults() } returns riskResultTables every { database.exposureWindows() } returns exposureWindowTables @@ -91,7 +101,8 @@ class BaseRiskLevelStorageTest : BaseTest() { ) = object : BaseRiskLevelStorage( scope = scope, riskResultDatabaseFactory = databaseFactory, - presenceTracingRiskRepository = presenceTracingRiskRepository + presenceTracingRiskRepository = presenceTracingRiskRepository, + riskCombinator = riskCombinator, ) { override val storedResultLimit: Int = storedResultLimit @@ -256,7 +267,7 @@ class BaseRiskLevelStorageTest : BaseTest() { // result from the combination with initial ew low risk result riskLevelResults[1].calculatedAt shouldBe ewCalculatedAt.minus(400L) - riskLevelResults[1].riskState shouldBe RiskState.CALCULATION_FAILED + riskLevelResults[1].riskState shouldBe RiskState.LOW_RISK verify { riskResultTables.latestEntries(2) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt similarity index 63% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt index 9e1122826a32e6f43db61015975e11d0421c3a17..3d23b8d37e02bafdd04f5ae0f76d094417f07845 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/CombineRiskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/storage/internal/RiskCombinatorTest.kt @@ -1,45 +1,81 @@ -package de.rki.coronawarnapp.risk.storage +package de.rki.coronawarnapp.risk.storage.internal import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.RiskState +import de.rki.coronawarnapp.risk.RiskState.CALCULATION_FAILED +import de.rki.coronawarnapp.risk.RiskState.INCREASED_RISK +import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK import org.joda.time.Instant import org.joda.time.LocalDate +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import testhelpers.BaseTest -class CombineRiskTest { +class RiskCombinatorTest : BaseTest() { + + @MockK lateinit var timeStamper: TimeStamper + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(1234567890) + } + + private fun createInstance() = RiskCombinator( + timeStamper = timeStamper + ) + + @Test + fun `Initial results`() { + createInstance().initialCombinedResult.apply { + riskState shouldBe LOW_RISK + } + } + + @Test + fun `Fallback results on empty data`() { + createInstance().latestCombinedResult.apply { + riskState shouldBe LOW_RISK + } + } @Test fun `combineRisk works`() { val ptRisk0 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 19), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptRisk1 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 20), - riskState = RiskState.INCREASED_RISK + riskState = INCREASED_RISK ) val ptRisk2 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 21), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptRisk3 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 22), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ptRisk4 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 23), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptRisk5 = PresenceTracingDayRisk( localDateUtc = LocalDate(2021, 3, 24), - riskState = RiskState.INCREASED_RISK + riskState = INCREASED_RISK ) val ewRisk0 = ExposureWindowDayRisk( @@ -81,33 +117,33 @@ class CombineRiskTest { val ptDayRiskList: List<PresenceTracingDayRisk> = listOf(ptRisk0, ptRisk1, ptRisk2, ptRisk3, ptRisk4, ptRisk5) val ewDayRiskList: List<ExposureWindowDayRisk> = listOf(ewRisk0, ewRisk1, ewRisk2, ewRisk3, ewRisk4, ewRisk5) - val result = combineRisk(ptDayRiskList, ewDayRiskList) + val result = createInstance().combineRisk(ptDayRiskList, ewDayRiskList) result.size shouldBe 7 result.single { it.localDate == LocalDate(2021, 3, 15) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 19) - }.riskState shouldBe RiskState.LOW_RISK + }.riskState shouldBe LOW_RISK result.single { it.localDate == LocalDate(2021, 3, 20) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 21) - }.riskState shouldBe RiskState.LOW_RISK + }.riskState shouldBe LOW_RISK result.single { it.localDate == LocalDate(2021, 3, 22) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 22) - }.riskState shouldBe RiskState.CALCULATION_FAILED + }.riskState shouldBe CALCULATION_FAILED result.single { it.localDate == LocalDate(2021, 3, 23) - }.riskState shouldBe RiskState.INCREASED_RISK + }.riskState shouldBe INCREASED_RISK result.single { it.localDate == LocalDate(2021, 3, 24) - }.riskState shouldBe RiskState.INCREASED_RISK + }.riskState shouldBe INCREASED_RISK } @Test @@ -116,69 +152,73 @@ class CombineRiskTest { val ptResult = PtRiskLevelResult( calculatedAt = startInstant.plus(1000L), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptResult2 = PtRiskLevelResult( calculatedAt = startInstant.plus(3000L), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ptResult3 = PtRiskLevelResult( calculatedAt = startInstant.plus(6000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ptResult4 = PtRiskLevelResult( calculatedAt = startInstant.plus(7000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ptResults = listOf(ptResult, ptResult2, ptResult4, ptResult3) val ewResult = createEwRiskLevelResult( calculatedAt = startInstant.plus(2000L), - riskState = RiskState.LOW_RISK + riskState = LOW_RISK ) val ewResult2 = createEwRiskLevelResult( calculatedAt = startInstant.plus(4000L), - riskState = RiskState.INCREASED_RISK + riskState = INCREASED_RISK ) val ewResult3 = createEwRiskLevelResult( calculatedAt = startInstant.plus(5000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ewResult4 = createEwRiskLevelResult( calculatedAt = startInstant.plus(8000L), - riskState = RiskState.CALCULATION_FAILED + riskState = CALCULATION_FAILED ) val ewResults = listOf(ewResult, ewResult4, ewResult2, ewResult3) - val result = combineEwPtRiskLevelResults(ptResults, ewResults).sortedByDescending { it.calculatedAt } + val result = createInstance().combineEwPtRiskLevelResults( + ptRiskResults = ptResults, + ewRiskResults = ewResults + ).sortedByDescending { it.calculatedAt } + result.size shouldBe 8 - result[0].riskState shouldBe RiskState.CALCULATION_FAILED + result[0].riskState shouldBe CALCULATION_FAILED result[0].calculatedAt shouldBe startInstant.plus(8000L) - result[1].riskState shouldBe RiskState.CALCULATION_FAILED + result[1].riskState shouldBe CALCULATION_FAILED result[1].calculatedAt shouldBe startInstant.plus(7000L) - result[2].riskState shouldBe RiskState.CALCULATION_FAILED + result[2].riskState shouldBe CALCULATION_FAILED result[2].calculatedAt shouldBe startInstant.plus(6000L) - result[3].riskState shouldBe RiskState.CALCULATION_FAILED + result[3].riskState shouldBe CALCULATION_FAILED result[3].calculatedAt shouldBe startInstant.plus(5000L) - result[4].riskState shouldBe RiskState.INCREASED_RISK + result[4].riskState shouldBe INCREASED_RISK result[4].calculatedAt shouldBe startInstant.plus(4000L) - result[5].riskState shouldBe RiskState.LOW_RISK + result[5].riskState shouldBe LOW_RISK result[5].calculatedAt shouldBe startInstant.plus(3000L) - result[6].riskState shouldBe RiskState.LOW_RISK + result[6].riskState shouldBe LOW_RISK result[6].calculatedAt shouldBe startInstant.plus(2000L) - result[7].riskState shouldBe RiskState.CALCULATION_FAILED + result[7].riskState shouldBe LOW_RISK result[7].calculatedAt shouldBe startInstant.plus(1000L) } @Test fun `max RiskState works`() { - combine(RiskState.INCREASED_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - combine(RiskState.INCREASED_RISK, RiskState.LOW_RISK) shouldBe RiskState.INCREASED_RISK - combine(RiskState.INCREASED_RISK, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED - combine(RiskState.LOW_RISK, RiskState.INCREASED_RISK) shouldBe RiskState.INCREASED_RISK - combine(RiskState.CALCULATION_FAILED, RiskState.INCREASED_RISK) shouldBe RiskState.CALCULATION_FAILED - combine(RiskState.LOW_RISK, RiskState.LOW_RISK) shouldBe RiskState.LOW_RISK - combine(RiskState.CALCULATION_FAILED, RiskState.LOW_RISK) shouldBe RiskState.CALCULATION_FAILED - combine(RiskState.CALCULATION_FAILED, RiskState.CALCULATION_FAILED) shouldBe RiskState.CALCULATION_FAILED + RiskCombinator.combine(INCREASED_RISK, INCREASED_RISK) shouldBe INCREASED_RISK + RiskCombinator.combine(INCREASED_RISK, LOW_RISK) shouldBe INCREASED_RISK + RiskCombinator.combine(INCREASED_RISK, CALCULATION_FAILED) shouldBe CALCULATION_FAILED + RiskCombinator.combine(LOW_RISK, INCREASED_RISK) shouldBe INCREASED_RISK + RiskCombinator.combine(CALCULATION_FAILED, INCREASED_RISK) shouldBe CALCULATION_FAILED + RiskCombinator.combine(LOW_RISK, LOW_RISK) shouldBe LOW_RISK + RiskCombinator.combine(CALCULATION_FAILED, LOW_RISK) shouldBe CALCULATION_FAILED + RiskCombinator.combine(CALCULATION_FAILED, CALCULATION_FAILED) shouldBe CALCULATION_FAILED } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt index 22595f0c3df4cbee7984b097e5528d19d122db34..71370335eeb30b6f17ba1c479cb12a3d0c467da3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/TaskControllerTest.kt @@ -3,10 +3,10 @@ package de.rki.coronawarnapp.task import de.rki.coronawarnapp.bugreporting.reportProblem import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.task.example.QueueingTask import de.rki.coronawarnapp.task.testtasks.SkippingTask import de.rki.coronawarnapp.task.testtasks.alerterror.AlertErrorTask import de.rki.coronawarnapp.task.testtasks.precondition.PreconditionTask +import de.rki.coronawarnapp.task.testtasks.queue.QueueingTask import de.rki.coronawarnapp.task.testtasks.silenterror.SilentErrorTask import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTask import de.rki.coronawarnapp.task.testtasks.timeout.TimeoutTask2 diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt index cb0848c9fbbb770cbb8baa2f7c3ee8b4b073b65e..1b0b3c944725d2499b6f232044132929c6ff569e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/SkippingTask.kt @@ -3,7 +3,7 @@ package de.rki.coronawarnapp.task.testtasks import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskFactory import de.rki.coronawarnapp.task.common.DefaultProgress -import de.rki.coronawarnapp.task.example.QueueingTask +import de.rki.coronawarnapp.task.testtasks.queue.QueueingTask import org.joda.time.Duration import javax.inject.Inject import javax.inject.Provider diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTask.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTask.kt similarity index 98% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTask.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTask.kt index d34f0e8890919b779fb7a51dc598213b1a255f5b..0c7d67d9dab2f9243f9043d362741b39614948c2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTask.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTask.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.task.example +package de.rki.coronawarnapp.task.testtasks.queue import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTaskModule.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTaskModule.kt similarity index 89% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTaskModule.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTaskModule.kt index 3b15fdb8f85de48ffef00ac92f3fd0f17186d7f8..ff14228d2d669b0f647ff5fc6795d17b08f3c00d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/example/QueueingTaskModule.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/task/testtasks/queue/QueueingTaskModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.task.example +package de.rki.coronawarnapp.task.testtasks.queue import dagger.Binds import dagger.Module diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt index c1adf45da1f8b75e2e530e7cd7bb86f1d120bf61..7efbed5de4b31e4457ff92bac769a757a49b9eaa 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/CheckInsViewModelTest.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import de.rki.coronawarnapp.eventregistration.checkins.CheckIn import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeUriParser +import de.rki.coronawarnapp.eventregistration.checkins.qrcode.TraceLocationVerifier import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutHandler import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass import de.rki.coronawarnapp.ui.eventregistration.attendee.checkins.items.ActiveCheckInVH @@ -41,6 +42,7 @@ class CheckInsViewModelTest : BaseTest() { @MockK lateinit var checkInsRepository: CheckInRepository @MockK lateinit var checkOutHandler: CheckOutHandler @MockK lateinit var cameraPermissionProvider: CameraPermissionProvider + @MockK lateinit var traceLocationVerifier: TraceLocationVerifier @BeforeEach fun setup() { @@ -48,6 +50,8 @@ class CheckInsViewModelTest : BaseTest() { every { savedState.set(any(), any<String>()) } just Runs every { checkInsRepository.checkInsWithinRetention } returns flowOf() every { cameraPermissionProvider.deniedPermanently } returns flowOf(false) + every { traceLocationVerifier.verifyTraceLocation(any()) } returns + TraceLocationVerifier.VerificationResult.Valid(mockk()) } @Test @@ -185,7 +189,9 @@ class CheckInsViewModelTest : BaseTest() { qrCodeUriParser = qrCodeUriParser, checkInsRepository = checkInsRepository, checkOutHandler = checkOutHandler, - cameraPermissionProvider = cameraPermissionProvider + cameraPermissionProvider = cameraPermissionProvider, + traceLocationVerifier = traceLocationVerifier, + cleanHistory = false ) companion object { diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 274a5e5f941e266f6db258ecf57cad71bd51e806..673ac7b96c843f1c2ed9909a24b0a68e7d383ff9 100644 --- a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -5,9 +5,11 @@ import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepo import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -97,7 +99,8 @@ class DefaultRiskLevelStorageTest : BaseTest() { ) = DefaultRiskLevelStorage( scope = scope, riskResultDatabaseFactory = databaseFactory, - presenceTracingRiskRepository = presenceTracingRiskRepository + presenceTracingRiskRepository = presenceTracingRiskRepository, + riskCombinator = RiskCombinator(TimeStamper()), ) @Test diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt index 72eae29914ebfe630caea23002104922563ca4dd..5502ead593df03320642893e077e6a6201216d88 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risk/storage/DefaultRiskLevelStorageTest.kt @@ -5,9 +5,11 @@ import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepo import de.rki.coronawarnapp.risk.EwRiskLevelTaskResult import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.DefaultRiskLevelStorage +import de.rki.coronawarnapp.risk.storage.internal.RiskCombinator import de.rki.coronawarnapp.risk.storage.internal.RiskResultDatabase import de.rki.coronawarnapp.risk.storage.internal.riskresults.PersistedRiskLevelResultDao import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -94,7 +96,8 @@ class DefaultRiskLevelStorageTest : BaseTestInstrumentation() { private fun createInstance() = DefaultRiskLevelStorage( scope = TestCoroutineScope(), riskResultDatabaseFactory = databaseFactory, - presenceTracingRiskRepository = presenceTracingRiskRepository + presenceTracingRiskRepository = presenceTracingRiskRepository, + riskCombinator = RiskCombinator(TimeStamper()) ) @Test