From b21a51959ead9c1b0e4990e08511c7e2813ee9bb Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Tue, 24 Nov 2020 09:32:21 +0100 Subject: [PATCH] Add test feature to allow faking high and low risk encounters via exposure windows. --- .../DefaultExposureWindowProvider.kt | 0 ...xposure-windows-increased-risk-random.json | 690 ++++++++++++++++++ .../exposure-windows-lowrisk-random.json | 38 + .../DefaultExposureWindowProvider.kt | 31 + .../FakeExposureWindowProvider.kt | 62 ++ .../ui/TestRiskLevelCalculationFragment.kt | 36 + ...iskLevelCalculationFragmentCWAViewModel.kt | 10 +- .../fragment_test_risk_level_calculation.xml | 45 +- .../rki/coronawarnapp/storage/TestSettings.kt | 25 +- .../coronawarnapp/storage/TestSettingsTest.kt | 5 +- .../ExposureWindowProviderTest.kt | 43 ++ .../ExposureWindowProviderTest.kt | 48 ++ 12 files changed, 1018 insertions(+), 15 deletions(-) rename Corona-Warn-App/src/{main => device}/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt (100%) create mode 100644 Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json create mode 100644 Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt create mode 100644 Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt create mode 100644 Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt create mode 100644 Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt similarity index 100% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt rename to Corona-Warn-App/src/device/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt diff --git a/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json new file mode 100644 index 000000000..544b74346 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-increased-risk-random.json @@ -0,0 +1,690 @@ +[ + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 299, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 73, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 73, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 72, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 72, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 2, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 299, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 3, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 4, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 1, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 3, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 4, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 1, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 2, + "calibrationConfidence": 0, + "infectiousness": 1, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 4, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 420, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 2, + "scanInstances": [] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 0, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 70, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + }, + { + "ageInDays": 1, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 3, + "scanInstances": [ + { + "minAttenuation": 70, + "secondsSinceLastScan": 0, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 70, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 70, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + } +] \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json new file mode 100644 index 000000000..675781ac4 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/assets/exposure-windows-lowrisk-random.json @@ -0,0 +1,38 @@ +[ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 4, + "calibrationConfidence": 0, + "infectiousness": 2, + "reportType": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + }, + { + "minAttenuation": 30, + "secondsSinceLastScan": 300, + "typicalAttenuation": 25 + } + ] + } +] \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt new file mode 100644 index 000000000..dd7306b0b --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.storage.TestSettings +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@Singleton +class DefaultExposureWindowProvider @Inject constructor( + private val client: ExposureNotificationClient, + private val testSettings: TestSettings, + private val fakeExposureWindowProvider: FakeExposureWindowProvider +) : ExposureWindowProvider { + + override suspend fun exposureWindows(): List<ExposureWindow> = suspendCoroutine { cont -> + when (val fakeSetting = testSettings.fakeExposureWindows.value) { + TestSettings.FakeExposureWindowTypes.DISABLED -> { + client.exposureWindows + .addOnSuccessListener { cont.resume(it) } + .addOnFailureListener { cont.resumeWithException(it) } + } + else -> { + fakeExposureWindowProvider.getExposureWindows(fakeSetting).let { cont.resume(it) } + } + } + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt new file mode 100644 index 000000000..086b6b5f9 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import android.content.Context +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import dagger.Reusable +import de.rki.coronawarnapp.storage.TestSettings.FakeExposureWindowTypes +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.fromJson +import org.joda.time.Duration +import org.joda.time.Instant +import javax.inject.Inject + +@Reusable +class FakeExposureWindowProvider @Inject constructor( + @AppContext val context: Context +) { + + fun getExposureWindows(testSettings: FakeExposureWindowTypes): List<ExposureWindow> { + val jsonInput = when (testSettings) { + FakeExposureWindowTypes.INCREASED_RISK_DEFAULT -> "exposure-windows-increased-risk-random.json" + FakeExposureWindowTypes.LOW_RISK_DEFAULT -> "exposure-windows-lowrisk-random.json" + else -> throw NotImplementedError() + }.let { context.assets.open(it) }.readBytes().toString(Charsets.UTF_8) + val jsonWindows: List<JsonWindow> = Gson().fromJson(jsonInput) + + return jsonWindows.map { jWindow -> + ExposureWindow.Builder().apply { + setDateMillisSinceEpoch( + Instant.now().minus(Duration.standardDays(jWindow.ageInDays.toLong())).millis + ) + setCalibrationConfidence(jWindow.calibrationConfidence) + setInfectiousness(jWindow.infectiousness) + setReportType(jWindow.reportType) + + jWindow.scanInstances.map { jScanInstance -> + ScanInstance.Builder().apply { + setMinAttenuationDb(jScanInstance.minAttenuation) + setSecondsSinceLastScan(jScanInstance.secondsSinceLastScan) + setTypicalAttenuationDb(jScanInstance.typicalAttenuation) + }.build() + }.let { setScanInstances(it) } + }.build() + } + } +} + +private data class JsonScanInstance( + @SerializedName("minAttenuation") val minAttenuation: Int, + @SerializedName("secondsSinceLastScan") val secondsSinceLastScan: Int, + @SerializedName("typicalAttenuation") val typicalAttenuation: Int +) + +private data class JsonWindow( + @SerializedName("ageInDays") val ageInDays: Int, + @SerializedName("calibrationConfidence") val calibrationConfidence: Int, + @SerializedName("infectiousness") val infectiousness: Int, + @SerializedName("reportType") val reportType: Int, + @SerializedName("scanInstances") val scanInstances: List<JsonScanInstance> +) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt index b0e40407c..7087a044d 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragment.kt @@ -2,12 +2,17 @@ package de.rki.coronawarnapp.test.risklevel.ui import android.os.Bundle import android.view.View +import android.widget.RadioButton +import android.widget.RadioGroup import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.children import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding +import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.util.di.AutoInject @@ -79,6 +84,37 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le vm.backendParameters.observe2(this) { binding.labelBackendParameters.text = it } + + vm.fakeWindowsState.observe2(this) { currentType -> + binding.apply { + if (fakeWindowsToggleGroup.childCount != TestSettings.FakeExposureWindowTypes.values().size) { + fakeWindowsToggleGroup.removeAllViews() + TestSettings.FakeExposureWindowTypes.values().forEach { type -> + RadioButton(requireContext()).apply { + id = ViewCompat.generateViewId() + text = type.name + layoutParams = RadioGroup.LayoutParams( + RadioGroup.LayoutParams.MATCH_PARENT, + RadioGroup.LayoutParams.WRAP_CONTENT + ) + fakeWindowsToggleGroup.addView(this) + } + } + } + + fakeWindowsToggleGroup.children.forEach { + it as RadioButton + it.isChecked = it.text == currentType.name + } + } + } + binding.fakeWindowsToggleGroup.apply { + setOnCheckedChangeListener { group, checkedId -> + val chip = group.findViewById<RadioButton>(checkedId) + if (!chip.isPressed) return@setOnCheckedChangeListener + vm.selectFakeExposureWindowMode(TestSettings.FakeExposureWindowTypes.valueOf(chip.text.toString())) + } + } } companion object { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt index 35c5e63f9..dd93a0e15 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModel.kt @@ -21,6 +21,7 @@ import de.rki.coronawarnapp.storage.AppDatabase import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking @@ -51,11 +52,14 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( private val keyCacheRepository: KeyCacheRepository, private val appConfigProvider: AppConfigProvider, tracingCardStateProvider: TracingCardStateProvider, - private val exposureResultStore: ExposureResultStore + private val exposureResultStore: ExposureResultStore, + private val testSettings: TestSettings ) : CWAViewModel( dispatcherProvider = dispatcherProvider ) { + val fakeWindowsState = testSettings.fakeExposureWindows.flow.asLiveData() + init { Timber.d("CWAViewModel: %s", this) Timber.d("SavedStateHandle: %s", handle) @@ -219,6 +223,10 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( launch { keyCacheRepository.clear() } } + fun selectFakeExposureWindowMode(newMode: TestSettings.FakeExposureWindowTypes) { + testSettings.fakeExposureWindows.update { newMode } + } + @AssistedInject.Factory interface Factory : CWAViewModelFactory<TestRiskLevelCalculationFragmentCWAViewModel> { fun create( diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml index 7d94e7c3d..905ec5581 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_risk_level_calculation.xml @@ -34,23 +34,44 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <TextView - style="@style/headline6" - android:accessibilityHeading="true" + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/environment_container" + style="@style/card" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Preview (no interaction possible)" /> + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/fake_windows_title" + style="@style/headline6" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Fake exposure windows" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <RadioGroup + android:id="@+id/fake_windows_toggle_group" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:orientation="vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/fake_windows_title" /> + </androidx.constraintlayout.widget.ConstraintLayout> <FrameLayout android:id="@+id/test_risk_card" style="@style/card" + gone="@{showRiskStatusCard == null || !showRiskStatusCard}" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - gone="@{showRiskStatusCard == null || !showRiskStatusCard}" - android:focusable="true" android:backgroundTint="@{tracingCard.getRiskInfoContainerBackgroundTint(context)}" - android:backgroundTintMode="src_over"> + android:backgroundTintMode="src_over" + android:focusable="true"> <include android:id="@+id/risk_card_content" @@ -94,10 +115,10 @@ <TextView android:id="@+id/label_aggregated_risk_result_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" android:text="Aggregated Risk Result" /> <TextView @@ -109,9 +130,9 @@ <TextView android:id="@+id/label_risk_additional_info_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" + android:accessibilityHeading="true" android:text="Risk Calculation Additional Information" /> <TextView @@ -123,9 +144,9 @@ <TextView android:id="@+id/label_backend_parameters_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" + android:accessibilityHeading="true" android:text="Backend Parameters" /> <TextView @@ -137,9 +158,9 @@ <TextView android:id="@+id/label_exposure_window_title" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" + android:accessibilityHeading="true" android:text="Exposure Windows" /> <TextView diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt index 261fd5fca..5f9b38fcb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TestSettings.kt @@ -1,14 +1,19 @@ package de.rki.coronawarnapp.storage import android.content.Context +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.FlowPreference import de.rki.coronawarnapp.util.preferences.createFlowPreference +import de.rki.coronawarnapp.util.serialization.BaseGson import javax.inject.Inject import javax.inject.Singleton @Singleton class TestSettings @Inject constructor( - @AppContext private val context: Context + @AppContext private val context: Context, + @BaseGson private val gson: Gson ) { private val prefs by lazy { context.getSharedPreferences("test_settings", Context.MODE_PRIVATE) @@ -18,4 +23,22 @@ class TestSettings @Inject constructor( key = "connections.metered.fake", defaultValue = false ) + + val fakeExposureWindows = FlowPreference( + preferences = prefs, + key = "riskleve.exposurewindows.fake", + reader = FlowPreference.gsonReader<FakeExposureWindowTypes>(gson, FakeExposureWindowTypes.DISABLED), + writer = FlowPreference.gsonWriter(gson) + ) + + enum class FakeExposureWindowTypes { + @SerializedName("DISABLED") + DISABLED, + + @SerializedName("INCREASED_RISK_DEFAULT") + INCREASED_RISK_DEFAULT, + + @SerializedName("LOW_RISK_DEFAULT") + LOW_RISK_DEFAULT + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt index f904964bf..75dd982d0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/TestSettingsTest.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.storage import android.content.Context +import com.google.gson.Gson import de.rki.coronawarnapp.util.CWADebug import io.mockk.MockKAnnotations import io.mockk.clearAllMocks @@ -16,6 +17,7 @@ class TestSettingsTest : BaseTest() { @MockK lateinit var context: Context private lateinit var mockPreferences: MockSharedPreferences + private val gson = Gson() @BeforeEach fun setup() { @@ -35,6 +37,7 @@ class TestSettingsTest : BaseTest() { } private fun buildInstance(): TestSettings = TestSettings( - context = context + context = context, + gson = gson ) } diff --git a/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt new file mode 100644 index 000000000..9d495c8c2 --- /dev/null +++ b/Corona-Warn-App/src/testDevice/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.util.CWADebug +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.gms.MockGMSTask +import java.io.File + +class ExposureWindowProviderTest : BaseTest() { + @MockK lateinit var googleENFClient: ExposureNotificationClient + + private val exampleKeyFiles = listOf(File("file1"), File("file2")) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { googleENFClient.exposureWindows } returns MockGMSTask.forValue(emptyList()) + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createProvider() = DefaultExposureWindowProvider( + client = googleENFClient + ) + + @Test + fun `fake exposure windows only in tester builds`() { + val instance = createProvider() + CWADebug.isDeviceForTestersBuild shouldBe false + } +} diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt new file mode 100644 index 000000000..f9b77f256 --- /dev/null +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProviderTest.kt @@ -0,0 +1,48 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import de.rki.coronawarnapp.storage.TestSettings +import de.rki.coronawarnapp.util.CWADebug +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.gms.MockGMSTask +import java.io.File + +class ExposureWindowProviderTest : BaseTest() { + @MockK lateinit var googleENFClient: ExposureNotificationClient + @MockK lateinit var testSettings: TestSettings + @MockK lateinit var fakeExposureWindowProvider: FakeExposureWindowProvider + + private val exampleKeyFiles = listOf(File("file1"), File("file2")) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { googleENFClient.exposureWindows } returns MockGMSTask.forValue(emptyList()) + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createProvider() = DefaultExposureWindowProvider( + client = googleENFClient, + testSettings = testSettings, + fakeExposureWindowProvider = fakeExposureWindowProvider + ) + + @Test + fun `fake exposure windows only in tester builds`() { + val instance = createProvider() + CWADebug.isDeviceForTestersBuild shouldBe true + } +} -- GitLab