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 0000000000000000000000000000000000000000..544b74346319309e35780eccde28274cb969fd73 --- /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 0000000000000000000000000000000000000000..675781ac432d5542f2d43c257997c715d78f3f6d --- /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 0000000000000000000000000000000000000000..dd7306b0b1f347ba356fbbbca101f5abd95dc827 --- /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 0000000000000000000000000000000000000000..086b6b5f9418d0e314743d8b07c8379a6522c5ae --- /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 b0e40407ce9b0895cc283865456e43f7303882d0..7087a044d87e8f1341a2ec5b785883fa78d8bc5e 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 35c5e63f9b8c3f63e4c06d605c7b1ec59e867b08..dd93a0e154d8e7ece32be12a0984a78c197c3560 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 7d94e7c3dca3a8e044acb4aac523109b7266aa7c..905ec558113eefa6b4b968a4abe5aec506b7b7b4 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 261fd5fcacf20a08176f1db87dbc42bb1e44b8a1..5f9b38fcb3cf3949be5d8fbd5af640560911d62c 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 f904964bfddde385950bac6c95efad7247fda59b..75dd982d0f4a0bc20c580e3bdc0bd08bf693e334 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 0000000000000000000000000000000000000000..9d495c8c22e32f7ad4fcaa7548c38ac13ebda676 --- /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 0000000000000000000000000000000000000000..f9b77f2560de2a6f1a7d0e0f84642cd3a9085468 --- /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 + } +}