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..ce213e8e03432873ea37ac457889cc65c92da657 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/FakeExposureWindowProvider.kt @@ -0,0 +1,63 @@ +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.TimeStamper +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.fromJson +import org.joda.time.Duration +import javax.inject.Inject + +@Reusable +class FakeExposureWindowProvider @Inject constructor( + @AppContext val context: Context, + @BaseGson val gson: Gson, + val timeStamper: TimeStamper +) { + + 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) + val nowUTC = timeStamper.nowUTC + return jsonWindows.map { jWindow -> + ExposureWindow.Builder().apply { + setDateMillisSinceEpoch(nowUTC.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..0ea69b5c0ecb6b649b3d2658e630ded2a4368f4d 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,45 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <TextView - style="@style/headline6" - android:accessibilityHeading="true" + <LinearLayout + 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:orientation="vertical" + 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" /> + + <TextView + android:layout_width="match_parent" + style="@style/TextAppearance.AppCompat.Caption" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="Takes effect the next time `ExposureNotificationClient.exposureWindows` is called, i.e. on risk level calculation." + android:layout_height="wrap_content" /> + + <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" /> + </LinearLayout> <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 +116,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 +131,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 +145,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 +159,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/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt index 64f2e75d7addfca50ab691aa7d42bc7d8cd3f4cd..2c5924ec9f54992538ea1c132b41c43fe5aece47 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProvider.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider import com.google.android.gms.common.api.ApiException +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeyFileProvider import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.exception.reporting.ReportingConstants import de.rki.coronawarnapp.nearby.modules.version.ENFVersion @@ -35,10 +36,19 @@ class DefaultDiagnosisKeyProvider @Inject constructor( // return false } + val keyFilesList = keyFiles.toList() + val provideDiagnosisKeysTask = if (enfVersion.isAtLeast(ENFVersion.V1_7)) { + Timber.i("Provide diagnosis keys with DiagnosisKeyFileProvider") + val diagnosisKeyFileProvider = DiagnosisKeyFileProvider(keyFilesList) + enfClient.provideDiagnosisKeys(diagnosisKeyFileProvider) + } else { + Timber.i("Provide diagnosis keys as list") + enfClient.provideDiagnosisKeys(keyFilesList) + } + return suspendCoroutine { cont -> Timber.d("Performing key submission.") - enfClient - .provideDiagnosisKeys(keyFiles.toList()) + provideDiagnosisKeysTask .addOnSuccessListener { cont.resume(true) } .addOnFailureListener { val wrappedException = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt index 7652fb055bdb64e4cbd76d4528a7047ffbfeef91..3ce93d4823d6b61d3c6a2634937087ee742f9a31 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt @@ -9,6 +9,7 @@ import javax.inject.Singleton import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +import kotlin.math.abs @Singleton class DefaultENFVersion @Inject constructor( @@ -39,9 +40,28 @@ class DefaultENFVersion @Inject constructor( } } + override suspend fun isAtLeast(compareVersion: Long): Boolean { + if (!compareVersion.isCorrectVersionLength) throw IllegalArgumentException("given version has incorrect length") + + getENFClientVersion()?.let { currentENFClientVersion -> + Timber.i("Comparing current ENF client version $currentENFClientVersion with $compareVersion") + return currentENFClientVersion >= compareVersion + } + + return false + } + private suspend fun internalGetENFClientVersion(): Long = suspendCoroutine { cont -> client.version .addOnSuccessListener { cont.resume(it) } .addOnFailureListener { cont.resumeWithException(it) } } + + // check if a raw long has the correct length to be considered an API version + private val Long.isCorrectVersionLength + get(): Boolean = abs(this).toString().length == GOOGLE_API_VERSION_FIELD_LENGTH + + companion object { + private const val GOOGLE_API_VERSION_FIELD_LENGTH = 8 + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt index b7d16994a91e0742d3910f3c22fd7323b31b6857..7b1b8d8303560d5d1fd2e93dd5a4d90dc301e23f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt @@ -12,7 +12,15 @@ interface ENFVersion { */ suspend fun requireMinimumVersion(required: Long) + /** + * Indicates if the client runs above a certain version + * + * @return isAboveVersion, if connected to an old unsupported version, return false + */ + suspend fun isAtLeast(compareVersion: Long): Boolean + companion object { const val V1_6 = 16000000L + const val V1_7 = 17000000L } } 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/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index 3af087fa8882737e837c5feac5090e1674eb3a07..d024cca935d97319b2a2836ee1d2c7a278b7cd78 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -448,7 +448,7 @@ <!-- YTXT: onboarding(together) - inform about the app --> <string name="onboarding_body">"Machen Sie Ihr Smartphone zum Corona-Warn-System. Überblicken Sie Ihren Risikostatus und erfahren Sie, ob in den letzten 14 Tagen Corona-positiv getestete Personen in Ihrer Nähe waren."</string> <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"Die App merkt sich Begegnungen zwischen Menschen, indem ihre Smartphones verschlüsselte Zufalls-IDs austauschen. Und zwar, ohne dabei auf persönliche Daten zuzugreifen."</string> + <string name="onboarding_body_emphasized">"Die App merkt sich Begegnungen zwischen Menschen, indem ihre Smartphones verschlüsselte Zufalls-IDs austauschen. Persönliche Daten werden dabei nicht ausgetauscht."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"Eine vielfältige Gruppe in einer Stadt benutzt Smartphones."</string> <!-- XACT: Onboarding (privacy) page title --> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt index da15e57215243c887afb317a87160ca618a71619..d484333fe3dfab9d173bb2e716572ff21f19a9e6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DefaultDiagnosisKeyProviderTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider +import com.google.android.gms.nearby.exposurenotification.DiagnosisKeyFileProvider import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import de.rki.coronawarnapp.nearby.modules.version.OutdatedENFVersionException @@ -36,6 +37,8 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { coEvery { googleENFClient.provideDiagnosisKeys(any<List<File>>()) } returns MockGMSTask.forValue(null) + coEvery { googleENFClient.provideDiagnosisKeys(any<DiagnosisKeyFileProvider>()) } returns MockGMSTask.forValue(null) + coEvery { enfVersion.requireMinimumVersion(any()) } returns Unit } @@ -70,7 +73,23 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { } @Test - fun `key provision is used on newer ENF versions`() { + fun `key provision is used with DiagnosisKeyFileProvider on ENF versions from 1_7 upwards`() { + coEvery { enfVersion.isAtLeast(any()) } returns true + + val provider = createProvider() + + runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true + + coVerifySequence { + submissionQuota.consumeQuota(1) + googleENFClient.provideDiagnosisKeys(any<DiagnosisKeyFileProvider>()) + } + } + + @Test + fun `key provision is used with key list on ENF versions 1_6`() { + coEvery { enfVersion.isAtLeast(any()) } returns false + val provider = createProvider() runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true @@ -81,9 +100,11 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { } } + @Test fun `quota is just monitored`() { coEvery { submissionQuota.consumeQuota(any()) } returns false + coEvery { enfVersion.isAtLeast(any()) } returns true val provider = createProvider() @@ -91,7 +112,7 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { coVerifySequence { submissionQuota.consumeQuota(1) - googleENFClient.provideDiagnosisKeys(exampleKeyFiles) + googleENFClient.provideDiagnosisKeys(any<DiagnosisKeyFileProvider>()) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt index 6d9e563b93dda599d64478739717998339f72d63..48a020e449e2a295afbef3b536432b2f928ffa57 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt @@ -109,4 +109,54 @@ internal class DefaultENFVersionTest { } } } + + @Test + fun `isAtLeast is true for newer version`() { + every { client.version } returns MockGMSTask.forValue(ENFVersion.V1_7) + + runBlockingTest { + createInstance().isAtLeast(ENFVersion.V1_6) shouldBe true + } + } + + @Test + fun `isAtLeast is true for equal version`() { + every { client.version } returns MockGMSTask.forValue(ENFVersion.V1_6) + + runBlockingTest { + createInstance().isAtLeast(ENFVersion.V1_6) shouldBe true + } + } + + @Test + fun `isAtLeast is false for older version`() { + every { client.version } returns MockGMSTask.forValue(ENFVersion.V1_6) + + runBlockingTest { + createInstance().isAtLeast(ENFVersion.V1_7) shouldBe false + } + } + + @Test + fun `invalid input for isAtLeast throws IllegalArgumentException`() { + runBlockingTest { + shouldThrow<IllegalArgumentException> { + createInstance().isAtLeast(16) + } + } + } + + @Test + fun `isAtLeast returns false when client not connected`() { + every { client.version } returns MockGMSTask.forError(ApiException(Status(API_NOT_CONNECTED))) + + runBlockingTest { + createInstance().apply { + shouldNotThrowAny { + isAtLeast(ENFVersion.V1_6) shouldBe false + isAtLeast(ENFVersion.V1_7) shouldBe false + } + } + } + } } 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 + } +}