diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt index 909a66247fd7405a3239631942052de2892add21..0375e974226341dae587918b107e4e51374622d0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt @@ -88,7 +88,8 @@ class DefaultExposureDetectionTracker @Inject constructor( mutate { this[identifier] = TrackedExposureDetection( identifier = identifier, - startedAt = timeStamper.nowUTC + startedAt = timeStamper.nowUTC, + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt index 20fdddefe71875201448539b662f89f782f986dd..e5ecf15fb0fbaa6ba111ed922c022c4cce5cf25a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/TrackedExposureDetection.kt @@ -9,7 +9,8 @@ data class TrackedExposureDetection( @SerializedName("identifier") val identifier: String, @SerializedName("startedAt") val startedAt: Instant, @SerializedName("result") val result: Result? = null, - @SerializedName("finishedAt") val finishedAt: Instant? = null + @SerializedName("finishedAt") val finishedAt: Instant? = null, + @SerializedName("enfVersion") val enfVersion: EnfVersion? = null ) { val isCalculating: Boolean @@ -28,4 +29,11 @@ data class TrackedExposureDetection( @SerializedName("TIMEOUT") TIMEOUT } + + enum class EnfVersion { + @SerializedName("V1_LEGACY_MODE") + V1_LEGACY_MODE, + @SerializedName("V2_WINDOW_MODE") + V2_WINDOW_MODE + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt index d3d813eb935764ccb77b309ba248e30a0284a334..22dee506434f413a1b73769c5ebf2b7c8e300074 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt @@ -8,6 +8,8 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.risk.RiskLevelResult.FailureReason import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.Task @@ -188,19 +190,27 @@ class RiskLevelTask @Inject constructor( } data class Config( - // TODO unit-test that not > 9 min + private val exposureDetectionTracker: ExposureDetectionTracker, override val executionTimeout: Duration = Duration.standardMinutes(8), - override val collisionBehavior: TaskFactory.Config.CollisionBehavior = TaskFactory.Config.CollisionBehavior.SKIP_IF_SIBLING_RUNNING + ) : TaskFactory.Config { - ) : TaskFactory.Config + override val preconditions: List<suspend () -> Boolean> + get() = listOf { + // check whether we already have a successful v2 exposure + exposureDetectionTracker.calculations.first().values.any { + it.enfVersion == TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE && it.isSuccessful + } + } + } class Factory @Inject constructor( - private val taskByDagger: Provider<RiskLevelTask> + private val taskByDagger: Provider<RiskLevelTask>, + private val exposureDetectionTracker: ExposureDetectionTracker ) : TaskFactory<DefaultProgress, RiskLevelTaskResult> { - override suspend fun createConfig(): TaskFactory.Config = Config() + override suspend fun createConfig(): TaskFactory.Config = Config(exposureDetectionTracker) override val taskProvider: () -> Task<DefaultProgress, RiskLevelTaskResult> = { taskByDagger.get() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt index 2c442de6f5aedad84c5541f73d3dd5aa264a8115..274b0db8df8b25b9841982516e82a158dcf4ff8a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/TimeVariables.kt @@ -79,7 +79,7 @@ object TimeVariables { */ fun getMinActivatedTracingTime(): Int = MIN_ACTIVATED_TRACING_TIME - private const val MILISECONDS_IN_A_SECOND = 1000 + private const val MILLISECONDS_IN_A_SECOND = 1000 private const val SECONDS_IN_A_MINUTES = 60 private const val MINUTES_IN_AN_HOUR = 60 private const val HOURS_IN_AN_DAY = 24 @@ -93,9 +93,9 @@ object TimeVariables { */ fun getManualKeyRetrievalDelay() = if (CWADebug.buildFlavor == CWADebug.BuildFlavor.DEVICE_FOR_TESTERS) { - MILISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES + MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES } else { - MILISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES * MINUTES_IN_AN_HOUR * HOURS_IN_AN_DAY + MILLISECONDS_IN_A_SECOND * SECONDS_IN_A_MINUTES * MINUTES_IN_AN_HOUR * HOURS_IN_AN_DAY } /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt index 36d2b2c85115e44d7c5800d879c1031c7c3c23de..6db01b71292562462a2db3d0cd7202e68f772f29 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskController.kt @@ -124,14 +124,14 @@ class TaskController @Inject constructor( private suspend fun processMap() = internalTaskData.updateSafely { Timber.tag(TAG).d("Processing task data (count=%d)", size) - // Procress all unprocessed finished tasks - procressFinishedTasks(this).let { + // Process all unprocessed finished tasks + processFinishedTasks(this).let { this.clear() this.putAll(it) } // Start new tasks - procressPendingTasks(this).let { + processPendingTasks(this).let { this.clear() this.putAll(it) } @@ -156,7 +156,7 @@ class TaskController @Inject constructor( ) } - private fun procressFinishedTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { + private fun processFinishedTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { val workMap = data.toMutableMap() workMap.values .filter { it.job.isCompleted && it.executionState != TaskState.ExecutionState.FINISHED } @@ -181,7 +181,7 @@ class TaskController @Inject constructor( return workMap } - private fun procressPendingTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { + private suspend fun processPendingTasks(data: Map<UUID, InternalTaskState>): Map<UUID, InternalTaskState> { val workMap = data.toMutableMap() workMap.values .filter { it.executionState == TaskState.ExecutionState.PENDING } @@ -198,12 +198,18 @@ class TaskController @Inject constructor( Timber.tag(TAG).v("Sibling are:\n%s", siblingTasks.joinToString("\n")) } + Timber.tag(TAG).v("Checking preconditions for request: %s", state.config) + val arePreconditionsMet = state.config.preconditions.fold(true) { allPreConditionsMet, precondition -> + allPreConditionsMet && precondition() + } + // Handle collision behavior for tasks of same type when { siblingTasks.isEmpty() -> { workMap[state.id] = state.toRunningState() } - state.config.collisionBehavior == CollisionBehavior.SKIP_IF_SIBLING_RUNNING -> { + !arePreconditionsMet || + state.config.collisionBehavior == CollisionBehavior.SKIP_IF_SIBLING_RUNNING -> { workMap[state.id] = state.toSkippedState() } state.config.collisionBehavior == CollisionBehavior.ENQUEUE -> { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt index 652bf1b58f63604bcc2aaa5cd17f895ea7f55ac4..25dfd34f5f19af7d6a7ebbfc0f055dd0936dd5de 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/TaskFactory.kt @@ -15,6 +15,9 @@ interface TaskFactory< val collisionBehavior: CollisionBehavior + val preconditions: List<suspend () -> Boolean> + get() = emptyList() + enum class CollisionBehavior { ENQUEUE, SKIP_IF_SIBLING_RUNNING diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt index 0521bdb63cc6523f953df8dd9cf190428c27f25b..9012e23cf22f3a22d07d72703bbba373bf6788a9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTrackerTest.kt @@ -95,7 +95,8 @@ class DefaultExposureDetectionTrackerTest : BaseTest() { key shouldBe expectedIdentifier value shouldBe TrackedExposureDetection( identifier = expectedIdentifier, - startedAt = Instant.EPOCH + startedAt = Instant.EPOCH, + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt index 61e3bcab5f453800bc1bd2b581cdd4f4ed0c6fbf..f6d10c1e31ad6001a9444be1db0ce83e8866c67e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt @@ -34,7 +34,8 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { "identifier": "b2b98400-058d-43e6-b952-529a5255248b", "startedAt": { "iMillis": 1234 - } + }, + "enfVersion": "V2_WINDOW_MODE" }, "aeb15509-fb34-42ce-8795-7a9ae0c2f389": { "identifier": "aeb15509-fb34-42ce-8795-7a9ae0c2f389", @@ -44,7 +45,8 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { "result": "UPDATED_STATE", "finishedAt": { "iMillis": 1603473968125 - } + }, + "enfVersion": "V1_LEGACY_MODE" } } """.trimIndent() @@ -52,13 +54,15 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { private val demoData = run { val calculation1 = TrackedExposureDetection( identifier = "b2b98400-058d-43e6-b952-529a5255248b", - startedAt = Instant.ofEpochMilli(1234) + startedAt = Instant.ofEpochMilli(1234), + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE ) val calculation2 = TrackedExposureDetection( identifier = "aeb15509-fb34-42ce-8795-7a9ae0c2f389", startedAt = Instant.ofEpochMilli(5678), finishedAt = Instant.ofEpochMilli(1603473968125), - result = TrackedExposureDetection.Result.UPDATED_STATE + result = TrackedExposureDetection.Result.UPDATED_STATE, + enfVersion = TrackedExposureDetection.EnfVersion.V1_LEGACY_MODE ) mapOf( calculation1.identifier to calculation1, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt index 5e1cbd3839de8c388a5281a9e8084e7b3e1981a3..1e5267810c9e8314c6a8a61a3ae6aac7f2e394ca 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt @@ -1,15 +1,90 @@ package de.rki.coronawarnapp.risk +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker +import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest class RiskLevelTaskConfigTest : BaseTest() { + @MockK lateinit var exposureDetectionTracker: ExposureDetectionTracker + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + @Test fun `risk level task max execution time is not above 9 minutes`() { - val config = RiskLevelTask.Config() - config.executionTimeout.isShorterThan(Duration.standardMinutes(9)) shouldBe true + RiskLevelTask.Config(exposureDetectionTracker) + .executionTimeout + .isShorterThan(Duration.standardMinutes(9)) shouldBe true + } + + @Test + fun `risk level preconditions are met`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(mapOf("" to TrackedExposureDetection( + identifier = "", + startedAt = Instant(), + result = TrackedExposureDetection.Result.NO_MATCHES, + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE + ))) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe true + } + } + + @Test + fun `risk level preconditions are not met, because there are no detections`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(emptyMap()) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe false + } + } + + @Test + fun `risk level preconditions are not met, because there are no enf V2 detections`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(mapOf("" to TrackedExposureDetection( + identifier = "", + startedAt = Instant(), + result = TrackedExposureDetection.Result.NO_MATCHES, + enfVersion = TrackedExposureDetection.EnfVersion.V1_LEGACY_MODE + ))) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe false + } + } + + @Test + fun `risk level preconditions are not met, because detection is not finished yet`() { + every { exposureDetectionTracker.calculations } returns MutableStateFlow(mapOf("" to TrackedExposureDetection( + identifier = "", + startedAt = Instant(), + enfVersion = TrackedExposureDetection.EnfVersion.V2_WINDOW_MODE + ))) + runBlocking { + RiskLevelTask.Config(exposureDetectionTracker) + .preconditions.fold(true) { result, precondition -> + result && precondition() + } shouldBe false + } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt index fe4e7a5c0a3d9519c5a40c825cd43c56165a3049..f62ca872c48a4d513795d1ee5561e063100a6e83 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/CWAWorkerFactoryTest.kt @@ -61,5 +61,3 @@ class CWAWorkerFactoryTest : BaseTest() { worker1 shouldNotBe worker2 } } - -