diff --git a/.circleci/config.yml b/.circleci/config.yml index 1f005f286ecff1198d18270136e180513e6f47c4..3e7d9b10cbbc2210e2db04ff498f4665902f7e5b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -223,9 +223,6 @@ jobs: - run-gradle-cmd: desc: JaCoCo report cmd: ":Corona-Warn-App:jacocoTestReportDeviceRelease -i" - - run: - name: Skip SonarCloud for external Pull Requests - command: '[[ -v CIRCLE_PR_REPONAME ]] && circleci-agent step halt || true' - scan-sonar quick_build_device_for_testers_signed: executor: android/android @@ -275,9 +272,10 @@ jobs: - run: name: Send to T-System command: | + fileName=$(find Corona-Warn-App/build/outputs/apk/deviceForTesters/release -name '*Corona-Warn-App*.apk') curl --location --request POST $tsystems_upload_url \ --header "Authorization: Bearer $tsystems_upload_bearer" \ - --form 'file=@Corona-Warn-App/build/outputs/apk/deviceForTesters/release/Corona-Warn-App-deviceForTesters-release.apk' \ + --form "file=@${fileName}" \ workflows: version: 2 quick_build: diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 13dca3db33d0f66c8b8af3bd6b1eb06db666aa83..8cbe448f6ffb21cd44d01a5f534581dff4675ce2 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -3,10 +3,10 @@ name: "Validate Gradle Wrapper" on: push: branches: - - master + - main pull_request: branches: - - master + - main jobs: validation: diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 83f49f75d73e527a3c7e21e3fb8fa5694c66c4da..255c61a8b5e1c2bfb4ad40b161fb60b148ff5e3b 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -163,11 +163,12 @@ android { } println("deviceForTesters adjusted versionName: $adjustedVersionName") } - - variant.outputs.each { output -> - def apkName = "Corona-Warn-App-${output.versionNameOverride}-${flavor.name}-${variant.buildType.name}.apk" - println("APK Name: $apkName") - output.outputFileName = apkName + if (flavor.name != "device") { + variant.outputs.each { output -> + def apkName = "Corona-Warn-App-${output.versionNameOverride}-${flavor.name}-${variant.buildType.name}.apk" + println("Override APK Name: $apkName") + output.outputFileName = apkName + } } } diff --git a/Corona-Warn-App/proguard-rules.pro b/Corona-Warn-App/proguard-rules.pro index 13dfa13cd055802fd1285f1182d31af5f22bf11a..a494dc86a67e75431934ce0fd0adeeab58b9ddee 100644 --- a/Corona-Warn-App/proguard-rules.pro +++ b/Corona-Warn-App/proguard-rules.pro @@ -72,9 +72,6 @@ -dontwarn sun.misc.** #-keep class com.google.gson.stream.** { *; } -# Application classes that will be serialized/deserialized over Gson --keep class com.google.gson.examples.android.model.** { <fields>; } - # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) -keep class * extends com.google.gson.TypeAdapter @@ -87,4 +84,6 @@ @com.google.gson.annotations.SerializedName <fields>; } -##---------------End: proguard configuration for Gson ---------- \ No newline at end of file +##---------------End: proguard configuration for Gson ---------- + +-keep class de.rki.coronawarnapp.server.protocols.internal.** { *; } \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt deleted file mode 100644 index 32df9c31bd567455f39b6541c680ada03eb64c44..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/storage/ExposureSummaryDaoTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -package de.rki.coronawarnapp.storage - -import android.content.Context -import androidx.room.Room -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -/** - * ExposureSummaryDao test. - */ -@RunWith(AndroidJUnit4::class) -class ExposureSummaryDaoTest { - private lateinit var dao: ExposureSummaryDao - private lateinit var db: AppDatabase - - @Before - fun setUp() { - val context = ApplicationProvider.getApplicationContext<Context>() - db = Room.inMemoryDatabaseBuilder( - context, AppDatabase::class.java - ).build() - dao = db.exposureSummaryDao() - } - - /** - * Test Create / Read DB operations. - */ - @Test - fun testCROperations() { - runBlocking { - val testEntity1 = ExposureSummaryEntity().apply { - this.daysSinceLastExposure = 1 - this.matchedKeyCount = 1 - this.maximumRiskScore = 1 - this.summationRiskScore = 1 - } - - val testEntity2 = ExposureSummaryEntity().apply { - this.daysSinceLastExposure = 2 - this.matchedKeyCount = 2 - this.maximumRiskScore = 2 - this.summationRiskScore = 2 - } - - assertThat(dao.getExposureSummaryEntities().isEmpty()).isTrue() - - val id1 = dao.insertExposureSummaryEntity(testEntity1) - var selectAll = dao.getExposureSummaryEntities() - var selectLast = dao.getLatestExposureSummary() - assertThat(dao.getExposureSummaryEntities().isEmpty()).isFalse() - assertThat(selectAll.size).isEqualTo(1) - assertThat(selectAll[0].id).isEqualTo(id1) - assertThat(selectLast).isNotNull() - assertThat(selectLast?.id).isEqualTo(id1) - - val id2 = dao.insertExposureSummaryEntity(testEntity2) - selectAll = dao.getExposureSummaryEntities() - selectLast = dao.getLatestExposureSummary() - assertThat(selectAll.isEmpty()).isFalse() - assertThat(selectAll.size).isEqualTo(2) - assertThat(selectLast).isNotNull() - assertThat(selectLast?.id).isEqualTo(id2) - } - } - - @After - fun closeDb() { - db.close() - } -} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt deleted file mode 100644 index 6771827fa9cab7f2cc67ea55cdb6a0c537a6f3ec..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/RiskLevelAndKeyRetrievalBenchmark.kt +++ /dev/null @@ -1,163 +0,0 @@ -package de.rki.coronawarnapp.test - -import android.content.Context -import android.text.format.Formatter -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionFinished -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.ApiSubmissionStarted -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadFinished -import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask.Progress.KeyFilesDownloadStarted -import de.rki.coronawarnapp.risk.RiskLevelTask -import de.rki.coronawarnapp.task.Task -import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.task.submitAndListen -import de.rki.coronawarnapp.util.di.AppInjector -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.map -import timber.log.Timber -import java.util.UUID - -class RiskLevelAndKeyRetrievalBenchmark( - private val context: Context, - private val countries: List<String> -) { - - /** - * the key cache instance used to store queried dates and hours - */ - private val keyCache = AppInjector.component.keyCacheRepository - - /** - * Calls the RetrieveDiagnosisKeysTransaction and RiskLevelTransaction and measures them. - * Results are displayed using a label - * @param callCount defines how often the transactions should be called (each call will be - * measured separately) - */ - suspend fun start( - callCount: Int, - onBenchmarkCompletedListener: OnBenchmarkCompletedListener - ) { - - var resultInfo = StringBuilder() - .append( - "MEASUREMENT Running for Countries:\n " + - "${countries.joinToString(", ")}\n\n" - ) - .append("Result: \n\n") - .append("#\t Combined \t Download \t Sub \t Risk \t File # \t F. size\n") - - onBenchmarkCompletedListener(resultInfo.toString()) - - repeat(callCount) { index -> - - keyCache.clear() - - var keyRetrievalError = "" - var keyFileCount: Int = -1 - var keyFileDownloadDuration: Long = -1 - var keyFilesSize: Long = -1 - var apiSubmissionDuration: Long = -1 - - measureDiagnosticKeyRetrieval( - label = "#$index", - countries = countries, - downloadFinished = { duration, keyCount, totalFileSize -> - keyFileCount = keyCount - keyFileDownloadDuration = duration - keyFilesSize = totalFileSize - }, apiSubmissionFinished = { duration -> - apiSubmissionDuration = duration - }) - - var calculationDuration: Long = -1 - var calculationError = "" - - measureKeyCalculation("#$index") { - if (it != null) calculationDuration = it - - // build result entry for current iteration with all gathered data - resultInfo.append( - "${index + 1}. \t ${calculationDuration + keyFileDownloadDuration + apiSubmissionDuration} ms \t " + - "$keyFileDownloadDuration ms " + "\t $apiSubmissionDuration ms" + - "\t $calculationDuration ms \t $keyFileCount \t " + - "${Formatter.formatFileSize(context, keyFilesSize)}\n" - ) - - if (keyRetrievalError.isNotEmpty()) { - resultInfo.append("Key Retrieval Error: $keyRetrievalError\n") - } - - if (calculationError.isNotEmpty()) { - resultInfo.append("Calculation Error: $calculationError\n") - } - - onBenchmarkCompletedListener(resultInfo.toString()) - } - } - } - - private suspend fun measureKeyCalculation(label: String, callback: (Long?) -> Unit) { - val uuid = UUID.randomUUID() - val t0 = System.currentTimeMillis() - AppInjector.component.taskController.tasks - .map { - it - .map { taskInfo -> taskInfo.taskState } - .filter { taskState -> taskState.request.id == uuid && taskState.isFinished } - } - .collect { - it.firstOrNull()?.also { state -> - Timber.v("MEASURE [Risk Level Calculation] $label finished") - callback.invoke( - if (state.error != null) - null - else - System.currentTimeMillis() - t0 - ) - } - } - Timber.v("MEASURE [Risk Level Calculation] $label started") - AppInjector.component.taskController.submit( - DefaultTaskRequest( - RiskLevelTask::class, - object : Task.Arguments {}, - uuid - ) - ) - } - - private suspend fun measureDiagnosticKeyRetrieval( - label: String, - countries: List<String>, - downloadFinished: (duration: Long, keyCount: Int, fileSize: Long) -> Unit, - apiSubmissionFinished: (duration: Long) -> Unit - ) { - var keyFileDownloadStart: Long = -1 - var apiSubmissionStarted: Long = -1 - - AppInjector.component.taskController.submitAndListen( - DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments(countries)) - ).collect { progress: Task.Progress -> - when (progress) { - is KeyFilesDownloadStarted -> { - Timber.v("MEASURE [Diagnostic Key Files] $label started") - keyFileDownloadStart = System.currentTimeMillis() - } - is KeyFilesDownloadFinished -> { - Timber.v("MEASURE [Diagnostic Key Files] $label finished") - val duration = System.currentTimeMillis() - keyFileDownloadStart - downloadFinished(duration, progress.keyCount, progress.fileSize) - } - is ApiSubmissionStarted -> { - apiSubmissionStarted = System.currentTimeMillis() - } - is ApiSubmissionFinished -> { - val duration = System.currentTimeMillis() - apiSubmissionStarted - apiSubmissionFinished(duration) - } - } - } - } -} - -typealias OnBenchmarkCompletedListener = (resultInfo: String) -> Unit diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt index 3244c5f34b18712f59e379f811d8d198268bca99..2466cb7d6598603c6d97ef9a27ce9805094073f0 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/LoggerState.kt @@ -3,11 +3,13 @@ package de.rki.coronawarnapp.test.api.ui import de.rki.coronawarnapp.util.CWADebug data class LoggerState( - val isLogging: Boolean + val isLogging: Boolean, + val logsize: Long ) { companion object { internal fun CWADebug.toLoggerState() = LoggerState( - isLogging = fileLogger?.isLogging ?: false + isLogging = fileLogger?.isLogging ?: false, + logsize = fileLogger?.logFile?.length() ?: 0L ) } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt index 694615a9ae234ae6f626b12a7ff9f0f179402ff1..c03b84f39023d1999f7d0e38b93fa25b1e6d0d10 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForAPIFragment.kt @@ -16,7 +16,6 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient -import com.google.android.gms.nearby.exposurenotification.ExposureSummary import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson @@ -32,25 +31,24 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper import de.rki.coronawarnapp.receiver.ExposureStateUpdateReceiver +import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.TimeVariables import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange import de.rki.coronawarnapp.sharing.ExposureSharingService import de.rki.coronawarnapp.storage.AppDatabase -import de.rki.coronawarnapp.storage.ExposureSummaryRepository -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.tracing.TracingIntervalRepository import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.util.KeyFileHelper -import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.joda.time.DateTime @@ -66,6 +64,8 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), InternalExposureNotificationPermissionHelper.Callback, AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + @Inject lateinit var enfClient: ENFClient + @Inject lateinit var exposureResultStore: ExposureResultStore private val vm: TestForApiFragmentViewModel by cwaViewModels { viewModelFactory } companion object { @@ -85,10 +85,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), } } - private val enfClient by lazy { - AppInjector.component.enfClient - } - private var myExposureKeysJSON: String? = null private var myExposureKeys: List<TemporaryExposureKey>? = mutableListOf() private var otherExposureKey: AppleLegacyKeyExchange.Key? = null @@ -96,8 +92,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper - private var token: String? = null - private lateinit var qrPager: ViewPager2 private lateinit var qrPagerAdapter: RecyclerView.Adapter<QRPagerAdapter.QRViewHolder> @@ -108,8 +102,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - token = UUID.randomUUID().toString() - internalExposureNotificationPermissionHelper = InternalExposureNotificationPermissionHelper(this, this) @@ -127,7 +119,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), binding.apply { buttonApiTestStart.setOnClickListener { start() } buttonApiGetExposureKeys.setOnClickListener { getExposureKeys() } - buttonApiGetCheckExposure.setOnClickListener { checkExposure() } buttonApiScanQrCode.setOnClickListener { IntentIntegrator.forSupportFragment(this@TestForAPIFragment) @@ -168,8 +159,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), buttonRetrieveExposureSummary.setOnClickListener { vm.launch { - val summary = ExposureSummaryRepository.getExposureSummaryRepository() - .getExposureSummaryEntities().toString() + val summary = exposureResultStore.entities.first().exposureWindows.toString() withContext(Dispatchers.Main) { showToast(summary) @@ -206,12 +196,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), } } - override fun onResume() { - super.onResume() - - updateExposureSummaryDisplay(null) - } - private val prettyKey = { key: AppleLegacyKeyExchange.Key -> StringBuilder() .append("\nKey data: ${key.keyData}") @@ -274,9 +258,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), if (null == otherExposureKey) { showToast("No other keys provided. Please fill the EditText with the JSON containing keys") } else { - token = UUID.randomUUID().toString() - LocalData.googleApiToken(token) - val appleKeyList = mutableListOf<AppleLegacyKeyExchange.Key>() for (key in otherExposureKeyList) { @@ -298,7 +279,7 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), val dir = File( File(requireContext().getExternalFilesDir(null), "key-export"), - token ?: "" + UUID.randomUUID().toString() ) dir.mkdirs() @@ -306,15 +287,13 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), lifecycleScope.launch { googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir) - Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token") + Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys") try { // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API enfClient.provideDiagnosisKeys( - googleFileList, - AppInjector.component.appConfigProvider.getAppConfig().exposureDetectionConfiguration, - token!! + googleFileList ) - showToast("Provided ${appleKeyList.size} keys to Google API with token $token") + showToast("Provided ${appleKeyList.size} keys to Google API") } catch (e: Exception) { e.report(ExceptionCategory.EXPOSURENOTIFICATION) } @@ -322,51 +301,6 @@ class TestForAPIFragment : Fragment(R.layout.fragment_test_for_a_p_i), } } - private fun checkExposure() { - Timber.d("Check Exposure with token $token") - - lifecycleScope.launch { - try { - val exposureSummary = - InternalExposureNotificationClient.asyncGetExposureSummary(token!!) - updateExposureSummaryDisplay(exposureSummary) - showToast("Updated Exposure Summary with token $token") - Timber.d("Received exposure with token $token from QR Code") - Timber.i(exposureSummary.toString()) - } catch (e: Exception) { - e.report(ExceptionCategory.EXPOSURENOTIFICATION) - } - } - } - - private fun updateExposureSummaryDisplay(exposureSummary: ExposureSummary?) { - - binding.labelExposureSummaryMatchedKeyCount.text = getString( - R.string.test_api_body_matchedKeyCount, - (exposureSummary?.matchedKeyCount ?: "-").toString() - ) - - binding.labelExposureSummaryDaysSinceLastExposure.text = getString( - R.string.test_api_body_daysSinceLastExposure, - (exposureSummary?.daysSinceLastExposure ?: "-").toString() - ) - - binding.labelExposureSummaryMaximumRiskScore.text = getString( - R.string.test_api_body_maximumRiskScore, - (exposureSummary?.maximumRiskScore ?: "-").toString() - ) - - binding.labelExposureSummarySummationRiskScore.text = getString( - R.string.test_api_body_summation_risk, - (exposureSummary?.summationRiskScore ?: "-").toString() - ) - - binding.labelExposureSummaryAttenuation.text = getString( - R.string.test_api_body_attenuation, - (exposureSummary?.attenuationDurationsInMinutes?.joinToString() ?: "-").toString() - ) - } - private fun updateKeysDisplay() { val myKeys = diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt index f2ebfd26e4c413d2b3c80cb8cce605669310ee03..d277c9b961d19aa3ab2eba3c8eb262c0fe71fe51 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/api/ui/TestForApiFragmentViewModel.kt @@ -18,7 +18,7 @@ class TestForApiFragmentViewModel @AssistedInject constructor( ) : CWAViewModel() { fun calculateRiskLevelClicked() { - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) + taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "TestForApiFragmentViewModel")) } val gmsState by smartLiveData { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt index 742411f39edaa87b0a911267cb3b4d527326c194..09567384fd47b06010a3379f788b0f8b5f2a6f78 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/appconfig/ui/AppConfigTestFragment.kt @@ -32,12 +32,9 @@ class AppConfigTestFragment : Fragment(R.layout.fragment_test_appconfig), AutoIn super.onViewCreated(view, savedInstanceState) vm.currentConfig.observe2(this) { data -> - binding.currentConfiguration.text = - data?.rawConfig?.toString() ?: "No config available." - binding.lastUpdate.text = data?.updatedAt?.let { timeFormatter.print(it) } ?: "n/a" - binding.timeOffset.text = data?.let { - "${it.localOffset.millis}ms (configType=${it.configType})" - } ?: "n/a" + binding.currentConfiguration.text = data.rawConfig.toString() + binding.lastUpdate.text = timeFormatter.print(data.updatedAt) + binding.timeOffset.text = "${data.localOffset.millis}ms (configType=${data.configType})" } vm.errorEvent.observe2(this) { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt index 234022907e2c603253e6ac70c8abffa1dd72143d..44d3e0cd08a2688511a8c20589bfe3b428659d36 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/crash.ui/SettingsCrashReportViewModel.kt @@ -4,7 +4,6 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.map -import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.bugreporting.event.BugEvent import de.rki.coronawarnapp.bugreporting.reportProblem @@ -12,9 +11,7 @@ import de.rki.coronawarnapp.bugreporting.storage.repository.BugRepository import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import timber.log.Timber -import java.lang.Exception class SettingsCrashReportViewModel @AssistedInject constructor( private val crashReportRepository: BugRepository @@ -28,7 +25,7 @@ class SettingsCrashReportViewModel @AssistedInject constructor( createBugEventFormattedText(it) } - fun deleteAllCrashReports() = viewModelScope.launch(Dispatchers.IO) { + fun deleteAllCrashReports() = launch(Dispatchers.IO) { crashReportRepository.clear() } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt index 91b1206da2ff70f7df02367cf75d340c70809c70..47f43dae0edf5cf59621975f0923cd8b83f15cee 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragment.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.test.debugoptions.ui import android.annotation.SuppressLint import android.os.Bundle +import android.text.format.Formatter import android.view.View import android.widget.RadioButton import android.widget.RadioGroup @@ -31,24 +32,14 @@ class DebugOptionsFragment : Fragment(R.layout.fragment_test_debugoptions), Auto override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Debug card - binding.backgroundNotificationsToggle.apply { - setOnClickListener { vm.setBackgroundNotifications(isChecked) } - } - vm.backgroundNotificationsToggleEvent.observe2(this@DebugOptionsFragment) { - showSnackBar("Background Notifications are activated: $it") - } - vm.debugOptionsState.observe2(this) { state -> - binding.apply { - backgroundNotificationsToggle.isChecked = state.areNotificationsEnabled - } - } binding.testLogfileToggle.apply { setOnClickListener { vm.setLoggerEnabled(isChecked) } } vm.loggerState.observe2(this) { state -> binding.apply { testLogfileToggle.isChecked = state.isLogging + val logSize = Formatter.formatShortFileSize(requireContext(), state.logsize) + testLogfileToggle.text = "Logfile enabled ($logSize)" testLogfileShare.setGone(!state.isLogging) } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt index c58e64556e2a35b1f6604de20ecaee41ba75638e..0c654b99f52600578c531ccdbe98c181a332ee81 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModel.kt @@ -1,15 +1,9 @@ package de.rki.coronawarnapp.test.debugoptions.ui import android.content.Context -import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.environment.EnvironmentSetup.Type.Companion.toEnvironmentType -import de.rki.coronawarnapp.risk.RiskLevelTask -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.TestSettings -import de.rki.coronawarnapp.task.TaskController -import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.test.api.ui.EnvironmentState.Companion.toEnvironmentState import de.rki.coronawarnapp.test.api.ui.LoggerState.Companion.toLoggerState import de.rki.coronawarnapp.util.CWADebug @@ -19,24 +13,14 @@ import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.ui.smartLiveData import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.io.File class DebugOptionsFragmentViewModel @AssistedInject constructor( @AppContext private val context: Context, private val envSetup: EnvironmentSetup, - private val testSettings: TestSettings, - private val taskController: TaskController, dispatcherProvider: DispatcherProvider ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { - val debugOptionsState by smartLiveData { - DebugOptionsState( - areNotificationsEnabled = LocalData.backgroundNotification() - ) - } - val environmentState by smartLiveData { envSetup.toEnvironmentState() } @@ -50,16 +34,6 @@ class DebugOptionsFragmentViewModel @AssistedInject constructor( } } - val backgroundNotificationsToggleEvent = SingleLiveEvent<Boolean>() - - fun setBackgroundNotifications(enabled: Boolean) { - debugOptionsState.update { - LocalData.backgroundNotification(enabled) - it.copy(areNotificationsEnabled = enabled) - } - backgroundNotificationsToggleEvent.postValue(enabled) - } - val loggerState by smartLiveData { CWADebug.toLoggerState() } @@ -71,15 +45,11 @@ class DebugOptionsFragmentViewModel @AssistedInject constructor( loggerState.update { CWADebug.toLoggerState() } } - fun calculateRiskLevelClicked() { - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) - } - val logShareEvent = SingleLiveEvent<File?>() fun shareLogFile() { CWADebug.fileLogger?.let { - viewModelScope.launch(context = Dispatchers.Default) { + launch { if (!it.logFile.exists()) return@launch val externalPath = File( diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsState.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsState.kt deleted file mode 100644 index da57e399eaf9c4caf03c07a9e2fa719d3375fa17..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package de.rki.coronawarnapp.test.debugoptions.ui - -data class DebugOptionsState( - val areNotificationsEnabled: Boolean -) diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index 36d71e9cb89f0fde006cbb5c9f2df53e228d6c33..ef3eff6e88c7362e04b9766d57312bedf0c536ae 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment +import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -23,6 +24,7 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { TestRiskLevelCalculationFragment.MENU_ITEM, KeyDownloadTestFragment.MENU_ITEM, TestTaskControllerFragment.MENU_ITEM, + SubmissionTestFragment.MENU_ITEM, SettingsCrashReportFragment.MENU_ITEM ).let { MutableLiveData(it) } } 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 0867b6e6bfcbce4261cde511273134389288fe83..b0e40407ce9b0895cc283865456e43f7303882d0 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 @@ -1,27 +1,20 @@ package de.rki.coronawarnapp.test.risklevel.ui -import android.content.Intent import android.os.Bundle import android.view.View import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.navArgs -import com.google.zxing.integration.android.IntentIntegrator -import com.google.zxing.integration.android.IntentResult import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestRiskLevelCalculationBinding -import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange -import de.rki.coronawarnapp.sharing.ExposureSharingService import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel -import de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted -import timber.log.Timber import javax.inject.Inject @Suppress("LongMethod") @@ -39,7 +32,6 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le ) private val settingsViewModel: SettingsViewModel by activityViewModels() - private val submissionViewModel: SubmissionViewModel by activityViewModels() private val binding: FragmentTestRiskLevelCalculationBinding by viewBindingLazy() @@ -51,10 +43,12 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le } binding.settingsViewModel = settingsViewModel - binding.submissionViewModel = submissionViewModel + + vm.showRiskStatusCard.observe2(this) { + binding.showRiskStatusCard = it + } binding.buttonRetrieveDiagnosisKeys.setOnClickListener { vm.retrieveDiagnosisKeys() } - binding.buttonProvideKeyViaQr.setOnClickListener { vm.scanLocalQRCodeAndProvide() } binding.buttonCalculateRiskLevel.setOnClickListener { vm.calculateRiskLevel() } binding.buttonClearDiagnosisKeyCache.setOnClickListener { vm.clearKeyCache() } @@ -66,66 +60,32 @@ class TestRiskLevelCalculationFragment : Fragment(R.layout.fragment_test_risk_le ).show() } - vm.riskScoreState.observe2(this) { state -> - binding.labelRiskScore.text = state.riskScoreMsg - binding.labelBackendParameters.text = state.backendParameters - binding.labelExposureSummary.text = state.exposureSummary - binding.labelFormula.text = state.formula - binding.labelExposureInfo.text = state.exposureInfo + vm.additionalRiskCalcInfo.observe2(this) { + binding.labelRiskAdditionalInfo.text = it } - vm.startENFObserver() - vm.apiKeysProvidedEvent.observe2(this) { event -> - Toast.makeText( - requireContext(), - "Provided ${event.keyCount} keys to Google API with token ${event.token}", - Toast.LENGTH_SHORT - ).show() + vm.aggregatedRiskResult.observe2(this) { + binding.labelAggregatedRiskResult.text = it } - vm.startLocalQRCodeScanEvent.observe2(this) { - IntentIntegrator.forSupportFragment(this) - .setOrientationLocked(false) - .setBeepEnabled(false) - .initiateScan() + vm.exposureWindowCountString.observe2(this) { + binding.labelExposureWindowCount.text = it } - } - - override fun onResume() { - super.onResume() - vm.calculateRiskLevel() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - val result: IntentResult = - IntentIntegrator.parseActivityResult(requestCode, resultCode, data) - ?: return super.onActivityResult(requestCode, resultCode, data) - if (result.contents == null) { - Toast.makeText(requireContext(), "Cancelled", Toast.LENGTH_LONG).show() - return + vm.exposureWindows.observe2(this) { + binding.labelExposureWindows.text = it } - ExposureSharingService.getOthersKeys(result.contents) { key: AppleLegacyKeyExchange.Key? -> - Timber.i("Keys scanned: %s", key) - if (key == null) { - Toast.makeText( - requireContext(), "No Key data found in QR code", Toast.LENGTH_SHORT - ).show() - return@getOthersKeys Unit - } - - val text = binding.transmissionNumber.text.toString() - val number = if (!text.isBlank()) Integer.valueOf(text) else 5 - vm.provideDiagnosisKey(number, key) + vm.backendParameters.observe2(this) { + binding.labelBackendParameters.text = it } } companion object { val TAG: String = TestRiskLevelCalculationFragment::class.simpleName!! val MENU_ITEM = TestMenuItem( - title = "Risklevel Calculation", - description = "Risklevel calculation related test options.", + title = "ENF v2 Calculation", + description = "Window Mode related overview.", targetId = R.id.test_risklevel_calculation_fragment ) } 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 d0a85e7f0f7f54678a5258bb921b8bca8595726b..b65f3240c90e9789e20bbc407d30d285eddf968c 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 @@ -1,96 +1,197 @@ package de.rki.coronawarnapp.test.risklevel.ui import android.content.Context -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import com.google.android.gms.nearby.exposurenotification.ExposureInformation import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject -import de.rki.coronawarnapp.appconfig.RiskCalculationConfig +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.risk.ExposureResult +import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.RiskLevel import de.rki.coronawarnapp.risk.RiskLevelTask -import de.rki.coronawarnapp.risk.RiskLevels import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.server.protocols.AppleLegacyKeyExchange +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult 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.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider -import de.rki.coronawarnapp.util.KeyFileHelper +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.sample -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.joda.time.Instant import timber.log.Timber -import java.io.File -import java.util.UUID +import java.util.Date import java.util.concurrent.TimeUnit -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( @Assisted private val handle: SavedStateHandle, @Assisted private val exampleArg: String?, @AppContext private val context: Context, // App context dispatcherProvider: DispatcherProvider, - private val enfClient: ENFClient, - private val riskLevels: RiskLevels, private val taskController: TaskController, private val keyCacheRepository: KeyCacheRepository, - tracingCardStateProvider: TracingCardStateProvider + private val appConfigProvider: AppConfigProvider, + tracingCardStateProvider: TracingCardStateProvider, + private val exposureResultStore: ExposureResultStore, + private val submissionRepository: SubmissionRepository ) : CWAViewModel( dispatcherProvider = dispatcherProvider ) { - val startLocalQRCodeScanEvent = SingleLiveEvent<Unit>() + init { + Timber.d("CWAViewModel: %s", this) + Timber.d("SavedStateHandle: %s", handle) + Timber.d("Example arg: %s", exampleArg) + } + val riskLevelResetEvent = SingleLiveEvent<Unit>() - val apiKeysProvidedEvent = SingleLiveEvent<DiagnosisKeyProvidedEvent>() - val riskScoreState = MutableLiveData<RiskScoreState>(RiskScoreState()) + + val showRiskStatusCard = submissionRepository.deviceUIStateFlow.map { + it.withSuccess(false) { true } + }.asLiveData(dispatcherProvider.Default) val tracingCardState = tracingCardStateProvider.state .sample(150L) .asLiveData(dispatcherProvider.Default) - init { - Timber.d("CWAViewModel: %s", this) - Timber.d("SavedStateHandle: %s", handle) - Timber.d("Example arg: %s", exampleArg) - } + val exposureWindowCountString = exposureResultStore + .entities + .map { "Retrieved ${it.exposureWindows.size} Exposure Windows" } + .asLiveData() + + val exposureWindows = exposureResultStore + .entities + .map { if (it.exposureWindows.isEmpty()) "Exposure windows list is empty" else it.exposureWindows.toString() } + .asLiveData() + + val aggregatedRiskResult = exposureResultStore + .entities + .map { if (it.aggregatedRiskResult != null) it.aggregatedRiskResult.toReadableString() else "Aggregated risk result is not available" } + .asLiveData() + + private fun AggregatedRiskResult.toReadableString(): String = StringBuilder() + .appendLine("Total RiskLevel: $totalRiskLevel") + .appendLine("Total Minimum Distinct Encounters With High Risk: $totalMinimumDistinctEncountersWithHighRisk") + .appendLine("Total Minimum Distinct Encounters With Low Risk: $totalMinimumDistinctEncountersWithLowRisk") + .appendLine("Most Recent Date With High Risk: $mostRecentDateWithHighRisk") + .appendLine("Most Recent Date With Low Risk: $mostRecentDateWithLowRisk") + .appendLine("Number of Days With High Risk: $numberOfDaysWithHighRisk") + .appendLine("Number of Days With Low Risk: $numberOfDaysWithLowRisk") + .toString() + + val backendParameters = appConfigProvider + .currentConfig + .map { it.toReadableString() } + .asLiveData() + + private fun ConfigData.toReadableString(): String = StringBuilder() + .appendLine("Transmission RiskLevel Multiplier: $transmissionRiskLevelMultiplier") + .appendLine() + .appendLine("Minutes At Attenuation Filters:") + .appendLine(minutesAtAttenuationFilters) + .appendLine() + .appendLine("Minutes At Attenuation Weights:") + .appendLine(minutesAtAttenuationWeights) + .appendLine() + .appendLine("Transmission RiskLevel Encoding:") + .appendLine(transmissionRiskLevelEncoding) + .appendLine() + .appendLine("Transmission RiskLevel Filters:") + .appendLine(transmissionRiskLevelFilters) + .appendLine() + .appendLine("Normalized Time Per Exposure Window To RiskLevel Mapping:") + .appendLine(normalizedTimePerExposureWindowToRiskLevelMapping) + .appendLine() + .appendLine("Normalized Time Per Day To RiskLevel Mapping List:") + .appendLine(normalizedTimePerDayToRiskLevelMappingList) + .toString() + + val additionalRiskCalcInfo = combine( + RiskLevelRepository.riskLevelScore, + RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated, + exposureResultStore.matchedKeyCount, + exposureResultStore.daysSinceLastExposure, + LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() + ) { riskLevelScore, + riskLevelScoreLastSuccessfulCalculated, + matchedKeyCount, + daysSinceLastExposure, + lastTimeDiagnosisKeysFromServerFetch -> + createAdditionalRiskCalcInfo( + riskLevelScore = riskLevelScore, + riskLevelScoreLastSuccessfulCalculated = riskLevelScoreLastSuccessfulCalculated, + matchedKeyCount = matchedKeyCount, + daysSinceLastExposure = daysSinceLastExposure, + lastTimeDiagnosisKeysFromServerFetch = lastTimeDiagnosisKeysFromServerFetch + ) + }.asLiveData() + + private suspend fun createAdditionalRiskCalcInfo( + riskLevelScore: Int, + riskLevelScoreLastSuccessfulCalculated: Int, + matchedKeyCount: Int, + daysSinceLastExposure: Int, + lastTimeDiagnosisKeysFromServerFetch: Date? + ): String = StringBuilder() + .appendLine("Risk Level: ${RiskLevel.forValue(riskLevelScore)}") + .appendLine("Last successful Risk Level: ${RiskLevel.forValue(riskLevelScoreLastSuccessfulCalculated)}") + .appendLine("Matched key count: $matchedKeyCount") + .appendLine("Days since last Exposure: $daysSinceLastExposure days") + .appendLine("Last Time Server Fetch: ${lastTimeDiagnosisKeysFromServerFetch?.time?.let { Instant.ofEpochMilli(it) }}") + .appendLine("Tracing Duration: ${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days") + .appendLine("Tracing Duration in last 14 days: ${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days") + .appendLine( + "Last time risk level calculation ${ + LocalData.lastTimeRiskLevelCalculation()?.let { Instant.ofEpochMilli(it) } + }" + ) + .toString() fun retrieveDiagnosisKeys() { + Timber.d("Starting download diagnosis keys task") launch { taskController.submitBlocking( - DefaultTaskRequest(DownloadDiagnosisKeysTask::class, DownloadDiagnosisKeysTask.Arguments()) + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments(), + originTag = "TestRiskLevelCalculationFragmentCWAViewModel.retrieveDiagnosisKeys()" + ) ) - calculateRiskLevel() } } fun calculateRiskLevel() { - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) + Timber.d("Starting calculate risk task") + taskController.submit( + DefaultTaskRequest( + RiskLevelTask::class, + originTag = "TestRiskLevelCalculationFragmentCWAViewModel.calculateRiskLevel()" + ) + ) } fun resetRiskLevel() { - viewModelScope.launch { + Timber.d("Resetting risk level") + launch { withContext(Dispatchers.IO) { try { // Preference reset @@ -100,10 +201,11 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( // Export File Reset keyCacheRepository.clear() + exposureResultStore.entities.value = ExposureResult(emptyList(), null) + LocalData.lastCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) LocalData.lastSuccessfullyCalculatedRiskLevel(RiskLevel.UNDETERMINED.raw) LocalData.lastTimeDiagnosisKeysFromServerFetch(null) - LocalData.googleApiToken(null) } catch (e: Exception) { e.report(ExceptionCategory.INTERNAL) } @@ -113,185 +215,9 @@ class TestRiskLevelCalculationFragmentCWAViewModel @AssistedInject constructor( } } - data class RiskScoreState( - val riskScoreMsg: String = "", - val backendParameters: String = "", - val exposureSummary: String = "", - val formula: String = "", - val exposureInfo: String = "" - ) - - fun startENFObserver() { - viewModelScope.launch { - try { - var workState = riskScoreState.value!! - - val googleToken = LocalData.googleApiToken() ?: UUID.randomUUID().toString() - val exposureSummary = - InternalExposureNotificationClient.asyncGetExposureSummary(googleToken) - - val expDetectConfig: RiskCalculationConfig = - AppInjector.component.appConfigProvider.getAppConfig() - - val riskLevelScore = riskLevels.calculateRiskScore( - expDetectConfig.attenuationDuration, - exposureSummary - ) - - val riskAsString = "Level: ${RiskLevelRepository.getLastCalculatedScore()}\n" + - "Last successful Level: " + - "${LocalData.lastSuccessfullyCalculatedRiskLevel()}\n" + - "Calculated Score: ${riskLevelScore}\n" + - "Last Time Server Fetch: ${LocalData.lastTimeDiagnosisKeysFromServerFetch()}\n" + - "Tracing Duration: " + - "${TimeUnit.MILLISECONDS.toDays(TimeVariables.getTimeActiveTracingDuration())} days \n" + - "Tracing Duration in last 14 days: " + - "${TimeVariables.getActiveTracingDaysInRetentionPeriod()} days \n" + - "Last time risk level calculation ${LocalData.lastTimeRiskLevelCalculation()}" - - workState = workState.copy(riskScoreMsg = riskAsString) - - val lowClass = - expDetectConfig.riskScoreClasses.riskClassesList?.find { low -> low.label == "LOW" } - val highClass = - expDetectConfig.riskScoreClasses.riskClassesList?.find { high -> high.label == "HIGH" } - - val configAsString = - "Attenuation Weight Low: ${expDetectConfig.attenuationDuration.weights?.low}\n" + - "Attenuation Weight Mid: ${expDetectConfig.attenuationDuration.weights?.mid}\n" + - "Attenuation Weight High: ${expDetectConfig.attenuationDuration.weights?.high}\n\n" + - "Attenuation Offset: ${expDetectConfig.attenuationDuration.defaultBucketOffset}\n" + - "Attenuation Normalization: " + - "${expDetectConfig.attenuationDuration.riskScoreNormalizationDivisor}\n\n" + - "Risk Score Low Class: ${lowClass?.min ?: 0} - ${lowClass?.max ?: 0}\n" + - "Risk Score High Class: ${highClass?.min ?: 0} - ${highClass?.max ?: 0}" - - workState = workState.copy(backendParameters = configAsString) - - val summaryAsString = - "Days Since Last Exposure: ${exposureSummary.daysSinceLastExposure}\n" + - "Matched Key Count: ${exposureSummary.matchedKeyCount}\n" + - "Maximum Risk Score: ${exposureSummary.maximumRiskScore}\n" + - "Attenuation Durations: [${ - exposureSummary.attenuationDurationsInMinutes?.get( - 0 - ) - }," + - "${exposureSummary.attenuationDurationsInMinutes?.get(1)}," + - "${exposureSummary.attenuationDurationsInMinutes?.get(2)}]\n" + - "Summation Risk Score: ${exposureSummary.summationRiskScore}" - - workState = workState.copy(exposureSummary = summaryAsString) - - val maxRisk = exposureSummary.maximumRiskScore - val atWeights = expDetectConfig.attenuationDuration.weights - val attenuationDurationInMin = - exposureSummary.attenuationDurationsInMinutes - val attenuationConfig = expDetectConfig.attenuationDuration - val formulaString = - "($maxRisk / ${attenuationConfig.riskScoreNormalizationDivisor}) * " + - "(${attenuationDurationInMin?.get(0)} * ${atWeights?.low} " + - "+ ${attenuationDurationInMin?.get(1)} * ${atWeights?.mid} " + - "+ ${attenuationDurationInMin?.get(2)} * ${atWeights?.high} " + - "+ ${attenuationConfig.defaultBucketOffset})" - - workState = workState.copy(formula = formulaString) - - val token = LocalData.googleApiToken() - if (token != null) { - val exposureInformation = asyncGetExposureInformation(token) - - var infoString = "" - exposureInformation.forEach { - infoString += "Attenuation duration in min.: " + - "[${it.attenuationDurationsInMinutes?.get(0)}, " + - "${it.attenuationDurationsInMinutes?.get(1)}," + - "${it.attenuationDurationsInMinutes?.get(2)}]\n" + - "Attenuation value: ${it.attenuationValue}\n" + - "Duration in min.: ${it.durationMinutes}\n" + - "Risk Score: ${it.totalRiskScore}\n" + - "Transmission Risk Level: ${it.transmissionRiskLevel}\n" + - "Date Millis Since Epoch: ${it.dateMillisSinceEpoch}\n\n" - } - - workState = workState.copy(exposureInfo = infoString) - } - - riskScoreState.postValue(workState) - } catch (e: Exception) { - e.report(ExceptionCategory.EXPOSURENOTIFICATION) - } - } - } - - private suspend fun asyncGetExposureInformation(token: String): List<ExposureInformation> = - suspendCoroutine { cont -> - enfClient.internalClient.getExposureInformation(token) - .addOnSuccessListener { - cont.resume(it) - }.addOnFailureListener { - cont.resumeWithException(it) - } - } - - data class DiagnosisKeyProvidedEvent( - val keyCount: Int, - val token: String - ) - - fun provideDiagnosisKey(transmissionNumber: Int, key: AppleLegacyKeyExchange.Key) { - val token = UUID.randomUUID().toString() - LocalData.googleApiToken(token) - - val appleKeyList = mutableListOf<AppleLegacyKeyExchange.Key>() - - AppleLegacyKeyExchange.Key.newBuilder() - .setKeyData(key.keyData) - .setRollingPeriod(144) - .setRollingStartNumber(key.rollingStartNumber) - .setTransmissionRiskLevel(transmissionNumber) - .build() - .also { appleKeyList.add(it) } - - val appleFiles = listOf( - AppleLegacyKeyExchange.File.newBuilder() - .addAllKeys(appleKeyList) - .build() - ) - - val dir = File(File(context.getExternalFilesDir(null), "key-export"), token) - dir.mkdirs() - - var googleFileList: List<File> - viewModelScope.launch { - googleFileList = KeyFileHelper.asyncCreateExportFiles(appleFiles, dir) - - Timber.i("Provide ${googleFileList.count()} files with ${appleKeyList.size} keys with token $token") - try { - // only testing implementation: this is used to wait for the broadcastreceiver of the OS / EN API - enfClient.provideDiagnosisKeys( - googleFileList, - AppInjector.component.appConfigProvider.getAppConfig().exposureDetectionConfiguration, - token - ) - apiKeysProvidedEvent.postValue( - DiagnosisKeyProvidedEvent( - keyCount = appleFiles.size, - token = token - ) - ) - } catch (e: Exception) { - e.report(ExceptionCategory.EXPOSURENOTIFICATION) - } - } - } - - fun scanLocalQRCodeAndProvide() { - startLocalQRCodeScanEvent.postValue(Unit) - } - fun clearKeyCache() { - viewModelScope.launch { keyCacheRepository.clear() } + Timber.d("Clearing key cache") + launch { keyCacheRepository.clear() } } @AssistedInject.Factory diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..e758be589d545f339e9700bdabdea8d6934a3749 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragment.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.test.submission.ui + +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestSubmissionBinding +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import javax.inject.Inject + +@SuppressLint("SetTextI18n") +class SubmissionTestFragment : Fragment(R.layout.fragment_test_submission), AutoInject { + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: SubmissionTestFragmentViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentTestSubmissionBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + vm.currentTestId.observe2(this) { + binding.registrationTokenCurrent.text = "Current: '$it'" + } + + binding.apply { + deleteTokenAction.setOnClickListener { vm.deleteRegistrationToken() } + scrambleTokenAction.setOnClickListener { vm.scrambleRegistrationToken() } + } + } + + companion object { + val TAG: String = SubmissionTestFragment::class.simpleName!! + val MENU_ITEM = TestMenuItem( + title = "Submission Test Options", + description = "Submission related test options..", + targetId = R.id.test_submission_fragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..f123767b6fb40c678849cb0723ff722422cb95bb --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.test.submission.ui + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class SubmissionTestFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(SubmissionTestFragmentViewModel::class) + abstract fun testKeyDownloadFragment( + factory: SubmissionTestFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..c5d678dedff0cc54c1c97663f696f15303b27288 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.test.submission.ui + +import androidx.lifecycle.asLiveData +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.MutableStateFlow +import java.util.UUID + +class SubmissionTestFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + private val internalToken = MutableStateFlow(LocalData.registrationToken()) + val currentTestId = internalToken.asLiveData() + + fun scrambleRegistrationToken() { + LocalData.registrationToken(UUID.randomUUID().toString()) + internalToken.value = LocalData.registrationToken() + } + + fun deleteRegistrationToken() { + LocalData.registrationToken(null) + internalToken.value = LocalData.registrationToken() + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<SubmissionTestFragmentViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt index 41cca0d6071aab1aca83653e9283893028312c39..e406155bac41e9b12c5287b985790baad7617a8f 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/ui/main/MainActivityTestModule.kt @@ -14,6 +14,8 @@ import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment import de.rki.coronawarnapp.test.menu.ui.TestMenuFragmentModule import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragmentModule +import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment +import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragmentModule import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragmentModule @@ -40,4 +42,7 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [KeyDownloadTestFragmentModule::class]) abstract fun keyDownload(): KeyDownloadTestFragment + + @ContributesAndroidInjector(modules = [SubmissionTestFragmentModule::class]) + abstract fun submissionTest(): SubmissionTestFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml index 867b86569315d89fa9c080b0944a56deee7970cb..bd1eeb247b6ff2ef4c04476c71df6cdda8331671 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_debugoptions.xml @@ -32,18 +32,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - <Switch - android:id="@+id/background_notifications_toggle" - style="@style/body1" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - android:text="@string/test_api_switch_background_notifications" - android:theme="@style/switchBase" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/debug_container_title" /> - <Switch android:id="@+id/test_logfile_toggle" style="@style/body1" @@ -55,7 +43,7 @@ android:theme="@style/switchBase" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/background_notifications_toggle" /> + app:layout_constraintTop_toBottomOf="@+id/debug_container_title" /> <Button android:id="@+id/test_logfile_share" diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml index a4eb49227c15f443d2dde6b13eb76d3affca190c..30b10c80b5acd13b4e1e7963521ba2fe44500099 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_for_a_p_i.xml @@ -61,36 +61,6 @@ android:layout_marginBottom="@dimen/spacing_tiny" android:text="@string/test_api_exposure_summary_headline" /> - <TextView - android:id="@+id/label_exposure_summary_matchedKeyCount" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/test_api_body_matchedKeyCount" /> - - <TextView - android:id="@+id/label_exposure_summary_daysSinceLastExposure" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/test_api_body_daysSinceLastExposure" /> - - <TextView - android:id="@+id/label_exposure_summary_maximumRiskScore" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/test_api_body_maximumRiskScore" /> - - <TextView - android:id="@+id/label_exposure_summary_summationRiskScore" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/test_api_body_summation_risk" /> - - <TextView - android:id="@+id/label_exposure_summary_attenuation" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="@string/test_api_body_attenuation" /> - <Button android:id="@+id/button_api_scan_qr_code" style="@style/buttonPrimary" @@ -106,14 +76,6 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_tiny" android:text="@string/test_api_button_enter_other_keys" /> - - <Button - android:id="@+id/button_api_get_check_exposure" - style="@style/buttonPrimary" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_tiny" - android:text="@string/test_api_button_check_exposure" /> </LinearLayout> <LinearLayout 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 9e29d88d479a233ef358cbe19976dfd67bd4798f..7d94e7c3dca3a8e044acb4aac523109b7266aa7c 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 @@ -11,8 +11,8 @@ <import type="de.rki.coronawarnapp.util.formatter.FormatterSubmissionHelper" /> <variable - name="submissionViewModel" - type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" /> + name="showRiskStatusCard" + type="Boolean" /> <variable name="settingsViewModel" @@ -32,7 +32,6 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="@dimen/spacing_normal" android:orientation="vertical"> <TextView @@ -48,7 +47,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:visibility="@{FormatterSubmissionHelper.formatShowRiskStatusCard(submissionViewModel.deviceUiState)}" + gone="@{showRiskStatusCard == null || !showRiskStatusCard}" android:focusable="true" android:backgroundTint="@{tracingCard.getRiskInfoContainerBackgroundTint(context)}" android:backgroundTintMode="src_over"> @@ -60,34 +59,6 @@ </FrameLayout> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:orientation="horizontal"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="Transmission Risk Level for scan: " /> - - <EditText - android:id="@+id/transmission_number" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:ems="10" - android:inputType="number" - android:text="5" /> - </LinearLayout> - - <Button - android:id="@+id/button_provide_key_via_qr" - style="@style/buttonPrimary" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginBottom="@dimen/spacing_normal" - android:text="Scan Local QR Code" /> - <Button android:id="@+id/button_retrieve_diagnosis_keys" style="@style/buttonPrimary" @@ -121,30 +92,30 @@ android:text="Clear Diagnosis-Key cache" /> <TextView - android:id="@+id/label_exposure_summary_title" + 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:text="Exposure Summary" /> + android:text="Aggregated Risk Result" /> <TextView - android:id="@+id/label_exposure_summary" + android:id="@+id/label_aggregated_risk_result" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="-" /> <TextView - android:id="@+id/label_risk_score_title" + 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:text="Risk Score" /> + android:text="Risk Calculation Additional Information" /> <TextView - android:id="@+id/label_risk_score" + android:id="@+id/label_risk_additional_info" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="-" /> @@ -164,31 +135,24 @@ android:text="-" /> <TextView - android:id="@+id/label_formula_title" + android:id="@+id/label_exposure_window_title" style="@style/headline6" android:accessibilityHeading="true" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Used Formula" /> + android:text="Exposure Windows" /> <TextView - android:id="@+id/label_formula" + android:id="@+id/label_exposure_window_count" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="-" /> <TextView - android:id="@+id/label_exposure_info_title" - style="@style/headline6" - android:accessibilityHeading="true" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:text="Exposure Information" /> - - <TextView - android:id="@+id/label_exposure_info" - android:layout_width="match_parent" + android:id="@+id/label_exposure_windows" + android:layout_width="wrap_content" android:layout_height="wrap_content" + android:paddingTop="5dp" android:text="-" /> </LinearLayout> diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml new file mode 100644 index 0000000000000000000000000000000000000000..ac69680e903cf5df93ecff2f503a2a65dcee8fe5 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="utf-8"?> + +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="HardcodedText"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="8dp" + android:orientation="vertical" + android:paddingBottom="32dp"> + + <androidx.constraintlayout.widget.ConstraintLayout + style="@style/card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:orientation="vertical"> + <TextView + android:id="@+id/registration_token_title" + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Submission registration token " + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + <TextView + android:id="@+id/registration_token_current" + style="@style/body2" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/registration_token_title" + tools:text="Current test ID: 1234567890" /> + <Button + android:id="@+id/delete_token_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_weight="1" + android:text="Delete token" + app:layout_constraintEnd_toStartOf="@+id/scramble_token_action" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/registration_token_current" /> + <Button + android:id="@+id/scramble_token_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_weight="1" + android:text="Random token" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/delete_token_action" + app:layout_constraintTop_toBottomOf="@+id/registration_token_current" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </LinearLayout> +</androidx.core.widget.NestedScrollView> \ No newline at end of file diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml index 226a618a77e11982b3a1ead5d9fbf879429c7188..14d89e5a3de75e9d667feafc4a67a991d2e0759d 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml @@ -31,6 +31,9 @@ <action android:id="@+id/action_test_menu_fragment_to_keyDownloadTestFragment" app:destination="@id/test_keydownload_fragment" /> + <action + android:id="@+id/action_test_menu_fragment_to_submissionTestFragment" + app:destination="@id/test_submission_fragment" /> </fragment> <fragment @@ -86,5 +89,10 @@ android:name="de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment" android:label="KeyDownloadTestFragment" tools:layout="@layout/fragment_test_keydownload" /> + <fragment + android:id="@+id/test_submission_fragment" + android:name="de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment" + android:label="SubmissionTestFragment" + tools:layout="@layout/fragment_test_submission" /> </navigation> diff --git a/Corona-Warn-App/src/main/assets/privacy_de.html b/Corona-Warn-App/src/main/assets/privacy_de.html index dd168314234a071eee96b203b3a0d4c60167b306..eb3a024ed1a780760397c4a5df80ed208e0e8b42 100644 --- a/Corona-Warn-App/src/main/assets/privacy_de.html +++ b/Corona-Warn-App/src/main/assets/privacy_de.html @@ -80,7 +80,7 @@ <p> Die Nutzung der App ist freiwillig. Es ist allein Ihre Entscheidung, ob Sie die App installieren, welche App-Funktionen Sie nutzen und ob Sie Daten mit - anderen teilen. Alle App-Funktionen, die eine Datenweitergabe erfordern, + anderen teilen. Alle App-Funktionen, die eine Datenweitergabe Ihrer Begegnungs- und Gesundheitsdaten erfordern, holen vorher Ihre ausdrückliche Einwilligung ein. Falls Sie eine Einwilligung nicht erteilen oder nachträglich widerrufen, entstehen Ihnen keine Nachteile. @@ -89,11 +89,16 @@ 3. Auf welcher Rechtsgrundlage werden Ihre Daten verarbeitet? </h1> <p> - Ihre Daten werden grundsätzlich nur auf Grundlage einer von Ihnen erteilten + Ihre Daten werden grundsätzlich auf Grundlage einer von Ihnen erteilten ausdrücklichen Einwilligung verarbeitet. Die Rechtsgrundlage ist Art. 6 Abs. 1 S. 1 lit. a DSGVO sowie im Falle von Gesundheitsdaten Art. 9 Abs. 2 lit. a DSGVO. Sie können eine erteilte Einwilligung jederzeit widerrufen. - Weitere Informationen zu Ihrem Widerrufsrecht finden Sie unter Punkt 12. + Weitere Informationen zu Ihrem Widerrufsrecht finden Sie unter Punkt 12. + Die Verarbeitung von Zugriffsdaten für den Abruf der täglichen Statistiken + (siehe hierzu Punkt 6 d.) erfolgt im Rahmen der Information der Öffentlichkeit + durch das RKI gem. § 4 Abs. 4 BGA-NachfG auf Basis von Art. 6 Abs. 1 S. 1 lit e. + DSGVO i.V.m § 3 BDSG. + </p> <h1> 4. An wen richtet sich die App? @@ -270,7 +275,7 @@ Gesundheitshinweise zu geben. </p> <p> - Hierzu ruft die App im Hintergrundbetrieb vom Serversystem täglich eine + Hierzu ruft die App im Hintergrundbetrieb vom Serversystem mehrmals täglich eine aktuelle Liste mit den Zufalls-IDs und eventuellen Angaben zum Symptombeginn von Nutzern ab, die Corona-positiv getestet wurden und freiwillig über die offizielle Corona-App eines am länderübergreifenden @@ -454,14 +459,11 @@ d. Informatorische Nutzung der App </h2> <p> - Soweit Sie die App nur informatorisch nutzen, also keine der oben genannten - Funktionen verwenden, findet die Verarbeitung ausschließlich lokal auf - Ihrem Smartphone statt und es werden keine personenbezogenen Daten durch - das RKI verarbeitet. In der App verlinkte Webseiten, z. B.: www.bundesregierung.de, werden im - Standard-Browser - (Android-Smartphones) oder in der App (iPhones) geöffnet und angezeigt. - Welche Daten dabei verarbeitet werden, wird von den jeweiligen Anbietern - der aufgerufenen Webseite festgelegt. + Die täglichen Statistiken, die in der App erscheinen, erhält die App + automatisch über das Serversystem. Dabei fallen Zugriffsdaten an. + In der App verlinkte Webseiten, z. B.: www.bundesregierung.de, werden im + Standard-Browser (Android-Smartphones) oder in der App (iPhones) geöffnet und angezeigt. + Welche Daten dabei verarbeitet werden, wird von den jeweiligen Anbietern der aufgerufenen Webseite festgelegt. </p> <h1> 7. Wie funktioniert das länderübergreifende Warnsystem? @@ -790,5 +792,5 @@ datenschutz@rki.de. </p> <p> - Stand: 15.10.2020 + Stand: 15.11.2020 </p> diff --git a/Corona-Warn-App/src/main/assets/privacy_en.html b/Corona-Warn-App/src/main/assets/privacy_en.html index ffce0a25d17049236cf4e60689b8a29d14b1e00d..411f8c5accb2a0ed0ad356308fcdea5d8ac0d5ec 100644 --- a/Corona-Warn-App/src/main/assets/privacy_en.html +++ b/Corona-Warn-App/src/main/assets/privacy_en.html @@ -1,9 +1,7 @@ <p> Privacy notice </p> -<p> - Last amended: 17 October 2020. -</p> + <p> This privacy notice explains how your data is processed and what data protection rights you have when using the German Federal Government’s @@ -95,7 +93,11 @@ legal basis is Art. 6(1) Sentence 1(a) GDPR and, in the case of health data, Art. 9(2)(a) GDPR. After granting your consent, you can withdraw it at any time. Please refer to Section 12 for further information about your - right of withdrawal. + right of withdrawal. On the basis of Art. 6(1) Sentence 1(e) GDPR in conjunction + with Sect. 3 of the German Federal Data Protection Act (BDSG), the processing of + access data for the retrieval of daily statistics (see Section 6 d.) is performed + as part of the RKI’s duty to inform the public pursuant to Sect. 4(4) of the Act + on Successor Agencies to the Federal Health Agency (BGA-NachfG). </p> <h1> 4. Who is the app aimed at? @@ -161,7 +163,7 @@ </h2> <p> As soon as you enable your iPhone’s or your Android smartphone’s COVID-19 - Exposure Notification System (which is called “Exposure Notification†or + Exposure Notification System (which is called “Exposure Notifications†or “COVID-19 Exposure Notifications†respectively), your smartphone transmits so-called exposure data via Bluetooth, which other smartphones in your vicinity can record. Your smartphone, in turn, also receives the exposure @@ -268,16 +270,14 @@ recommendations for what to do next. </p> <p> - For this purpose, the app runs in the background and retrieves an - up-to-date list every day of random IDs, and possible information about the - onset of symptoms, from the server system. This list contains the random - IDs and possible symptom information of users who have tested positive for - coronavirus and voluntarily used the warning feature in their app, which is - the official coronavirus app in any country participating in the - transnational warning system (see Section 7) (hereinafter referred to as a <strong>positive - list</strong>). The random IDs in the positive list also - contain a transmission risk value and an indication of the type of - diagnosis (see Section 6 c.). + For this purpose, the app retrieves an up-to-date list from the server system + several times a day. This list contains the random IDs, along with any voluntary + symptom information, of users who have tested positive for coronavirus and used + the warning feature in their app, which is the official coronavirus app in any + country participating in the transnational warning system (see Section 7) + (hereinafter referred to as a <strong>positive list</strong>). The random IDs in + the positive list also contain a transmission risk value and an indication of the type + of diagnosis (see Section 6 c.). </p> <p> The app passes the random IDs to the COVID-19 Exposure Notification System, @@ -443,13 +443,11 @@ d. Using the app for information purposes only </h2> <p> - As long as you use the app for information purposes only, i.e. do not use - any of the features mentioned above, then processing only takes place - locally on your smartphone and the RKI will not process any personal data. - Websites linked in the app, such as www.bundesregierung.de, are - opened and displayed in your smartphone’s standard browser (Android - smartphones) or within the app (iPhones). The data processed here is - determined by the respective providers of the websites accessed. + The app automatically receives the daily statistics that appear in the app + via the server system. This generates access data. Websites linked in the app, + such as www.bundesregierung.de, are opened and displayed in your smartphone’s + standard browser (Android smartphones) or within the app (iPhones). Which data + is processed in this context depends on the respective providers of the websites accessed. </p> <h1>7. How does the transnational warning system work? @@ -767,5 +765,5 @@ 13353 Berlin, or by emailing datenschutz@rki.de. </p> <p> - *** + Last amended: 15 November 2020 </p> diff --git a/Corona-Warn-App/src/main/assets/privacy_tr.html b/Corona-Warn-App/src/main/assets/privacy_tr.html index 55243e394d5031feef9b974a735dce3ed2b231d2..cebbc84a12a3faf296ba8a5961597b2705c86aa7 100644 --- a/Corona-Warn-App/src/main/assets/privacy_tr.html +++ b/Corona-Warn-App/src/main/assets/privacy_tr.html @@ -79,22 +79,25 @@ <p> Uygulamanın kullanımı isteÄŸe baÄŸlıdır; Uygulamayı yüklemeniz, Uygulamanın hangi iÅŸlevlerini kullanmanız ve verileri diÄŸer kiÅŸilerle paylaÅŸmanız - noktasında yalnızca siz karar verirsiniz. Uygulamanın veri aktarımını - gerektiren tüm iÅŸlevleri, öncesinde sizin açık bir ÅŸekilde onay vermenizi - ister. Onay vermezseniz veya sonradan bu onayı geri alırsanız, bu durum - sizin için bir sakınca doÄŸurmaz. + noktasında yalnızca siz karar verirsiniz. Maruz kalma veya saÄŸlık verilerinizin + aktarılmasını gerektiren Uygulamanın tüm iÅŸlevleri, sizden önceden açıkça rızanızı + vermenizi gerektirir. Rızanızı vermezseniz veya sonradan bu rızayı geri alırsanız, + bu durum sizin için bir sakınca doÄŸurmaz. </p> <h1> 3. Verileriniz iÅŸlenmesinde hangi yasal dayanaklar söz konusudur? </h1> <p> - Verileriniz esas itibariyle yalnızca açık bir ÅŸekilde verdiÄŸiniz onay + Verileriniz esas itibariyle açık bir ÅŸekilde verdiÄŸiniz onay temelinde iÅŸlenir. Bu baÄŸlamdaki yasal dayanak, GVKT (Genel Veri Koruma Tüzüğü) madde 6, fıkra 1, cümle 1, bent a ve saÄŸlık verileri durumundaki yasal dayanak ise GVKT madde 9, fıkra 2, bent a’dır. VerdiÄŸiniz onayı, istediÄŸiniz zaman geri alabilirsiniz. Onayınız geri alma hakkı ile ilgili - ayrıntılı bilgileri madde 12’de bulabilirsiniz. + ayrıntılı bilgileri madde 12’de bulabilirsiniz. Günlük istatistiklerin alınması + için eriÅŸim verilerinin iÅŸlenmesi (bkz. Madde 6 d.), GVKT madde 6, fıkra 1, cümle 1, bent + e ile baÄŸlantılı olarak BGA-NachfG (Federal SaÄŸlık Kurumu Halef KuruluÅŸları + hakkında Kanun) madde 4, fıkra 4 uyarınca RKI tarafından toplumun bilgilendirilmesi kapsamında gerçekleÅŸir. </p> <h1> 4. Uygulama kimleri hedefler? @@ -271,14 +274,13 @@ bilgileri temin etmektir. </p> <p> - Bu amaç için Uygulama, arka planda çalışarak sunucu sisteminden, Korona - testi pozitif çıkan ve sınır ötesi uyarı sistemine katılan ülkelerin resmi - Korona uygulamaları aracılığıyla gönüllü olarak bir uyarı tetikleyen - kullanıcılardan rastgele kimlik numaraları ve varsa semptomların - baÅŸlangıcına iliÅŸkin bilgiyi içeren günlük bir liste çağırır (bundan böyle: <strong>pozitif - liste</strong>). Pozitif listedeki rastgele kimlik - numaraları, ek olarak ayrıca bir taşıma riski deÄŸeri ve tanı tipi hakkında - bilgi de içerir (bkz. Madde 6 c.). + Bu amaç doÄŸrultusunda Uygulama, arka planda çalışarak sunucu sisteminden, + Korona testi pozitif çıkan ve sınır ötesi uyarı sistemine katılan ülkelerin + resmi Korona uygulamaları aracılığıyla bir uyarı tetikleyen kullanıcılardan + rastgele kimlik numaraları ve varsa semptomların baÅŸlangıcına iliÅŸkin bilgiyi + içeren listeleri günde birçok kez çağırır (bundan böyle: <strong>pozitif liste</strong>). + Pozitif listedeki rastgele kimlik numaraları, ek olarak ayrıca bir taşıma riski + deÄŸeri ve tanı tipi hakkında bilgi de içerir (bkz. Madde 6 c.). </p> <p> Uygulama bu rastgele kimlik numaralarını, COVID-19 bildirim sistemine @@ -446,13 +448,11 @@ d. Uygulamanın bilgilenme amaçlı kullanımı </h2> <p> - Uygulamayı yalnızca bilgi edinme amaçlı kullanıyorsanız, yani yukarıda - belirtilen iÅŸlevlerden hiçbirini kullanmıyorsanız, veri iÅŸleme yalnızca - kendi akıllı telefonunuzda gerçekleÅŸir ve RKI tarafından hiçbir kiÅŸisel - veri iÅŸlenmez. Uygulamada, örneÄŸin www.bundesregierung.de gibi - baÄŸlantılı web siteleri açılır ve standart tarayıcıda (Android akıllı - telefonlar) veya Uygulamada (iPhone’lar) görüntülenir. Hangi verilerin - iÅŸleneceÄŸi, eriÅŸilen web sitesinin ilgili saÄŸlayıcısı tarafından + Uygulama otomatik olarak sunucu sistemi üzerinden günlük istatistikleri alır + ve bunlar Uygulamada görüntülenir. Bu sırada eriÅŸim verileri oluÅŸur. Uygulamada, + örneÄŸin www.bundesregierung.de gibi baÄŸlantılı web siteleri açılır ve standart + tarayıcıda (Android akıllı telefonlar) veya Uygulamada (iPhone’lar) görüntülenir. + Hangi verilerin iÅŸleneceÄŸi, eriÅŸilen web sitesinin ilgili saÄŸlayıcısı tarafından belirlenmektedir. </p> <h1> @@ -772,5 +772,4 @@ veya e-posta yoluyla: datenschutz@rki.de. </p> <p> - Yayım tarihi: 17.10.2020 -</p> + Baskı: 15.11.2020 diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt index ebe7c473fc3d6037becb4bcab9981d8fe5fccf6f..941a2b4e1890e8c0f69d4804f7c9f454313a7242 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/CoronaWarnApplication.kt @@ -11,6 +11,7 @@ import androidx.work.WorkManager import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import de.rki.coronawarnapp.appconfig.ConfigChangeDetector import de.rki.coronawarnapp.bugreporting.loghistory.LogHistoryTree import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.exception.reporting.ErrorReportReceiver @@ -23,7 +24,6 @@ import de.rki.coronawarnapp.util.ForegroundState import de.rki.coronawarnapp.util.WatchdogService import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent -import de.rki.coronawarnapp.worker.BackgroundWorkHelper import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -44,6 +44,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var taskController: TaskController @Inject lateinit var foregroundState: ForegroundState @Inject lateinit var workManager: WorkManager + @Inject lateinit var configChangeDetector: ConfigChangeDetector @Inject lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler @LogHistoryTree @Inject lateinit var rollingLogHistory: Timber.Tree @@ -66,10 +67,6 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { registerActivityLifecycleCallbacks(activityLifecycleCallback) - // notification to test the WakeUpService from Google when the app was force stopped - BackgroundWorkHelper.sendDebugNotification( - "Application onCreate", "App was woken up" - ) watchdogService.launch() foregroundState.isInForeground @@ -79,6 +76,8 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { if (LocalData.onboardingCompletedTimestamp() != null) { deadmanNotificationScheduler.schedulePeriodic() } + + configChangeDetector.launch() } private val activityLifecycleCallback = object : ActivityLifecycleCallbacks { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt index 19ae812f371b271dcd77c11f8037b61d8d393f09..26283253c14f7814cd03a08ec043b7b62cbb9ea9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigModule.kt @@ -3,12 +3,12 @@ package de.rki.coronawarnapp.appconfig import android.content.Context import dagger.Module import dagger.Provides -import de.rki.coronawarnapp.appconfig.download.AppConfigApiV1 -import de.rki.coronawarnapp.appconfig.download.AppConfigHttpCache +import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2 import de.rki.coronawarnapp.appconfig.mapping.CWAConfigMapper import de.rki.coronawarnapp.appconfig.mapping.ExposureDetectionConfigMapper +import de.rki.coronawarnapp.appconfig.mapping.ExposureWindowRiskCalculationConfigMapper import de.rki.coronawarnapp.appconfig.mapping.KeyDownloadParametersMapper -import de.rki.coronawarnapp.appconfig.mapping.RiskCalculationConfigMapper +import de.rki.coronawarnapp.appconfig.sources.remote.AppConfigHttpCache import de.rki.coronawarnapp.environment.download.DownloadCDNHttpClient import de.rki.coronawarnapp.environment.download.DownloadCDNServerUrl import de.rki.coronawarnapp.util.di.AppContext @@ -42,7 +42,7 @@ class AppConfigModule { @DownloadCDNServerUrl url: String, gsonConverterFactory: GsonConverterFactory, @AppConfigHttpCache cache: Cache - ): AppConfigApiV1 { + ): AppConfigApiV2 { val cachingClient = client.newBuilder().apply { cache(cache) @@ -57,21 +57,23 @@ class AppConfigModule { .baseUrl(url) .addConverterFactory(gsonConverterFactory) .build() - .create(AppConfigApiV1::class.java) + .create(AppConfigApiV2::class.java) } @Provides - fun cwaMapper(mapper: CWAConfigMapper): CWAConfig.Mapper = mapper + fun cwaMapper(mapper: CWAConfigMapper): + CWAConfig.Mapper = mapper @Provides fun downloadMapper(mapper: KeyDownloadParametersMapper): KeyDownloadConfig.Mapper = mapper @Provides - fun exposurMapper(mapper: ExposureDetectionConfigMapper): ExposureDetectionConfig.Mapper = - mapper + fun exposureMapper(mapper: ExposureDetectionConfigMapper): + ExposureDetectionConfig.Mapper = mapper @Provides - fun riskMapper(mapper: RiskCalculationConfigMapper): RiskCalculationConfig.Mapper = mapper + fun windowRiskMapper(mapper: ExposureWindowRiskCalculationConfigMapper): + ExposureWindowRiskCalculationConfig.Mapper = mapper companion object { private val HTTP_TIMEOUT_APPCONFIG = Duration.standardSeconds(10) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt index 71906e83169d8e0ef4a68e34aa1208ec958defc5..ab47bc6b2443d8173084797a0fcf293d9d17ced4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigProvider.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.appconfig +import de.rki.coronawarnapp.appconfig.internal.AppConfigSource import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.HotDataFlow @@ -13,7 +14,7 @@ import javax.inject.Singleton @Singleton class AppConfigProvider @Inject constructor( - private val source: AppConfigSource, + private val appConfigSource: AppConfigSource, private val dispatcherProvider: DispatcherProvider, @AppScope private val scope: CoroutineScope ) { @@ -22,9 +23,9 @@ class AppConfigProvider @Inject constructor( loggingTag = "AppConfigProvider", scope = scope, coroutineContext = dispatcherProvider.IO, - sharingBehavior = SharingStarted.WhileSubscribed(replayExpirationMillis = 0) + sharingBehavior = SharingStarted.Lazily ) { - source.retrieveConfig() + appConfigSource.getConfigData() } val currentConfig: Flow<ConfigData> = configHolder.data @@ -34,7 +35,7 @@ class AppConfigProvider @Inject constructor( // we'd still like to have that new config in any case. val deferred = scope.async(context = dispatcherProvider.IO) { configHolder.updateBlocking { - source.retrieveConfig() + appConfigSource.getConfigData() } } return deferred.await() @@ -42,7 +43,7 @@ class AppConfigProvider @Inject constructor( suspend fun clear() { Timber.tag(TAG).v("clear()") - source.clear() + appConfigSource.clear() } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigSource.kt deleted file mode 100644 index fa981d6dc18048fd2a70a74e5bc42672db1f5bff..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/AppConfigSource.kt +++ /dev/null @@ -1,94 +0,0 @@ -package de.rki.coronawarnapp.appconfig - -import de.rki.coronawarnapp.appconfig.download.AppConfigServer -import de.rki.coronawarnapp.appconfig.download.AppConfigStorage -import de.rki.coronawarnapp.appconfig.download.DefaultAppConfigSource -import de.rki.coronawarnapp.appconfig.mapping.ConfigParser -import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import kotlinx.coroutines.withContext -import org.joda.time.Duration -import org.joda.time.Instant -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AppConfigSource @Inject constructor( - private val server: AppConfigServer, - private val storage: AppConfigStorage, - private val parser: ConfigParser, - private val defaultAppConfig: DefaultAppConfigSource, - private val dispatcherProvider: DispatcherProvider -) { - - suspend fun retrieveConfig(): ConfigData = withContext(dispatcherProvider.IO) { - Timber.v("retrieveConfig()") - val (serverBytes, serverError) = try { - server.downloadAppConfig() to null - } catch (e: Exception) { - Timber.tag(TAG).w(e, "Failed to download AppConfig from server .") - null to e - } - - var parsedConfig: ConfigData? = serverBytes?.let { configDownload -> - try { - parser.parse(configDownload.rawData).let { - Timber.tag(TAG).d("Got a valid AppConfig from server, saving.") - storage.setStoredConfig(configDownload) - DefaultConfigData( - mappedConfig = it, - serverTime = configDownload.serverTime, - localOffset = configDownload.localOffset, - identifier = configDownload.etag, - configType = ConfigData.Type.FROM_SERVER - ) - } - } catch (e: Exception) { - Timber.tag(TAG).e(e, "Failed to parse AppConfig from server, trying fallback.") - null - } - } - - if (parsedConfig == null) { - parsedConfig = storage.getStoredConfig()?.let { storedDownloadConfig -> - try { - storedDownloadConfig.let { - DefaultConfigData( - mappedConfig = parser.parse(it.rawData), - serverTime = it.serverTime, - localOffset = it.localOffset, - identifier = it.etag, - configType = ConfigData.Type.LAST_RETRIEVED - ) - } - } catch (e: Exception) { - Timber.tag(TAG).e(e, "Fallback config exists but could not be parsed!") - throw e - } - } - } - - if (parsedConfig == null) { - Timber.tag(TAG).w("Current or fallback config was unavailable, using default.") - parsedConfig = DefaultConfigData( - mappedConfig = parser.parse(defaultAppConfig.getRawDefaultConfig()), - serverTime = Instant.EPOCH, - localOffset = Duration.standardHours(12), - identifier = "fallback.local", - configType = ConfigData.Type.LOCAL_DEFAULT - ) - } - - return@withContext parsedConfig - } - - suspend fun clear() { - storage.setStoredConfig(null) - - server.clearCache() - } - - companion object { - private const val TAG = "AppConfigRetriever" - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt index 0a11bf822114ca77b02a3bdd283c0a83b6dad69a..89532fb14ebdc44427de454e33ce6f4de4994de2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/CWAConfig.kt @@ -1,16 +1,17 @@ package de.rki.coronawarnapp.appconfig import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper -import de.rki.coronawarnapp.server.protocols.internal.AppFeaturesOuterClass -import de.rki.coronawarnapp.server.protocols.internal.AppVersionConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppFeaturesOuterClass interface CWAConfig { - val appVersion: AppVersionConfig.ApplicationVersionConfiguration + val latestVersionCode: Long + + val minVersionCode: Long val supportedCountries: List<String> - val appFeatureus: AppFeaturesOuterClass.AppFeatures + val appFeatures: AppFeaturesOuterClass.AppFeatures interface Mapper : ConfigMapper<CWAConfig> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt new file mode 100644 index 0000000000000000000000000000000000000000..588451c9dec8b5393a6c55e1aaf9fbb00bc2317b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetector.kt @@ -0,0 +1,63 @@ +package de.rki.coronawarnapp.appconfig + +import androidx.annotation.VisibleForTesting +import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.risk.RiskLevelData +import de.rki.coronawarnapp.risk.RiskLevelTask +import de.rki.coronawarnapp.storage.RiskLevelRepository +import de.rki.coronawarnapp.task.TaskController +import de.rki.coronawarnapp.task.common.DefaultTaskRequest +import de.rki.coronawarnapp.util.coroutine.AppScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import timber.log.Timber +import javax.inject.Inject + +class ConfigChangeDetector @Inject constructor( + private val appConfigProvider: AppConfigProvider, + private val taskController: TaskController, + @AppScope private val appScope: CoroutineScope, + private val riskLevelData: RiskLevelData +) { + + fun launch() { + Timber.v("Monitoring config changes.") + appConfigProvider.currentConfig + .distinctUntilChangedBy { it.identifier } + .onEach { + Timber.v("Running app config change checks.") + check(it.identifier) + } + .catch { Timber.e(it, "App config change checks failed.") } + .launchIn(appScope) + } + + @VisibleForTesting + internal fun check(newIdentifier: String) { + if (riskLevelData.lastUsedConfigIdentifier == null) { + // No need to reset anything if we didn't calculate a risklevel yet. + Timber.d("Config changed, but no previous identifier is available.") + return + } + + val oldConfigId = riskLevelData.lastUsedConfigIdentifier + if (newIdentifier != oldConfigId) { + Timber.i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.") + RiskLevelRepositoryDeferrer.resetRiskLevel() + taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector")) + } else { + Timber.v("Config identifier ($oldConfigId) didn't change, NOOP.") + } + } + + @VisibleForTesting + internal object RiskLevelRepositoryDeferrer { + + fun resetRiskLevel() { + RiskLevelRepository.setRiskLevelScore(RiskLevel.UNDETERMINED) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt index e903926da969d2243dc02bc4332f41d6aff51f02..ffe3d8c379724cb321d1eea2dc42f757553c97f9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ConfigData.kt @@ -44,4 +44,10 @@ interface ConfigData : ConfigMapping { */ LOCAL_DEFAULT } + + /** + * Has the config validity expired? + * Is this configs update date, past the maximum cache age? + */ + fun isValid(nowUTC: Instant): Boolean } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt index be47c2f17b4d1834f1de0d0370eb2e4918138de5..a27962042d4328cf8164f1d2b11a6969ecd4c6db 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureDetectionConfig.kt @@ -1,8 +1,7 @@ package de.rki.coronawarnapp.appconfig -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper -import de.rki.coronawarnapp.server.protocols.internal.ExposureDetectionParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.ExposureDetectionParameters import org.joda.time.Duration interface ExposureDetectionConfig { @@ -11,8 +10,7 @@ interface ExposureDetectionConfig { val minTimeBetweenDetections: Duration val overallDetectionTimeout: Duration - val exposureDetectionConfiguration: ExposureConfiguration - val exposureDetectionParameters: ExposureDetectionParameters.ExposureDetectionParametersAndroid + val exposureDetectionParameters: ExposureDetectionParameters.ExposureDetectionParametersAndroid? interface Mapper : ConfigMapper<ExposureDetectionConfig> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt new file mode 100644 index 0000000000000000000000000000000000000000..80eadd589d6bf68fc19648513195ab5e36841036 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/ExposureWindowRiskCalculationConfig.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.appconfig + +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass + +interface ExposureWindowRiskCalculationConfig { + val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter> + val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight> + val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding + val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter> + val transmissionRiskLevelMultiplier: Double + val normalizedTimePerExposureWindowToRiskLevelMapping: + List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> + val normalizedTimePerDayToRiskLevelMappingList: + List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> + + interface Mapper { + fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/RiskCalculationConfig.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/RiskCalculationConfig.kt deleted file mode 100644 index 2c19c0be637e843d0137d3db4983f1ccce8de97c..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/RiskCalculationConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.appconfig - -import de.rki.coronawarnapp.appconfig.mapping.ConfigMapper -import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass -import de.rki.coronawarnapp.server.protocols.internal.RiskScoreClassificationOuterClass - -interface RiskCalculationConfig { - - val minRiskScore: Int - - val attenuationDuration: AttenuationDurationOuterClass.AttenuationDuration - - val riskScoreClasses: RiskScoreClassificationOuterClass.RiskScoreClassification - - interface Mapper : ConfigMapper<RiskCalculationConfig> -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV1.kt deleted file mode 100644 index 0c3f61077cc884adf45aa8a02a78e2ed5fe7154d..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV1.kt +++ /dev/null @@ -1,14 +0,0 @@ -package de.rki.coronawarnapp.appconfig.download - -import okhttp3.ResponseBody -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Path - -interface AppConfigApiV1 { - - @GET("/version/v1/configuration/country/{country}/app_config") - suspend fun getApplicationConfiguration( - @Path("country") country: String - ): Response<ResponseBody> -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt new file mode 100644 index 0000000000000000000000000000000000000000..5d1c0f2c3d0e9654b9af57aa6121bb3cd9d53950 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiV2.kt @@ -0,0 +1,11 @@ +package de.rki.coronawarnapp.appconfig.download + +import okhttp3.ResponseBody +import retrofit2.Response +import retrofit2.http.GET + +interface AppConfigApiV2 { + + @GET("/version/v1/app_config_android") + suspend fun getApplicationConfiguration(): Response<ResponseBody> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSource.kt deleted file mode 100644 index ddb4e4528de00d44c08b7ba6e0910371c2ab9916..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSource.kt +++ /dev/null @@ -1,16 +0,0 @@ -package de.rki.coronawarnapp.appconfig.download - -import android.content.Context -import dagger.Reusable -import de.rki.coronawarnapp.util.di.AppContext -import javax.inject.Inject - -@Reusable -class DefaultAppConfigSource @Inject constructor( - @AppContext private val context: Context -) { - - fun getRawDefaultConfig(): ByteArray { - return context.assets.open("default_app_config.bin").readBytes() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..1e5cc4959e860fec7548e6e1a1954451487261f8 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSource.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.appconfig.internal + +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.sources.fallback.DefaultAppConfigSource +import de.rki.coronawarnapp.appconfig.sources.local.LocalAppConfigSource +import de.rki.coronawarnapp.appconfig.sources.remote.RemoteAppConfigSource +import de.rki.coronawarnapp.util.TimeStamper +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class AppConfigSource @Inject constructor( + private val remoteAppConfigSource: RemoteAppConfigSource, + private val localAppConfigSource: LocalAppConfigSource, + private val defaultAppConfigSource: DefaultAppConfigSource, + private val timeStamper: TimeStamper +) { + + suspend fun getConfigData(): ConfigData { + Timber.tag(TAG).d("getConfigData()") + + val localConfig = localAppConfigSource.getConfigData() + if (localConfig != null && localConfig.isValid(timeStamper.nowUTC)) { + Timber.tag(TAG).d("Returning local config, still valid.") + return localConfig + } else { + Timber.tag(TAG).d("Local app config was unavailable(${localConfig == null} or invalid.") + } + + val remoteConfig = remoteAppConfigSource.getConfigData() + + return when { + remoteConfig != null -> { + Timber.tag(TAG).d("Returning remote config.") + remoteConfig + } + localConfig != null -> { + Timber.tag(TAG).d("Remote config was unavailable, returning local config, even if expired.") + localConfig + } + else -> { + Timber.tag(TAG).w("Remote & Local config available! Returning DEFAULT!") + defaultAppConfigSource.getConfigData() + } + } + } + + suspend fun clear() { + Timber.tag(TAG).d("clear()") + remoteAppConfigSource.clear() + localAppConfigSource.clear() + } + + companion object { + private const val TAG = "AppConfigSource" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationCorruptException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationCorruptException.kt similarity index 86% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationCorruptException.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationCorruptException.kt index bd6940034f8af15de4d110ab65ece6eb9a34cd94..65876d4f255a200d6f44235fcdd3a66ff1457144 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationCorruptException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationCorruptException.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.internal import de.rki.coronawarnapp.exception.reporting.ErrorCodes import de.rki.coronawarnapp.exception.reporting.ReportedException diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationInvalidException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationInvalidException.kt similarity index 88% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationInvalidException.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationInvalidException.kt index 63cb1069e92febfd84f5a894a8d9dbf3c26ea618..15ec3692bca6ba188403a32a2e17de7a8dac0fea 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ApplicationConfigurationInvalidException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ApplicationConfigurationInvalidException.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.internal import de.rki.coronawarnapp.exception.reporting.ErrorCodes import de.rki.coronawarnapp.exception.reporting.ReportedException diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/DefaultConfigData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt similarity index 57% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/DefaultConfigData.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt index d2ab41944ce8533f43ab7a82be7421899f823790..f7bde81b1a52cfd96f878fa0dc43d310df3f7afd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/DefaultConfigData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/ConfigDataContainer.kt @@ -1,15 +1,22 @@ -package de.rki.coronawarnapp.appconfig +package de.rki.coronawarnapp.appconfig.internal +import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.mapping.ConfigMapping import org.joda.time.Duration import org.joda.time.Instant -data class DefaultConfigData( +data class ConfigDataContainer( val serverTime: Instant, + val cacheValidity: Duration, val mappedConfig: ConfigMapping, override val identifier: String, override val localOffset: Duration, override val configType: ConfigData.Type ) : ConfigData, ConfigMapping by mappedConfig { override val updatedAt: Instant = serverTime.plus(localOffset) + + override fun isValid(nowUTC: Instant): Boolean { + val expiresAt = updatedAt.plus(cacheValidity) + return nowUTC.isBefore(expiresAt) + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ConfigDownload.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/InternalConfigData.kt similarity index 77% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ConfigDownload.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/InternalConfigData.kt index d6114e5a89fa050b3773d9263efbfa5a1801d552..6eb41191dd06a5f5b81e030232193cf2221f2c10 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/ConfigDownload.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/internal/InternalConfigData.kt @@ -1,20 +1,21 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.internal import com.google.gson.annotations.SerializedName import org.joda.time.Duration import org.joda.time.Instant -data class ConfigDownload( +data class InternalConfigData( @SerializedName("rawData") val rawData: ByteArray, @SerializedName("etag") val etag: String, @SerializedName("serverTime") val serverTime: Instant, - @SerializedName("localOffset") val localOffset: Duration + @SerializedName("localOffset") val localOffset: Duration, + @SerializedName("cacheValidity") val cacheValidity: Duration ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false - other as ConfigDownload + other as InternalConfigData if (!rawData.contentEquals(other.rawData)) return false if (serverTime != other.serverTime) return false diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt index 8d78dddcdb0e87acb6e06cb32e7f3405e426a2b1..8c2e9502ffe4d60f99dfab4d11b24ea62e67a4b1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapper.kt @@ -3,24 +3,24 @@ package de.rki.coronawarnapp.appconfig.mapping import androidx.annotation.VisibleForTesting import dagger.Reusable import de.rki.coronawarnapp.appconfig.CWAConfig -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import de.rki.coronawarnapp.server.protocols.internal.AppFeaturesOuterClass -import de.rki.coronawarnapp.server.protocols.internal.AppVersionConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.AppFeaturesOuterClass import timber.log.Timber import javax.inject.Inject @Reusable class CWAConfigMapper @Inject constructor() : CWAConfig.Mapper { - override fun map(rawConfig: AppConfig.ApplicationConfiguration): CWAConfig { + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): CWAConfig { return CWAConfigContainer( - appVersion = rawConfig.appVersion, + latestVersionCode = rawConfig.latestVersionCode, + minVersionCode = rawConfig.minVersionCode, supportedCountries = rawConfig.getMappedSupportedCountries(), - appFeatureus = rawConfig.appFeatures + appFeatures = rawConfig.appFeatures ) } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun AppConfig.ApplicationConfiguration.getMappedSupportedCountries(): List<String> = + internal fun AppConfigAndroid.ApplicationConfigurationAndroid.getMappedSupportedCountries(): List<String> = when { supportedCountriesList == null -> emptyList() supportedCountriesList.size == 1 && !VALID_CC.matches(supportedCountriesList.single()) -> { @@ -31,9 +31,10 @@ class CWAConfigMapper @Inject constructor() : CWAConfig.Mapper { } data class CWAConfigContainer( - override val appVersion: AppVersionConfig.ApplicationVersionConfiguration, + override val latestVersionCode: Long, + override val minVersionCode: Long, override val supportedCountries: List<String>, - override val appFeatureus: AppFeaturesOuterClass.AppFeatures + override val appFeatures: AppFeaturesOuterClass.AppFeatures ) : CWAConfig companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt index 58c4b88b2f7beaff9765715e7c1ed54a4916f199..1c822d0f2a7b8b5080b0de703f59362489fb83a0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapper.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.appconfig.mapping -import de.rki.coronawarnapp.server.protocols.internal.AppConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid interface ConfigMapper<T> { - fun map(rawConfig: AppConfig.ApplicationConfiguration): T + fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): T } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt index 9858ec812bfc0b74c37330b2d44704decd085a5d..08a358c7a40c3734859822addcf878a5abf31b79 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigMapping.kt @@ -2,16 +2,16 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig -import de.rki.coronawarnapp.appconfig.RiskCalculationConfig -import de.rki.coronawarnapp.server.protocols.internal.AppConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid interface ConfigMapping : CWAConfig, KeyDownloadConfig, ExposureDetectionConfig, - RiskCalculationConfig { + ExposureWindowRiskCalculationConfig { @Deprecated("Try to access a more specific config type, avoid the RAW variant.") - val rawConfig: AppConfig.ApplicationConfiguration + val rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt index 8449b81b7cf3c5e996dc8121f97de585bc0ef693..54ccf2419a424b1fcbd86acd3ed50d2c6c1e74d4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParser.kt @@ -3,9 +3,9 @@ package de.rki.coronawarnapp.appconfig.mapping import dagger.Reusable import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig -import de.rki.coronawarnapp.appconfig.RiskCalculationConfig -import de.rki.coronawarnapp.server.protocols.internal.AppConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid import timber.log.Timber import javax.inject.Inject @@ -14,7 +14,7 @@ class ConfigParser @Inject constructor( private val cwaConfigMapper: CWAConfig.Mapper, private val keyDownloadConfigMapper: KeyDownloadConfig.Mapper, private val exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper, - private val riskCalculationConfigMapper: RiskCalculationConfig.Mapper + private val exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper ) { fun parse(configBytes: ByteArray): ConfigMapping = try { @@ -24,7 +24,7 @@ class ConfigParser @Inject constructor( cwaConfig = cwaConfigMapper.map(it), keyDownloadConfig = keyDownloadConfigMapper.map(it), exposureDetectionConfig = exposureDetectionConfigMapper.map(it), - riskCalculationConfig = riskCalculationConfigMapper.map(it) + exposureWindowRiskCalculationConfig = exposureWindowRiskCalculationConfigMapper.map(it) ) } } catch (e: Exception) { @@ -32,8 +32,8 @@ class ConfigParser @Inject constructor( throw e } - private fun parseRawArray(configBytes: ByteArray): AppConfig.ApplicationConfiguration { + private fun parseRawArray(configBytes: ByteArray): AppConfigAndroid.ApplicationConfigurationAndroid { Timber.v("Parsing config (size=%dB)", configBytes.size) - return AppConfig.ApplicationConfiguration.parseFrom(configBytes) + return AppConfigAndroid.ApplicationConfigurationAndroid.parseFrom(configBytes) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt index 783385ddfdd3e7c06198eaff8c0ef5ba5a2961ff..81643e178e87768eea0208249bf3268ade972c25 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/DefaultConfigMapping.kt @@ -2,18 +2,18 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig -import de.rki.coronawarnapp.appconfig.RiskCalculationConfig -import de.rki.coronawarnapp.server.protocols.internal.AppConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid data class DefaultConfigMapping( - override val rawConfig: AppConfig.ApplicationConfiguration, + override val rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid, val cwaConfig: CWAConfig, val keyDownloadConfig: KeyDownloadConfig, val exposureDetectionConfig: ExposureDetectionConfig, - val riskCalculationConfig: RiskCalculationConfig + val exposureWindowRiskCalculationConfig: ExposureWindowRiskCalculationConfig ) : ConfigMapping, CWAConfig by cwaConfig, KeyDownloadConfig by keyDownloadConfig, ExposureDetectionConfig by exposureDetectionConfig, - RiskCalculationConfig by riskCalculationConfig + ExposureWindowRiskCalculationConfig by exposureWindowRiskCalculationConfig diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt index 92473af001e20e37facf9758760eb9724195c0b3..5110334a3cbb27f5b89cf0671428fc95b3de6b32 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapper.kt @@ -1,20 +1,22 @@ package de.rki.coronawarnapp.appconfig.mapping import androidx.annotation.VisibleForTesting -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import dagger.Reusable import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import de.rki.coronawarnapp.server.protocols.internal.ExposureDetectionParameters.ExposureDetectionParametersAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.ExposureDetectionParameters.ExposureDetectionParametersAndroid import org.joda.time.Duration import javax.inject.Inject @Reusable class ExposureDetectionConfigMapper @Inject constructor() : ExposureDetectionConfig.Mapper { - override fun map(rawConfig: AppConfig.ApplicationConfiguration): ExposureDetectionConfig { - val exposureParams = rawConfig.androidExposureDetectionParameters + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureDetectionConfig { + val exposureParams = if (rawConfig.hasExposureDetectionParameters()) { + rawConfig.exposureDetectionParameters + } else { + null + } return ExposureDetectionConfigContainer( - exposureDetectionConfiguration = rawConfig.mapRiskScoreToExposureConfiguration(), exposureDetectionParameters = exposureParams, maxExposureDetectionsPerUTCDay = exposureParams.maxExposureDetectionsPerDay(), minTimeBetweenDetections = exposureParams.minTimeBetweenExposureDetections(), @@ -23,8 +25,7 @@ class ExposureDetectionConfigMapper @Inject constructor() : ExposureDetectionCon } data class ExposureDetectionConfigContainer( - override val exposureDetectionConfiguration: ExposureConfiguration, - override val exposureDetectionParameters: ExposureDetectionParametersAndroid, + override val exposureDetectionParameters: ExposureDetectionParametersAndroid?, override val maxExposureDetectionsPerUTCDay: Int, override val minTimeBetweenDetections: Duration, override val overallDetectionTimeout: Duration @@ -33,77 +34,28 @@ class ExposureDetectionConfigMapper @Inject constructor() : ExposureDetectionCon // If we are outside the valid data range, fallback to default value. @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -fun ExposureDetectionParametersAndroid.overAllDetectionTimeout(): Duration = when { - overallTimeoutInSeconds > 3600 -> Duration.standardMinutes(15) - overallTimeoutInSeconds <= 0 -> Duration.standardMinutes(15) - else -> Duration.standardSeconds(overallTimeoutInSeconds.toLong()) -} +fun ExposureDetectionParametersAndroid?.overAllDetectionTimeout(): Duration = + if (this == null || overallTimeoutInSeconds > 3600 || overallTimeoutInSeconds <= 0) { + Duration.standardMinutes(15) + } else { + Duration.standardSeconds(overallTimeoutInSeconds.toLong()) + } // If we are outside the valid data range, fallback to default value. @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -fun ExposureDetectionParametersAndroid.maxExposureDetectionsPerDay(): Int = when { - maxExposureDetectionsPerInterval > 6 -> 6 - maxExposureDetectionsPerInterval < 0 -> 6 - else -> maxExposureDetectionsPerInterval -} +fun ExposureDetectionParametersAndroid?.maxExposureDetectionsPerDay(): Int = + if (this == null || maxExposureDetectionsPerInterval > 6 || maxExposureDetectionsPerInterval < 0) { + 6 + } else { + maxExposureDetectionsPerInterval + } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -fun ExposureDetectionParametersAndroid.minTimeBetweenExposureDetections(): Duration { - val detectionsPerDay = maxExposureDetectionsPerDay() +fun ExposureDetectionParametersAndroid?.minTimeBetweenExposureDetections(): Duration { + val detectionsPerDay = this.maxExposureDetectionsPerDay() return if (detectionsPerDay == 0) { Duration.standardDays(99) } else { (24 / detectionsPerDay).let { Duration.standardHours(it.toLong()) } } } - -@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) -fun AppConfig.ApplicationConfiguration.mapRiskScoreToExposureConfiguration(): ExposureConfiguration = - ExposureConfiguration - .ExposureConfigurationBuilder() - .setTransmissionRiskScores( - this.exposureConfig.transmission.appDefined1Value, - this.exposureConfig.transmission.appDefined2Value, - this.exposureConfig.transmission.appDefined3Value, - this.exposureConfig.transmission.appDefined4Value, - this.exposureConfig.transmission.appDefined5Value, - this.exposureConfig.transmission.appDefined6Value, - this.exposureConfig.transmission.appDefined7Value, - this.exposureConfig.transmission.appDefined8Value - ) - .setDurationScores( - this.exposureConfig.duration.eq0MinValue, - this.exposureConfig.duration.gt0Le5MinValue, - this.exposureConfig.duration.gt5Le10MinValue, - this.exposureConfig.duration.gt10Le15MinValue, - this.exposureConfig.duration.gt15Le20MinValue, - this.exposureConfig.duration.gt20Le25MinValue, - this.exposureConfig.duration.gt25Le30MinValue, - this.exposureConfig.duration.gt30MinValue - ) - .setDaysSinceLastExposureScores( - this.exposureConfig.daysSinceLastExposure.ge14DaysValue, - this.exposureConfig.daysSinceLastExposure.ge12Lt14DaysValue, - this.exposureConfig.daysSinceLastExposure.ge10Lt12DaysValue, - this.exposureConfig.daysSinceLastExposure.ge8Lt10DaysValue, - this.exposureConfig.daysSinceLastExposure.ge6Lt8DaysValue, - this.exposureConfig.daysSinceLastExposure.ge4Lt6DaysValue, - this.exposureConfig.daysSinceLastExposure.ge2Lt4DaysValue, - this.exposureConfig.daysSinceLastExposure.ge0Lt2DaysValue - ) - .setAttenuationScores( - this.exposureConfig.attenuation.gt73DbmValue, - this.exposureConfig.attenuation.gt63Le73DbmValue, - this.exposureConfig.attenuation.gt51Le63DbmValue, - this.exposureConfig.attenuation.gt33Le51DbmValue, - this.exposureConfig.attenuation.gt27Le33DbmValue, - this.exposureConfig.attenuation.gt15Le27DbmValue, - this.exposureConfig.attenuation.gt10Le15DbmValue, - this.exposureConfig.attenuation.le10DbmValue - ) - .setMinimumRiskScore(this.minRiskScore) - .setDurationAtAttenuationThresholds( - this.attenuationDuration.thresholds.lower, - this.attenuationDuration.thresholds.upper - ) - .build() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..ab55b97ee827ef27a091e1575c0eae048e50ee7a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/ExposureWindowRiskCalculationConfigMapper.kt @@ -0,0 +1,52 @@ +package de.rki.coronawarnapp.appconfig.mapping + +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import javax.inject.Inject + +@Reusable +class ExposureWindowRiskCalculationConfigMapper @Inject constructor() : + ExposureWindowRiskCalculationConfig.Mapper { + + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): ExposureWindowRiskCalculationConfig { + if (!rawConfig.hasRiskCalculationParameters()) { + throw ApplicationConfigurationInvalidException( + message = "Risk Calculation Parameters are missing" + ) + } + + val riskCalculationParameters = rawConfig.riskCalculationParameters + + return ExposureWindowRiskCalculationContainer( + minutesAtAttenuationFilters = riskCalculationParameters + .minutesAtAttenuationFiltersList, + minutesAtAttenuationWeights = riskCalculationParameters + .minutesAtAttenuationWeightsList, + transmissionRiskLevelEncoding = riskCalculationParameters + .trlEncoding, + transmissionRiskLevelFilters = riskCalculationParameters + .trlFiltersList, + transmissionRiskLevelMultiplier = riskCalculationParameters + .transmissionRiskLevelMultiplier, + normalizedTimePerExposureWindowToRiskLevelMapping = riskCalculationParameters + .normalizedTimePerEWToRiskLevelMappingList, + normalizedTimePerDayToRiskLevelMappingList = riskCalculationParameters + .normalizedTimePerDayToRiskLevelMappingList + ) + } + + data class ExposureWindowRiskCalculationContainer( + override val minutesAtAttenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>, + override val minutesAtAttenuationWeights: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>, + override val transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding, + override val transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter>, + override val transmissionRiskLevelMultiplier: Double, + override val normalizedTimePerExposureWindowToRiskLevelMapping: + List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>, + override val normalizedTimePerDayToRiskLevelMappingList: + List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> + ) : ExposureWindowRiskCalculationConfig +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt index 4c55393ec77de81c5d97d75e2804218d419fbcf6..f1a5a2671e661788c9f5086b18aefd016af84bb0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/KeyDownloadParametersMapper.kt @@ -4,8 +4,8 @@ import androidx.annotation.VisibleForTesting import dagger.Reusable import de.rki.coronawarnapp.appconfig.KeyDownloadConfig import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import de.rki.coronawarnapp.server.protocols.internal.KeyDownloadParameters.KeyDownloadParametersAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.KeyDownloadParameters.KeyDownloadParametersAndroid import org.joda.time.Duration import org.joda.time.LocalDate import org.joda.time.LocalTime @@ -15,8 +15,12 @@ import javax.inject.Inject @Reusable class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapper { - override fun map(rawConfig: AppConfig.ApplicationConfiguration): KeyDownloadConfig { - val rawParameters = rawConfig.androidKeyDownloadParameters + override fun map(rawConfig: AppConfigAndroid.ApplicationConfigurationAndroid): KeyDownloadConfig { + val rawParameters = if (rawConfig.hasKeyDownloadParameters()) { + rawConfig.keyDownloadParameters + } else { + null + } return KeyDownloadConfigContainer( individualDownloadTimeout = rawParameters.individualTimeout(), @@ -27,21 +31,25 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp } // If we are outside the valid data range, fallback to default value. - private fun KeyDownloadParametersAndroid.individualTimeout(): Duration = when { - downloadTimeoutInSeconds > 1800 -> Duration.standardSeconds(60) - downloadTimeoutInSeconds <= 0 -> Duration.standardSeconds(60) - else -> Duration.standardSeconds(downloadTimeoutInSeconds.toLong()) - } + private fun KeyDownloadParametersAndroid?.individualTimeout(): Duration = + if (this == null || downloadTimeoutInSeconds > 1800 || downloadTimeoutInSeconds <= 0) { + Duration.standardSeconds(60) + } else { + Duration.standardSeconds(downloadTimeoutInSeconds.toLong()) + } // If we are outside the valid data range, fallback to default value. - private fun KeyDownloadParametersAndroid.overAllTimeout(): Duration = when { - overallTimeoutInSeconds > 1800 -> Duration.standardMinutes(8) - overallTimeoutInSeconds <= 0 -> Duration.standardMinutes(8) - else -> Duration.standardSeconds(overallTimeoutInSeconds.toLong()) - } + private fun KeyDownloadParametersAndroid?.overAllTimeout(): Duration = + if (this == null || overallTimeoutInSeconds > 1800 || overallTimeoutInSeconds <= 0) { + Duration.standardMinutes(8) + } else { + Duration.standardSeconds(overallTimeoutInSeconds.toLong()) + } - private fun KeyDownloadParametersAndroid.mapDayEtags(): List<RevokedKeyPackage.Day> = - this.revokedDayPackagesList.mapNotNull { + private fun KeyDownloadParametersAndroid?.mapDayEtags(): List<RevokedKeyPackage.Day> { + if (this == null) return emptyList() + + return this.revokedDayPackagesList.mapNotNull { try { RevokedKeyPackage.Day( etag = it.etag, @@ -53,9 +61,12 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp null } } + } - private fun KeyDownloadParametersAndroid.mapHourEtags(): List<RevokedKeyPackage.Hour> = - this.revokedHourPackagesList.mapNotNull { + private fun KeyDownloadParametersAndroid?.mapHourEtags(): List<RevokedKeyPackage.Hour> { + if (this == null) return emptyList() + + return this.revokedHourPackagesList.mapNotNull { try { RevokedKeyPackage.Hour( etag = it.etag, @@ -68,6 +79,7 @@ class KeyDownloadParametersMapper @Inject constructor() : KeyDownloadConfig.Mapp null } } + } data class KeyDownloadConfigContainer( override val individualDownloadTimeout: Duration, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapper.kt deleted file mode 100644 index dd36d4ea99f8f56af32e83fee682d90cc164460f..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapper.kt +++ /dev/null @@ -1,26 +0,0 @@ -package de.rki.coronawarnapp.appconfig.mapping - -import dagger.Reusable -import de.rki.coronawarnapp.appconfig.RiskCalculationConfig -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass -import de.rki.coronawarnapp.server.protocols.internal.RiskScoreClassificationOuterClass -import javax.inject.Inject - -@Reusable -class RiskCalculationConfigMapper @Inject constructor() : RiskCalculationConfig.Mapper { - - override fun map(rawConfig: AppConfig.ApplicationConfiguration): RiskCalculationConfig { - return RiskCalculationContainer( - minRiskScore = rawConfig.minRiskScore, - riskScoreClasses = rawConfig.riskScoreClasses, - attenuationDuration = rawConfig.attenuationDuration - ) - } - - data class RiskCalculationContainer( - override val minRiskScore: Int, - override val attenuationDuration: AttenuationDurationOuterClass.AttenuationDuration, - override val riskScoreClasses: RiskScoreClassificationOuterClass.RiskScoreClassification - ) : RiskCalculationConfig -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..7000a6c71cb59f6be2550516a1f17376648fcdc7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSource.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.appconfig.sources.fallback + +import android.content.Context +import dagger.Reusable +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.appconfig.mapping.ConfigParser +import de.rki.coronawarnapp.util.di.AppContext +import org.joda.time.Duration +import org.joda.time.Instant +import javax.inject.Inject + +@Reusable +class DefaultAppConfigSource @Inject constructor( + @AppContext private val context: Context, + private val configParser: ConfigParser +) { + + fun getRawDefaultConfig(): ByteArray { + return context.assets.open("default_app_config_android.bin").readBytes() + } + + fun getConfigData(): ConfigData = ConfigDataContainer( + mappedConfig = configParser.parse(getRawDefaultConfig()), + serverTime = Instant.EPOCH, + localOffset = Duration.ZERO, + identifier = "fallback.local", + configType = ConfigData.Type.LOCAL_DEFAULT, + cacheValidity = Duration.ZERO + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt similarity index 68% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorage.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt index 1d939896ce9e0b2f7eadf831cb40586ac73ce50b..dda995f9b17ddd06d498c0292e125490eed35eeb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorage.kt @@ -1,7 +1,10 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.local import android.content.Context import com.google.gson.Gson +import de.rki.coronawarnapp.appconfig.internal.InternalConfigData +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.serialization.BaseGson @@ -38,17 +41,18 @@ class AppConfigStorage @Inject constructor( private val configFile = File(configDir, "appconfig.json") private val mutex = Mutex() - suspend fun getStoredConfig(): ConfigDownload? = mutex.withLock { + suspend fun getStoredConfig(): InternalConfigData? = mutex.withLock { Timber.v("get() AppConfig") if (!configFile.exists() && legacyConfigFile.exists()) { Timber.i("Returning legacy config.") return@withLock try { - ConfigDownload( + InternalConfigData( rawData = legacyConfigFile.readBytes(), serverTime = timeStamper.nowUTC, localOffset = Duration.ZERO, - etag = "legacy.migration" + etag = "legacy.migration", + cacheValidity = Duration.standardMinutes(5) ) } catch (e: Exception) { Timber.e(e, "Legacy config exits but couldn't be read.") @@ -57,14 +61,17 @@ class AppConfigStorage @Inject constructor( } return@withLock try { - gson.fromJson<ConfigDownload>(configFile) + gson.fromJson<InternalConfigData>(configFile).also { + requireNotNull(it.rawData) + } } catch (e: Exception) { Timber.e(e, "Couldn't load config.") + if (configFile.delete()) Timber.w("Config file was deleted.") null } } - suspend fun setStoredConfig(value: ConfigDownload?): Unit = mutex.withLock { + suspend fun setStoredConfig(value: InternalConfigData?): Unit = mutex.withLock { Timber.v("set(...) AppConfig: %s", value) if (configDir.mkdirs()) Timber.v("Parent folder created.") @@ -73,7 +80,12 @@ class AppConfigStorage @Inject constructor( Timber.v("Overwriting %d from %s", configFile.length(), configFile.lastModified()) } - if (value != null) { + if (value == null) { + if (configFile.delete()) Timber.d("Config file was deleted (value=null).") + return + } + + try { gson.toJson(value, configFile) if (legacyConfigFile.exists()) { @@ -81,8 +93,11 @@ class AppConfigStorage @Inject constructor( Timber.i("Legacy config file deleted, superseeded.") } } - } else { - configFile.delete() + } catch (e: Exception) { + // We'll not rethrow as we could still keep working just with the remote config, + // but we will notify the user. + Timber.e(e, "Failed to config data to local storage.") + e.report(ExceptionCategory.INTERNAL) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..c6a015c9abec1077a8e01c3a17dc3dfd722b868e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSource.kt @@ -0,0 +1,52 @@ +package de.rki.coronawarnapp.appconfig.sources.local + +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.appconfig.mapping.ConfigParser +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocalAppConfigSource @Inject constructor( + private val storage: AppConfigStorage, + private val parser: ConfigParser, + private val dispatcherProvider: DispatcherProvider +) { + + suspend fun getConfigData(): ConfigData? = withContext(dispatcherProvider.IO) { + Timber.tag(TAG).v("retrieveConfig()") + + val configDownload = storage.getStoredConfig() + if (configDownload == null) { + Timber.tag(TAG).d("No stored config available.") + return@withContext null + } + + return@withContext try { + configDownload.let { + ConfigDataContainer( + mappedConfig = parser.parse(it.rawData), + serverTime = it.serverTime, + localOffset = it.localOffset, + identifier = it.etag, + configType = ConfigData.Type.LAST_RETRIEVED, + cacheValidity = it.cacheValidity + ) + } + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Fallback config exists but could not be parsed!") + null + } + } + + suspend fun clear() { + storage.setStoredConfig(null) + } + + companion object { + private const val TAG = "LocalAppConfigSource" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigHttpCache.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigHttpCache.kt similarity index 71% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigHttpCache.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigHttpCache.kt index 253ac97d3fbedc4a7c0c5429cad471f77cac0837..0d4e68160cfa2f0b4f99686b73748dba1828bda7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigHttpCache.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigHttpCache.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.remote import javax.inject.Qualifier diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt similarity index 76% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigServer.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt index 7d174ac15251e8ca5f928e8b135f1985f32050bd..b9ae35a337a8921ec33729aea891fc2b66c97b18 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/download/AppConfigServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt @@ -1,15 +1,18 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.remote import dagger.Lazy import dagger.Reusable -import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode -import de.rki.coronawarnapp.environment.download.DownloadCDNHomeCountry +import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2 +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException +import de.rki.coronawarnapp.appconfig.internal.InternalConfigData import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.ZipHelper.readIntoMap import de.rki.coronawarnapp.util.ZipHelper.unzip import de.rki.coronawarnapp.util.retrofit.etag import de.rki.coronawarnapp.util.security.VerificationKeys import okhttp3.Cache +import okhttp3.CacheControl import org.joda.time.Duration import org.joda.time.Instant import org.joda.time.format.DateTimeFormat @@ -21,17 +24,16 @@ import javax.inject.Inject @Reusable class AppConfigServer @Inject constructor( - private val api: Lazy<AppConfigApiV1>, + private val api: Lazy<AppConfigApiV2>, private val verificationKeys: VerificationKeys, private val timeStamper: TimeStamper, - @DownloadCDNHomeCountry private val homeCountry: LocationCode, @AppConfigHttpCache private val cache: Cache ) { - internal suspend fun downloadAppConfig(): ConfigDownload { + internal suspend fun downloadAppConfig(): InternalConfigData { Timber.tag(TAG).d("Fetching app config.") - val response = api.get().getApplicationConfiguration(homeCountry.identifier) + val response = api.get().getApplicationConfiguration() if (!response.isSuccessful) throw HttpException(response) val rawConfig = with( @@ -56,19 +58,26 @@ class AppConfigServer @Inject constructor( // If this is a cached response, we need the original timestamp to calculate the time offset val localTime = response.getCacheTimestamp() ?: timeStamper.nowUTC + val headers = response.headers() + // Shouldn't happen, but hey ¯\_(ツ)_/¯ - val etag = - response.headers().etag() ?: throw ApplicationConfigurationInvalidException(message = "Server has no ETAG.") + val etag = headers.etag() + ?: throw ApplicationConfigurationInvalidException(message = "Server has no ETAG.") val serverTime = response.getServerDate() ?: localTime val offset = Duration(serverTime, localTime) Timber.tag(TAG).v("Time offset was %dms", offset.millis) - return ConfigDownload( + val cacheControl = CacheControl.parse(headers) + + val maxCacheAge = Duration.standardSeconds(cacheControl.maxAgeSeconds.toLong()) + + return InternalConfigData( rawData = rawConfig, etag = etag, serverTime = serverTime, - localOffset = offset + localOffset = offset, + cacheValidity = maxCacheAge ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5006f0279a83676f128d16bb1b1a5f68d7caa92 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSource.kt @@ -0,0 +1,57 @@ +package de.rki.coronawarnapp.appconfig.sources.remote + +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.appconfig.mapping.ConfigParser +import de.rki.coronawarnapp.appconfig.sources.local.AppConfigStorage +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RemoteAppConfigSource @Inject constructor( + private val server: AppConfigServer, + private val storage: AppConfigStorage, + private val parser: ConfigParser, + private val dispatcherProvider: DispatcherProvider +) { + + suspend fun getConfigData(): ConfigData? = withContext(dispatcherProvider.IO) { + Timber.tag(TAG).v("retrieveConfig()") + + val configDownload = try { + server.downloadAppConfig() + } catch (e: Exception) { + Timber.tag(TAG).w(e, "Failed to download AppConfig from server .") + return@withContext null + } + + return@withContext try { + parser.parse(configDownload.rawData).let { + Timber.tag(TAG).d("Got a valid AppConfig from server, saving.") + storage.setStoredConfig(configDownload) + ConfigDataContainer( + mappedConfig = it, + serverTime = configDownload.serverTime, + localOffset = configDownload.localOffset, + identifier = configDownload.etag, + configType = ConfigData.Type.FROM_SERVER, + cacheValidity = configDownload.cacheValidity + ) + } + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to parse AppConfig from server, trying fallback.") + null + } + } + + fun clear() { + server.clearCache() + } + + companion object { + private const val TAG = "AppConfigRetriever" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt index 870c80617d260aafb04eedc58c461b86ae003372..d04dc5137d1dc33c3eb31577ef33fade451f4594 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTask.kt @@ -15,18 +15,14 @@ import de.rki.coronawarnapp.task.TaskFactory import de.rki.coronawarnapp.task.TaskFactory.Config.CollisionBehavior import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.ui.toLazyString -import de.rki.coronawarnapp.worker.BackgroundWorkHelper import kotlinx.coroutines.channels.ConflatedBroadcastChannel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.first -import org.joda.time.DateTime -import org.joda.time.DateTimeZone import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber import java.util.Date -import java.util.UUID import javax.inject.Inject import javax.inject.Provider @@ -50,10 +46,6 @@ class DownloadDiagnosisKeysTask @Inject constructor( Timber.d("Running with arguments=%s", arguments) arguments as Arguments - if (arguments.withConstraints) { - if (!noKeysFetchedToday()) return object : Task.Result {} - } - /** * Handles the case when the ENClient got disabled but the Task is still scheduled * in a background job. Also it acts as a failure catch in case the orchestration code did @@ -68,10 +60,6 @@ class DownloadDiagnosisKeysTask @Inject constructor( val currentDate = Date(timeStamper.nowUTC.millis) Timber.tag(TAG).d("Using $currentDate as current date in task.") - /**************************************************** - * RETRIEVE TOKEN - ****************************************************/ - val token = retrieveToken(rollbackItems) throwIfCancelled() // RETRIEVE RISK SCORE PARAMETERS @@ -94,11 +82,12 @@ class DownloadDiagnosisKeysTask @Inject constructor( if (wasLastDetectionPerformedRecently(now, exposureConfig, trackedExposureDetections)) { // At most one detection every 6h + Timber.tag(TAG).i("task aborted, because detection was performed recently") return object : Task.Result {} } if (hasRecentDetectionAndNoNewFiles(now, keySyncResult, trackedExposureDetections)) { - // Last check was within 24h, and there are no new files. + Timber.tag(TAG).i("task aborted, last check was within 24h, and there are no new files") return object : Task.Result {} } @@ -113,20 +102,17 @@ class DownloadDiagnosisKeysTask @Inject constructor( ) Timber.tag(TAG).d("Attempting submission to ENF") - val isSubmissionSuccessful = enfClient.provideDiagnosisKeys( - keyFiles = availableKeyFiles, - configuration = exposureConfig.exposureDetectionConfiguration, - token = token - ) - Timber.tag(TAG).d("Diagnosis Keys provided (success=%s, token=%s)", isSubmissionSuccessful, token) - - internalProgress.send(Progress.ApiSubmissionFinished) - throwIfCancelled() + val isSubmissionSuccessful = enfClient.provideDiagnosisKeys(availableKeyFiles) + Timber.tag(TAG).d("Diagnosis Keys provided (success=%s)", isSubmissionSuccessful) + // EXPOSUREAPP-3878 write timestamp immediately after submission, + // so that progress observers can rely on a clean app state if (isSubmissionSuccessful) { saveTimestamp(currentDate, rollbackItems) } + internalProgress.send(Progress.ApiSubmissionFinished) + return object : Task.Result {} } catch (error: Exception) { Timber.tag(TAG).e(error) @@ -181,35 +167,6 @@ class DownloadDiagnosisKeysTask @Inject constructor( LocalData.lastTimeDiagnosisKeysFromServerFetch(currentDate) } - private fun retrieveToken(rollbackItems: MutableList<RollbackItem>): String { - val googleAPITokenForRollback = LocalData.googleApiToken() - rollbackItems.add { - LocalData.googleApiToken(googleAPITokenForRollback) - } - return UUID.randomUUID().toString().also { - LocalData.googleApiToken(it) - } - } - - private fun noKeysFetchedToday(): Boolean { - val currentDate = DateTime(timeStamper.nowUTC, DateTimeZone.UTC) - val lastFetch = DateTime( - LocalData.lastTimeDiagnosisKeysFromServerFetch(), - DateTimeZone.UTC - ) - return (LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || - currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay()).also { - if (it) { - Timber.tag(TAG) - .d("No keys fetched today yet (last=%s, now=%s)", lastFetch, currentDate) - BackgroundWorkHelper.sendDebugNotification( - "Start Task", - "No keys fetched today yet \n${DateTime.now()}\nUTC: $currentDate" - ) - } - } - } - private fun rollback(rollbackItems: MutableList<RollbackItem>) { try { Timber.tag(TAG).d("Initiate Rollback") @@ -249,8 +206,7 @@ class DownloadDiagnosisKeysTask @Inject constructor( } class Arguments( - val requestedCountries: List<String>? = null, - val withConstraints: Boolean = false + val requestedCountries: List<String>? = null ) : Task.Arguments data class Config( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt index ad63bbb134ffc3583c9fea34bc4c31e1ee2cb724..a15a2fa6969f165ac04109a4a55e3022a8bf878a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncTool.kt @@ -11,7 +11,6 @@ import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo.Type import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.storage.DeviceStorage import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDate -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalTime import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -19,6 +18,7 @@ import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext +import org.joda.time.DateTimeZone import org.joda.time.Instant import org.joda.time.LocalDate import org.joda.time.LocalTime @@ -117,10 +117,10 @@ class HourPackageSyncTool @Inject constructor( @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun expectNewHourPackages(cachedHours: List<CachedKey>, now: Instant): Boolean { - val previousHour = now.toLocalTime().minusHours(1) - val newestHour = cachedHours.map { it.info.toDateTime() }.maxOrNull()?.toLocalTime() + val today = now.toDateTime(DateTimeZone.UTC) + val newestHour = cachedHours.map { it.info.toDateTime() }.maxOrNull() - return previousHour.hourOfDay != newestHour?.hourOfDay + return today.minusHours(1).hourOfDay != newestHour?.hourOfDay || today.toLocalDate() != newestHour.toLocalDate() } @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt index 46499c31230bf37e06c79f5c124a5e02f9f61215..767788b052571ee50d306089dec02aba0a7513d6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/diagnosiskeys/download/KeyPackageSyncSettings.kt @@ -1,9 +1,11 @@ package de.rki.coronawarnapp.diagnosiskeys.download +import android.annotation.SuppressLint import android.content.Context import com.google.gson.Gson import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.preferences.FlowPreference +import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.serialization.BaseGson import org.joda.time.Instant import javax.inject.Inject @@ -32,6 +34,11 @@ class KeyPackageSyncSettings @Inject constructor( writer = FlowPreference.gsonWriter(gson) ) + @SuppressLint("ApplySharedPref") + fun clear() { + prefs.clearAndNotify() + } + data class LastDownload( val startedAt: Instant, val finishedAt: Instant? = null, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt index 4fa8b6effbc3f77ca7a780ac791cd07eb93f8ffc..fc89eff4627de3bd0fb3069f8937876563fb0341 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/http/CwaWebException.kt @@ -27,7 +27,6 @@ open class CwaSuccessResponseWithCodeMismatchNotSupportedError(statusCode: Int) open class CwaInformationalNotSupportedError(statusCode: Int) : CwaWebException(statusCode) open class CwaRedirectNotSupportedError(statusCode: Int) : CwaWebException(statusCode) -class CwaUnknownHostException : CwaWebException(901) class BadRequestException : CwaClientError(400) class UnauthorizedException : CwaClientError(401) class ForbiddenException : CwaClientError(403) @@ -44,5 +43,6 @@ class ServiceUnavailableException : CwaServerError(503) class GatewayTimeoutException : CwaServerError(504) class HTTPVersionNotSupported : CwaServerError(505) class NetworkAuthenticationRequiredException : CwaServerError(511) +class CwaUnknownHostException : CwaServerError(597) class NetworkReadTimeoutException : CwaServerError(598) class NetworkConnectTimeoutException : CwaServerError(599) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt index bdaf800b81943a52a073d86be4c6f94551347c1c..5f177f2920d7e8bc8bf9738df7e19189f9aa6f0b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/reporting/ExceptionReporter.kt @@ -15,9 +15,11 @@ import de.rki.coronawarnapp.util.HasHumanReadableError import de.rki.coronawarnapp.util.tryHumanReadableError import java.io.PrintWriter import java.io.StringWriter +import java.util.concurrent.CancellationException -fun Throwable.report(exceptionCategory: ExceptionCategory) = +fun Throwable.report(exceptionCategory: ExceptionCategory) { this.report(exceptionCategory, null, null) +} fun Throwable.report( exceptionCategory: ExceptionCategory, @@ -26,7 +28,14 @@ fun Throwable.report( ) { if (CWADebug.isAUnitTest) return + // CancellationException is a part of normal operation. It is used to cancel a running + // asynchronous operation. It is not a failure and should not be reported as such. + if (this is CancellationException) return + reportProblem(tag = prefix, info = suffix) + + if (CWADebug.isAUnitTest) return + val context = CoronaWarnApplication.getAppContext() val intent = Intent(ReportingConstants.ERROR_REPORT_LOCAL_BROADCAST_CHANNEL) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt index bc347abb919fbbeff9ada2c2d768b3ef679334ff..1061abc7e164af4ff5ac41ec2e63776004ebcce3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/HttpErrorParser.kt @@ -26,6 +26,7 @@ import de.rki.coronawarnapp.exception.http.UnauthorizedException import de.rki.coronawarnapp.exception.http.UnsupportedMediaTypeException import okhttp3.Interceptor import okhttp3.Response +import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection @@ -66,6 +67,8 @@ class HttpErrorParser : Interceptor { throw CwaWebException(code) } } + } catch (err: SocketTimeoutException) { + throw NetworkConnectTimeoutException() } catch (err: UnknownHostException) { throw CwaUnknownHostException() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt index c3de7abd0615b66f64d5eb6255c6d1c255e571e8..587603da757c029bf13e0a553538130c9737fa96 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFClient.kt @@ -2,18 +2,21 @@ package de.rki.coronawarnapp.nearby -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider +import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus +import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.joda.time.Instant import timber.log.Timber import java.io.File +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -23,31 +26,26 @@ class ENFClient @Inject constructor( private val diagnosisKeyProvider: DiagnosisKeyProvider, private val tracingStatus: TracingStatus, private val scanningSupport: ScanningSupport, - private val exposureDetectionTracker: ExposureDetectionTracker -) : DiagnosisKeyProvider, TracingStatus, ScanningSupport { + private val exposureWindowProvider: ExposureWindowProvider, + private val exposureDetectionTracker: ExposureDetectionTracker, + private val enfVersion: ENFVersion +) : DiagnosisKeyProvider, TracingStatus, ScanningSupport, ExposureWindowProvider, ENFVersion by enfVersion { // TODO Remove this once we no longer need direct access to the ENF Client, // i.e. in **[InternalExposureNotificationClient]** internal val internalClient: ExposureNotificationClient get() = googleENFClient - override suspend fun provideDiagnosisKeys( - keyFiles: Collection<File>, - configuration: ExposureConfiguration?, - token: String - ): Boolean { - Timber.d( - "asyncProvideDiagnosisKeys(keyFiles=%s, configuration=%s, token=%s)", - keyFiles, configuration, token - ) + override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean { + Timber.d("asyncProvideDiagnosisKeys(keyFiles=$keyFiles)") return if (keyFiles.isEmpty()) { Timber.d("No key files submitted, returning early.") true } else { Timber.d("Forwarding %d key files to our DiagnosisKeyProvider.", keyFiles.size) - exposureDetectionTracker.trackNewExposureDetection(token) - diagnosisKeyProvider.provideDiagnosisKeys(keyFiles, configuration, token) + exposureDetectionTracker.trackNewExposureDetection(UUID.randomUUID().toString()) + diagnosisKeyProvider.provideDiagnosisKeys(keyFiles) } } @@ -72,4 +70,6 @@ class ENFClient @Inject constructor( .filter { !it.isCalculating && it.isSuccessful } .maxByOrNull { it.finishedAt ?: Instant.EPOCH } } + + override suspend fun exposureWindows(): List<ExposureWindow> = exposureWindowProvider.exposureWindows() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt index 9d98d5b33bf809dbc19995310857d2cda775fcb6..1d4220ee0df1f0640c4d5587ddd56bfdbd669e77 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ENFModule.kt @@ -9,10 +9,14 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.DefaultExposureDetec import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DefaultDiagnosisKeyProvider import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider +import de.rki.coronawarnapp.nearby.modules.exposurewindow.DefaultExposureWindowProvider +import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider import de.rki.coronawarnapp.nearby.modules.locationless.DefaultScanningSupport import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport import de.rki.coronawarnapp.nearby.modules.tracing.DefaultTracingStatus import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus +import de.rki.coronawarnapp.nearby.modules.version.DefaultENFVersion +import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Singleton @@ -39,8 +43,17 @@ class ENFModule { fun scanningSupport(scanningSupport: DefaultScanningSupport): ScanningSupport = scanningSupport + @Singleton + @Provides + fun exposureWindowProvider(exposureWindowProvider: DefaultExposureWindowProvider): ExposureWindowProvider = + exposureWindowProvider + @Singleton @Provides fun calculationTracker(exposureDetectionTracker: DefaultExposureDetectionTracker): ExposureDetectionTracker = exposureDetectionTracker + + @Singleton + @Provides + fun enfClientVersion(enfVersion: DefaultENFVersion): ENFVersion = enfVersion } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt index b0c92e94c97c65e27ed314b736c0a94589c994c8..f0f0ed5c5ff56d8758407875de879465d2e79560 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/ExposureStateUpdateWorker.kt @@ -4,14 +4,13 @@ import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.google.android.gms.common.api.ApiException -import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.NoTokenException import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.risk.ExposureResult +import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.risk.RiskLevelTask -import de.rki.coronawarnapp.storage.ExposureSummaryRepository import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory @@ -20,23 +19,22 @@ import timber.log.Timber class ExposureStateUpdateWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, + private val exposureResultStore: ExposureResultStore, + private val enfClient: ENFClient, private val taskController: TaskController ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { try { Timber.v("worker to persist exposure summary started") - val token = inputData.getString(ExposureNotificationClient.EXTRA_TOKEN) - ?: throw NoTokenException(IllegalArgumentException("no token was found in the intent")) - Timber.v("valid token $token retrieved") - InternalExposureNotificationClient - .asyncGetExposureSummary(token).also { - ExposureSummaryRepository.getExposureSummaryRepository() - .insertExposureSummaryEntity(it) - Timber.v("exposure summary state updated: $it") - } + enfClient.exposureWindows().let { + exposureResultStore.entities.value = ExposureResult(it, null) + Timber.v("exposure summary state updated: $it") + } - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) + taskController.submit( + DefaultTaskRequest(RiskLevelTask::class, originTag = "ExposureStateUpdateWorker") + ) Timber.v("risk level calculation triggered") } catch (e: ApiException) { e.report(ExceptionCategory.EXPOSURENOTIFICATION) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt index 53c0aa60066a0dc91d8212d7d03d93bfda8faeb3..3ded662b655f7aa9dc7b87cdada9cbd6425031a6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/InternalExposureNotificationClient.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.nearby -import com.google.android.gms.nearby.exposurenotification.ExposureSummary import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.risk.TimeVariables @@ -89,15 +88,6 @@ object InternalExposureNotificationClient { } } - suspend fun getVersion(): Long = suspendCoroutine { cont -> - exposureNotificationClient.version - .addOnSuccessListener { - cont.resume(it) - }.addOnFailureListener { - cont.resumeWithException(it) - } - } - /** * Retrieves key history from the data store on the device for uploading to your * internet-accessible server. Calling this method prompts Google Play services to display @@ -118,22 +108,4 @@ object InternalExposureNotificationClient { cont.resumeWithException(it) } } - - /** - * Retrieves the ExposureSummary object that matches the token from - * provideDiagnosisKeys() that you provide to the method. The ExposureSummary - * object provides a high-level overview of the exposure that a user has experienced. - * - * @param token - * @return - */ - suspend fun asyncGetExposureSummary(token: String): ExposureSummary = - suspendCoroutine { cont -> - exposureNotificationClient.getExposureSummary(token) - .addOnSuccessListener { - cont.resume(it) - }.addOnFailureListener { - cont.resumeWithException(it) - } - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt index c5e4b8073deb172dcb4856afb9d2406af38ef94d..909a66247fd7405a3239631942052de2892add21 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/DefaultExposureDetectionTracker.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.plus import org.joda.time.Duration import timber.log.Timber +import java.util.UUID import javax.inject.Inject import javax.inject.Singleton import kotlin.math.min @@ -63,7 +64,7 @@ class DefaultExposureDetectionTracker @Inject constructor( } } - delay(TIMEOUT_CHECK_INTERVALL.millis) + delay(TIMEOUT_CHECK_INTERVAL.millis) } }.launchIn(scope + dispatcherProvider.Default) } @@ -93,34 +94,17 @@ class DefaultExposureDetectionTracker @Inject constructor( } } - override fun finishExposureDetection(identifier: String, result: Result) { + override fun finishExposureDetection(identifier: String?, result: Result) { Timber.i("finishExposureDetection(token=%s, result=%s)", identifier, result) detectionStates.updateSafely { mutate { - val existing = this[identifier] - if (existing != null) { - if (existing.result == Result.TIMEOUT) { - Timber.w("Detection is late, already hit timeout, still updating.") - } else if (existing.result != null) { - Timber.e("Duplicate callback. Result is already set for detection!") - } - this[identifier] = existing.copy( - result = result, - finishedAt = timeStamper.nowUTC - ) + if (identifier == null) { + val id = this.findUnfinishedOrCreateIdentifier() + finishDetection(id, result) } else { - Timber.e( - "Unknown detection finished (token=%s, result=%s)", - identifier, - result - ) - this[identifier] = TrackedExposureDetection( - identifier = identifier, - result = result, - startedAt = timeStamper.nowUTC, - finishedAt = timeStamper.nowUTC - ) + finishDetection(identifier, result) } + val toKeep = entries .sortedByDescending { it.value.startedAt } // Keep newest .subList(0, min(entries.size, MAX_ENTRY_SIZE)) @@ -134,9 +118,59 @@ class DefaultExposureDetectionTracker @Inject constructor( } } + private fun Map<String, TrackedExposureDetection>.findUnfinishedOrCreateIdentifier(): String { + val newestUnfinishedDetection = this + .map { it.value } + .filter { it.finishedAt == null } + .maxByOrNull { it.startedAt.millis } + + return if (newestUnfinishedDetection != null) { + Timber.d("findUnfinishedOrCreateIdentifier(): Found unfinished detection, return identifier") + newestUnfinishedDetection.identifier + } else { + Timber.d("findUnfinishedOrCreateIdentifier(): No unfinished detection found, create identifier") + UUID.randomUUID().toString() + } + } + + private fun MutableMap<String, TrackedExposureDetection>.finishDetection(identifier: String, result: Result) { + Timber.i("finishDetection(token=%s, result=%s)", identifier, result) + val existing = this[identifier] + if (existing != null) { + if (existing.result == Result.TIMEOUT) { + Timber.w("Detection is late, already hit timeout, still updating.") + } else if (existing.result != null) { + Timber.e("Duplicate callback. Result is already set for detection!") + } + this[identifier] = existing.copy( + result = result, + finishedAt = timeStamper.nowUTC + ) + } else { + Timber.e( + "Unknown detection finished (token=%s, result=%s)", + identifier, + result + ) + this[identifier] = TrackedExposureDetection( + identifier = identifier, + result = result, + startedAt = timeStamper.nowUTC, + finishedAt = timeStamper.nowUTC + ) + } + } + + override fun clear() { + Timber.i("clear()") + detectionStates.updateSafely { + emptyMap() + } + } + companion object { private const val TAG = "DefaultExposureDetectionTracker" private const val MAX_ENTRY_SIZE = 5 - private val TIMEOUT_CHECK_INTERVALL = Duration.standardMinutes(3) + private val TIMEOUT_CHECK_INTERVAL = Duration.standardMinutes(3) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt index 4d9bcf0c6e318c96a314d71bdd0050f0ecf2bd88..0d6a6e86b50ea87bc3d58913e6773f339b9f410a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTracker.kt @@ -7,5 +7,7 @@ interface ExposureDetectionTracker { fun trackNewExposureDetection(identifier: String) - fun finishExposureDetection(identifier: String, result: TrackedExposureDetection.Result) + fun finishExposureDetection(identifier: String? = null, result: TrackedExposureDetection.Result) + + fun clear() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt index 01e93c4b9ab724a44dc953b0466c2b6a7b366c31..c4299cafe56f85562c86d276a10d910bb5e39287 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorage.kt @@ -2,6 +2,8 @@ package de.rki.coronawarnapp.nearby.modules.detectiontracker import android.content.Context import com.google.gson.Gson +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.fromJson @@ -44,11 +46,13 @@ class ExposureDetectionTrackerStorage @Inject constructor( if (!storageFile.exists()) return@withLock emptyMap() gson.fromJson<Map<String, TrackedExposureDetection>>(storageFile).also { + require(it.size >= 0) Timber.v("Loaded detection data: %s", it) lastCalcuationData = it } } catch (e: Exception) { Timber.e(e, "Failed to load tracked detections.") + if (storageFile.delete()) Timber.w("Storage file was deleted.") emptyMap() } } @@ -63,6 +67,7 @@ class ExposureDetectionTrackerStorage @Inject constructor( gson.toJson(data, storageFile) } catch (e: Exception) { Timber.e(e, "Failed to save tracked detections.") + e.report(ExceptionCategory.INTERNAL) } } } 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 59114d58d4c0bab4709d0568ffb39c052923913d..64f2e75d7addfca50ab691aa7d42bc7d8cd3f4cd 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,12 +1,9 @@ -@file:Suppress("DEPRECATION") - package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider import com.google.android.gms.common.api.ApiException -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.exception.reporting.ReportingConstants -import de.rki.coronawarnapp.util.GoogleAPIVersion +import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import timber.log.Timber import java.io.File import javax.inject.Inject @@ -17,100 +14,41 @@ import kotlin.coroutines.suspendCoroutine @Singleton class DefaultDiagnosisKeyProvider @Inject constructor( - private val googleAPIVersion: GoogleAPIVersion, + private val enfVersion: ENFVersion, private val submissionQuota: SubmissionQuota, private val enfClient: ExposureNotificationClient ) : DiagnosisKeyProvider { - override suspend fun provideDiagnosisKeys( - keyFiles: Collection<File>, - configuration: ExposureConfiguration?, - token: String - ): Boolean { - return try { - if (keyFiles.isEmpty()) { - Timber.d("No key files submitted, returning early.") - return true - } - - val usedConfiguration = if (configuration == null) { - Timber.w("Passed configuration was NULL, creating fallback.") - ExposureConfiguration.ExposureConfigurationBuilder().build() - } else { - configuration - } - - if (googleAPIVersion.isAtLeast(GoogleAPIVersion.V16)) { - provideKeys(keyFiles, usedConfiguration, token) - } else { - provideKeysLegacy(keyFiles, usedConfiguration, token) - } - } catch (e: Exception) { - Timber.e( - e, "Error during provideDiagnosisKeys(keyFiles=%s, configuration=%s, token=%s)", - keyFiles, configuration, token - ) - throw e + override suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean { + if (keyFiles.isEmpty()) { + Timber.d("No key files submitted, returning early.") + return true } - } - private suspend fun provideKeys( - files: Collection<File>, - configuration: ExposureConfiguration, - token: String - ): Boolean { - Timber.d("Using non-legacy key provision.") + // Check version of ENF, WindowMode since v1.5, but version check since v1.6 + // Will throw if requirement is not satisfied + enfVersion.requireMinimumVersion(ENFVersion.V1_6) if (!submissionQuota.consumeQuota(1)) { - Timber.w("Not enough quota available.") - // TODO Currently only logging, we'll be more strict in a future release - // return false - } - - performSubmission(files, configuration, token) - return true - } - - /** - * We use Batch Size 1 and thus submit multiple times to the API. - * This means that instead of directly submitting all files at once, we have to split up - * our file list as this equals a different batch for Google every time. - */ - private suspend fun provideKeysLegacy( - keyFiles: Collection<File>, - configuration: ExposureConfiguration, - token: String - ): Boolean { - Timber.d("Using LEGACY key provision.") - - if (!submissionQuota.consumeQuota(keyFiles.size)) { - Timber.w("Not enough quota available.") - // TODO What about proceeding with partial submission? - // TODO Currently only logging, we'll be more strict in a future release - // return false + Timber.e("No key files submitted because not enough quota available.") + // Needs discussion until armed, concerns: Hiding other underlying issues. +// return false } - keyFiles.forEach { performSubmission(listOf(it), configuration, token) } - return true - } - - private suspend fun performSubmission( - keyFiles: Collection<File>, - configuration: ExposureConfiguration, - token: String - ): Void = suspendCoroutine { cont -> - Timber.d("Performing key submission.") - enfClient - .provideDiagnosisKeys(keyFiles.toList(), configuration, token) - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { - val wrappedException = when { - it is ApiException && it.statusCode == ReportingConstants.STATUS_CODE_REACHED_REQUEST_LIMIT -> { - QuotaExceededException(cause = it) - } - else -> it + return suspendCoroutine { cont -> + Timber.d("Performing key submission.") + enfClient + .provideDiagnosisKeys(keyFiles.toList()) + .addOnSuccessListener { cont.resume(true) } + .addOnFailureListener { + val wrappedException = + when (it is ApiException && + it.statusCode == ReportingConstants.STATUS_CODE_REACHED_REQUEST_LIMIT) { + true -> QuotaExceededException(cause = it) + false -> it + } + cont.resumeWithException(wrappedException) } - cont.resumeWithException(wrappedException) - } + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt index accedeed05ba0a7be5e2259326da4960db3ab3b7..b3339619f5b4e96635d1326a43cfb8d1cc09951e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/DiagnosisKeyProvider.kt @@ -1,25 +1,19 @@ package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import java.io.File interface DiagnosisKeyProvider { /** - * Takes an ExposureConfiguration object. Inserts a list of files that contain key - * information into the on-device database. Provide the keys of confirmed cases retrieved - * from your internet-accessible server to the Google Play service once requested from the - * API. Information about the file format is in the Exposure Key Export File Format and - * Verification document that is linked from google.com/covid19/exposurenotifications. + * Inserts a list of files that contain key information into the on-device database. + * Provide the keys of confirmed cases retrieved from your internet-accessible server to + * the Google Play service once requested from the API. Information about the file format + * is in the Exposure Key Export File Format and Verification document that is linked + * from google.com/covid19/exposurenotifications. * * @param keyFiles - * @param configuration - * @param token * @return */ - suspend fun provideDiagnosisKeys( - keyFiles: Collection<File>, - configuration: ExposureConfiguration?, - token: String - ): Boolean + + suspend fun provideDiagnosisKeys(keyFiles: Collection<File>): Boolean } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt index d9bd53506983a3d65e700bd694c21e6ca8d2a618..b671c3502ad5bf0fc014461b1e182895a6c25875 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuota.kt @@ -86,6 +86,10 @@ class SubmissionQuota @Inject constructor( companion object { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal const val DEFAULT_QUOTA = 20 + /** + * This quota should be 6 when using ExposureWindow + * See: https://developers.google.com/android/exposure-notifications/release-notes + */ + internal const val DEFAULT_QUOTA = 6 } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..5f83c9bba28645b8e7e2e6b1cbb298dbdc71dd00 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/DefaultExposureWindowProvider.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +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 +) : ExposureWindowProvider { + override suspend fun exposureWindows(): List<ExposureWindow> = suspendCoroutine { cont -> + client.exposureWindows + .addOnSuccessListener { cont.resume(it) } + .addOnFailureListener { cont.resumeWithException(it) } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt new file mode 100644 index 0000000000000000000000000000000000000000..713715c879f0f75746ff3503d4d1116875d7fca7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/exposurewindow/ExposureWindowProvider.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.nearby.modules.exposurewindow + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow + +interface ExposureWindowProvider { + suspend fun exposureWindows(): List<ExposureWindow> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt index 1c2581e299982b099739d281873f7e971bab9c3b..c2319109f5914784469d885563b9d82fb7511809 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatus.kt @@ -3,9 +3,10 @@ package de.rki.coronawarnapp.nearby.modules.tracing import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.flow.shareLatest +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.sendBlocking import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -23,28 +24,32 @@ import kotlin.coroutines.suspendCoroutine @Singleton class DefaultTracingStatus @Inject constructor( - private val client: ExposureNotificationClient + private val client: ExposureNotificationClient, + @AppScope val scope: CoroutineScope ) : TracingStatus { override val isTracingEnabled: Flow<Boolean> = callbackFlow<Boolean> { - var isRunning = true - while (isRunning && isActive) { + while (true) { try { - sendBlocking(pollIsEnabled()) + send(pollIsEnabled()) } catch (e: Exception) { Timber.w(e, "ENF isEnabled failed.") - sendBlocking(false) + send(false) e.report(ExceptionCategory.EXPOSURENOTIFICATION, TAG, null) cancel("ENF isEnabled failed", e) } + if (!isActive) break delay(POLLING_DELAY_MS) } - awaitClose { isRunning = false } } .distinctUntilChanged() .onStart { Timber.v("isTracingEnabled FLOW start") } .onEach { Timber.v("isTracingEnabled FLOW emission: %b", it) } .onCompletion { Timber.v("isTracingEnabled FLOW completed.") } + .shareLatest( + tag = TAG, + scope = scope + ) private suspend fun pollIsEnabled(): Boolean = suspendCoroutine { cont -> client.isEnabled 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 new file mode 100644 index 0000000000000000000000000000000000000000..7652fb055bdb64e4cbd76d4528a7047ffbfeef91 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersion.kt @@ -0,0 +1,47 @@ +package de.rki.coronawarnapp.nearby.modules.version + +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.CommonStatusCodes +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import timber.log.Timber +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +@Singleton +class DefaultENFVersion @Inject constructor( + private val client: ExposureNotificationClient +) : ENFVersion { + + override suspend fun getENFClientVersion(): Long? = try { + internalGetENFClientVersion() + } catch (e: Exception) { + Timber.w(e, "Failed to get ENFClient version.") + null + } + + override suspend fun requireMinimumVersion(required: Long) { + try { + val currentVersion = internalGetENFClientVersion() + if (currentVersion < required) { + val error = OutdatedENFVersionException(current = currentVersion, required = required) + Timber.e(error, "Version requirement not satisfied.") + throw error + } else { + Timber.d("Version requirement satisfied: current=$currentVersion, required=$required") + } + } catch (apiException: ApiException) { + if (apiException.statusCode != CommonStatusCodes.API_NOT_CONNECTED) { + throw apiException + } + } + } + + private suspend fun internalGetENFClientVersion(): Long = suspendCoroutine { cont -> + client.version + .addOnSuccessListener { cont.resume(it) } + .addOnFailureListener { cont.resumeWithException(it) } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..b7d16994a91e0742d3910f3c22fd7323b31b6857 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/ENFVersion.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.nearby.modules.version + +interface ENFVersion { + /** + * May return null if the API is currently not connected. + */ + suspend fun getENFClientVersion(): Long? + + /** + * Throws an [OutdatedENFVersionException] if the client runs an old unsupported version of the ENF + * If the API is currently not connected, no exception will be thrown, we expect this to only be a temporary state + */ + suspend fun requireMinimumVersion(required: Long) + + companion object { + const val V1_6 = 16000000L + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/OutdatedENFVersionException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/OutdatedENFVersionException.kt new file mode 100644 index 0000000000000000000000000000000000000000..5cf38d6fb2e81becdbce33e9fba99fac2ec6127b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/nearby/modules/version/OutdatedENFVersionException.kt @@ -0,0 +1,6 @@ +package de.rki.coronawarnapp.nearby.modules.version + +class OutdatedENFVersionException( + val current: Long, + val required: Long +) : Exception("Client is using an outdated ENF version: current=$current, required=$required") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt index dab0da7f05f50d7b6b75c1e4d81d2187113f4c00..72a049c16999f42880d6a0d159cf2501ecda5088 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiver.kt @@ -11,12 +11,11 @@ import com.google.android.gms.nearby.exposurenotification.ExposureNotificationCl import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient.EXTRA_TOKEN import dagger.android.AndroidInjection import de.rki.coronawarnapp.exception.ExceptionCategory.INTERNAL -import de.rki.coronawarnapp.exception.NoTokenException import de.rki.coronawarnapp.exception.UnknownBroadcastException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker -import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection +import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection.Result import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -30,11 +29,8 @@ import javax.inject.Inject * new keys are processed. Then the [ExposureStateUpdateReceiver] will receive the corresponding action in its * [onReceive] function. * - * Inside this receiver no further action or calculation will be done but it is rather used to inform the - * [de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction] that the processing of the diagnosis keys is - * finished and the Exposure Summary can be retrieved in order to calculate a risk level to show to the user. - * - * @see de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction + * Inside this receiver no further action or calculation will be done but it is rather used to start + * a worker that launches the RiskLevelTask which then makes use of the new data this notifies us of. * */ class ExposureStateUpdateReceiver : BroadcastReceiver() { @@ -42,25 +38,33 @@ class ExposureStateUpdateReceiver : BroadcastReceiver() { @Inject @AppScope lateinit var scope: CoroutineScope @Inject lateinit var dispatcherProvider: DispatcherProvider @Inject lateinit var exposureDetectionTracker: ExposureDetectionTracker - lateinit var context: Context + @Inject lateinit var workManager: WorkManager override fun onReceive(context: Context, intent: Intent) { Timber.tag(TAG).d("onReceive(context=%s, intent=%s)", context, intent) AndroidInjection.inject(this, context) - this.context = context val action = intent.action Timber.tag(TAG).v("Looking up action: %s", action) val async = goAsync() - scope.launch(context = dispatcherProvider.Default) { + + scope.launch(context = scope.coroutineContext) { try { - when (action) { - ACTION_EXPOSURE_STATE_UPDATED -> processStateUpdates(intent) - ACTION_EXPOSURE_NOT_FOUND -> processNotFound(intent) - else -> throw UnknownBroadcastException(action) + intent.getStringExtra(EXTRA_TOKEN)?.let { + Timber.tag(TAG).w("Received unknown token from ENF: %s", it) } + + trackDetection(action) + + val data = Data.Builder().build() + OneTimeWorkRequest + .Builder(ExposureStateUpdateWorker::class.java) + .setInputData(data) + .build() + .let { workManager.enqueue(it) } } catch (e: Exception) { + Timber.e(e, "Failed to process intent.") e.report(INTERNAL) } finally { Timber.tag(TAG).i("Finished processing broadcast.") @@ -69,45 +73,18 @@ class ExposureStateUpdateReceiver : BroadcastReceiver() { } } - private fun processStateUpdates(intent: Intent) { - Timber.tag(TAG).i("Processing ACTION_EXPOSURE_STATE_UPDATED") - - val workManager = WorkManager.getInstance(context) - - val token = intent.requireToken() - - val data = Data - .Builder() - .putString(EXTRA_TOKEN, token) - .build() - - OneTimeWorkRequest - .Builder(ExposureStateUpdateWorker::class.java) - .setInputData(data) - .build() - .let { workManager.enqueue(it) } - - exposureDetectionTracker.finishExposureDetection( - token, - TrackedExposureDetection.Result.UPDATED_STATE - ) - } - - private fun processNotFound(intent: Intent) { - Timber.tag(TAG).i("Processing ACTION_EXPOSURE_NOT_FOUND") - - val token = intent.requireToken() - - exposureDetectionTracker.finishExposureDetection( - token, - TrackedExposureDetection.Result.NO_MATCHES - ) + private fun trackDetection(action: String?) { + when (action) { + ACTION_EXPOSURE_STATE_UPDATED -> { + exposureDetectionTracker.finishExposureDetection(identifier = null, result = Result.UPDATED_STATE) + } + ACTION_EXPOSURE_NOT_FOUND -> { + exposureDetectionTracker.finishExposureDetection(identifier = null, result = Result.NO_MATCHES) + } + else -> throw UnknownBroadcastException(action) + } } - private fun Intent.requireToken(): String = getStringExtra(EXTRA_TOKEN).also { - Timber.tag(TAG).v("Extracted token: %s", it) - } ?: throw NoTokenException(IllegalArgumentException("no token was found in the intent")) - companion object { private val TAG: String? = ExposureStateUpdateReceiver::class.simpleName } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt index 68d9b8b15df9a37596fab84e1438c6272dc14954..ab39dda9e64d0849d655603e9c720ff53819c7f8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/DefaultRiskLevels.kt @@ -1,231 +1,325 @@ package de.rki.coronawarnapp.risk +import android.text.TextUtils import androidx.annotation.VisibleForTesting -import androidx.core.app.NotificationManagerCompat -import com.google.android.gms.nearby.exposurenotification.ExposureSummary -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.appconfig.AppConfigProvider -import de.rki.coronawarnapp.exception.RiskLevelCalculationException -import de.rki.coronawarnapp.notification.NotificationHelper -import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_INITIAL -import de.rki.coronawarnapp.risk.RiskLevel.UNKNOWN_RISK_OUTDATED_RESULTS -import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass.AttenuationDuration -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.RiskLevelRepository -import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.Infectiousness +import com.google.android.gms.nearby.exposurenotification.ReportType +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig +import de.rki.coronawarnapp.risk.result.AggregatedRiskPerDateResult +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.result.RiskResult +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import org.joda.time.Instant import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -import kotlin.math.round @Singleton -class DefaultRiskLevels @Inject constructor( - private val appConfigProvider: AppConfigProvider -) : RiskLevels { - - override fun updateRepository(riskLevel: RiskLevel, time: Long) { - val rollbackItems = mutableListOf<RollbackItem>() - try { - Timber.tag(TAG).v("Update the risk level with $riskLevel") - val lastCalculatedRiskLevelScoreForRollback = - RiskLevelRepository.getLastCalculatedScore() - updateRiskLevelScore(riskLevel) - rollbackItems.add { - updateRiskLevelScore(lastCalculatedRiskLevelScoreForRollback) - } +class DefaultRiskLevels @Inject constructor() : RiskLevels { - // risk level calculation date update - val lastCalculatedRiskLevelDate = LocalData.lastTimeRiskLevelCalculation() - LocalData.lastTimeRiskLevelCalculation(time) - rollbackItems.add { - LocalData.lastTimeRiskLevelCalculation(lastCalculatedRiskLevelDate) - } - } catch (error: Exception) { - Timber.tag(TAG).e(error, "Updating the RiskLevelRepository failed.") - - try { - Timber.tag(TAG).d("Initiate Rollback") - for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke() - } catch (rollbackException: Exception) { - Timber.tag(TAG).e(rollbackException, "RiskLevelRepository rollback failed.") - } + override fun determineRisk( + appConfig: ExposureWindowRiskCalculationConfig, + exposureWindows: List<ExposureWindow> + ): AggregatedRiskResult { + val riskResultsPerWindow = + exposureWindows.mapNotNull { window -> + calculateRisk(appConfig, window)?.let { window to it } + }.toMap() - throw error - } + return aggregateResults(appConfig, riskResultsPerWindow) } - override fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean { - // if the last calculation is longer in the past as the defined threshold we return the stale state - val timeSinceLastDiagnosisKeyFetchFromServer = - TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer() - ?: throw RiskLevelCalculationException( - IllegalArgumentException( - "Time since last exposure calculation is null" - ) - ) - /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the - defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */ - return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() > - TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold() - } + private fun ExposureWindow.dropDueToMinutesAtAttenuation( + attenuationFilters: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter> + ) = + attenuationFilters.any { attenuationFilter -> + // Get total seconds at attenuation in exposure window + val secondsAtAttenuation: Double = scanInstances + .filter { attenuationFilter.attenuationRange.inRange(it.minAttenuationDb) } + .fold(.0) { acc, scanInstance -> acc + scanInstance.secondsSinceLastScan } - override fun calculationNotPossibleBecauseOfNoKeys() = - (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also { - if (it) { - Timber.tag(TAG) - .v("No last time diagnosis keys from server fetch timestamp was found") - } + val minutesAtAttenuation = secondsAtAttenuation / 60 + return attenuationFilter.dropIfMinutesInRange.inRange(minutesAtAttenuation) } - override suspend fun isIncreasedRisk(lastExposureSummary: ExposureSummary): Boolean { - val appConfiguration = appConfigProvider.getAppConfig() - Timber.tag(TAG).v("Retrieved configuration from backend") - // custom attenuation parameters to weigh the attenuation - // values provided by the Google API - val attenuationParameters = appConfiguration.attenuationDuration - // these are the defined risk classes. They will divide the calculated - // risk score into the low and increased risk - val riskScoreClassification = appConfiguration.riskScoreClasses - - // calculate the risk score based on the values collected by the Google EN API and - // the backend configuration - val riskScore = calculateRiskScore( - attenuationParameters, - lastExposureSummary - ).also { - Timber.tag(TAG).v("Calculated risk with the given config: $it") - } + private fun ExposureWindow.determineTransmissionRiskLevel( + transmissionRiskLevelEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding + ): Int { - // get the high risk score class - val highRiskScoreClass = - riskScoreClassification.riskClassesList.find { it.label == "HIGH" } - ?: throw RiskLevelCalculationException(IllegalStateException("No high risk score class found")) + val reportTypeOffset = when (reportType) { + ReportType.RECURSIVE -> transmissionRiskLevelEncoding + .reportTypeOffsetRecursive + ReportType.SELF_REPORT -> transmissionRiskLevelEncoding + .reportTypeOffsetSelfReport + ReportType.CONFIRMED_CLINICAL_DIAGNOSIS -> transmissionRiskLevelEncoding + .reportTypeOffsetConfirmedClinicalDiagnosis + ReportType.CONFIRMED_TEST -> transmissionRiskLevelEncoding + .reportTypeOffsetConfirmedTest + else -> throw UnknownReportTypeException() + } - // if the calculated risk score is above the defined level threshold we return the high level risk score - if (withinDefinedLevelThreshold( - riskScore, - highRiskScoreClass.min, - highRiskScoreClass.max - ) - ) { - Timber.tag(TAG) - .v("$riskScore is above the defined min value ${highRiskScoreClass.min}") - return true - } else if (riskScore > highRiskScoreClass.max) { - throw RiskLevelCalculationException( - IllegalStateException("Risk score is above the max threshold for score class") - ) + val infectiousnessOffset = when (infectiousness) { + Infectiousness.HIGH -> transmissionRiskLevelEncoding + .infectiousnessOffsetHigh + else -> transmissionRiskLevelEncoding + .infectiousnessOffsetStandard } - return false + return reportTypeOffset + infectiousnessOffset } - override fun isActiveTracingTimeAboveThreshold(): Boolean { - val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration() - val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours() - val durationTracingIsActiveThreshold = - TimeVariables.getMinActivatedTracingTime().toLong() + private fun dropDueToTransmissionRiskLevel( + transmissionRiskLevel: Int, + transmissionRiskLevelFilters: List<RiskCalculationParametersOuterClass.TrlFilter> + ) = + transmissionRiskLevelFilters.any { + it.dropIfTrlInRange.inRange(transmissionRiskLevel) + } - return (activeTracingDurationInHours >= durationTracingIsActiveThreshold).also { - Timber.tag(TAG).v( - "Active tracing time ($activeTracingDurationInHours h) is above threshold " + - "($durationTracingIsActiveThreshold h): $it" - ) - if (it) { - Timber.tag(TAG).v("Active tracing time is not enough") - } + private fun ExposureWindow.determineWeightedSeconds( + minutesAtAttenuationWeight: List<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight> + ): Double = + scanInstances.fold(.0) { seconds, scanInstance -> + val weight: Double = + minutesAtAttenuationWeight + .filter { it.attenuationRange.inRange(scanInstance.minAttenuationDb) } + .map { it.weight } + .firstOrNull() ?: .0 + seconds + scanInstance.secondsSinceLastScan * weight } - } - override fun calculateRiskScore( - attenuationParameters: AttenuationDuration, - exposureSummary: ExposureSummary - ): Double { - /** all attenuation values are capped to [TimeVariables.MAX_ATTENUATION_DURATION] */ - val weightedAttenuationLow = - attenuationParameters.weights.low - .times(exposureSummary.attenuationDurationsInMinutes[0].capped()) - val weightedAttenuationMid = - attenuationParameters.weights.mid - .times(exposureSummary.attenuationDurationsInMinutes[1].capped()) - val weightedAttenuationHigh = - attenuationParameters.weights.high - .times(exposureSummary.attenuationDurationsInMinutes[2].capped()) - - val maximumRiskScore = exposureSummary.maximumRiskScore.toDouble() - - val defaultBucketOffset = attenuationParameters.defaultBucketOffset.toDouble() - val normalizationDivisor = attenuationParameters.riskScoreNormalizationDivisor.toDouble() - - val attenuationStrings = - "Weighted Attenuation: ($weightedAttenuationLow + $weightedAttenuationMid + " + - "$weightedAttenuationHigh + $defaultBucketOffset)" - Timber.v(attenuationStrings) - - val weightedAttenuationDuration = - weightedAttenuationLow - .plus(weightedAttenuationMid) - .plus(weightedAttenuationHigh) - .plus(defaultBucketOffset) - - Timber.v("Formula used: ($maximumRiskScore / $normalizationDivisor) * $weightedAttenuationDuration") - - val riskScore = (maximumRiskScore / normalizationDivisor) * weightedAttenuationDuration - - return round(riskScore.times(DECIMAL_MULTIPLIER)).div(DECIMAL_MULTIPLIER) - } + private fun determineRiskLevel( + normalizedTime: Double, + timeToRiskLevelMapping: List<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping> + ): ProtoRiskLevel? = + timeToRiskLevelMapping + .filter { it.normalizedTimeRange.inRange(normalizedTime) } + .map { it.riskLevel } + .firstOrNull() - @VisibleForTesting - internal fun Int.capped() = - if (this > TimeVariables.getMaxAttenuationDuration()) { - TimeVariables.getMaxAttenuationDuration() - } else { - this + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun calculateRisk( + appConfig: ExposureWindowRiskCalculationConfig, + exposureWindow: ExposureWindow + ): RiskResult? { + if (exposureWindow.dropDueToMinutesAtAttenuation(appConfig.minutesAtAttenuationFilters)) { + Timber.d("%s dropped due to minutes at attenuation filter", exposureWindow) + return null } - @VisibleForTesting - internal fun withinDefinedLevelThreshold(riskScore: Double, min: Int, max: Int) = - riskScore >= min && riskScore <= max - - /** - * Updates the Risk Level Score in the repository with the calculated Risk Level - * - * @param riskLevel - */ - @VisibleForTesting - internal fun updateRiskLevelScore(riskLevel: RiskLevel) { - val lastCalculatedScore = RiskLevelRepository.getLastCalculatedScore() - Timber.d("last CalculatedS core is ${lastCalculatedScore.raw} and Current Risk Level is ${riskLevel.raw}") - - if (RiskLevel.riskLevelChangedBetweenLowAndHigh( - lastCalculatedScore, - riskLevel - ) && !LocalData.submissionWasSuccessful() - ) { + val transmissionRiskLevel: Int = exposureWindow.determineTransmissionRiskLevel( + appConfig.transmissionRiskLevelEncoding + ) + + if (dropDueToTransmissionRiskLevel(transmissionRiskLevel, appConfig.transmissionRiskLevelFilters)) { Timber.d( - "Notification Permission = ${ - NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled() - }" + "%s dropped due to transmission risk level filter, level is %s", + exposureWindow, + transmissionRiskLevel ) + return null + } - NotificationHelper.sendNotification( - CoronaWarnApplication.getAppContext().getString(R.string.notification_body) - ) + val transmissionRiskValue: Double = + transmissionRiskLevel * appConfig.transmissionRiskLevelMultiplier + + Timber.d("%s's transmissionRiskValue is: %s", exposureWindow, transmissionRiskValue) + + val weightedMinutes: Double = exposureWindow.determineWeightedSeconds( + appConfig.minutesAtAttenuationWeights + ) / 60f + + Timber.d("%s's weightedMinutes are: %s", exposureWindow, weightedMinutes) + + val normalizedTime: Double = transmissionRiskValue * weightedMinutes + + Timber.d("%s's normalizedTime is: %s", exposureWindow, normalizedTime) + + val riskLevel: ProtoRiskLevel? = determineRiskLevel( + normalizedTime, + appConfig.normalizedTimePerExposureWindowToRiskLevelMapping + ) + + if (riskLevel == null) { + Timber.e("Exposure Window: $exposureWindow could not be mapped to a risk level") + throw NormalizedTimePerExposureWindowToRiskLevelMappingMissingException() + } + + Timber.d("%s's riskLevel is: %s", exposureWindow, riskLevel) + + return RiskResult( + transmissionRiskLevel = transmissionRiskLevel, + normalizedTime = normalizedTime, + riskLevel = riskLevel + ) + } - Timber.d("Risk level changed and notification sent. Current Risk level is ${riskLevel.raw}") + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun aggregateResults( + appConfig: ExposureWindowRiskCalculationConfig, + exposureWindowsAndResult: Map<ExposureWindow, RiskResult> + ): AggregatedRiskResult { + val uniqueDatesMillisSinceEpoch = exposureWindowsAndResult.keys + .map { it.dateMillisSinceEpoch } + .toSet() + + Timber.d( + "uniqueDates: %s", { TextUtils.join(System.lineSeparator(), uniqueDatesMillisSinceEpoch) } + ) + val exposureHistory = uniqueDatesMillisSinceEpoch.map { + aggregateRiskPerDate(appConfig, it, exposureWindowsAndResult) } - if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK && - riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK) { - LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true - Timber.d("Risk level changed LocalData is updated. Current Risk level is ${riskLevel.raw}") + Timber.d("exposureHistory size: ${exposureHistory.size}") + + // 6. Determine `Total Risk` + val totalRiskLevel = + if (exposureHistory.any { + it.riskLevel == RiskCalculationParametersOuterClass + .NormalizedTimeToRiskLevelMapping + .RiskLevel + .HIGH + }) { + RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH + } else { + RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW + } + + Timber.d("totalRiskLevel: ${totalRiskLevel.name} (${totalRiskLevel.ordinal})") + + // 7. Determine `Date of Most Recent Date with Low Risk` + val mostRecentDateWithLowRisk = + exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.LOW) + + Timber.d("mostRecentDateWithLowRisk: $mostRecentDateWithLowRisk") + + // 8. Determine `Date of Most Recent Date with High Risk` + val mostRecentDateWithHighRisk = + exposureHistory.mostRecentDateForRisk(ProtoRiskLevel.HIGH) + + Timber.d("mostRecentDateWithHighRisk: $mostRecentDateWithHighRisk") + + // 9. Determine `Total Minimum Distinct Encounters With Low Risk` + val totalMinimumDistinctEncountersWithLowRisk = exposureHistory + .sumBy { it.minimumDistinctEncountersWithLowRisk } + + Timber.d("totalMinimumDistinctEncountersWithLowRisk: $totalMinimumDistinctEncountersWithLowRisk") + + // 10. Determine `Total Minimum Distinct Encounters With High Risk` + val totalMinimumDistinctEncountersWithHighRisk = exposureHistory + .sumBy { it.minimumDistinctEncountersWithHighRisk } + + Timber.d("totalMinimumDistinctEncountersWithHighRisk: $totalMinimumDistinctEncountersWithHighRisk") + + // 11. Determine `Number of Days With Low Risk` + val numberOfDaysWithLowRisk = + exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.LOW) + + Timber.d("numberOfDaysWithLowRisk: $numberOfDaysWithLowRisk") + + // 12. Determine `Number of Days With High Risk` + val numberOfDaysWithHighRisk = + exposureHistory.numberOfDaysForRisk(ProtoRiskLevel.HIGH) + + Timber.d("numberOfDaysWithHighRisk: $numberOfDaysWithHighRisk") + + return AggregatedRiskResult( + totalRiskLevel = totalRiskLevel, + totalMinimumDistinctEncountersWithLowRisk = totalMinimumDistinctEncountersWithLowRisk, + totalMinimumDistinctEncountersWithHighRisk = totalMinimumDistinctEncountersWithHighRisk, + mostRecentDateWithLowRisk = mostRecentDateWithLowRisk, + mostRecentDateWithHighRisk = mostRecentDateWithHighRisk, + numberOfDaysWithLowRisk = numberOfDaysWithLowRisk, + numberOfDaysWithHighRisk = numberOfDaysWithHighRisk + ) + } + + private fun List<AggregatedRiskPerDateResult>.mostRecentDateForRisk(riskLevel: ProtoRiskLevel): Instant? = + filter { it.riskLevel == riskLevel } + .maxOfOrNull { it.dateMillisSinceEpoch } + ?.let { Instant.ofEpochMilli(it) } + + private fun List<AggregatedRiskPerDateResult>.numberOfDaysForRisk(riskLevel: ProtoRiskLevel): Int = + filter { it.riskLevel == riskLevel } + .size + + private fun aggregateRiskPerDate( + appConfig: ExposureWindowRiskCalculationConfig, + dateMillisSinceEpoch: Long, + exposureWindowsAndResult: Map<ExposureWindow, RiskResult> + ): AggregatedRiskPerDateResult { + // 1. Group `Exposure Windows by Date` + val exposureWindowsAndResultForDate = exposureWindowsAndResult + .filter { it.key.dateMillisSinceEpoch == dateMillisSinceEpoch } + + // 2. Determine `Normalized Time per Date` + val normalizedTime = exposureWindowsAndResultForDate.values + .sumOf { it.normalizedTime } + + Timber.d("Aggregating result for date $dateMillisSinceEpoch - ${Instant.ofEpochMilli(dateMillisSinceEpoch)}") + + // 3. Determine `Risk Level per Date` + val riskLevel = try { + appConfig.normalizedTimePerDayToRiskLevelMappingList + .filter { it.normalizedTimeRange.inRange(normalizedTime) } + .map { it.riskLevel } + .first() + } catch (e: Exception) { + throw NormalizedTimePerDayToRiskLevelMappingMissingException() } - RiskLevelRepository.setRiskLevelScore(riskLevel) + + Timber.d("riskLevel: ${riskLevel.name} (${riskLevel.ordinal})") + + // 4. Determine `Minimum Distinct Encounters With Low Risk per Date` + val minimumDistinctEncountersWithLowRisk = + exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.LOW) + + Timber.d("minimumDistinctEncountersWithLowRisk: $minimumDistinctEncountersWithLowRisk") + + // 5. Determine `Minimum Distinct Encounters With High Risk per Date` + val minimumDistinctEncountersWithHighRisk = + exposureWindowsAndResultForDate.minimumDistinctEncountersForRisk(ProtoRiskLevel.HIGH) + + Timber.d("minimumDistinctEncountersWithHighRisk: $minimumDistinctEncountersWithHighRisk") + + return AggregatedRiskPerDateResult( + dateMillisSinceEpoch = dateMillisSinceEpoch, + riskLevel = riskLevel, + minimumDistinctEncountersWithLowRisk = minimumDistinctEncountersWithLowRisk, + minimumDistinctEncountersWithHighRisk = minimumDistinctEncountersWithHighRisk + ) } + private fun Map<ExposureWindow, RiskResult>.minimumDistinctEncountersForRisk(riskLevel: ProtoRiskLevel): Int = + filter { it.value.riskLevel == riskLevel } + .map { "${it.value.transmissionRiskLevel}_${it.key.calibrationConfidence}" } + .distinct() + .size + companion object { - private val TAG = DefaultRiskLevels::class.java.simpleName - private const val DECIMAL_MULTIPLIER = 100 + + open class RiskLevelMappingMissingException(msg: String) : Exception(msg) + + class NormalizedTimePerExposureWindowToRiskLevelMappingMissingException : RiskLevelMappingMissingException( + "Failed to map the normalized Time per Exposure Window to a Risk Level" + ) + + class NormalizedTimePerDayToRiskLevelMappingMissingException : RiskLevelMappingMissingException( + "Failed to map the normalized Time per Day to a Risk Level" + ) + + class UnknownReportTypeException : Exception( + "The Report Type returned by the ENF is not known" + ) + + private fun <T : Number> RiskCalculationParametersOuterClass.Range.inRange(value: T): Boolean = + when { + minExclusive && value.toDouble() <= min -> false + !minExclusive && value.toDouble() < min -> false + maxExclusive && value.toDouble() >= max -> false + !maxExclusive && value.toDouble() > max -> false + else -> true + } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b26fc49702912b12b519529d3df6e2eced4c749 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ExposureResultStore.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.risk + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ExposureResultStore @Inject constructor() { + + val entities = MutableStateFlow( + ExposureResult( + exposureWindows = emptyList(), + aggregatedRiskResult = null + ) + ) + + internal val internalMatchedKeyCount = MutableStateFlow(0) + val matchedKeyCount: Flow<Int> = internalMatchedKeyCount + + internal val internalDaysSinceLastExposure = MutableStateFlow(0) + val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure +} + +data class ExposureResult( + val exposureWindows: List<ExposureWindow>, + val aggregatedRiskResult: AggregatedRiskResult? +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ProtoRiskLevel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ProtoRiskLevel.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff0013d8e28a58ced5e842815589e6b79196af97 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/ProtoRiskLevel.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.risk + +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass + +typealias ProtoRiskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt new file mode 100644 index 0000000000000000000000000000000000000000..83372c3f5d5d671f21c29ceb1c478e23df6fed79 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelData.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import androidx.core.content.edit +import de.rki.coronawarnapp.util.di.AppContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RiskLevelData @Inject constructor( + @AppContext private val context: Context +) { + + private val prefs by lazy { + context.getSharedPreferences(NAME_SHARED_PREFS, Context.MODE_PRIVATE) + } + + /** + * The identifier of the config used during the last risklevel calculation + */ + var lastUsedConfigIdentifier: String? + get() = prefs.getString(PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID, null) + set(value) = prefs.edit(true) { + putString(PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID, value) + } + + companion object { + private const val NAME_SHARED_PREFS = "risklevel_localdata" + private const val PKEY_RISKLEVEL_CALC_LAST_CONFIG_ID = "risklevel.config.identifier.last" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt index 81959b959a242b1a4640ffd23c399780f48cea1a..8069454c2b5225e5b157f831777d184519d483d7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelTask.kt @@ -1,12 +1,18 @@ package de.rki.coronawarnapp.risk import android.content.Context -import com.google.android.gms.nearby.exposurenotification.ExposureSummary +import androidx.annotation.VisibleForTesting +import androidx.core.app.NotificationManagerCompat +import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.RiskLevelCalculationException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.risk.RiskLevel.INCREASED_RISK import de.rki.coronawarnapp.risk.RiskLevel.LOW_LEVEL_RISK import de.rki.coronawarnapp.risk.RiskLevel.NO_CALCULATION_POSSIBLE_TRACING_OFF @@ -22,6 +28,7 @@ import de.rki.coronawarnapp.task.TaskFactory import de.rki.coronawarnapp.task.common.DefaultProgress import de.rki.coronawarnapp.util.BackgroundModeStatus import de.rki.coronawarnapp.util.ConnectivityHelper.isNetworkEnabled +import de.rki.coronawarnapp.util.TimeAndDateExtensions.millisecondsToHours import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.channels.ConflatedBroadcastChannel @@ -38,7 +45,10 @@ class RiskLevelTask @Inject constructor( @AppContext private val context: Context, private val enfClient: ENFClient, private val timeStamper: TimeStamper, - private val backgroundModeStatus: BackgroundModeStatus + private val backgroundModeStatus: BackgroundModeStatus, + private val riskLevelData: RiskLevelData, + private val appConfigProvider: AppConfigProvider, + private val exposureResultStore: ExposureResultStore ) : Task<DefaultProgress, RiskLevelTask.Result> { private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() @@ -49,8 +59,7 @@ class RiskLevelTask @Inject constructor( override suspend fun run(arguments: Task.Arguments): Result { try { Timber.d("Running with arguments=%s", arguments) - // If there is no connectivity the transaction will set the last calculated - // risk level + // If there is no connectivity the transaction will set the last calculated risk level if (!isNetworkEnabled(context)) { RiskLevelRepository.setLastCalculatedRiskLevelAsCurrent() return Result(UNDETERMINED) @@ -60,36 +69,37 @@ class RiskLevelTask @Inject constructor( return Result(NO_CALCULATION_POSSIBLE_TRACING_OFF) } - with(riskLevels) { - return Result( - when { - calculationNotPossibleBecauseOfNoKeys().also { - checkCancel() - } -> UNKNOWN_RISK_INITIAL - - calculationNotPossibleBecauseOfOutdatedResults().also { - checkCancel() - } -> if (backgroundJobsEnabled()) { - UNKNOWN_RISK_OUTDATED_RESULTS - } else { - UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL - } - - isIncreasedRisk(getNewExposureSummary()).also { - checkCancel() - } -> INCREASED_RISK - - !isActiveTracingTimeAboveThreshold().also { - checkCancel() - } -> UNKNOWN_RISK_INITIAL - - else -> LOW_LEVEL_RISK - }.also { + val configData: ConfigData = appConfigProvider.getAppConfig() + + return Result( + when { + calculationNotPossibleBecauseOfNoKeys().also { + checkCancel() + } -> UNKNOWN_RISK_INITIAL + + calculationNotPossibleBecauseOfOutdatedResults().also { checkCancel() - updateRepository(it, timeStamper.nowUTC.millis) + } -> if (backgroundJobsEnabled()) { + UNKNOWN_RISK_OUTDATED_RESULTS + } else { + UNKNOWN_RISK_OUTDATED_RESULTS_MANUAL } - ) - } + + isIncreasedRisk(configData).also { + checkCancel() + } -> INCREASED_RISK + + !isActiveTracingTimeAboveThreshold().also { + checkCancel() + } -> UNKNOWN_RISK_INITIAL + + else -> LOW_LEVEL_RISK + }.also { + checkCancel() + updateRepository(it, timeStamper.nowUTC.millis) + riskLevelData.lastUsedConfigIdentifier = configData.identifier + } + ) } catch (error: Exception) { Timber.tag(TAG).e(error) error.report(ExceptionCategory.EXPOSURENOTIFICATION) @@ -100,22 +110,119 @@ class RiskLevelTask @Inject constructor( } } + private fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean { + // if the last calculation is longer in the past as the defined threshold we return the stale state + val timeSinceLastDiagnosisKeyFetchFromServer = + TimeVariables.getTimeSinceLastDiagnosisKeyFetchFromServer() + ?: throw RiskLevelCalculationException( + IllegalArgumentException("Time since last exposure calculation is null") + ) + /** we only return outdated risk level if the threshold is reached AND the active tracing time is above the + defined threshold because [UNKNOWN_RISK_INITIAL] overrules [UNKNOWN_RISK_OUTDATED_RESULTS] */ + return timeSinceLastDiagnosisKeyFetchFromServer.millisecondsToHours() > + TimeVariables.getMaxStaleExposureRiskRange() && isActiveTracingTimeAboveThreshold() + } + + private fun calculationNotPossibleBecauseOfNoKeys() = + (TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() == null).also { + if (it) { + Timber.tag(TAG) + .v("No last time diagnosis keys from server fetch timestamp was found") + } + } + + private fun isActiveTracingTimeAboveThreshold(): Boolean { + val durationTracingIsActive = TimeVariables.getTimeActiveTracingDuration() + val activeTracingDurationInHours = durationTracingIsActive.millisecondsToHours() + val durationTracingIsActiveThreshold = TimeVariables.getMinActivatedTracingTime().toLong() + + return (activeTracingDurationInHours >= durationTracingIsActiveThreshold).also { + Timber.tag(TAG).v( + "Active tracing time ($activeTracingDurationInHours h) is above threshold " + + "($durationTracingIsActiveThreshold h): $it" + ) + } + } + + private suspend fun isIncreasedRisk(configData: ExposureWindowRiskCalculationConfig): Boolean { + val exposureWindows = enfClient.exposureWindows() + + return riskLevels.determineRisk(configData, exposureWindows).apply { + // TODO This should be solved differently, by saving a more specialised result object + if (isIncreasedRisk()) { + exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithHighRisk + exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithHighRisk + } else { + exposureResultStore.internalMatchedKeyCount.value = totalMinimumDistinctEncountersWithLowRisk + exposureResultStore.internalDaysSinceLastExposure.value = numberOfDaysWithLowRisk + } + exposureResultStore.entities.value = ExposureResult(exposureWindows, this) + }.isIncreasedRisk() + } + + private fun updateRepository(riskLevel: RiskLevel, time: Long) { + val rollbackItems = mutableListOf<RollbackItem>() + try { + Timber.tag(TAG).v("Update the risk level with $riskLevel") + val lastCalculatedRiskLevelScoreForRollback = RiskLevelRepository.getLastCalculatedScore() + updateRiskLevelScore(riskLevel) + rollbackItems.add { + updateRiskLevelScore(lastCalculatedRiskLevelScoreForRollback) + } + + // risk level calculation date update + val lastCalculatedRiskLevelDate = LocalData.lastTimeRiskLevelCalculation() + LocalData.lastTimeRiskLevelCalculation(time) + rollbackItems.add { + LocalData.lastTimeRiskLevelCalculation(lastCalculatedRiskLevelDate) + } + } catch (error: Exception) { + Timber.tag(TAG).e(error, "Updating the RiskLevelRepository failed.") + + try { + Timber.tag(TAG).d("Initiate Rollback") + for (rollbackItem: RollbackItem in rollbackItems) rollbackItem.invoke() + } catch (rollbackException: Exception) { + Timber.tag(TAG).e(rollbackException, "RiskLevelRepository rollback failed.") + } + + throw error + } + } + /** - * If there is no persisted exposure summary we try to get a new one with the last persisted - * Google API token that was used in the [de.rki.coronawarnapp.transaction.RetrieveDiagnosisKeysTransaction] + * Updates the Risk Level Score in the repository with the calculated Risk Level * - * @return a exposure summary from the Google Exposure Notification API + * @param riskLevel */ - private suspend fun getNewExposureSummary(): ExposureSummary { - val googleToken = LocalData.googleApiToken() - ?: throw RiskLevelCalculationException(IllegalStateException("Exposure summary is not persisted")) + @VisibleForTesting + internal fun updateRiskLevelScore(riskLevel: RiskLevel) { + val lastCalculatedScore = RiskLevelRepository.getLastCalculatedScore() + Timber.d("last CalculatedS core is ${lastCalculatedScore.raw} and Current Risk Level is ${riskLevel.raw}") + + if (RiskLevel.riskLevelChangedBetweenLowAndHigh(lastCalculatedScore, riskLevel) && + !LocalData.submissionWasSuccessful() + ) { + Timber.d( + "Notification Permission = ${ + NotificationManagerCompat.from(CoronaWarnApplication.getAppContext()).areNotificationsEnabled() + }" + ) - val exposureSummary = - InternalExposureNotificationClient.asyncGetExposureSummary(googleToken) + NotificationHelper.sendNotification( + CoronaWarnApplication.getAppContext().getString(R.string.notification_body) + ) + + Timber.d("Risk level changed and notification sent. Current Risk level is ${riskLevel.raw}") + } + if (lastCalculatedScore.raw == RiskLevelConstants.INCREASED_RISK && + riskLevel.raw == RiskLevelConstants.LOW_LEVEL_RISK + ) { + LocalData.isUserToBeNotifiedOfLoweredRiskLevel = true - return exposureSummary.also { - Timber.tag(TAG).v("Generated new exposure summary with $googleToken") + Timber.d("Risk level changed LocalData is updated. Current Risk level is ${riskLevel.raw}") } + RiskLevelRepository.setRiskLevelScore(riskLevel) } private fun checkCancel() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt index b8cd2f00c6b4ea61636c4593b03d43b65520078d..a3ee1addcbcb891f02a737e1e913732362049617 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevels.kt @@ -1,29 +1,13 @@ package de.rki.coronawarnapp.risk -import com.google.android.gms.nearby.exposurenotification.ExposureSummary -import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult interface RiskLevels { - fun calculationNotPossibleBecauseOfNoKeys(): Boolean - - fun calculationNotPossibleBecauseOfOutdatedResults(): Boolean - - /** - * true if threshold is reached / if the duration of the activated tracing time is above the - * defined value - */ - fun isActiveTracingTimeAboveThreshold(): Boolean - - suspend fun isIncreasedRisk(lastExposureSummary: ExposureSummary): Boolean - - fun updateRepository( - riskLevel: RiskLevel, - time: Long - ) - - fun calculateRiskScore( - attenuationParameters: AttenuationDurationOuterClass.AttenuationDuration, - exposureSummary: ExposureSummary - ): Double + fun determineRisk( + appConfig: ExposureWindowRiskCalculationConfig, + exposureWindows: List<ExposureWindow> + ): AggregatedRiskResult } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..99c140888f23c365690f3671430e015cf966d96b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskPerDateResult.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.risk.result + +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass + +data class AggregatedRiskPerDateResult( + val dateMillisSinceEpoch: Long, + val riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel, + val minimumDistinctEncountersWithLowRisk: Int, + val minimumDistinctEncountersWithHighRisk: Int +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..07595cd56e098af5055339c2ce3cbfcbf07ed19b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/AggregatedRiskResult.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.risk.result + +import de.rki.coronawarnapp.risk.ProtoRiskLevel +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import org.joda.time.Instant + +data class AggregatedRiskResult( + val totalRiskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel, + val totalMinimumDistinctEncountersWithLowRisk: Int, + val totalMinimumDistinctEncountersWithHighRisk: Int, + val mostRecentDateWithLowRisk: Instant?, + val mostRecentDateWithHighRisk: Instant?, + val numberOfDaysWithLowRisk: Int, + val numberOfDaysWithHighRisk: Int +) { + + fun isIncreasedRisk(): Boolean = totalRiskLevel == ProtoRiskLevel.HIGH +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..ee04a41efb0ae2a353bb2dcd7e232ca31cd9059c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/result/RiskResult.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.risk.result + +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass + +data class RiskResult( + val transmissionRiskLevel: Int, + val normalizedTime: Double, + val riskLevel: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel +) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt index 66fc87e9d7b3b18e41d0a565b070c6c8e4eb2ad4..1f37983a9d50edb4ab09fa0b658c65833b3d1e77 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/AppDatabase.kt @@ -62,7 +62,6 @@ abstract class AppDatabase : RoomDatabase() { val keyRepository = AppInjector.component.keyCacheRepository runBlocking { keyRepository.clear() } // TODO this is not nice TracingIntervalRepository.resetInstance() - ExposureSummaryRepository.resetInstance() } private fun buildDatabase(context: Context): AppDatabase { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/ExposureSummaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/ExposureSummaryRepository.kt deleted file mode 100644 index bd02029445025f01ca0deee0ece67037445c4874..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/ExposureSummaryRepository.kt +++ /dev/null @@ -1,70 +0,0 @@ -package de.rki.coronawarnapp.storage - -import com.google.android.gms.nearby.exposurenotification.ExposureSummary -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow - -class ExposureSummaryRepository(private val exposureSummaryDao: ExposureSummaryDao) { - companion object { - @Volatile - private var instance: ExposureSummaryRepository? = null - - private fun getInstance(exposureSummaryDao: ExposureSummaryDao) = - instance ?: synchronized(this) { - instance ?: ExposureSummaryRepository(exposureSummaryDao).also { instance = it } - } - - fun resetInstance() = synchronized(this) { - instance = null - } - - fun getExposureSummaryRepository(): ExposureSummaryRepository { - return getInstance( - AppDatabase.getInstance(CoronaWarnApplication.getAppContext()) - .exposureSummaryDao() - ) - } - - private val internalMatchedKeyCount = MutableStateFlow(0) - val matchedKeyCount: Flow<Int> = internalMatchedKeyCount - - private val internalDaysSinceLastExposure = MutableStateFlow(0) - val daysSinceLastExposure: Flow<Int> = internalDaysSinceLastExposure - } - - suspend fun getExposureSummaryEntities() = exposureSummaryDao.getExposureSummaryEntities() - .map { it.convertToExposureSummary() } - - suspend fun insertExposureSummaryEntity(exposureSummary: ExposureSummary) = - ExposureSummaryEntity().apply { - this.daysSinceLastExposure = exposureSummary.daysSinceLastExposure - this.matchedKeyCount = exposureSummary.matchedKeyCount - this.maximumRiskScore = exposureSummary.maximumRiskScore - this.summationRiskScore = exposureSummary.summationRiskScore - this.attenuationDurationsInMinutes = - exposureSummary.attenuationDurationsInMinutes.toTypedArray().toList() - }.run { - exposureSummaryDao.insertExposureSummaryEntity(this) - internalMatchedKeyCount.value = matchedKeyCount - internalDaysSinceLastExposure.value = daysSinceLastExposure - } - - suspend fun getLatestExposureSummary(token: String) { - if (InternalExposureNotificationClient.asyncIsEnabled()) - InternalExposureNotificationClient.asyncGetExposureSummary(token).also { - internalMatchedKeyCount.value = it.matchedKeyCount - internalDaysSinceLastExposure.value = it.daysSinceLastExposure - } - } - - private fun ExposureSummaryEntity.convertToExposureSummary() = - ExposureSummary.ExposureSummaryBuilder() - .setAttenuationDurations(this.attenuationDurationsInMinutes.toIntArray()) - .setDaysSinceLastExposure(this.daysSinceLastExposure) - .setMatchedKeyCount(this.matchedKeyCount) - .setMaximumRiskScore(this.maximumRiskScore) - .setSummationRiskScore(this.summationRiskScore) - .build() -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt index 04b08cdd10b8ef86ebcb046d27c86077fdc6fbf9..fd533993e2ecbee697a3ec5fc5c2215ce056b7c1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/LocalData.kt @@ -5,9 +5,11 @@ import androidx.core.content.edit import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R import de.rki.coronawarnapp.risk.RiskLevel +import de.rki.coronawarnapp.util.preferences.createFlowPreference import de.rki.coronawarnapp.util.security.SecurityHelper.globalEncryptedSharedPreferencesInstance import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map import java.util.Date /** @@ -381,40 +383,24 @@ object LocalData { * SERVER FETCH DATA ****************************************************/ - /** - * Gets the last time the server fetched the diagnosis keys from the server as Date object - * from the EncryptedSharedPrefs - * - * @return timestamp as Date - */ - // TODO should be changed to Long as well to align with other timestamps - fun lastTimeDiagnosisKeysFromServerFetch(): Date? { - val time = getSharedPreferenceInstance().getLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_diagnosis_keys_fetch), - 0L - ) - if (time == 0L) return null - - return Date(time) + private val dateMapperForFetchTime: (Long) -> Date? = { + if (it != 0L) Date(it) else null } - /** - * Sets the last time the server fetched the diagnosis keys from the server as Date object - * from the EncryptedSharedPrefs - * - * @param value timestamp as Date - */ - fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) { - getSharedPreferenceInstance().edit(true) { - putLong( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_timestamp_diagnosis_keys_fetch), - value?.time ?: 0L - ) - } + private val lastTimeDiagnosisKeysFetchedFlowPref by lazy { + getSharedPreferenceInstance() + .createFlowPreference<Long>(key = "preference_timestamp_diagnosis_keys_fetch", 0L) } + fun lastTimeDiagnosisKeysFromServerFetchFlow() = lastTimeDiagnosisKeysFetchedFlowPref.flow + .map { dateMapperForFetchTime(it) } + + fun lastTimeDiagnosisKeysFromServerFetch() = + dateMapperForFetchTime(lastTimeDiagnosisKeysFetchedFlowPref.value) + + fun lastTimeDiagnosisKeysFromServerFetch(value: Date?) = + lastTimeDiagnosisKeysFetchedFlowPref.update { value?.time ?: 0L } + /** * Gets the last time of successful risk level calculation as long * from the EncryptedSharedPrefs @@ -446,34 +432,6 @@ object LocalData { } } - /**************************************************** - * EXPOSURE NOTIFICATION DATA - ****************************************************/ - - /** - * Gets the last token that was used to provide the diagnosis keys to the Exposure Notification API - * - * @return UUID as string - */ - fun googleApiToken(): String? = getSharedPreferenceInstance().getString( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_string_google_api_token), - null - ) - - /** - * Sets the last token that was used to provide the diagnosis keys to the Exposure Notification API - * - * @param value UUID as string - */ - fun googleApiToken(value: String?) = getSharedPreferenceInstance().edit(true) { - putString( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_string_google_api_token), - value - ) - } - /**************************************************** * SETTINGS DATA ****************************************************/ @@ -705,19 +663,6 @@ object LocalData { CoronaWarnApplication.getAppContext().getString(R.string.preference_teletan), null ) - fun backgroundNotification(value: Boolean) = getSharedPreferenceInstance().edit(true) { - putBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_background_notification), - value - ) - } - - fun backgroundNotification(): Boolean = getSharedPreferenceInstance().getBoolean( - CoronaWarnApplication.getAppContext() - .getString(R.string.preference_background_notification), false - ) - /**************************************************** * ENCRYPTED SHARED PREFERENCES HANDLING ****************************************************/ @@ -740,4 +685,8 @@ object LocalData { putBoolean(PREFERENCE_INTEROPERABILITY_IS_USED_AT_LEAST_ONCE, value) } } + + fun clear() { + lastTimeDiagnosisKeysFetchedFlowPref.update { 0L } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt index 559318598d7d747f8e540e6f3bcf389a2cdcf8d5..a4d580da04fefd742dce2756adef1343e28f14ff 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/RiskLevelRepository.kt @@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow object RiskLevelRepository { - private val internalRisklevelScore = MutableStateFlow(RiskLevelConstants.UNKNOWN_RISK_INITIAL) + private val internalRisklevelScore = MutableStateFlow(getLastSuccessfullyCalculatedScore().raw) val riskLevelScore: Flow<Int> = internalRisklevelScore private val internalRiskLevelScoreLastSuccessfulCalculated = @@ -78,7 +78,7 @@ object RiskLevelRepository { * * @return */ - private fun getLastSuccessfullyCalculatedScore(): RiskLevel = + fun getLastSuccessfullyCalculatedScore(): RiskLevel = LocalData.lastSuccessfullyCalculatedRiskLevel() /** diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt index 21d0216e4ee4e69161d478dc9d1830b709f03294..9fa27a5ef59fa1b7d671376196553a5986027741 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/SubmissionRepository.kt @@ -1,8 +1,6 @@ package de.rki.coronawarnapp.storage import androidx.annotation.VisibleForTesting -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.exception.http.CwaWebException @@ -10,9 +8,9 @@ import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.submission.SubmissionSettings -import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.Event +import de.rki.coronawarnapp.util.NetworkRequestWrapper +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.formatter.TestResult @@ -45,17 +43,12 @@ class SubmissionRepository @Inject constructor( } } - private val uiStateErrorInternal = MutableLiveData<Event<CwaWebException>>(null) - val uiStateError: LiveData<Event<CwaWebException>> = uiStateErrorInternal - - private val uiStateStateFlowInternal = MutableStateFlow(ApiRequestState.IDLE) - val uiStateStateFlow: Flow<ApiRequestState> = uiStateStateFlowInternal - private val testResultReceivedDateFlowInternal = MutableStateFlow(Date()) val testResultReceivedDateFlow: Flow<Date> = testResultReceivedDateFlowInternal - private val deviceUIStateFlowInternal = MutableStateFlow(DeviceUIState.UNPAIRED) - val deviceUIStateFlow: Flow<DeviceUIState> = deviceUIStateFlowInternal + private val deviceUIStateFlowInternal = + MutableStateFlow<NetworkRequestWrapper<DeviceUIState, Throwable>>(NetworkRequestWrapper.RequestIdle) + val deviceUIStateFlow: Flow<NetworkRequestWrapper<DeviceUIState, Throwable>> = deviceUIStateFlowInternal // to be used by new submission flow screens val hasGivenConsentToSubmission = submissionSettings.hasGivenConsent.flow @@ -88,29 +81,29 @@ class SubmissionRepository @Inject constructor( fun refreshDeviceUIState(refreshTestResult: Boolean = true) { var refresh = refreshTestResult - deviceUIStateFlowInternal.value.let { + deviceUIStateFlowInternal.value.withSuccess { if (it != DeviceUIState.PAIRED_NO_RESULT && it != DeviceUIState.UNPAIRED) { refresh = false Timber.d("refreshDeviceUIState: Change refresh, state ${it.name} doesn't require refresh") } } - uiStateStateFlowInternal.value = ApiRequestState.STARTED + deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestStarted + scope.launch { try { deviceUIStateFlowInternal.value = refreshUIState(refresh) - uiStateStateFlowInternal.value = ApiRequestState.SUCCESS } catch (err: CwaWebException) { - uiStateErrorInternal.postValue(Event(err)) - uiStateStateFlowInternal.value = ApiRequestState.FAILED + deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestFailed(err) } catch (err: Exception) { + deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestFailed(err) err.report(ExceptionCategory.INTERNAL) } } } // TODO this should be more UI agnostic - suspend fun refreshUIState(refreshTestResult: Boolean): DeviceUIState { + suspend fun refreshUIState(refreshTestResult: Boolean): NetworkRequestWrapper<DeviceUIState, Throwable> { var uiState = DeviceUIState.UNPAIRED if (LocalData.submissionWasSuccessful()) { @@ -129,31 +122,30 @@ class SubmissionRepository @Inject constructor( } } } - return uiState + return NetworkRequestWrapper.RequestSuccessful(uiState) } - suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { - val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid) + suspend fun asyncRegisterDeviceViaTAN(tan: String) { + val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan) LocalData.registrationToken(registrationData.registrationToken) - LocalData.testGUID(null) + LocalData.teletan(null) updateTestResult(registrationData.testResult) LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) BackgroundNoise.getInstance().scheduleDummyPattern() - return registrationData.testResult } - suspend fun asyncRegisterDeviceViaTAN(tan: String) { - val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan) + suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { + val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid) LocalData.registrationToken(registrationData.registrationToken) - LocalData.teletan(null) + LocalData.testGUID(null) updateTestResult(registrationData.testResult) LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) BackgroundNoise.getInstance().scheduleDummyPattern() + return registrationData.testResult } fun reset() { - uiStateStateFlowInternal.value = ApiRequestState.IDLE - deviceUIStateFlowInternal.value = DeviceUIState.UNPAIRED + deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestIdle revokeConsentToSubmission() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt index 0658b10604134435f471c5ef6f351372f18bdcb0..75be4b8cf6503bb73bdf1a9dbda5c92e1fad2206 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingRepository.kt @@ -2,8 +2,6 @@ package de.rki.coronawarnapp.storage import android.content.Context import de.rki.coronawarnapp.diagnosiskeys.download.DownloadDiagnosisKeysTask -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.risk.RiskLevelTask @@ -15,18 +13,20 @@ import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.timer.TimerHelper import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.util.ConnectivityHelper +import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.di.AppContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.Instant +import org.joda.time.Duration import timber.log.Timber import java.util.Date +import java.util.NoSuchElementException import javax.inject.Inject import javax.inject.Singleton @@ -43,30 +43,18 @@ class TracingRepository @Inject constructor( @AppContext private val context: Context, @AppScope private val scope: CoroutineScope, private val taskController: TaskController, - enfClient: ENFClient + enfClient: ENFClient, + private val timeStamper: TimeStamper ) { - private val internalLastTimeDiagnosisKeysFetched = MutableStateFlow<Date?>(null) - val lastTimeDiagnosisKeysFetched: Flow<Date?> = internalLastTimeDiagnosisKeysFetched + val lastTimeDiagnosisKeysFetched: Flow<Date?> = LocalData.lastTimeDiagnosisKeysFromServerFetchFlow() private val internalActiveTracingDaysInRetentionPeriod = MutableStateFlow(0L) val activeTracingDaysInRetentionPeriod: Flow<Long> = internalActiveTracingDaysInRetentionPeriod - /** - * Refresh the last time diagnosis keys fetched date with the current shared preferences state. - * - * @see LocalData - */ - fun refreshLastTimeDiagnosisKeysFetchedDate() { - internalLastTimeDiagnosisKeysFetched.value = - LocalData.lastTimeDiagnosisKeysFromServerFetch() - } - - private val retrievingDiagnosisKeys = MutableStateFlow(false) private val internalIsRefreshing = - retrievingDiagnosisKeys.combine(taskController.tasks) { retrievingDiagnosisKeys, tasks -> - retrievingDiagnosisKeys || tasks.isRiskLevelTaskRunning() - } + taskController.tasks.map { it.isDownloadDiagnosisKeysTaskRunning() || it.isRiskLevelTaskRunning() } + val tracingProgress: Flow<TracingProgress> = combine( internalIsRefreshing, enfClient.isPerformingExposureDetection() @@ -82,27 +70,31 @@ class TracingRepository @Inject constructor( it.taskState.isActive && it.taskState.request.type == RiskLevelTask::class } + private fun List<TaskInfo>.isDownloadDiagnosisKeysTaskRunning() = any { + it.taskState.isActive && it.taskState.request.type == DownloadDiagnosisKeysTask::class + } + /** * Refresh the diagnosis keys. For that isRefreshing is set to true which is displayed in the ui. * Afterwards the RetrieveDiagnosisKeysTransaction and the RiskLevelTransaction are started. * Regardless of whether the transactions where successful or not the * lastTimeDiagnosisKeysFetchedDate is updated. But the the value will only be updated after a * successful go through from the RetrievelDiagnosisKeysTransaction. - * - * @see RiskLevelRepository */ fun refreshDiagnosisKeys() { scope.launch { - retrievingDiagnosisKeys.value = true taskController.submitBlocking( DefaultTaskRequest( DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments() + DownloadDiagnosisKeysTask.Arguments(), + originTag = "TracingRepository.refreshDiagnosisKeys()" + ) + ) + taskController.submit( + DefaultTaskRequest( + RiskLevelTask::class, originTag = "TracingRepository.refreshDiagnosisKeys()" ) ) - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) - refreshLastTimeDiagnosisKeysFetchedDate() - retrievingDiagnosisKeys.value = false TimerHelper.startManualKeyRetrievalTimer() } } @@ -126,19 +118,6 @@ class TracingRepository @Inject constructor( */ // TODO temp place, this needs to go somewhere better fun refreshRiskLevel() { - - // get the current date and the date the diagnosis keys were fetched the last time - val currentDate = DateTime(Instant.now(), DateTimeZone.UTC) - val lastFetch = DateTime( - LocalData.lastTimeDiagnosisKeysFromServerFetch(), - DateTimeZone.UTC - ) - - // check if the keys were not already retrieved today - val keysWereNotRetrievedToday = - LocalData.lastTimeDiagnosisKeysFromServerFetch() == null || - currentDate.withTimeAtStartOfDay() != lastFetch.withTimeAtStartOfDay() - // check if the network is enabled to make the server fetch val isNetworkEnabled = ConnectivityHelper.isNetworkEnabled(context) @@ -146,56 +125,50 @@ class TracingRepository @Inject constructor( // model the keys are only fetched on button press of the user val isBackgroundJobEnabled = ConnectivityHelper.autoModeEnabled(context) - Timber.tag(TAG).v("Keys were not retrieved today $keysWereNotRetrievedToday") + val wasNotYetFetched = LocalData.lastTimeDiagnosisKeysFromServerFetch() == null + Timber.tag(TAG).v("Network is enabled $isNetworkEnabled") Timber.tag(TAG).v("Background jobs are enabled $isBackgroundJobEnabled") + Timber.tag(TAG).v("Was not yet fetched from server $wasNotYetFetched") - if (keysWereNotRetrievedToday && isNetworkEnabled && isBackgroundJobEnabled) { - // TODO shouldn't access this directly - retrievingDiagnosisKeys.value = true - - // start the fetching and submitting of the diagnosis keys + if (isNetworkEnabled && isBackgroundJobEnabled) { scope.launch { - taskController.submitBlocking( - DefaultTaskRequest( - DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments() + if (wasNotYetFetched || downloadDiagnosisKeysTaskDidNotRunRecently()) { + Timber.tag(TAG).v("Start the fetching and submitting of the diagnosis keys") + + taskController.submitBlocking( + DefaultTaskRequest( + DownloadDiagnosisKeysTask::class, + DownloadDiagnosisKeysTask.Arguments(), + originTag = "TracingRepository.refreshRisklevel()" + ) ) - ) - refreshLastTimeDiagnosisKeysFetchedDate() - TimerHelper.checkManualKeyRetrievalTimer() + TimerHelper.checkManualKeyRetrievalTimer() - taskController.submit(DefaultTaskRequest(RiskLevelTask::class)) - // TODO shouldn't access this directly - retrievingDiagnosisKeys.value = false + taskController.submit( + DefaultTaskRequest(RiskLevelTask::class, originTag = "TracingRepository.refreshRiskLevel()") + ) + } } } } - /** - * Exposure summary - * Refresh the following variables in TracingRepository - * - daysSinceLastExposure - * - matchedKeysCount - * - * @see TracingRepository - */ - fun refreshExposureSummary() { - scope.launch { - try { - val token = LocalData.googleApiToken() - if (token != null) { - ExposureSummaryRepository.getExposureSummaryRepository() - .getLatestExposureSummary(token) - } - Timber.tag(TAG).v("retrieved latest exposure summary from db") - } catch (e: Exception) { - e.report( - ExceptionCategory.EXPOSURENOTIFICATION, - TAG, - null - ) - } + private suspend fun downloadDiagnosisKeysTaskDidNotRunRecently(): Boolean { + val currentDate = timeStamper.nowUTC + val taskLastFinishedAt = try { + taskController.tasks.first() + .filter { it.taskState.type == DownloadDiagnosisKeysTask::class } + .mapNotNull { it.taskState.finishedAt } + .sortedDescending() + .first() + } catch (e: NoSuchElementException) { + Timber.tag(TAG).v("download did not run recently - no task with a finishedAt date found") + return true + } + + return currentDate.isAfter(taskLastFinishedAt.plus(Duration.standardHours(1))).also { + Timber.tag(TAG) + .v("download did not run recently: %s (last=%s, now=%s)", it, taskLastFinishedAt, currentDate) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt index 769df6264d8d24e3abd186fd2acfd57e110e30e4..0743eb1aaa05b52dccde5cde3b2f29e057bded17 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/Symptoms.kt @@ -6,6 +6,9 @@ import org.joda.time.LocalDate @Parcelize data class Symptoms( + /** + * this is null if there are no symptoms or there is no information + */ val startOfSymptoms: StartOf?, val symptomIndication: Indication ) : Parcelable { @@ -36,7 +39,7 @@ data class Symptoms( companion object { val NO_INFO_GIVEN = Symptoms( - startOfSymptoms = null, // FIXME should this be null? + startOfSymptoms = null, symptomIndication = Indication.NO_INFORMATION ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt index df111a364ba660bc00759312bd44578ea3c371b0..e34681d7755b32565a6865a90c69a688e2184391 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/task/common/DefaultTaskRequest.kt @@ -8,7 +8,8 @@ import kotlin.reflect.KClass data class DefaultTaskRequest( override val type: KClass<out Task<Task.Progress, Task.Result>>, override val arguments: Task.Arguments = object : Task.Arguments {}, - override val id: UUID = UUID.randomUUID() + override val id: UUID = UUID.randomUUID(), + val originTag: String? = null ) : TaskRequest { fun toNewTask(): DefaultTaskRequest = copy(id = UUID.randomUUID()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt index e6b1683ce430f4073e569959a96785f79ae6eec2..caac006b32562d2d7ccffa0881ad81d6eb3eec9a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragment.kt @@ -1,27 +1,58 @@ package de.rki.coronawarnapp.ui.information +import android.content.Intent import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentInformationBinding import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.ExternalActionHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.setGone import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import timber.log.Timber +import javax.inject.Inject /** * Basic Fragment which links to static and web content. */ -class InformationFragment : Fragment(R.layout.fragment_information) { +class InformationFragment : Fragment(R.layout.fragment_information), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: InformationFragmentViewModel by cwaViewModels { viewModelFactory } private val binding: FragmentInformationBinding by viewBindingLazy() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + + vm.currentENFVersion.observe2(this) { + binding.informationEnfVersion.apply { + setGone(it == null) + text = it + } + } + vm.appVersion.observe2(this) { + binding.informationVersion.text = it + } + + binding.informationEnfVersion.setOnClickListener { + try { + startActivity(Intent(ExposureNotificationClient.ACTION_EXPOSURE_NOTIFICATION_SETTINGS)) + } catch (e: Exception) { + Timber.e(e, "Can't open ENF settings.") + } + } + setButtonOnClickListener() setAccessibilityDelegate() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..7b7473491c6c067a89555f75535491ffbdd81b27 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentModule.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.ui.information + +import dagger.Binds +import dagger.Module +import dagger.android.ContributesAndroidInjector +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class InformationFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(InformationFragmentViewModel::class) + abstract fun informationFragmentViewModel( + factory: InformationFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> + + @ContributesAndroidInjector + abstract fun informationFragment(): InformationFragment +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..27f53ae57b79ddf2ea0c09a62fed833f2125a3b4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationFragmentViewModel.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.ui.information + +import android.content.Context +import androidx.lifecycle.asLiveData +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.BuildConfig +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf + +class InformationFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + enfClient: ENFClient, + @AppContext private val context: Context +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + val currentENFVersion = flow { + val enfVersion = enfClient.getENFClientVersion() + ?.let { "ENF ${context.getString(R.string.information_version).format(it)}" } + emit(enfVersion) + }.asLiveData(context = dispatcherProvider.Default) + + val appVersion = flowOf( + context.getString(R.string.information_version).format(BuildConfig.VERSION_NAME) + ).asLiveData(context = dispatcherProvider.Default) + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<InformationFragmentViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt index efb78a5cbf81f040fe8b6cac1c78e64785a86303..fd0e01406f0c89425fac3b81ffb0a5bc0bbcbc8e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivity.kt @@ -10,13 +10,11 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.ViewModelProviders -import androidx.lifecycle.lifecycleScope import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector import de.rki.coronawarnapp.R import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler -import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.base.startActivitySafely import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel @@ -30,7 +28,6 @@ import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import de.rki.coronawarnapp.worker.BackgroundWorkScheduler -import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -105,16 +102,10 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { settingsViewModel.updateBackgroundJobEnabled(ConnectivityHelper.autoModeEnabled(this)) scheduleWork() checkShouldDisplayBackgroundWarning() - doBackgroundNoiseCheck() + vm.doBackgroundNoiseCheck() deadmanScheduler.schedulePeriodic() } - private fun doBackgroundNoiseCheck() { - lifecycleScope.launch { - BackgroundNoise.getInstance().foregroundScheduleCheck() - } - } - private fun showEnergyOptimizedEnabledForBackground() { val dialog = DialogHelper.DialogInstance( this, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt index 7a596bfc0666c156fe94d05749be586282b531e7..998c4e89dd36104eba97220c65164608a4961f36 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityModule.kt @@ -4,6 +4,7 @@ import dagger.Binds import dagger.Module import dagger.android.ContributesAndroidInjector import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.ui.information.InformationFragmentModule import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragment import de.rki.coronawarnapp.ui.interoperability.InteroperabilityConfigurationFragmentModule import de.rki.coronawarnapp.ui.main.home.HomeFragmentModule @@ -23,7 +24,8 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey HomeFragmentModule::class, RiskDetailsFragmentModule::class, SettingFragmentsModule::class, - SubmissionFragmentModule::class + SubmissionFragmentModule::class, + InformationFragmentModule::class ] ) abstract class MainActivityModule { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt index 8345f33c5aafac4b55a784fd5090d4ebb8a6779d..02dbea80d6c3e99fe272b5c8980600a2a8f35a70 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/MainActivityViewModel.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.main import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -28,6 +29,12 @@ class MainActivityViewModel @AssistedInject constructor( } } + fun doBackgroundNoiseCheck() { + launch { + BackgroundNoise.getInstance().foregroundScheduleCheck() + } + } + @AssistedInject.Factory interface Factory : SimpleCWAViewModelFactory<MainActivityViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt index aba0e9850f7be7eb0f6945755d7259da9ff4d1b9..af99a6380e0446e1338420c9a1e973b67b68eb96 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragment.kt @@ -5,7 +5,6 @@ import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentHomeBinding import de.rki.coronawarnapp.util.DialogHelper @@ -18,7 +17,6 @@ import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels -import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -100,7 +98,7 @@ class HomeFragment : Fragment(R.layout.fragment_home), AutoInject { } } - lifecycleScope.launch { vm.observeTestResultToSchedulePositiveTestResultReminder() } + vm.observeTestResultToSchedulePositiveTestResultReminder() } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt index 5b08c3235b18f20546e4a9c8bd4995d91c528f1f..e1f8d5f6098612b854678ffbb93b89cf3c675b8d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentViewModel.kt @@ -76,10 +76,11 @@ class HomeFragmentViewModel @AssistedInject constructor( private var isLoweredRiskLevelDialogBeingShown = false - suspend fun observeTestResultToSchedulePositiveTestResultReminder() = + fun observeTestResultToSchedulePositiveTestResultReminder() = launch { submissionCardsStateProvider.state .first { it.isPositiveSubmissionCardVisible() } .also { testResultNotificationService.schedulePositiveTestResultReminder() } + } // TODO only lazy to keep tests going which would break because of LocalData access val showLoweredRiskLevelDialog: LiveData<Boolean> by lazy { @@ -103,8 +104,6 @@ class HomeFragmentViewModel @AssistedInject constructor( submissionRepository.refreshDeviceUIState() // TODO the ordering here is weird, do we expect these to run in sequence? tracingRepository.refreshRiskLevel() - tracingRepository.refreshExposureSummary() - tracingRepository.refreshLastTimeDiagnosisKeysFetchedDate() tracingRepository.refreshActiveTracingDaysInRetentionPeriod() TimerHelper.checkManualKeyRetrievalTimer() tracingRepository.refreshLastSuccessfullyCalculatedScore() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt index 6592ad0bae0059a1eb86540ff09d9f3f94515d2a..517a077b37f4a65a2c9b01b65737da156353a039 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardState.kt @@ -3,7 +3,7 @@ package de.rki.coronawarnapp.ui.main.home import android.content.Context import android.graphics.drawable.Drawable import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.exception.http.CwaServerError import de.rki.coronawarnapp.util.DeviceUIState import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_ERROR import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_NEGATIVE @@ -12,72 +12,113 @@ import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE_TELETAN import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_REDEEMED import de.rki.coronawarnapp.util.DeviceUIState.SUBMITTED_FINAL +import de.rki.coronawarnapp.util.NetworkRequestWrapper +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess data class SubmissionCardState( - val deviceUiState: DeviceUIState, - val isDeviceRegistered: Boolean, - val uiStateState: ApiRequestState + val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>, + val isDeviceRegistered: Boolean ) { - fun isRiskCardVisible(): Boolean = deviceUiState != PAIRED_POSITIVE && - deviceUiState != PAIRED_POSITIVE_TELETAN && - deviceUiState != SUBMITTED_FINAL + fun isRiskCardVisible(): Boolean = + deviceUiState.withSuccess(true) { + when (it) { + PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN, SUBMITTED_FINAL -> false + else -> true + } + } fun isUnregisteredCardVisible(): Boolean = !isDeviceRegistered fun isFetchingCardVisible(): Boolean = - isDeviceRegistered && (uiStateState == ApiRequestState.STARTED || uiStateState == ApiRequestState.FAILED) + isDeviceRegistered && when (deviceUiState) { + is NetworkRequestWrapper.RequestFailed -> deviceUiState.error is CwaServerError + is NetworkRequestWrapper.RequestStarted -> true + else -> false + } fun isFailedCardVisible(): Boolean = - isDeviceRegistered && uiStateState == ApiRequestState.SUCCESS && deviceUiState == PAIRED_REDEEMED + isDeviceRegistered && when (deviceUiState) { + is NetworkRequestWrapper.RequestFailed -> deviceUiState.error !is CwaServerError + is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == PAIRED_REDEEMED + else -> false + } - fun isPositiveSubmissionCardVisible(): Boolean = uiStateState == ApiRequestState.SUCCESS && - (deviceUiState == PAIRED_POSITIVE || - deviceUiState == PAIRED_POSITIVE_TELETAN) + fun isPositiveSubmissionCardVisible(): Boolean = + deviceUiState.withSuccess(false) { + when (it) { + PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN -> true + else -> false + } + } fun isSubmissionDoneCardVisible(): Boolean = - uiStateState == ApiRequestState.SUCCESS && deviceUiState == SUBMITTED_FINAL + when (deviceUiState) { + is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == SUBMITTED_FINAL + else -> false + } fun isContentCardVisible(): Boolean = - uiStateState == ApiRequestState.SUCCESS && (deviceUiState == PAIRED_ERROR || - deviceUiState == PAIRED_NEGATIVE || - deviceUiState == PAIRED_NO_RESULT) + deviceUiState.withSuccess(false) { + when (it) { + PAIRED_ERROR, PAIRED_NEGATIVE, PAIRED_NO_RESULT -> true + else -> false + } + } - fun getContentCardTitleText(c: Context): String = when (deviceUiState) { - PAIRED_ERROR, PAIRED_REDEEMED, PAIRED_NEGATIVE -> R.string.submission_status_card_title_available - PAIRED_NO_RESULT -> R.string.submission_status_card_title_pending - else -> R.string.submission_status_card_title_pending - }.let { c.getString(it) } + fun getContentCardTitleText(c: Context): String = + deviceUiState.withSuccess(R.string.submission_status_card_title_pending) { + when (it) { + PAIRED_ERROR, PAIRED_REDEEMED, PAIRED_NEGATIVE -> R.string.submission_status_card_title_available + PAIRED_NO_RESULT -> R.string.submission_status_card_title_pending + else -> R.string.submission_status_card_title_pending + } + }.let { c.getString(it) } - fun getContentCardSubTitleText(c: Context): String = when (deviceUiState) { - PAIRED_NEGATIVE -> R.string.submission_status_card_subtitle_negative - PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_subtitle_invalid - else -> null - }?.let { c.getString(it) } ?: "" + fun getContentCardSubTitleText(c: Context): String = + deviceUiState.withSuccess(null) { + when (it) { + PAIRED_NEGATIVE -> R.string.submission_status_card_subtitle_negative + PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_subtitle_invalid + else -> null + } + }?.let { c.getString(it) } ?: "" - fun getContentCardSubTitleTextColor(c: Context): Int = when (deviceUiState) { - PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen - PAIRED_ERROR, PAIRED_REDEEMED -> R.color.colorTextSemanticNeutral - else -> R.color.colorTextPrimary1 - }.let { c.getColor(it) } + fun getContentCardSubTitleTextColor(c: Context): Int = + deviceUiState.withSuccess(R.color.colorTextPrimary1) { + when (it) { + PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen + PAIRED_ERROR, PAIRED_REDEEMED -> R.color.colorTextSemanticNeutral + else -> R.color.colorTextPrimary1 + } + }.let { c.getColor(it) } - fun isContentCardStatusTextVisible(): Boolean = when (deviceUiState) { - PAIRED_NEGATIVE, PAIRED_REDEEMED, PAIRED_ERROR -> true - else -> false - } + fun isContentCardStatusTextVisible(): Boolean = + deviceUiState.withSuccess(false) { + when (it) { + PAIRED_NEGATIVE, PAIRED_REDEEMED, PAIRED_ERROR -> true + else -> false + } + } - fun getContentCardBodyText(c: Context): String = when (deviceUiState) { - PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_body_invalid - PAIRED_NEGATIVE -> R.string.submission_status_card_body_negative - PAIRED_NO_RESULT -> R.string.submission_status_card_body_pending - else -> R.string.submission_status_card_body_pending - }.let { c.getString(it) } + fun getContentCardBodyText(c: Context): String = + deviceUiState.withSuccess(R.string.submission_status_card_body_pending) { + when (it) { + PAIRED_ERROR, PAIRED_REDEEMED -> R.string.submission_status_card_body_invalid + PAIRED_NEGATIVE -> R.string.submission_status_card_body_negative + PAIRED_NO_RESULT -> R.string.submission_status_card_body_pending + else -> R.string.submission_status_card_body_pending + } + }.let { c.getString(it) } - fun getContentCardIcon(c: Context): Drawable? = when (deviceUiState) { - PAIRED_NO_RESULT -> R.drawable.ic_main_illustration_pending - PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN -> R.drawable.ic_main_illustration_pending - PAIRED_NEGATIVE -> R.drawable.ic_main_illustration_negative - PAIRED_ERROR, PAIRED_REDEEMED -> R.drawable.ic_main_illustration_invalid - else -> R.drawable.ic_main_illustration_invalid - }.let { c.getDrawable(it) } + fun getContentCardIcon(c: Context): Drawable? = + deviceUiState.withSuccess(R.drawable.ic_main_illustration_invalid) { + when (it) { + PAIRED_NO_RESULT -> R.drawable.ic_main_illustration_pending + PAIRED_POSITIVE, PAIRED_POSITIVE_TELETAN -> R.drawable.ic_main_illustration_pending + PAIRED_NEGATIVE -> R.drawable.ic_main_illustration_negative + PAIRED_ERROR, PAIRED_REDEEMED -> R.drawable.ic_main_illustration_invalid + else -> R.drawable.ic_main_illustration_invalid + } + }.let { c.getDrawable(it) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt index 0b4e70ab4a2cbd1943e0335df14278e5f0a65e6e..56a9f6fef075385e8e5debda3f8746ea44900fe9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/SubmissionCardsStateProvider.kt @@ -3,8 +3,6 @@ package de.rki.coronawarnapp.ui.main.home import dagger.Reusable import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository -import de.rki.coronawarnapp.ui.submission.ApiRequestState -import de.rki.coronawarnapp.util.DeviceUIState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onCompletion @@ -19,12 +17,10 @@ class SubmissionCardsStateProvider @Inject constructor( ) { val state: Flow<SubmissionCardState> = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.uiStateStateFlow + submissionRepository.deviceUIStateFlow ) { args -> SubmissionCardState( - deviceUiState = args[0] as DeviceUIState, - uiStateState = args[1] as ApiRequestState, + deviceUiState = args[0], isDeviceRegistered = LocalData.registrationToken() != null ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt index 9d996c01ebf7532defd6ebd9235a603fa05040d7..a710675c0b0b106baf0222a0ccc1355063e5c3ec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/onboarding/OnboardingTracingFragmentViewModel.kt @@ -1,6 +1,5 @@ package de.rki.coronawarnapp.ui.onboarding -import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report @@ -10,7 +9,6 @@ import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import kotlinx.coroutines.launch class OnboardingTracingFragmentViewModel @AssistedInject constructor( private val interoperabilityRepository: InteroperabilityRepository @@ -25,7 +23,7 @@ class OnboardingTracingFragmentViewModel @AssistedInject constructor( // Reset tracing state in onboarding fun resetTracing() { - viewModelScope.launch { + launch { try { if (InternalExposureNotificationClient.asyncIsEnabled()) { InternalExposureNotificationClient.asyncStop() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt index 65158317199da7f4678075a8ff16336f16a74176..7f84c13b03b80093910b1d6bf50a9364696aa39a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultFragment.kt @@ -6,7 +6,6 @@ import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.activity.OnBackPressedCallback import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionTestResultBinding import de.rki.coronawarnapp.exception.http.CwaClientError @@ -14,14 +13,13 @@ import de.rki.coronawarnapp.exception.http.CwaServerError import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withFailure import de.rki.coronawarnapp.util.di.AutoInject -import de.rki.coronawarnapp.util.observeEvent import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels -import kotlinx.coroutines.launch import javax.inject.Inject class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_result), @@ -78,6 +76,11 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_ viewModel.uiState.observe2(this) { binding.uiState = it + it.deviceUiState.withFailure { + if (it is CwaWebException) { + DialogHelper.showDialog(buildErrorDialog(it)) + } + } } // registers callback when the os level back is pressed @@ -98,10 +101,6 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_ DialogHelper.showDialog(tracingRequiredDialog) } - viewModel.uiStateError.observeEvent(viewLifecycleOwner) { - DialogHelper.showDialog(buildErrorDialog(it)) - } - viewModel.showRedeemedTokenWarning.observe2(this) { val dialog = DialogHelper.DialogInstance( requireActivity(), @@ -134,7 +133,7 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_ } } - lifecycleScope.launch { viewModel.observeTestResultToSchedulePositiveTestResultReminder() } + viewModel.observeTestResultToSchedulePositiveTestResultReminder() } override fun onResume() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt index 08114cc0ed73d83167e804bb258824f66ab6d601..42423b2d532959556bc51559cfdfc82235fd1e47 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/SubmissionTestResultViewModel.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.ui.submission.testresult import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import com.squareup.inject.assisted.AssistedInject -import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.notification.TestResultNotificationService import de.rki.coronawarnapp.storage.LocalData @@ -11,7 +10,7 @@ import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.Event +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -37,31 +36,36 @@ class SubmissionTestResultViewModel @AssistedInject constructor( private val tokenErrorMutex = Mutex() val uiState: LiveData<TestResultUIState> = combineTransform( - submissionRepository.uiStateStateFlow, submissionRepository.deviceUIStateFlow, submissionRepository.testResultReceivedDateFlow - ) { apiRequestState, deviceUiState, resultDate -> + ) { deviceUiState, resultDate -> tokenErrorMutex.withLock { - if (!wasRedeemedTokenErrorShown && deviceUiState == DeviceUIState.PAIRED_REDEEMED) { - wasRedeemedTokenErrorShown = true - showRedeemedTokenWarning.postValue(Unit) + if (!wasRedeemedTokenErrorShown) { + deviceUiState.withSuccess { + if (it == DeviceUIState.PAIRED_REDEEMED) { + wasRedeemedTokenErrorShown = true + showRedeemedTokenWarning.postValue(Unit) + } + } } } TestResultUIState( - apiRequestState = apiRequestState, deviceUiState = deviceUiState, testResultReceivedDate = resultDate ).let { emit(it) } }.asLiveData(context = dispatcherProvider.Default) - suspend fun observeTestResultToSchedulePositiveTestResultReminder() = + fun observeTestResultToSchedulePositiveTestResultReminder() = launch { submissionRepository.deviceUIStateFlow - .first { it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN } + .first { request -> + request.withSuccess(false) { + it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN + } + } .also { testResultNotificationService.schedulePositiveTestResultReminder() } - - val uiStateError: LiveData<Event<CwaWebException>> = submissionRepository.uiStateError + } fun onBackPressed() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt index b1909eb805d6cbaa923b41336f14d60cb30aa45b..a34a02cad13d8a16e1c550ef47aa4c2ce27dfd2d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/TestResultUIState.kt @@ -1,11 +1,10 @@ package de.rki.coronawarnapp.ui.submission.testresult -import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.NetworkRequestWrapper import java.util.Date data class TestResultUIState( - val apiRequestState: ApiRequestState, - val deviceUiState: DeviceUIState, + val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>, val testResultReceivedDate: Date? ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt index 7e2f05d5beb481da9c76f9bcaacf0eb5f77f90ac..80db0f28aba95ec7aa073aa399fdaf769fdf3f4a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/card/TracingCardStateProvider.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.ui.tracing.card import dagger.Reusable -import de.rki.coronawarnapp.storage.ExposureSummaryRepository +import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SettingsRepository import de.rki.coronawarnapp.storage.TracingRepository @@ -20,7 +20,8 @@ class TracingCardStateProvider @Inject constructor( tracingStatus: GeneralTracingStatus, backgroundModeStatus: BackgroundModeStatus, settingsRepository: SettingsRepository, - tracingRepository: TracingRepository + tracingRepository: TracingRepository, + exposureResultStore: ExposureResultStore ) { // TODO Refactor these singletons away @@ -37,10 +38,10 @@ class TracingCardStateProvider @Inject constructor( tracingRepository.tracingProgress.onEach { Timber.v("tracingProgress: $it") }, - ExposureSummaryRepository.matchedKeyCount.onEach { + exposureResultStore.matchedKeyCount.onEach { Timber.v("matchedKeyCount: $it") }, - ExposureSummaryRepository.daysSinceLastExposure.onEach { + exposureResultStore.daysSinceLastExposure.onEach { Timber.v("daysSinceLastExposure: $it") }, tracingRepository.activeTracingDaysInRetentionPeriod.onEach { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt index 2075f82ed79aa0788b3aeec2707de2a86c338597..6008444b1d0cb5c7906a92b1cb25014c9dc07d5b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/RiskDetailsFragmentViewModel.kt @@ -36,8 +36,6 @@ class RiskDetailsFragmentViewModel @AssistedInject constructor( fun refreshData() { tracingRepository.refreshRiskLevel() - tracingRepository.refreshExposureSummary() - tracingRepository.refreshLastTimeDiagnosisKeysFetchedDate() TimerHelper.checkManualKeyRetrievalTimer() tracingRepository.refreshActiveTracingDaysInRetentionPeriod() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt index f07401a32682540a0227eaacdc45e854a11d78df..388bca23b501d24bda54b33b85110df0bceb0826 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/details/TracingDetailsStateProvider.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.ui.tracing.details import dagger.Reusable -import de.rki.coronawarnapp.storage.ExposureSummaryRepository +import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.storage.RiskLevelRepository import de.rki.coronawarnapp.storage.SettingsRepository import de.rki.coronawarnapp.storage.TracingRepository @@ -21,7 +21,8 @@ class TracingDetailsStateProvider @Inject constructor( tracingStatus: GeneralTracingStatus, backgroundModeStatus: BackgroundModeStatus, settingsRepository: SettingsRepository, - tracingRepository: TracingRepository + tracingRepository: TracingRepository, + exposureResultStore: ExposureResultStore ) { // TODO Refactore these singletons away @@ -30,8 +31,8 @@ class TracingDetailsStateProvider @Inject constructor( RiskLevelRepository.riskLevelScore, RiskLevelRepository.riskLevelScoreLastSuccessfulCalculated, tracingRepository.tracingProgress, - ExposureSummaryRepository.matchedKeyCount, - ExposureSummaryRepository.daysSinceLastExposure, + exposureResultStore.matchedKeyCount, + exposureResultStore.daysSinceLastExposure, tracingRepository.activeTracingDaysInRetentionPeriod, tracingRepository.lastTimeDiagnosisKeysFetched, backgroundModeStatus.isAutoModeEnabled, @@ -53,8 +54,8 @@ class TracingDetailsStateProvider @Inject constructor( ) val isInformationBodyNoticeVisible = riskDetailPresenter.isInformationBodyNoticeVisible( - riskLevelScore - ) + riskLevelScore + ) TracingDetailsState( tracingStatus = status, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt index 56ef3c2d2747d7f329aa3a1eb04e769ed12f9ece..489f83c59a0bbe31475fd18214d8637bd4cb2752 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragment.kt @@ -5,17 +5,14 @@ import android.os.Bundle import android.view.View import android.view.accessibility.AccessibilityEvent import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSettingsTracingBinding -import de.rki.coronawarnapp.exception.ExceptionCategory -import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.nearby.InternalExposureNotificationPermissionHelper -import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.ui.doNavigate import de.rki.coronawarnapp.ui.main.MainActivity +import de.rki.coronawarnapp.ui.tracing.settings.SettingsTracingFragmentViewModel.Event import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.ExternalActionHelper @@ -25,7 +22,6 @@ import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels import de.rki.coronawarnapp.worker.BackgroundWorkScheduler -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -47,7 +43,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), private val binding: FragmentSettingsTracingBinding by viewBindingLazy() - private lateinit var internalExposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper + private lateinit var exposureNotificationPermissionHelper: InternalExposureNotificationPermissionHelper override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -63,11 +59,21 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), TracingSettingsState.BluetoothDisabled, TracingSettingsState.LocationDisabled -> setOnClickListener(null) TracingSettingsState.TracingInActive, - TracingSettingsState.TracingActive -> setOnClickListener { startStopTracing() } + TracingSettingsState.TracingActive -> setOnClickListener { vm.startStopTracing() } } } } + exposureNotificationPermissionHelper = InternalExposureNotificationPermissionHelper(this, this) + + vm.events.observe2(this) { + when (it) { + Event.RequestPermissions -> exposureNotificationPermissionHelper.requestPermissionToStartTracing() + Event.ShowConsentDialog -> showConsentDialog() + Event.ManualCheckingDialog -> showManualCheckingRequiredDialog() + } + } + setButtonOnClickListener() } @@ -77,7 +83,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - internalExposureNotificationPermissionHelper.onResolutionComplete( + exposureNotificationPermissionHelper.onResolutionComplete( requestCode, resultCode ) @@ -98,13 +104,10 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), val location = binding.settingsTracingStatusLocation.tracingStatusCardButton val interoperability = binding.settingsInteroperabilityRow.settingsPlainRow - internalExposureNotificationPermissionHelper = - InternalExposureNotificationPermissionHelper(this, this) switch.setOnCheckedChangeListener { view, _ -> - // Make sure that listener is called by user interaction if (view.isPressed) { - startStopTracing() + vm.startStopTracing() // Focus on the body text after to announce the tracing status for accessibility reasons binding.settingsTracingSwitchRow.settingsSwitchRowHeaderBody .sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) @@ -131,39 +134,6 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), ) } - private fun startStopTracing() { - // if tracing is enabled when listener is activated it should be disabled - lifecycleScope.launch { - try { - if (InternalExposureNotificationClient.asyncIsEnabled()) { - InternalExposureNotificationClient.asyncStop() - BackgroundWorkScheduler.stopWorkScheduler() - } else { - // tracing was already activated - if (LocalData.initialTracingActivationTimestamp() != null) { - internalExposureNotificationPermissionHelper.requestPermissionToStartTracing() - } else { - // tracing was never activated - // ask for consent via dialog for initial tracing activation when tracing was not - // activated during onboarding - showConsentDialog() - // check if background processing is switched off, if it is, show the manual calculation dialog explanation before turning on. - val activity = requireActivity() as MainActivity - if (!activity.backgroundPrioritization.isBackgroundActivityPrioritized) { - showManualCheckingRequiredDialog() - } - } - } - } catch (exception: Exception) { - exception.report( - ExceptionCategory.EXPOSURENOTIFICATION, - TAG, - null - ) - } - } - } - private fun showManualCheckingRequiredDialog() { val dialog = DialogHelper.DialogInstance( requireActivity(), @@ -186,7 +156,7 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), R.string.onboarding_button_enable, R.string.onboarding_button_cancel, true, { - internalExposureNotificationPermissionHelper.requestPermissionToStartTracing() + exposureNotificationPermissionHelper.requestPermissionToStartTracing() }, { // Declined }) @@ -194,6 +164,6 @@ class SettingsTracingFragment : Fragment(R.layout.fragment_settings_tracing), } companion object { - private val TAG: String? = SettingsTracingFragment::class.simpleName + internal val TAG: String? = SettingsTracingFragment::class.simpleName } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt index c9a5462932dcd507732537d002b785f6a7b659fe..6b2e57dfa22ec73593740d62705248d4ce98e6fe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/tracing/settings/SettingsTracingFragmentViewModel.kt @@ -4,13 +4,20 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient +import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.ui.tracing.details.TracingDetailsState import de.rki.coronawarnapp.ui.tracing.details.TracingDetailsStateProvider +import de.rki.coronawarnapp.util.BackgroundPrioritization import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.shareLatest +import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -20,7 +27,8 @@ import timber.log.Timber class SettingsTracingFragmentViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, tracingDetailsStateProvider: TracingDetailsStateProvider, - tracingStatus: GeneralTracingStatus + tracingStatus: GeneralTracingStatus, + private val backgroundPrioritization: BackgroundPrioritization ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val tracingDetailsState: LiveData<TracingDetailsState> = tracingDetailsStateProvider.state @@ -36,6 +44,47 @@ class SettingsTracingFragmentViewModel @AssistedInject constructor( ) .asLiveData(dispatcherProvider.Main) + val events = SingleLiveEvent<Event>() + + fun startStopTracing() { + // if tracing is enabled when listener is activated it should be disabled + launch { + try { + if (InternalExposureNotificationClient.asyncIsEnabled()) { + InternalExposureNotificationClient.asyncStop() + BackgroundWorkScheduler.stopWorkScheduler() + } else { + // tracing was already activated + if (LocalData.initialTracingActivationTimestamp() != null) { + events.postValue(Event.RequestPermissions) + } else { + // tracing was never activated + // ask for consent via dialog for initial tracing activation when tracing was not + // activated during onboarding + events.postValue(Event.ShowConsentDialog) + // check if background processing is switched off, + // if it is, show the manual calculation dialog explanation before turning on. + if (!backgroundPrioritization.isBackgroundActivityPrioritized) { + events.postValue(Event.ManualCheckingDialog) + } + } + } + } catch (exception: Exception) { + exception.report( + ExceptionCategory.EXPOSURENOTIFICATION, + SettingsTracingFragment.TAG, + null + ) + } + } + } + + sealed class Event { + object RequestPermissions : Event() + object ShowConsentDialog : Event() + object ManualCheckingDialog : Event() + } + @AssistedInject.Factory interface Factory : SimpleCWAViewModelFactory<SettingsTracingFragmentViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt deleted file mode 100644 index a806825282e67f110262017d748c2485ca9d51ea..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package de.rki.coronawarnapp.ui.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData -import com.squareup.inject.assisted.AssistedInject -import de.rki.coronawarnapp.storage.SubmissionRepository -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel -import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory - -class SubmissionViewModel @AssistedInject constructor( - submissionRepository: SubmissionRepository -) : CWAViewModel() { - - val deviceUiState: LiveData<DeviceUIState> = submissionRepository.deviceUIStateFlow.asLiveData() - - @AssistedInject.Factory - interface Factory : SimpleCWAViewModelFactory<SubmissionViewModel> -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt index 42dfb2f60231fc8461b4b164d8d14bad74a37160..695e297a1d7d12d223abc79a9303a4a69890d62a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/UpdateChecker.kt @@ -7,8 +7,7 @@ import androidx.core.content.ContextCompat.startActivity import de.rki.coronawarnapp.BuildConfig import de.rki.coronawarnapp.R import de.rki.coronawarnapp.appconfig.CWAConfig -import de.rki.coronawarnapp.appconfig.download.ApplicationConfigurationCorruptException -import de.rki.coronawarnapp.server.protocols.internal.AppVersionConfig.SemanticVersion +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException import de.rki.coronawarnapp.ui.LauncherActivity import de.rki.coronawarnapp.util.di.AppInjector import timber.log.Timber @@ -69,30 +68,19 @@ class UpdateChecker(private val activity: LauncherActivity) { private suspend fun checkIfUpdatesNeededFromServer(): Boolean { val cwaAppConfig: CWAConfig = AppInjector.component.appConfigProvider.getAppConfig() - val minVersionFromServer = cwaAppConfig.appVersion.android.min - val minVersionFromServerString = - constructSemanticVersionString(minVersionFromServer) + val minVersionFromServer = cwaAppConfig.minVersionCode - Timber.e( - "minVersionStringFromServer:%s", constructSemanticVersionString( - minVersionFromServer - ) + Timber.d( + "minVersionFromServer:%s", + minVersionFromServer ) - Timber.e("Current app version:%s", BuildConfig.VERSION_NAME) + Timber.d("Current app version:%s", BuildConfig.VERSION_CODE) val needsImmediateUpdate = VersionComparator.isVersionOlder( - BuildConfig.VERSION_NAME, - minVersionFromServerString + BuildConfig.VERSION_CODE.toLong(), + minVersionFromServer ) Timber.e("needs update:$needsImmediateUpdate") return needsImmediateUpdate } - - private fun constructSemanticVersionString( - semanticVersion: SemanticVersion - ): String { - return semanticVersion.major.toString() + "." + - semanticVersion.minor.toString() + "." + - semanticVersion.patch.toString() - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt index 2a706d3e1a09f5e3f1084ea6328d39da63f231a8..117932d4f43adfabbcf0a59aa825f0ec2a3e8b34 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/update/VersionComparator.kt @@ -15,32 +15,7 @@ object VersionComparator { * @param versionToCompareTo * @return true if currentVersion is older than versionToCompareTo, else false */ - fun isVersionOlder(currentVersion: String, versionToCompareTo: String): Boolean { - var isVersionOlder = false - - val delimiter = "." - - val currentVersionParts = currentVersion.split(delimiter) - val currentVersionMajor = currentVersionParts[0].toInt() - val currentVersionMinor = currentVersionParts[1].toInt() - val currentVersionPatch = currentVersionParts[2].toInt() - - val versionToCompareParts = versionToCompareTo.split(delimiter) - val versionToCompareMajor = versionToCompareParts[0].toInt() - val versionToCompareMinor = versionToCompareParts[1].toInt() - val versionToComparePatch = versionToCompareParts[2].toInt() - - if (versionToCompareMajor > currentVersionMajor) { - isVersionOlder = true - } else if (versionToCompareMajor == currentVersionMajor) { - if (versionToCompareMinor > currentVersionMinor) { - isVersionOlder = true - } else if ((versionToCompareMinor == currentVersionMinor) && - (versionToComparePatch > currentVersionPatch) - ) { - isVersionOlder = true - } - } - return isVersionOlder + fun isVersionOlder(currentVersion: Long, versionToCompareTo: Long): Boolean { + return currentVersion < versionToCompareTo } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt index 13ea08dfd0c431dc596b9bf024d90788dd3fac2f..2e26c1991f2aae2889e89542223fe0116dd341ed 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/BackgroundModeStatus.kt @@ -6,8 +6,6 @@ import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.flow.shareLatest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.channels.sendBlocking import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow @@ -23,18 +21,19 @@ class BackgroundModeStatus @Inject constructor( @AppScope private val appScope: CoroutineScope ) { - val isBackgroundRestricted: Flow<Boolean> = callbackFlow<Boolean> { - var isRunning = true - while (isRunning && isActive) { + val isBackgroundRestricted: Flow<Boolean?> = callbackFlow<Boolean> { + while (true) { try { - sendBlocking(pollIsBackgroundRestricted()) + send(pollIsBackgroundRestricted()) } catch (e: Exception) { Timber.w(e, "isBackgroundRestricted failed.") cancel("isBackgroundRestricted failed", e) } + + if (!isActive) break + delay(POLLING_DELAY_MS) } - awaitClose { isRunning = false } } .distinctUntilChanged() .shareLatest( @@ -43,17 +42,18 @@ class BackgroundModeStatus @Inject constructor( ) val isAutoModeEnabled: Flow<Boolean> = callbackFlow<Boolean> { - var isRunning = true - while (isRunning && isActive) { + while (true) { try { - sendBlocking(pollIsAutoMode()) + send(pollIsAutoMode()) } catch (e: Exception) { Timber.w(e, "autoModeEnabled failed.") cancel("autoModeEnabled failed", e) } + + if (!isActive) break + delay(POLLING_DELAY_MS) } - awaitClose { isRunning = false } } .distinctUntilChanged() .shareLatest( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt index cade97a58323736139c16c3898b04b26944a21fe..195bd3551aae9a2ba6a3d5306e2faeb64bcbb52c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/CWADebug.kt @@ -11,10 +11,10 @@ object CWADebug { fun init(application: Application) { if (isDebugBuildOrMode) System.setProperty("kotlinx.coroutines.debug", "on") - if (BuildConfig.DEBUG) { + if (isDeviceForTestersBuild) { Timber.plant(Timber.DebugTree()) } - if ((buildFlavor == BuildFlavor.DEVICE_FOR_TESTERS || BuildConfig.DEBUG)) { + if (isDeviceForTestersBuild) { fileLogger = FileLogger(application) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt index 6e9d54d3353ecebdf76145d9fc7de6d2e6252c1d..6317d11ee81ca9f42ebe779abbbd69a37d5262b5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DataReset.kt @@ -22,8 +22,11 @@ package de.rki.coronawarnapp.util import android.annotation.SuppressLint import android.content.Context import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.diagnosiskeys.download.KeyPackageSyncSettings import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository +import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker 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.interoperability.InteroperabilityRepository @@ -44,7 +47,9 @@ class DataReset @Inject constructor( private val keyCacheRepository: KeyCacheRepository, private val appConfigProvider: AppConfigProvider, private val interoperabilityRepository: InteroperabilityRepository, - private val submissionRepository: SubmissionRepository + private val submissionRepository: SubmissionRepository, + private val exposureDetectionTracker: ExposureDetectionTracker, + private val keyPackageSyncSettings: KeyPackageSyncSettings ) { private val mutex = Mutex() @@ -57,6 +62,8 @@ class DataReset @Inject constructor( Timber.w("CWA LOCAL DATA DELETION INITIATED.") // Database Reset AppDatabase.reset(context) + // Because LocalData does not behave like a normal shared preference + LocalData.clear() // Shared Preferences Reset SecurityHelper.resetSharedPrefs() // Reset the current risk level stored in LiveData @@ -66,6 +73,8 @@ class DataReset @Inject constructor( keyCacheRepository.clear() appConfigProvider.clear() interoperabilityRepository.clear() + exposureDetectionTracker.clear() + keyPackageSyncSettings.clear() Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt index 7cfd0d07b2c5fbbc01e76b1002709c5bc06b92ca..eab3573bc450f2792c8ed8fa085f16cdd13f0ce6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/DefaultBackgroundPrioritization.kt @@ -1,8 +1,10 @@ package de.rki.coronawarnapp.util +import dagger.Reusable import de.rki.coronawarnapp.util.device.PowerManagement import javax.inject.Inject +@Reusable class DefaultBackgroundPrioritization @Inject constructor( private val powerManagement: PowerManagement ) : BackgroundPrioritization { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt deleted file mode 100644 index 6af00068b370a93acc562d74cf16b23d8819617c..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/GoogleAPIVersion.kt +++ /dev/null @@ -1,40 +0,0 @@ -package de.rki.coronawarnapp.util - -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.common.api.CommonStatusCodes -import dagger.Reusable -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import javax.inject.Inject -import kotlin.math.abs - -@Reusable -class GoogleAPIVersion @Inject constructor() { - /** - * 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 { - if (!compareVersion.isCorrectVersionLength) { - throw IllegalArgumentException("given version has incorrect length") - } - return try { - val currentVersion = InternalExposureNotificationClient.getVersion() - currentVersion >= compareVersion - } catch (apiException: ApiException) { - if (apiException.statusCode != CommonStatusCodes.API_NOT_CONNECTED) { - throw apiException - } - return false - } - } - - // 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 - const val V16 = 16000000L - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/NetworkRequestWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/NetworkRequestWrapper.kt new file mode 100644 index 0000000000000000000000000000000000000000..47d1c57e48abe6bf8d816ad9305c0d9366ea74ee --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/NetworkRequestWrapper.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.util + +sealed class NetworkRequestWrapper<out T, out U> { + object RequestIdle : NetworkRequestWrapper<Nothing, Nothing>() + object RequestStarted : NetworkRequestWrapper<Nothing, Nothing>() + data class RequestSuccessful<T, U>(val data: T) : NetworkRequestWrapper<T, U>() + data class RequestFailed<T, U>(val error: U) : NetworkRequestWrapper<T, U>() + + companion object { + fun <T, U, W> NetworkRequestWrapper<T, U>?.withSuccess(without: W, block: (data: T) -> W): W { + return if (this is RequestSuccessful) { + block(this.data) + } else { + without + } + } + + fun <T, U> NetworkRequestWrapper<T, U>?.withSuccess(block: (data: T) -> Unit) { + if (this is RequestSuccessful) { + block(this.data) + } + } + + fun <T, U> NetworkRequestWrapper<T, U>?.withFailure(block: (error: U) -> Unit) { + if (this is RequestFailed) { + block(this.error) + } + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt index 00d62d17067f43ef080520518ba36bf2d4ee5d8f..8a5729554f5166ee7d9eb192d72e899374ca2bcf 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/WatchdogService.kt @@ -11,7 +11,6 @@ import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.task.submitBlocking import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.worker.BackgroundWorkHelper import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.launch import timber.log.Timber @@ -45,22 +44,18 @@ class WatchdogService @Inject constructor( val wakeLock = createWakeLock() // A wifi lock to wake up the wifi connection in case the device is dozing val wifiLock = createWifiLock() - BackgroundWorkHelper.sendDebugNotification( - "Automatic mode is on", "Check if we have downloaded keys already today" - ) + + Timber.d("Automatic mode is on, check if we have downloaded keys already today") + val state = taskController.submitBlocking( DefaultTaskRequest( DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments(null, true) + DownloadDiagnosisKeysTask.Arguments(), + originTag = "WatchdogService" ) ) if (state.isFailed) { - BackgroundWorkHelper.sendDebugNotification( - "RetrieveDiagnosisKeysTransaction failed", - (state.error?.localizedMessage - ?: "Unknown exception occurred in onCreate") + "\n\n" + (state.error?.cause - ?: "Cause is unknown").toString() - ) + Timber.e(state.error, "RetrieveDiagnosisKeysTransaction failed") // retry the key retrieval in case of an error with a scheduled work BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt index eb3b29093e4a6c772d65dd7167ecbeda941588e0..e8d283d9c7695095ffa9995d76aa3cb2c232873a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/debug/FileLogger.kt @@ -1,39 +1,45 @@ package de.rki.coronawarnapp.util.debug import android.content.Context +import de.rki.coronawarnapp.util.CWADebug import timber.log.Timber import java.io.File -class FileLogger constructor(private val context: Context) { +class FileLogger constructor(context: Context) { val logFile = File(context.cacheDir, "FileLoggerTree.log") - val triggerFile = File(context.filesDir, "FileLoggerTree.trigger") + + private val blockerFile = File(context.filesDir, "FileLoggerTree.blocker") private var loggerTree: FileLoggerTree? = null val isLogging: Boolean get() = loggerTree != null init { - if (triggerFile.exists()) { + if (!blockerFile.exists()) { start() } } fun start() { + if (!CWADebug.isDeviceForTestersBuild) return + if (loggerTree != null) return loggerTree = FileLoggerTree(logFile).also { Timber.plant(it) it.start() - triggerFile.createNewFile() + blockerFile.delete() } } fun stop() { + if (!CWADebug.isDeviceForTestersBuild) return + loggerTree?.let { it.stop() logFile.delete() - triggerFile.delete() + blockerFile.createNewFile() loggerTree = null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt index af2cb89a00f2cbb07c2f27e5abdff7d7728abafd..6a869aa15a472ceb4d54b28ebbb2413f016556d4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/di/ApplicationComponent.kt @@ -22,7 +22,6 @@ import de.rki.coronawarnapp.receiver.ReceiverBinder import de.rki.coronawarnapp.risk.RiskModule import de.rki.coronawarnapp.service.ServiceBinder import de.rki.coronawarnapp.storage.SettingsRepository -import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionModule import de.rki.coronawarnapp.submission.SubmissionTaskModule import de.rki.coronawarnapp.task.TaskController @@ -89,8 +88,6 @@ interface ApplicationComponent : AndroidInjector<CoronaWarnApplication> { val playbook: Playbook - val interoperabilityRepository: InteroperabilityRepository - val taskController: TaskController @AppScope val appScope: AppCoroutineScope diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt index 701dc3239b455bff16edc0b2d656f64f1e8c43ad..947b6d55c29ea2894a9050b31a16069d2440c3f3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/FlowExtensions.kt @@ -5,7 +5,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -17,7 +17,7 @@ import timber.log.Timber * Helper method to create a new flow without suspending and without initial value * The flow collector will just wait for the first value */ -fun <T> Flow<T>.shareLatest( +fun <T : Any> Flow<T>.shareLatest( tag: String? = null, scope: CoroutineScope, started: SharingStarted = SharingStarted.WhileSubscribed(replayExpirationMillis = 0) @@ -40,7 +40,7 @@ fun <T> Flow<T>.shareLatest( started = started, initialValue = null ) - .mapNotNull { it } + .filterNotNull() @Suppress("UNCHECKED_CAST", "LongParameterList") inline fun <T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, R> combine( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt index 91086980e2bd33336558e889a15272962e37bd83..1fe9db2d964a52c0bc216428d4a5e6fe1ec415c4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/flow/HotDataFlow.kt @@ -15,6 +15,8 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.plus +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import timber.log.Timber import kotlin.coroutines.CoroutineContext @@ -37,20 +39,32 @@ class HotDataFlow<T : Any>( extraBufferCapacity = Int.MAX_VALUE, onBufferOverflow = BufferOverflow.SUSPEND ) + private val valueGuard = Mutex() private val internalProducer: Flow<Holder<T>> = channelFlow { - var currentValue = startValueProvider().also { - Timber.tag(tag).v("startValue=%s", it) - val updatedBy: suspend T.() -> T = { it } - send(Holder.Data(value = it, updatedBy = updatedBy)) + var currentValue = valueGuard.withLock { + startValueProvider().also { + Timber.tag(tag).v("startValue=%s", it) + val updatedBy: suspend T.() -> T = { it } + send(Holder.Data(value = it, updatedBy = updatedBy)) + } } + Timber.tag(tag).v("startValue=%s", currentValue) - updateActions.collect { updateAction -> - currentValue = updateAction(currentValue).also { - currentValue = it - send(Holder.Data(value = it, updatedBy = updateAction)) + updateActions + .onCompletion { + Timber.tag(tag).v("updateActions onCompletion -> resetReplayCache()") + updateActions.resetReplayCache() } - } + .collect { updateAction -> + currentValue = valueGuard.withLock { + updateAction(currentValue).also { + send(Holder.Data(value = it, updatedBy = updateAction)) + } + } + } + + Timber.tag(tag).v("internal channelFlow finished.") } private val internalFlow = internalProducer @@ -65,7 +79,10 @@ class HotDataFlow<T : Any>( throw it } } - .onCompletion { Timber.tag(tag).v("Internal onCompletion") } + .onCompletion { err -> + if (err != null) Timber.tag(tag).w(err, "internal onCompletion due to error") + else Timber.tag(tag).v("internal onCompletion") + } .shareIn( scope = scope + coroutineContext, replay = 1, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterInformationHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterInformationHelper.kt deleted file mode 100644 index 913424b0671ddfef1a1a9a00e79304ab5b6486f7..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterInformationHelper.kt +++ /dev/null @@ -1,13 +0,0 @@ -@file:JvmName("FormatterInformationHelper") - -package de.rki.coronawarnapp.util.formatter - -import de.rki.coronawarnapp.BuildConfig -import de.rki.coronawarnapp.CoronaWarnApplication -import de.rki.coronawarnapp.R - -fun formatVersion(): String { - val appContext = CoronaWarnApplication.getAppContext() - val versionName: String = BuildConfig.VERSION_NAME - return appContext.getString(R.string.information_version).format(versionName) -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt index 156f8efee0289c727be6c85d0aa6e4a96b116674..e573e202356a4b6faef650422585624285eb8c6d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelper.kt @@ -8,11 +8,13 @@ import android.text.Spannable import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.style.ForegroundColorSpan +import android.view.View import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R import de.rki.coronawarnapp.submission.Symptoms -import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.NetworkRequestWrapper +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUIFormat import java.util.Date import java.util.Locale @@ -49,33 +51,37 @@ fun isEnableSymptomCalendarButtonByState(currentState: Symptoms.StartOf?): Boole return currentState != null } -fun formatTestResultSpinnerVisible(uiStateState: ApiRequestState?): Int = - formatVisibility(uiStateState != ApiRequestState.SUCCESS) - -fun formatTestResultVisible(uiStateState: ApiRequestState?): Int = - formatVisibility(uiStateState == ApiRequestState.SUCCESS) - -fun formatTestResultStatusText(uiState: DeviceUIState?): String { - val appContext = CoronaWarnApplication.getAppContext() - return when (uiState) { - DeviceUIState.PAIRED_NEGATIVE -> appContext.getString(R.string.test_result_card_status_negative) - DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getString(R.string.test_result_card_status_positive) - else -> appContext.getString(R.string.test_result_card_status_invalid) +fun formatTestResultSpinnerVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.VISIBLE) { + View.GONE } -} -fun formatTestResultStatusColor(uiState: DeviceUIState?): Int { - val appContext = CoronaWarnApplication.getAppContext() - return when (uiState) { - DeviceUIState.PAIRED_NEGATIVE -> appContext.getColor(R.color.colorTextSemanticGreen) - DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN -> appContext.getColor(R.color.colorTextSemanticRed) - else -> appContext.getColor(R.color.colorTextSemanticRed) +fun formatTestResultVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.GONE) { + View.VISIBLE } -} -fun formatTestResult(uiState: DeviceUIState?): Spannable { +fun formatTestResultStatusText(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): String = + uiState.withSuccess(R.string.test_result_card_status_invalid) { + when (it) { + DeviceUIState.PAIRED_NEGATIVE -> R.string.test_result_card_status_negative + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> R.string.test_result_card_status_positive + else -> R.string.test_result_card_status_invalid + } + }.let { CoronaWarnApplication.getAppContext().getString(it) } + +fun formatTestResultStatusColor(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(R.color.colorTextSemanticRed) { + when (it) { + DeviceUIState.PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> R.color.colorTextSemanticRed + else -> R.color.colorTextSemanticRed + } + }.let { CoronaWarnApplication.getAppContext().getColor(it) } + +fun formatTestResult(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Spannable { val appContext = CoronaWarnApplication.getAppContext() return SpannableStringBuilder() .append(appContext.getString(R.string.test_result_card_virus_name_text)) @@ -87,33 +93,36 @@ fun formatTestResult(uiState: DeviceUIState?): Spannable { ) } -fun formatTestResultCardContent(uiState: DeviceUIState?): Spannable { - val appContext = CoronaWarnApplication.getAppContext() - return when (uiState) { - DeviceUIState.PAIRED_NO_RESULT -> - SpannableString(appContext.getString(R.string.test_result_card_status_pending)) - DeviceUIState.PAIRED_ERROR, - DeviceUIState.PAIRED_REDEEMED -> - SpannableString(appContext.getString(R.string.test_result_card_status_invalid)) - - DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN, - DeviceUIState.PAIRED_NEGATIVE -> formatTestResult(uiState) - else -> SpannableString("") +fun formatTestResultCardContent(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Spannable { + return uiState.withSuccess(SpannableString("")) { + val appContext = CoronaWarnApplication.getAppContext() + when (it) { + DeviceUIState.PAIRED_NO_RESULT -> + SpannableString(appContext.getString(R.string.test_result_card_status_pending)) + DeviceUIState.PAIRED_ERROR, + DeviceUIState.PAIRED_REDEEMED -> + SpannableString(appContext.getString(R.string.test_result_card_status_invalid)) + + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_NEGATIVE -> formatTestResult(uiState) + else -> SpannableString("") + } } } -fun formatTestStatusIcon(uiState: DeviceUIState?): Drawable? { - val appContext = CoronaWarnApplication.getAppContext() - return when (uiState) { - DeviceUIState.PAIRED_NO_RESULT -> appContext.getDrawable(R.drawable.ic_test_result_illustration_pending) - DeviceUIState.PAIRED_POSITIVE_TELETAN, - DeviceUIState.PAIRED_POSITIVE -> appContext.getDrawable(R.drawable.ic_test_result_illustration_positive) - DeviceUIState.PAIRED_NEGATIVE -> appContext.getDrawable(R.drawable.ic_test_result_illustration_negative) - DeviceUIState.PAIRED_ERROR, - DeviceUIState.PAIRED_REDEEMED -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid) - else -> appContext.getDrawable(R.drawable.ic_test_result_illustration_invalid) - } +fun formatTestStatusIcon(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Drawable? { + return uiState.withSuccess(R.drawable.ic_test_result_illustration_invalid) { + when (it) { + DeviceUIState.PAIRED_NO_RESULT -> R.drawable.ic_test_result_illustration_pending + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_POSITIVE -> R.drawable.ic_test_result_illustration_positive + DeviceUIState.PAIRED_NEGATIVE -> R.drawable.ic_test_result_illustration_negative + DeviceUIState.PAIRED_ERROR, + DeviceUIState.PAIRED_REDEEMED -> R.drawable.ic_test_result_illustration_invalid + else -> R.drawable.ic_test_result_illustration_invalid + } + }.let { CoronaWarnApplication.getAppContext().getDrawable(it) } } fun formatTestResultRegisteredAtText(registeredAt: Date?): String { @@ -122,24 +131,30 @@ fun formatTestResultRegisteredAtText(registeredAt: Date?): String { .format(registeredAt?.toUIFormat(appContext)) } -fun formatTestResultPendingStepsVisible(uiState: DeviceUIState?): Int = - formatVisibility(uiState == DeviceUIState.PAIRED_NO_RESULT) +fun formatTestResultPendingStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.GONE) { formatVisibility(it == DeviceUIState.PAIRED_NO_RESULT) } -fun formatTestResultNegativeStepsVisible(uiState: DeviceUIState?): Int = - formatVisibility(uiState == DeviceUIState.PAIRED_NEGATIVE) +fun formatTestResultNegativeStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.GONE) { formatVisibility(it == DeviceUIState.PAIRED_NEGATIVE) } -fun formatTestResultPositiveStepsVisible(uiState: DeviceUIState?): Int = - formatVisibility(uiState == DeviceUIState.PAIRED_POSITIVE || uiState == DeviceUIState.PAIRED_POSITIVE_TELETAN) +fun formatTestResultPositiveStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.GONE) { + formatVisibility(it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN) + } -fun formatTestResultInvalidStepsVisible(uiState: DeviceUIState?): Int = - formatVisibility(uiState == DeviceUIState.PAIRED_ERROR || uiState == DeviceUIState.PAIRED_REDEEMED) +fun formatTestResultInvalidStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.GONE) { + formatVisibility(it == DeviceUIState.PAIRED_ERROR || it == DeviceUIState.PAIRED_REDEEMED) + } -fun formatShowRiskStatusCard(deviceUiState: DeviceUIState?): Int = - formatVisibility( - deviceUiState != DeviceUIState.PAIRED_POSITIVE && - deviceUiState != DeviceUIState.PAIRED_POSITIVE_TELETAN && - deviceUiState != DeviceUIState.SUBMITTED_FINAL - ) +fun formatShowRiskStatusCard(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = + uiState.withSuccess(View.GONE) { + formatVisibility( + it != DeviceUIState.PAIRED_POSITIVE && + it != DeviceUIState.PAIRED_POSITIVE_TELETAN && + it != DeviceUIState.SUBMITTED_FINAL + ) + } fun formatCountryIsoTagToLocalizedName(isoTag: String?): String { val country = if (isoTag != null) Locale("", isoTag).displayCountry else "" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt index c64cbedfde89f3c164c26e3eb1ea2204102d2bd3..2e97359373b6de42a333ade72b7b4542c5e42a73 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/FlowPreference.kt @@ -6,6 +6,7 @@ import com.google.gson.Gson import de.rki.coronawarnapp.util.serialization.fromJson import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import timber.log.Timber class FlowPreference<T> constructor( private val preferences: SharedPreferences, @@ -17,6 +18,21 @@ class FlowPreference<T> constructor( private val flowInternal = MutableStateFlow(internalValue) val flow: Flow<T> = flowInternal + private val preferenceChangeListener = + SharedPreferences.OnSharedPreferenceChangeListener { changedPrefs, changedKey -> + if (changedKey != key) return@OnSharedPreferenceChangeListener + + val newValue = reader(changedPrefs, changedKey) + val currentvalue = flowInternal.value + if (currentvalue != newValue && flowInternal.compareAndSet(currentvalue, newValue)) { + Timber.v("%s:%s changed to %s", changedPrefs, changedKey, newValue) + } + } + + init { + preferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener) + } + private var internalValue: T get() = reader(preferences, key) set(newValue) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..0469b3840d2b9417a4ad728314f57cfa871a63ec --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/preferences/SharedPreferenceExtensions.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.util.preferences + +import android.content.SharedPreferences +import androidx.core.content.edit +import timber.log.Timber + +fun SharedPreferences.clearAndNotify() { + val currentKeys = this.all.keys.toSet() + Timber.v("%s clearAndNotify(): %s", this, currentKeys) + edit { + currentKeys.forEach { remove(it) } + } + // Clear does not notify anyone using registerOnSharedPreferenceChangeListener + edit(commit = true) { + clear() + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt index 370ab58fd4ce02e2358bc1dd9f66557020403546..c1c145e4965c2cd874b65a4c107f0976e59557dd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt @@ -27,6 +27,7 @@ import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.exception.CwaSecurityException import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent +import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.security.SecurityConstants.CWA_APP_SQLITE_DB_PW import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MAX_LENGTH import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MIN_LENGTH @@ -80,7 +81,7 @@ object SecurityHelper { @SuppressLint("ApplySharedPref") fun resetSharedPrefs() { - globalEncryptedSharedPreferencesInstance.edit().clear().commit() + globalEncryptedSharedPreferencesInstance.clearAndNotify() } private fun getStoredDbPassword(): ByteArray? = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt index 601f833c3ded2777122e62d6814e07edff3380a3..f014dc54c28ca112b2a49db88917add6c83fedca 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/serialization/GsonExtensions.kt @@ -11,12 +11,13 @@ inline fun <reified T> Gson.fromJson(json: String): T = fromJson( object : TypeToken<T>() {}.type ) -inline fun <reified T> Gson.fromJson(file: File): T = file.reader().use { +inline fun <reified T> Gson.fromJson(file: File): T = file.bufferedReader().use { fromJson(it, object : TypeToken<T>() {}.type) } -inline fun <reified T> Gson.toJson(data: T, file: File) = file.writer().use { writer -> +inline fun <reified T> Gson.toJson(data: T, file: File) = file.bufferedWriter().use { writer -> toJson(data, writer) + writer.flush() } fun <T : Any> KClass<T>.getDefaultGsonTypeAdapter(): TypeAdapter<T> = Gson().getAdapter(this.java) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt index 9f3eddff208662fb70a1996c9756b4fcec245f27..40cd6f7685f0ef272a58f763ef318c676be1d5c2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/viewmodel/CWAViewModel.kt @@ -5,8 +5,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.rki.coronawarnapp.util.coroutine.DefaultDispatcherProvider import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import timber.log.Timber import kotlin.coroutines.CoroutineContext @@ -29,7 +29,13 @@ abstract class CWAViewModel constructor( fun launch( context: CoroutineContext = dispatcherProvider.Default, block: suspend CoroutineScope.() -> Unit - ): Job = viewModelScope.launch(context = context, block = block) + ) { + try { + viewModelScope.launch(context = context, block = block) + } catch (e: CancellationException) { + Timber.w(e, "launch()ed coroutine was canceled.") + } + } @CallSuper override fun onCleared() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt index 2b8f2cebec6728c11a56ad81194fdb577c10720d..8f1569b31291269029d022e4da11633af2177d4b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkHelper.kt @@ -2,9 +2,6 @@ package de.rki.coronawarnapp.worker import androidx.work.Constraints import androidx.work.NetworkType -import de.rki.coronawarnapp.notification.NotificationHelper -import de.rki.coronawarnapp.storage.LocalData -import timber.log.Timber import kotlin.random.Random /** @@ -57,18 +54,4 @@ object BackgroundWorkHelper { .Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() - - /** - * Send debug notification to check background jobs execution - * - * @param title: String - * @param content: String - * - * @see LocalData.backgroundNotification() - */ - fun sendDebugNotification(title: String, content: String) { - Timber.d("sendDebugNotification(title=%s, content=%s)", title, content) - if (!LocalData.backgroundNotification()) return - NotificationHelper.sendNotification(title, content, true) - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt index 59631fb84cbdf88ac69cf8dd916eb2412224fb68..cc7b277a69444e7cb9dd6298fa1c612cf75a477a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkScheduler.kt @@ -3,9 +3,8 @@ package de.rki.coronawarnapp.worker import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingWorkPolicy import androidx.work.Operation -import androidx.work.WorkManager import androidx.work.WorkInfo -import de.rki.coronawarnapp.BuildConfig +import androidx.work.WorkManager import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.storage.LocalData import timber.log.Timber @@ -91,8 +90,7 @@ object BackgroundWorkScheduler { LocalData.initialPollingForTestResultTimeStamp(System.currentTimeMillis()) notificationBody.append("[DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER]") } - BackgroundWorkHelper.sendDebugNotification( - "Background Job Starting", notificationBody.toString()) + Timber.d("Background Job Starting: %s", notificationBody) } /** @@ -144,8 +142,7 @@ object BackgroundWorkScheduler { workManager.cancelAllWorkByTag(workTag.tag) .also { it.logOperationCancelByTag(workTag) } } - BackgroundWorkHelper.sendDebugNotification( - "All Background Jobs Stopped", "All Background Jobs Stopped") + Timber.d("All Background Jobs Stopped") } /** @@ -277,28 +274,24 @@ object BackgroundWorkScheduler { * Log operation schedule */ private fun Operation.logOperationSchedule(workType: WorkType) = - this.result.addListener({ - Timber.d("${workType.uniqueName} completed.") - BackgroundWorkHelper.sendDebugNotification( - "Background Job Started", "${workType.uniqueName} scheduled") - }, { it.run() }) - .also { if (BuildConfig.DEBUG) Timber.d("${workType.uniqueName} scheduled.") } + this.result.addListener( + { Timber.d("${workType.uniqueName} completed.") }, + { it.run() } + ).also { Timber.d("${workType.uniqueName} scheduled.") } /** * Log operation cancellation */ private fun Operation.logOperationCancelByTag(workTag: WorkTag) = - this.result.addListener({ - Timber.d("All work with tag ${workTag.tag} canceled.") - BackgroundWorkHelper.sendDebugNotification( - "Background Job canceled", "${workTag.tag} canceled") - }, { it.run() }) - .also { if (BuildConfig.DEBUG) Timber.d("Canceling all work with tag ${workTag.tag}") } + this.result.addListener( + { Timber.d("All work with tag ${workTag.tag} canceled.") }, + { it.run() } + ).also { Timber.d("Canceling all work with tag ${workTag.tag}") } /** * Log work active status */ private fun logWorkActiveStatus(tag: String, active: Boolean) { - if (BuildConfig.DEBUG) Timber.d("Work type $tag is active: $active") + Timber.d("Work type $tag is active: $active") } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt index ef5f51077634e914630f793378753b17336e4432..5546e8947deebec1c3536ed4e206da4e22fbfbc3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalOneTimeWorker.kt @@ -32,29 +32,19 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( override suspend fun doWork(): Result { Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") - BackgroundWorkHelper.sendDebugNotification( - "KeyOneTime Executing: Start", "KeyOneTime started. Run attempt: $runAttemptCount " - ) - var result = Result.success() taskController.submitBlocking( DefaultTaskRequest( DownloadDiagnosisKeysTask::class, - DownloadDiagnosisKeysTask.Arguments(null, true) + DownloadDiagnosisKeysTask.Arguments(), + originTag = "DiagnosisKeyRetrievalOneTimeWorker" ) ).error?.also { error: Throwable -> - Timber.w( - error, "$id: Error during startWithConstraints()." - ) + Timber.w(error, "$id: Error when submitting DownloadDiagnosisKeysTask.") if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Timber.w(error, "$id: Retry attempts exceeded.") - BackgroundWorkHelper.sendDebugNotification( - "KeyOneTime Executing: Failure", - "KeyOneTime failed with $runAttemptCount attempts" - ) - return Result.failure() } else { Timber.d(error, "$id: Retrying.") @@ -62,10 +52,6 @@ class DiagnosisKeyRetrievalOneTimeWorker @AssistedInject constructor( } } - BackgroundWorkHelper.sendDebugNotification( - "KeyOneTime Executing: End", "KeyOneTime result: $result " - ) - Timber.d("$id: doWork() finished with %s", result) return result } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt index f0dc4742ce5993b5425f8feabb4d67173df09538..4bcd5bc8f1a684461d0a1e0d92cf290487c4741e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisKeyRetrievalPeriodicWorker.kt @@ -31,10 +31,6 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor( override suspend fun doWork(): Result { Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") - BackgroundWorkHelper.sendDebugNotification( - "KeyPeriodic Executing: Start", "KeyPeriodic started. Run attempt: $runAttemptCount" - ) - var result = Result.success() try { BackgroundWorkScheduler.scheduleDiagnosisKeyOneTimeWork() @@ -46,11 +42,6 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor( if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Timber.w(e, "$id: Retry attempts exceeded.") - BackgroundWorkHelper.sendDebugNotification( - "KeyPeriodic Executing: Failure", - "KeyPeriodic failed with $runAttemptCount attempts" - ) - return Result.failure() } else { Timber.d(e, "$id: Retrying.") @@ -58,10 +49,6 @@ class DiagnosisKeyRetrievalPeriodicWorker @AssistedInject constructor( } } - BackgroundWorkHelper.sendDebugNotification( - "KeyPeriodic Executing: End", "KeyPeriodic result: $result " - ) - Timber.d("$id: doWork() finished with %s", result) return result } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt index dd3bcea83189fbf7934533fd8073232917b60b31..1453a32bb33c4e5b207d49b68093c49fd667564b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorker.kt @@ -28,10 +28,6 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( private val submissionService: SubmissionService ) : CoroutineWorker(context, workerParams) { - companion object { - private val TAG: String? = DiagnosisTestResultRetrievalPeriodicWorker::class.simpleName - } - /** * Work execution * @@ -46,16 +42,10 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( override suspend fun doWork(): Result { Timber.d("$id: doWork() started. Run attempt: $runAttemptCount") - BackgroundWorkHelper.sendDebugNotification( - "TestResult Executing: Start", "TestResult started. Run attempt: $runAttemptCount " - ) if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Timber.d("$id doWork() failed after $runAttemptCount attempts. Rescheduling") - BackgroundWorkHelper.sendDebugNotification( - "TestResult Executing: Failure", "TestResult failed with $runAttemptCount attempts" - ) BackgroundWorkScheduler.scheduleDiagnosisKeyPeriodicWork() Timber.d("$id Rescheduled background worker") @@ -81,9 +71,6 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( result = Result.retry() } - BackgroundWorkHelper.sendDebugNotification( - "TestResult Executing: End", "TestResult result: $result " - ) Timber.d("$id: doWork() finished with %s", result) return result @@ -133,9 +120,6 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( LocalData.initialPollingForTestResultTimeStamp(0L) BackgroundWorkScheduler.WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() Timber.d("$id: Background worker stopped") - BackgroundWorkHelper.sendDebugNotification( - "TestResult Stopped", "TestResult Stopped" - ) } @AssistedInject.Factory diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information.xml b/Corona-Warn-App/src/main/res/layout/fragment_information.xml index d51c313a9172fd2dba61cbbcdfa0a807b3e55a57..b18582525731b852608527024c79dc44b8c39503 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information.xml @@ -1,12 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - - <data> - - <import type="de.rki.coronawarnapp.util.formatter.FormatterInformationHelper" /> - - </data> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/information_container" @@ -117,11 +112,27 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" android:focusable="true" - android:text="@{FormatterInformationHelper.formatVersion()}" + tools:text="v1.8.0-RC1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline_body" app:layout_constraintTop_toBottomOf="@+id/information_legal" /> + <TextView + android:id="@+id/information_enf_version" + style="@style/body2Medium" + android:visibility="gone" + tools:visibility="visible" + android:layout_width="@dimen/match_constraint" + android:paddingTop="@dimen/spacing_tiny" + android:paddingBottom="@dimen/spacing_tiny" + android:layout_height="wrap_content" + android:focusable="true" + android:background="?selectableItemBackground" + tools:text="16000000" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/guideline_body" + app:layout_constraintTop_toBottomOf="@+id/information_version" /> + <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline_body" android:layout_width="wrap_content" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml index d5a9a1cd10884ea60e9a489a467157ee8a0f1ccb..1c977477629c010d92caec824dc2ce8c00b41d77 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml @@ -5,9 +5,9 @@ <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/submission_done_container" - android:contentDescription="@string/submission_done_title" android:layout_width="match_parent" android:layout_height="match_parent" + android:contentDescription="@string/submission_done_title" android:fillViewport="true" tools:context=".ui.submission.fragment.SubmissionQRCodeInfo"> @@ -17,228 +17,20 @@ android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" app:icon="@{@drawable/ic_back}" - app:title="@string/submission_qr_info_headline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintBottom_toTopOf="@id/submission_qr_code_info_scrollview" /> + app:layout_constraintTop_toTopOf="parent" + app:title="@string/submission_qr_info_headline" /> - <ScrollView - android:id="@+id/submission_qr_code_info_scrollview" - android:layout_width="match_parent" - android:layout_height="wrap_content" + <include + android:id="@+id/include_submission_qr_code_info" + layout="@layout/include_submission_qr_code_info" + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/match_constraint" + app:layout_constraintBottom_toTopOf="@id/guideline_action" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_qr_code_info_header"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:focusable="true"> - - <ImageView - android:id="@+id/submission_qr_info_illustration" - android:layout_width="@dimen/match_constraint" - android:layout_height="wrap_content" - android:focusable="true" - android:src="@drawable/ic_illustrations_qr_code_scan_info" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_1" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:focusable="true" - android:layout_marginTop="@dimen/bullet_point_spacing_after" - android:layout_marginHorizontal="@dimen/guideline_card" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/submission_qr_info_illustration"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_1_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/circle" - android:backgroundTint="@color/card_dark" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <ImageView - style="@style/icon" - android:layout_width="@dimen/icon_size_risk_details_behavior" - android:layout_height="@dimen/icon_size_risk_details_behavior" - android:layout_margin="@dimen/icon_margin_risk_details_behavior" - android:focusable="false" - android:importantForAccessibility="no" - android:src="@drawable/ic_qr_icon_personal_result" - android:tint="@color/button_primary" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <TextView - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_small" - android:text="@string/submission_qr_info_point_1_body" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/qr_info_step_1_icon" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_2" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:focusable="true" - android:layout_marginTop="@dimen/bullet_point_spacing_after" - android:layout_marginHorizontal="@dimen/guideline_card" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/qr_info_step_1"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_2_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/circle" - android:backgroundTint="@color/card_dark" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <ImageView - style="@style/icon" - android:layout_width="@dimen/icon_size_risk_details_behavior" - android:layout_height="@dimen/icon_size_risk_details_behavior" - android:layout_margin="@dimen/icon_margin_risk_details_behavior" - android:focusable="false" - android:importantForAccessibility="no" - android:src="@drawable/ic_qr_icon_test_scan" - android:tint="@color/button_primary" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <TextView - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_small" - android:text="@string/submission_qr_info_point_2_body" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/qr_info_step_2_icon" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_3" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:focusable="true" - android:layout_marginTop="@dimen/bullet_point_spacing_after" - android:layout_marginHorizontal="@dimen/guideline_card" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/qr_info_step_2"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_3_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/circle" - android:backgroundTint="@color/card_dark" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <ImageView - style="@style/icon" - android:layout_width="@dimen/icon_size_risk_details_behavior" - android:layout_height="@dimen/icon_size_risk_details_behavior" - android:layout_margin="@dimen/icon_margin_risk_details_behavior" - android:focusable="false" - android:importantForAccessibility="no" - android:src="@drawable/ic_qr_icon_multiple_tests" - android:tint="@color/button_primary" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <TextView - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_small" - android:text="@string/submission_qr_info_point_3_body" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/qr_info_step_3_icon" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_4" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:focusable="true" - android:layout_marginTop="@dimen/bullet_point_spacing_after" - android:layout_marginHorizontal="@dimen/guideline_card" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/qr_info_step_3"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/qr_info_step_4_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:background="@drawable/circle" - android:backgroundTint="@color/card_dark" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> - - <ImageView - style="@style/icon" - android:layout_width="@dimen/icon_size_risk_details_behavior" - android:layout_height="@dimen/icon_size_risk_details_behavior" - android:layout_margin="@dimen/icon_margin_risk_details_behavior" - android:focusable="false" - android:importantForAccessibility="no" - android:src="@drawable/ic_qr_icon_info" - android:tint="@color/button_primary" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - - <TextView - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_small" - android:text="@string/submission_qr_info_point_4_body" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/qr_info_step_4_icon" - app:layout_constraintTop_toTopOf="parent" /> - </androidx.constraintlayout.widget.ConstraintLayout> - </androidx.constraintlayout.widget.ConstraintLayout> - - </ScrollView> + app:layout_constraintTop_toBottomOf="@+id/submission_qr_code_info_header" /> <Button android:id="@+id/submission_qr_info_button_next" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml index a34166dd59df62849ed4df4572518c4ef5ea3e24..ecf73ebc437f034b2ec90d622215160aba1e9a20 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_calendar.xml @@ -9,10 +9,6 @@ <import type="de.rki.coronawarnapp.submission.Symptoms.StartOf" /> - <variable - name="submissionViewModel" - type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" /> - </data> <ScrollView diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml index 0fb5a5e024a58ccb1cb3699b28add5c405dee041..e577341719fc20305e51f0ef2b7ea5eaeb8d37e2 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_symptom_intro.xml @@ -7,10 +7,6 @@ <import type="de.rki.coronawarnapp.util.formatter.FormatterSubmissionHelper" /> - <variable - name="submissionViewModel" - type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" /> - </data> <ScrollView diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml index 3521faa0f64126875ffb9317c6782159fa998fff..9b40a94aa99fca525d7cf72b5313f2b2e1f8cef3 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result.xml @@ -34,7 +34,7 @@ style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{FormatterSubmissionHelper.formatTestResultSpinnerVisible(uiState.apiRequestState)}" + android:visibility="@{FormatterSubmissionHelper.formatTestResultSpinnerVisible(uiState.deviceUiState)}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -48,7 +48,7 @@ android:layout_width="@dimen/match_constraint" android:layout_height="@dimen/match_constraint" android:layout_marginBottom="@dimen/button_padding_top_bottom" - android:visibility="@{FormatterSubmissionHelper.formatTestResultVisible(uiState.apiRequestState)}" + android:visibility="@{FormatterSubmissionHelper.formatTestResultVisible(uiState.deviceUiState)}" app:layout_constraintBottom_toTopOf="@+id/include_submission_test_result_buttons" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_qr_code_info.xml b/Corona-Warn-App/src/main/res/layout/include_submission_qr_code_info.xml new file mode 100644 index 0000000000000000000000000000000000000000..2f68632b6174c70d157e0e067b55b1eca08b06ae --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_submission_qr_code_info.xml @@ -0,0 +1,219 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <ScrollView + android:id="@+id/submission_qr_code_info_scrollview" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusable="true"> + + <ImageView + android:id="@+id/submission_qr_info_illustration" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:focusable="true" + android:src="@drawable/ic_illustrations_qr_code_scan_info" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_1" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="@dimen/bullet_point_spacing_after" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/submission_qr_info_illustration"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_1_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/circle" + android:backgroundTint="@color/card_dark" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + style="@style/icon" + android:layout_width="@dimen/icon_size_risk_details_behavior" + android:layout_height="@dimen/icon_size_risk_details_behavior" + android:layout_margin="@dimen/icon_margin_risk_details_behavior" + android:focusable="false" + android:importantForAccessibility="no" + android:src="@drawable/ic_qr_icon_personal_result" + android:tint="@color/button_primary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_small" + android:text="@string/submission_qr_info_point_1_body" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/qr_info_step_1_icon" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_2" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="@dimen/bullet_point_spacing_after" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qr_info_step_1"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_2_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/circle" + android:backgroundTint="@color/card_dark" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + style="@style/icon" + android:layout_width="@dimen/icon_size_risk_details_behavior" + android:layout_height="@dimen/icon_size_risk_details_behavior" + android:layout_margin="@dimen/icon_margin_risk_details_behavior" + android:focusable="false" + android:importantForAccessibility="no" + android:src="@drawable/ic_qr_icon_test_scan" + android:tint="@color/button_primary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_small" + android:text="@string/submission_qr_info_point_2_body" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/qr_info_step_2_icon" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_3" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="@dimen/bullet_point_spacing_after" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qr_info_step_2"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_3_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/circle" + android:backgroundTint="@color/card_dark" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + style="@style/icon" + android:layout_width="@dimen/icon_size_risk_details_behavior" + android:layout_height="@dimen/icon_size_risk_details_behavior" + android:layout_margin="@dimen/icon_margin_risk_details_behavior" + android:focusable="false" + android:importantForAccessibility="no" + android:src="@drawable/ic_qr_icon_multiple_tests" + android:tint="@color/button_primary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_small" + android:text="@string/submission_qr_info_point_3_body" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/qr_info_step_3_icon" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_4" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/guideline_card" + android:layout_marginTop="@dimen/bullet_point_spacing_after" + android:focusable="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qr_info_step_3"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qr_info_step_4_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/circle" + android:backgroundTint="@color/card_dark" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <ImageView + style="@style/icon" + android:layout_width="@dimen/icon_size_risk_details_behavior" + android:layout_height="@dimen/icon_size_risk_details_behavior" + android:layout_margin="@dimen/icon_margin_risk_details_behavior" + android:focusable="false" + android:importantForAccessibility="no" + android:src="@drawable/ic_qr_icon_info" + android:tint="@color/button_primary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_small" + android:text="@string/submission_qr_info_point_4_body" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/qr_info_step_4_icon" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> + </androidx.constraintlayout.widget.ConstraintLayout> + + </ScrollView> +</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml b/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml index d95f4ed9f6f4201980e57caa01ec1e06c2d1ff6e..6cbbd318066a22c37059ae2209bd487f7851b341 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_symptom_length_selection.xml @@ -8,10 +8,6 @@ <import type="de.rki.coronawarnapp.submission.Symptoms.StartOf" /> - <variable - name="submissionViewModel" - type="de.rki.coronawarnapp.ui.viewmodel.SubmissionViewModel" /> - </data> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml b/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml index dcdcdea37b799ecb255d0e23b0b3d68d0499f0ad..01364c7898c6942746da48f840b496513e186caf 100644 --- a/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml +++ b/Corona-Warn-App/src/main/res/layout/include_test_result_card.xml @@ -13,7 +13,7 @@ <variable name="deviceUIState" - type="de.rki.coronawarnapp.util.DeviceUIState" /> + type="de.rki.coronawarnapp.util.NetworkRequestWrapper<de.rki.coronawarnapp.util.DeviceUIState,java.lang.Throwable>" /> </data> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/Corona-Warn-App/src/main/res/values-bg/strings.xml b/Corona-Warn-App/src/main/res/values-bg/strings.xml index 7b20d46a9c3aa5b49191a72f9d811bc7c7f13a15..469ac6b4b49c3c138f8703f8e2d5db12bedf9726 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -20,12 +20,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -109,6 +105,10 @@ <string name="notification_headline">"Corona-Warn-App"</string> <!-- XTXT: Notification body --> <string name="notification_body">"Имате нови ÑÑŠÐ¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¾Ñ‚ приложението Corona-Warn-App."</string> + <!-- XHED: Notification title - Reminder to share a positive test result--> + <string name="notification_headline_share_positive_result">"Можете да помогнете!"</string> + <!-- XTXT: Notification body - Reminder to share a positive test result--> + <string name="notification_body_share_positive_result">"МолÑ, Ñподелете резултата от теÑта Ñи и предупредете оÑтаналите потребители."</string> <!-- #################################### App Auto Update @@ -146,15 +146,13 @@ <item quantity="many">"%1$s Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</item> </plurals> <!-- XTXT: risk card - tracing active for x out of 14 days --> - <string name="risk_card_body_saved_days">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е било активно през %1$s от изминалите 14 дни."</string> + <string name="risk_card_body_saved_days">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк беше активно през %1$s от изминалите 14 дни."</string> <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"ÐÑма прекъÑване на региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT; risk card - no update done yet --> - <string name="risk_card_body_not_yet_fetched">"Ð’Ñе още не е извършвана проверка на излаганиÑта на риÑк."</string> + <string name="risk_card_body_not_yet_fetched">"Ð’Ñе още не е извършвана проверка на контактите."</string> <!-- XTXT: risk card - last successful update --> <string name="risk_card_body_time_fetched">"Ðктуализирано: %1$s"</string> - <!-- XTXT: risk card - next update --> - <string name="risk_card_body_next_update">"Ежедневно актуализиране"</string> <!-- XTXT: risk card - hint to open the app daily --> <string name="risk_card_body_open_daily">"Бележка: МолÑ, отварÑйте приложението вÑеки ден, за да актуализирате ÑÐ²Ð¾Ñ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк."</string> <!-- XBUT: risk card - update risk --> @@ -185,13 +183,19 @@ <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"Ð’ момента не Ñе извършва региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"ПоÑледно региÑтриране на излагане на риÑк:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"ПоÑледна проверка за излагане на риÑк:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"Ðевъзможно региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> <string name="risk_card_outdated_risk_body">"РегиÑтърът на излаганиÑта на риÑк не е обновÑван повече от 24 чаÑа."</string> <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"ВашиÑÑ‚ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк не е обновÑван от повече от 48 чаÑа. МолÑ, актуализирайте го."</string> + <!-- XHED: risk card - risk check failed headline, no internet connection --> + <string name="risk_card_check_failed_no_internet_headline">"Проверката за излагане на риÑк е неуÑпешна"</string> + <!-- XTXT: risk card - risk check failed, please check your internet connection --> + <string name="risk_card_check_failed_no_internet_body">"СинхронизациÑта на Ñлучайни ИД ÑÑŠÑ Ñървъра е неуÑпешна. Можете да Ñ Ñ€ÐµÑтартирате ръчно."</string> + <!-- XTXT: risk card - risk check failed, restart button --> + <string name="risk_card_check_failed_no_internet_restart_button">"РеÑтартиране"</string> <!-- #################################### Risk Card - Progress @@ -245,9 +249,9 @@ <!-- XACT: main overview page title --> <string name="main_overview_accessibility_title">"Общ преглед"</string> <!-- XHED: App overview subtitle for tracing explanation--> - <string name="main_overview_subtitle_tracing">"РегиÑтриране на излаганиÑта на риÑк"</string> + <string name="main_overview_subtitle_tracing">"РегиÑÑ‚ÑŠÑ€ на риÑковете"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е една от трите оÑновни функции на приложението. Когато е активирана, вÑички оÑъщеÑтвени контакти между уÑтройÑтва Ñе запиÑват, без да е необходимо да правите друго."</string> + <string name="main_overview_body_tracing">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк е една от трите оÑновни функции на приложението. Когато е активирана, вÑички оÑъщеÑтвени контакти между Ñмартфоните Ñе запиÑват, без да е необходимо да правите друго."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"РиÑк от заразÑване"</string> <!-- YTXT: App overview body text about risk levels --> @@ -357,7 +361,7 @@ <item quantity="many">"Изложени Ñте на повишен риÑк от заразÑване, защото преди %1$s дни Ñте имали продължителен и близък контакт Ñ Ð¿Ð¾Ð½Ðµ едно лице, диагноÑтицирано Ñ COVID-19."</item> </plurals> <!-- YTXT: risk details - risk calculation explanation --> - <string name="risk_details_information_body_notice">"РиÑкът от заразÑване Ñе изчиÑлÑва въз оÑнова на данните за излагане (продължителноÑÑ‚ и близоÑÑ‚ на контакта), региÑтрирани на вашето локално уÑтройÑтво. Ðикой оÑвен Ð’Ð°Ñ Ð½Ðµ може да види или да получи данни за Вашето ниво на риÑк."</string> + <string name="risk_details_information_body_notice">"РиÑкът от заразÑване Ñе изчиÑлÑва въз оÑнова на данните за излагане (продължителноÑÑ‚ и близоÑÑ‚ на контакта), региÑтрирани във Ð²Ð°ÑˆÐ¸Ñ Ñмартфон. Ðикой оÑвен Ð’Ð°Ñ Ð½Ðµ може да види или да получи данни за Вашето ниво на риÑк."</string> <!-- YTXT: risk details - risk calculation explanation for increased risk --> <string name="risk_details_information_body_notice_increased">"Това е причината да определим Ð’Ð°ÑˆÐ¸Ñ Ñ€Ð¸Ñк от заразÑване като повишен. РиÑкът от заразÑване Ñе изчиÑлÑва въз оÑнова на данните за излагане (продължителноÑÑ‚ и близоÑÑ‚ на контакта), региÑтрирани на вашето локално уÑтройÑтво. Ðикой оÑвен Ð’Ð°Ñ Ð½Ðµ може да види или да получи данни за Вашето ниво на риÑк. Когато Ñе приберете у дома, избÑгвайте близките контакти Ñ Ñ‡Ð»ÐµÐ½Ð¾Ð²ÐµÑ‚Ðµ на домакинÑтвото Ñи."</string> <!-- NOTR --> @@ -415,7 +419,7 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"Повече защита за Ð’Ð°Ñ Ð¸ за вÑички наÑ. С помощта на приложението Corona-Warn-App можем да прекъÑнем веригите на заразÑване много по-бързо."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"Превърнете уÑтройÑтвото Ñи в предупредителна ÑиÑтема за коронавируÑ. Прегледайте ÑÐ²Ð¾Ñ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк и разберете дали през поÑледните 14 дни Ñте имали близък контакт Ñ Ð»Ð¸Ñ†Ðµ, диагноÑтицирано Ñ COVID-19."</string> + <string name="onboarding_body">"Превърнете Ñмартфона Ñи в предупредителна ÑиÑтема за коронавируÑ. Прегледайте ÑÐ²Ð¾Ñ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк и разберете дали през поÑледните 14 дни Ñте имали близък контакт Ñ Ð»Ð¸Ñ†Ðµ, диагноÑтицирано Ñ COVID-19."</string> <!-- YTXT: onboarding(together) - explain application --> <string name="onboarding_body_emphasized">"Приложението региÑтрира контакти на лица поÑредÑтвом обмÑна на криптирани Ñлучайни ИД кодове между уÑтройÑтвата им, без да оÑъщеÑтвÑва доÑтъп до каквито и да било лични данни."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> @@ -432,13 +436,13 @@ <!-- XHED: onboarding(tracing) - how to enable tracing --> <string name="onboarding_tracing_headline">"Как да активирате региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XHED: onboarding(tracing) - two/three line headline under an illustration --> - <string name="onboarding_tracing_subtitle">"За да уÑтановите дали за Ð’Ð°Ñ ÑъщеÑтвува риÑк от заразÑване, Ñ‚Ñ€Ñбва да активирате функциÑта за региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк."</string> + <string name="onboarding_tracing_subtitle">"За да уÑтановите дали Ñте заÑтрашени от заразÑване, Ñ‚Ñ€Ñбва да активирате функциÑта за региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк."</string> <!-- YTXT: onboarding(tracing) - explain tracing --> <string name="onboarding_tracing_body">"РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк Ñе извършва Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰Ñ‚Ð° на Bluetooth връзка, при коÑто ВашиÑÑ‚ Ñмартфон получава криптираните Ñлучайни идентификационни кодове на други потребители и изпраща до техните уÑтройÑтва Вашите Ñлучайни ИД. ФункциÑта може да бъде дезактивирана по вÑÑко време. "</string> <!-- YTXT: onboarding(tracing) - explain tracing --> <string name="onboarding_tracing_body_emphasized">"Криптираните Ñлучайни идентификатори предават Ñамо Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° дата, продължителноÑÑ‚ и близоÑÑ‚ на контакта (изчиÑлена от Ñилата на Ñигнала). СамоличноÑтта Ви не може да бъде уÑтановена по Ñлучайните ИД."</string> <!-- YTXT: onboarding(tracing) - easy language explain tracing link--> - <string name="onboarding_tracing_easy_language_explanation"><a href="https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache">"Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението на опроÑтен и жеÑтомимичен език."</a></string> + <string name="onboarding_tracing_easy_language_explanation"><a href="https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache">"Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° приложението на опроÑтен и жеÑтомимичен език"</a></string> <!-- NOTR: onboarding(tracing) - easy language explain tracing link URL--> <string name="onboarding_tracing_easy_language_explanation_url">"https://www.bundesregierung.de/breg-de/themen/corona-warn-app/corona-warn-app-leichte-sprache-gebaerdensprache"</string> <!-- XBUT: onboarding(tracing) - button enable tracing --> @@ -452,7 +456,7 @@ <!-- XBUT: onboarding(tracing) - negative button (right) --> <string name="onboarding_tracing_dialog_button_negative">"Ðазад"</string> <!-- XACT: onboarding(tracing) - dialog about background jobs header text --> - <string name="onboarding_background_fetch_dialog_headline">"Фоновите актуализации Ñа дезактивирани"</string> + <string name="onboarding_background_fetch_dialog_headline">"Фоновото опреÑнÑване за приложението е дезактивирано"</string> <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"Дезактивирали Ñте фоновите актуализации за приложението Corona-Warn-App. МолÑ, активирайте ги, за да използвате автоматичното региÑтриране на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк. Ðко не го направите, региÑтрирането на излаганиÑта може да бъде Ñтартирано Ñамо ръчно от приложението. Може да активирате фоновите актуализации за приложението от наÑтройките на Вашето уÑтройÑтво."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> @@ -494,7 +498,7 @@ <!-- XACT: Onboarding (datashare) page title --> <string name="onboarding_notifications_accessibility_title">"Въведение - Ñтраница 6 от 6: Получаване на Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸ идентифициране на риÑкове"</string> <!-- XHED: onboarding(datashare) - about positive tests --> - <string name="onboarding_notifications_headline">"Получаване на Ð¿Ñ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸ идентифициране на риÑкове"</string> + <string name="onboarding_notifications_headline">"ÐŸÑ€ÐµÐ´ÑƒÐ¿Ñ€ÐµÐ¶Ð´ÐµÐ½Ð¸Ñ Ð¸ риÑкове"</string> <!-- XHED: onboarding(datashare) - two/three line headline under an illustration --> <string name="onboarding_notifications_subtitle">"Приложението може да Ви уведомÑва автоматично за Ð’Ð°ÑˆÐ¸Ñ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк от заразÑване и да Ви предупреждава за нови заразÑÐ²Ð°Ð½Ð¸Ñ Ð½Ð° хора, Ñ ÐºÐ¾Ð¸Ñ‚Ð¾ Ñте били в контакт. Позволете на приложението да Ви изпраща извеÑтиÑ."</string> <!-- YTXT: onboarding(datashare) - explain test --> @@ -524,7 +528,7 @@ <!-- XTXT: settings - off, like a label next to a setting --> <string name="settings_off">"Изключено"</string> <!-- XHED: settings(tracing) - page title --> - <string name="settings_tracing_title">"РегиÑтриране на излаганиÑта на риÑк"</string> + <string name="settings_tracing_title">"РегиÑÑ‚ÑŠÑ€ на риÑковете"</string> <!-- XHED: settings(tracing) - headline bellow illustration --> <string name="settings_tracing_headline">"Как работи региÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк"</string> <!-- XTXT: settings(tracing) - explain text in settings overview under headline --> @@ -606,7 +610,7 @@ <!-- XTXT: settings(notification) - next to a switch --> <string name="settings_notifications_subtitle_update_risk">"ПромÑна на Ð’Ð°ÑˆÐ¸Ñ Ñ€Ð¸Ñк от заразÑване"</string> <!-- XTXT: settings(notification) - next to a switch --> - <string name="settings_notifications_subtitle_update_test">"Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð½Ð° Ð’Ð°ÑˆÐ¸Ñ Ñ‚ÐµÑÑ‚ за COVID-19"</string> + <string name="settings_notifications_subtitle_update_test">"Ðаличие на резултат от Ваш теÑÑ‚ за COVID-19"</string> <!-- XBUT: settings(notification) - go to operating settings --> <string name="settings_notifications_button_open_settings">"Към наÑтройките за уÑтройÑтвото"</string> <!-- XACT: main (overview) - illustraction description, explanation image, displays notificatin status, active --> @@ -652,7 +656,7 @@ <!-- XTXT: settings(background priority) - explains user what to do on card if background priority is enabled --> <string name="settings_background_priority_card_body">"Можете да активирате и дезактивирате приоритетната работа във фонов режим от наÑтройките на уÑтройÑтвото."</string> <!-- XBUT: settings(background priority) - go to operating system settings button on card --> - <string name="settings_background_priority_card_button">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="settings_background_priority_card_button">"Към наÑтройките на уÑтройÑтвото"</string> <!-- XHED : settings(background priority) - headline on card about the current status and what to do --> <string name="settings_background_priority_card_headline">"ПромÑна на приоритетната работа във фонов режим"</string> @@ -671,7 +675,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"ИнÑтитутът „Роберт Кох“ (RKI) е федералната Ñлужба за общеÑтвено здравеопазване в ГерманиÑ. Той е издател на приложението Corona-Warn-App по поръчка на федералното правителÑтво. Приложението е предназначено да бъде дигитално допълнение на вече въведените мерки за опазване на общеÑтвеното здраве: Ñоциално диÑтанциране, поддържане на виÑока хигиена и ноÑене на маÑки."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Хората, които използват приложението, помагат за проÑледÑване и прекъÑване на веригите на заразÑване. Приложението запазва във Вашето уÑтройÑтво данните за контактите Ви Ñ Ð´Ñ€ÑƒÐ³Ð¸ хора. Получавате извеÑтие, ако Ñте били в контакт Ñ Ð»Ð¸Ñ†Ð°, които впоÑледÑтвие Ñа били диагноÑтицирани Ñ COVID-19. Вашата ÑамоличноÑÑ‚ и неприкоÑновеноÑтта на данните Ви Ñа защитени по вÑÑко време."</string> + <string name="information_about_body">"Ð’Ñеки, който използва приложението, помага за проÑледÑване и прекъÑване на веригите на заразÑване. Приложението запазва във Ð’Ð°ÑˆÐ¸Ñ Ñмартфон данните за контактите Ви Ñ Ð´Ñ€ÑƒÐ³Ð¸ хора. Получавате извеÑтие, ако Ñте били в контакт Ñ Ð»Ð¸Ñ†Ð°, които впоÑледÑтвие Ñа били диагноÑтицирани Ñ COVID-19. Вашата ÑамоличноÑÑ‚ и неприкоÑновеноÑтта на данните Ви Ñа защитени по вÑÑко време."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Група лица използват Ñмартфоните Ñи, придвижвайки Ñе из града."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -689,13 +693,13 @@ <!-- XTXT: Path to the full blown terms html, to translate it exchange "_de" to "_en" and provide the corresponding html file --> <string name="information_terms_html_path">"terms_en.html"</string> <!-- XHED: Page title for technical contact and hotline information page, also menu item / button text --> - <string name="information_contact_title">"Гореща Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки въпроÑи"</string> + <string name="information_contact_title">"ТехничеÑки въпроÑи"</string> <!-- XHED: Subtitle for technical contact and hotline information page --> <string name="information_contact_headline">"Как можем да Ви помогнем?"</string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body">"За техничеÑки въпроÑи отноÑно Corona-Warn-App Ñе обадете на горещата линиÑ.\n\nЛицата ÑÑŠÑ Ñлухови Ð·Ð°Ñ‚Ñ€ÑƒÐ´Ð½ÐµÐ½Ð¸Ñ Ð¼Ð¾Ð³Ð°Ñ‚ да използват релейните уÑлуги Tess (за превод между пиÑмен немÑки и жеÑтомимичен език), за да Ñе обаждат на горещата телефонна линиÑ. Може да изтеглите приложението от Google Play."</string> <!-- XHED: Subtitle for technical contact and hotline information page --> - <string name="information_contact_subtitle_phone">"Гореща Ð»Ð¸Ð½Ð¸Ñ Ð·Ð° техничеÑки въпроÑи:"</string> + <string name="information_contact_subtitle_phone">"ТехничеÑки въпроÑи:"</string> <!-- XLNK: Button / hyperlink to phone call for technical contact and hotline information page --> <string name="information_contact_button_phone">"+49 800 7540001"</string> <!-- XBUT: CAUTION - ONLY UPDATE THE NUMBER IF NEEDED, ONLY NUMBERS AND NO SPECIAL CHARACTERS EXCEPT "+" and "space" ALLOWED IN THIS FIELD; --> @@ -703,9 +707,9 @@ <!-- XTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_phone">"ÐашиÑÑ‚ екип за обÑлужване на клиенти е готов да Ви помогне."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"Езици: немÑки, английÑки, турÑки\nРаботно време:"<xliff:g id="line_break">"\n"</xliff:g>"понеделник до неделÑ: 7:00 - 22:00"<xliff:g id="line_break">"\n(Ñ Ð¸Ð·ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ðµ на национални празници)"</xliff:g><xliff:g id="line_break">"\nОбаждането е безплатно."</xliff:g></string> + <string name="information_contact_body_open">"Езици: английÑки, немÑки, турÑки\nРаботно време:"<xliff:g id="line_break">"\n"</xliff:g>"понеделник до неделÑ: 7:00 - 22:00"<xliff:g id="line_break">"\n(Ñ Ð¸Ð·ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ðµ на национални празници)"</xliff:g><xliff:g id="line_break">"\nОбаждането е безплатно."</xliff:g></string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_other">"Ðко имате въпроÑи, Ñвързани ÑÑŠÑ Ð·Ð´Ñ€Ð°Ð²ÐµÑ‚Ð¾, Ñе обадете на Ð»Ð¸Ñ‡Ð½Ð¸Ñ Ñи лекар или на горещата Ð»Ð¸Ð½Ð¸Ñ Ð½Ð° Ñлужбата за медицинÑка помощ на телефон 116 117"</string> + <string name="information_contact_body_other">"Ðко имате въпроÑи, Ñвързани ÑÑŠÑ Ð·Ð´Ñ€Ð°Ð²ÐµÑ‚Ð¾, Ñе обадете на Ð»Ð¸Ñ‡Ð½Ð¸Ñ Ñи лекар или на горещата Ð»Ð¸Ð½Ð¸Ñ Ð½Ð° Ñлужбата за медицинÑка помощ на телефон 116 117."</string> <!-- XACT: describes illustration --> <string name="information_contact_illustration_description">"Мъж ÑÑŠÑ Ñлушалки провежда телефонен разговор."</string> <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website --> @@ -788,9 +792,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"Ðазад"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Грешки в теÑта"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"QR кодът вече не е валиден"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"Възникнаха грешки при проверката на Ð’Ð°ÑˆÐ¸Ñ Ñ‚ÐµÑÑ‚. Този QR код вече е изтекъл."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"ВашиÑÑ‚ теÑÑ‚ е направен преди повече от 21 дни и вече не може да бъде региÑтриран в приложението. Ðко в бъдеще Ñи правите нов теÑÑ‚, Ð¼Ð¾Ð»Ñ Ñканирайте QR кода в момента, в който го получите."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> @@ -851,7 +855,7 @@ <!-- Submission Test Result --> <!-- XHED: Page headline for test result --> - <string name="submission_test_result_headline">"Резултат от теÑта"</string> + <string name="submission_test_result_headline">"Резултат от теÑÑ‚"</string> <!-- XHED: Page subheadline for test result --> <string name="submission_test_result_subtitle">"Как работи:"</string> <!-- XHED: Page headline for results next steps --> @@ -865,15 +869,15 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Ðктуализиране"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Изтриване на теÑÑ‚"</string> + <string name="submission_test_result_pending_remove_test_button">"Изтриване на теÑта"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Резултатът от Ð’Ð°ÑˆÐ¸Ñ Ñ‚ÐµÑÑ‚"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"ВашиÑÑ‚ лабораторен резултат не потвърждава заразÑване Ñ ÐºÐ¾Ñ€Ð¾Ð½Ð°Ð²Ð¸Ñ€ÑƒÑ SARS-CoV-2.\n\nМолÑ, изтрийте теÑта от приложението Corona-Warn-App, за да можете да запазите нов код на теÑÑ‚, ако е необходимо."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Изтриване на теÑÑ‚"</string> + <string name="submission_test_result_negative_remove_test_button">"Изтриване на теÑта"</string> <!-- XHED: Page headline for other warnings screen --> - <string name="submission_test_result_positive_steps_warning_others_heading">"Предупреждаване на оÑтаналите потребители"</string> + <string name="submission_test_result_positive_steps_warning_others_heading">"Предупредете другите"</string> <!-- YTXT: Body text for for other warnings screen--> <string name="submission_test_result_positive_steps_warning_others_body">"Споделете Ñвоите Ñлучайни идентификатори и предупредете околните.\nПомогнете ни да определÑме риÑка от заразÑване за Ñ‚ÑÑ… по-точно, като Ñподелите и кога за пръв път Ñте забелÑзали Ñимптомите на коронавируÑната инфекциÑ."</string> <!-- XBUT: positive test result : continue button --> @@ -952,13 +956,13 @@ <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"РегиÑтрирайте теÑта Ñи, като Ñканирате QR кода на документа."</string> <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"СъглаÑие"</string> + <string name="submission_dispatcher_qr_privacy_dialog_headline">"ПоверителноÑÑ‚"</string> <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Acceptâ€, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacyâ€."</string> <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Приемам"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Разрешавам"</string> <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Ðе приемам"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Ðе разрешавам"</string> <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"ТÐРкод"</string> <!-- YTXT: Body text for TAN code dispatcher option --> @@ -972,7 +976,7 @@ <!-- Submission Positive Other Warning --> <!-- XHED: Page title for the positive result additional warning page--> - <string name="submission_positive_other_warning_title">"Предупреждаване на оÑтаналите потребители"</string> + <string name="submission_positive_other_warning_title">"Предупредете другите"</string> <!-- XHED: Page headline for the positive result additional warning page--> <string name="submission_positive_other_warning_headline">"МолÑ, помогнете на вÑички наÑ!"</string> <!-- YTXT: Body text for the positive result additional warning page--> @@ -980,7 +984,7 @@ <!-- XBUT: other warning continue button --> <string name="submission_positive_other_warning_button">"Приемам"</string> <!-- XACT: other warning - illustration description, explanation image --> - <string name="submission_positive_other_illustration_description">"УÑтройÑтво предава на ÑиÑтемата Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° положителен резултат от теÑÑ‚."</string> + <string name="submission_positive_other_illustration_description">"Смартфонът предава на ÑиÑтемата Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð·Ð° положителен резултат от теÑÑ‚."</string> <!-- XHED: Title for the interop country list--> <string name="submission_interoperability_list_title">"Ð’ момента в международното региÑтриране на излаганиÑта учаÑтват Ñледните държави:"</string> @@ -1071,9 +1075,9 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"РегиÑтрирайте теÑта, като въведете ТÐРкода в приложението."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"Езици: \nнемÑки, английÑки, турÑки\n\nРаботно време:\nот понеделник до неделÑ: 24 чаÑа\n\nОбажданиÑта Ñа безплатни."</string> + <string name="submission_contact_operating_hours_body">"Езици: \nанглийÑки, немÑки, турÑки\n\nРаботно време:\nот понеделник до неделÑ: 24 чаÑа\n\nОбажданиÑта Ñа безплатни."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="submission_contact_body_other">"Ðко имате въпроÑи, Ñвързани ÑÑŠÑ Ð·Ð´Ñ€Ð°Ð²ÐµÑ‚Ð¾, Ñе обадете на Ð»Ð¸Ñ‡Ð½Ð¸Ñ Ñи лекар или на горещата Ð»Ð¸Ð½Ð¸Ñ Ð½Ð° Ñлужбата за медицинÑка помощ на телефон 116 117"</string> + <string name="submission_contact_body_other">"Ðко имате въпроÑи, Ñвързани ÑÑŠÑ Ð·Ð´Ñ€Ð°Ð²ÐµÑ‚Ð¾, Ñе обадете на Ð»Ð¸Ñ‡Ð½Ð¸Ñ Ñи лекар или на горещата Ð»Ð¸Ð½Ð¸Ñ Ð½Ð° Ñлужбата за медицинÑка помощ на телефон 116 117."</string> <!-- XACT: Submission contact page title --> <string name="submission_contact_accessibility_title">"Обадете Ñе на горещата Ð»Ð¸Ð½Ð¸Ñ Ð¸ поиÑкайте ТÐРкод"</string> @@ -1100,7 +1104,7 @@ <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> - <string name="submission_status_card_title_fetching">"Извършва Ñе извличане на данни"</string> + <string name="submission_status_card_title_fetching">"Извършва Ñе извличане на данни..."</string> <!-- XHED: Page title for the various submission status: unregistered --> <string name="submission_status_card_title_unregistered">"Правили ли Ñте Ñи теÑÑ‚?"</string> <!-- XHED: Page title for the various submission status: pending --> @@ -1201,7 +1205,10 @@ <string name="errors_google_update_needed">"Вашето приложение Corona-Warn-App е инÑталирано правилно, но „СиÑтемата за извеÑÑ‚Ñване при Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк от заразÑване Ñ COVID-19†не Ñе предлага за операционната ÑиÑтема на Ð’Ð°ÑˆÐ¸Ñ Ñмартфон. Това означава, че не можете да използвате приложението Corona-Warn-App. Повече Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¼Ð¾Ð¶Ðµ да намерите в Ñтраницата „ЧЗВ“ на адреÑ: https://www.coronawarn.app/en/faq/"</string> <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day --> <string name="errors_google_api_error">"Приложението Corona-Warn-App работи правилно, но не можем да актуализираме Ð’Ð°ÑˆÐ¸Ñ ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ð° риÑк. РегиÑтрирането на Ð¸Ð·Ð»Ð°Ð³Ð°Ð½Ð¸Ñ Ð½Ð° риÑк вÑе още е активно и функционира правилно. За повече Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¿Ð¾Ñетете Ñтраницата „ЧЗВ“ на адреÑ: https://www.coronawarn.app/en/faq/"</string> - + <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_title">"Лимитът вече е доÑтигнат"</string> + <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_description">"Ðе може да правите повече проверки за излагане на риÑк до ÐºÑ€Ð°Ñ Ð½Ð° денÑ, тъй като Ñте доÑтигнали макÑÐ¸Ð¼Ð°Ð»Ð½Ð¸Ñ Ð±Ñ€Ð¾Ð¹ проверки, определен от вашата операционна ÑиÑтема. МолÑ, проверете ÑтатуÑа Ñи на риÑк утре."</string> <!-- #################################### Generic Error Messages ###################################### --> @@ -1248,17 +1255,7 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> @@ -1359,7 +1356,7 @@ <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> <string name="interoperability_onboarding_randomid_download_free">"Ежедневното изтеглÑне на ÑпиÑъка ÑÑŠÑ Ñлучайни идентификатори обикновено е безплатно за ВаÑ. Това означава, че мобилните оператори не такÑуват използваните от приложението данни и не начиÑлÑват за Ñ‚ÑÑ… такÑи за роуминг в други държави от ЕС. За повече подробноÑти Ñе обърнете към ÑÐ²Ð¾Ñ Ð¼Ð¾Ð±Ð¸Ð»ÐµÐ½ оператор."</string> <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. --> - <string name="interoperability_onboarding_list_title">"Ð’ момента в обмена учаÑтват Ñредните държави:"</string> + <string name="interoperability_onboarding_list_title">"Ð’ момента в обмена учаÑтват Ñледните държави:"</string> <!-- XTXT: Description of the expanded terms in delta interopoerability screen part 1 --> <string name="interoperability_onboarding_delta_expanded_terms_text_part_1">"УÑловиÑта за ползване Ñъщо бÑха актуализирани, за да отразÑÑ‚ разширената функционалноÑÑ‚ на приложението."</string> @@ -1389,6 +1386,6 @@ <!-- YMSW: Subtitle for the interoperability onboarding if country download fails --> <string name="interoperability_onboarding_list_subtitle_failrequest_no_network">"Възможно е да нÑмате доÑтъп до интернет. МолÑ, проверете дали имате връзка."</string> <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available --> - <string name="interoperability_onboarding_list_button_title_no_network">"Към наÑтройките за уÑтройÑтвото"</string> + <string name="interoperability_onboarding_list_button_title_no_network">"Към наÑтройките на уÑтройÑтвото"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml index 459ed81835081b14fa6e97f66c4a04b9515ef7c0..588bcf6cec02bc523d15264ca1892eed49da2f3c 100644 --- a/Corona-Warn-App/src/main/res/values-de/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/legal_strings.xml @@ -11,5 +11,5 @@ <!-- XHED: onboarding(tracing) - headline for consent information --> <string name="onboarding_tracing_headline_consent">"Einwilligungserklärung"</string> <!-- YTXT: onboarding(tracing) - body for consent information --> - <string name="onboarding_tracing_body_consent">"Um zu erfahren, ob Sie Risiko-Begegnungen mit App-Nutzern der teilnehmenden Länder hatten und für Sie ein Ansteckungsrisiko besteht, müssen Sie die Risiko-Ermittlung aktivieren. Der Aktivierung der Risiko-Ermittlung und der damit im Zusammenhang stehenden Datenverarbeitung durch die App stimmen Sie mit Antippen des Buttons „Risiko-Ermittlung aktivieren“ zu.\n\nUm die Risiko-Ermittlung nutzen zu können müssen Sie zudem auf Ihrem Android-Smartphone die von Google bereitgestellte Funktion „COVID-19-Benachrichtigungen“ aktivieren und für die Corona-Warn-App freigeben.\n\nAnschließend erzeugt Ihr Android-Smartphone kontinuierlich Zufalls-IDs und versendet diese per Bluetooth, damit diese von Smartphones in Ihrer Umgebung empfangen werden können. Umgekehrt empfängt Ihr Android-Smartphone die Zufalls-IDs von anderen Smartphones. Die eigenen und die von anderen Smartphones empfangenen Zufalls-IDs werden von Ihrem Android-Smartphone aufgezeichnet und dort für 14 Tage gespeichert.\n\nFür die Risiko-Ermittlung lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die ihr Testergebnis (genauer gesagt: die eigenen Zufalls-IDs) zur Warnung anderer über ihre offizielle Corona-App geteilt haben. Diese Liste wird dann mit den von Ihrem Android-Smartphone aufgezeichneten Zufalls-IDs anderer Nutzer verglichen.\n\nWenn dabei eine Risiko-Begegnung festgestellt wird, werden Sie von der App informiert. In diesem Fall erhält die App Zugriff auf die von Ihrem Android-Smartphone zu der Risiko-Begegnung aufgezeichneten Daten (Datum, Dauer und Bluetooth-Signalstärke des Kontakts). Aus der Bluetooth-Signalstärke wird der räumliche Abstand zu dem anderen Nutzer abgeleitet (je stärker das Signal, desto geringer der Abstand). Diese Angaben werden von der App ausgewertet, um Ihr Ansteckungsrisiko zu berechnen und Ihnen Verhaltensempfehlungen zu geben. Diese Auswertung wird ausschließlich lokal auf Ihrem Android-Smartphone durchgeführt.\n\nAußer Ihnen erfährt niemand (auch nicht das RKI oder die Gesundheitsbehörden teilnehmender Länder), ob Sie eine Risiko-Begegnung hatten und welches Ansteckungsrisiko für Sie ermittelt wird.\n\nSie können Ihr Einverständnis zur Risiko-Ermittlung jederzeit zurücknehmen, indem Sie die Funktion über den Schieberegler innerhalb der App deaktivieren oder die App löschen. Wenn Sie die Risiko-Ermittlung wieder nutzen möchten, können Sie den Schieberegler erneut aktivieren oder die App erneut installieren. Wenn Sie die Risiko-Ermittlung deaktivieren, prüft die App nicht mehr, ob Sie Risiko-Begegnungen hatten. Um auch das Aussenden und den Empfang der Zufalls-IDs anzuhalten, müssen Sie die Funktion „COVID-19-Benachrichtigungen“ in den Einstellungen Ihres Android-Smartphones deaktivieren. Bitte beachten Sie, dass die durch diese Funktion von Ihrem Android-Smartphone aufgezeichneten fremden und eigenen Zufalls-IDs nicht von der App gelöscht werden. Diese können Sie nur in den Einstellungen Ihres Android-Smartphones dauerhaft löschen.\n\nDie Datenschutzerklärung der App (einschließlich Informationen zur Datenverarbeitung für die länderübergreifende Risiko-Ermittlung) finden Sie unter dem Menüpunkt „App-Informationen“ > „Datenschutz“."</string> + <string name="onboarding_tracing_body_consent">"Um zu erfahren, ob Sie Risiko-Begegnungen mit App-Nutzern der teilnehmenden Länder hatten und für Sie ein Infektionsrisiko besteht, müssen Sie die Risiko-Ermittlung aktivieren. Der Aktivierung der Risiko-Ermittlung und der damit im Zusammenhang stehenden Datenverarbeitung durch die App stimmen Sie mit Antippen des Buttons „Risiko-Ermittlung aktivieren“ zu.\n\nUm die Risiko-Ermittlung nutzen zu können, müssen Sie zudem auf Ihrem Android-Smartphone die von Google bereitgestellte Funktion „COVID-19-Benachrichtigungen“ aktivieren und für die Corona-Warn-App freigeben.\n\nBei aktiviertem COVID-19-Benachrichtigungssystem erzeugt Ihr Android-Smartphone kontinuierlich Zufalls-IDs und versendet diese per Bluetooth, damit diese von Smartphones in Ihrer Umgebung empfangen werden können. Umgekehrt empfängt Ihr Android-Smartphone die Zufalls-IDs von anderen Smartphones. Die eigenen und die von anderen Smartphones empfangenen Zufalls-IDs werden von Ihrem Android-Smartphone aufgezeichnet und dort für 14 Tage gespeichert.\n\nFür die Risiko-Ermittlung lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die diese über ihre offizielle Corona-App geteilt haben. Diese Liste wird dann mit den von Ihrem Smartphone aufgezeichneten Zufalls-IDs anderer Nutzer verglichen.\n\nWenn dabei eine Risiko-Begegnung festgestellt wird, werden Sie von der App informiert. In diesem Fall erhält die App Zugriff auf die von Ihrem Smartphone zu der Risiko-Begegnung aufgezeichneten Daten (Datum, Dauer und Bluetooth-Signalstärke des Kontakts). Aus der Bluetooth-Signalstärke wird der räumliche Abstand zu dem anderen Nutzer abgeleitet (je stärker das Signal, desto geringer der Abstand). Diese Angaben werden von der App ausgewertet, um Ihr Infektionsrisiko zu berechnen und Ihnen Verhaltensempfehlungen zu geben. Diese Auswertung wird ausschließlich lokal auf Ihrem Smartphone durchgeführt.\n\nAußer Ihnen erfährt niemand (auch nicht das RKI oder die Gesundheitsbehörden teilnehmender Länder), ob Sie eine Risiko-Begegnung hatten und welches Infektionsrisiko für Sie ermittelt wird.\n\nZum Widerruf Ihrer Einwilligung in die Risiko-Ermittlung können Sie die Funktion über den Schieberegler innerhalb der App deaktivieren oder die App löschen. Wenn Sie die Risiko-Ermittlung wieder nutzen möchten, können Sie den Schieberegler erneut aktivieren oder die App erneut installieren. Wenn Sie die Risiko-Ermittlung deaktivieren, prüft die App nicht mehr, ob Sie Risiko-Begegnungen hatten. Um auch das Aussenden und den Empfang der Zufalls-IDs anzuhalten, müssen Sie das COVID-19-Benachrichtigungssystem in den Einstellungen Ihres Android-Smartphones deaktivieren. Bitte beachten Sie, dass die vom COVID-19-Benachrichtigungssystem Ihres Android-Smartphones aufgezeichneten fremden und eigenen Zufalls-IDs nicht von der App gelöscht werden. Diese können Sie nur in den Einstellungen Ihres Android-Smartphones dauerhaft löschen.\n\nDie Datenschutzerklärung der App (einschließlich Informationen zur Datenverarbeitung für die länderübergreifende Risiko-Ermittlung) finden Sie unter dem Menüpunkt „App-Informationen“ > „Datenschutz“."</string> </resources> 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 8f9708aabee049efb3b21341f28b15daf0f7af92..b46a566c1061247858e8c6c868077170607de0e8 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -21,12 +21,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -233,11 +229,11 @@ ###################################### --> <!-- XHED: Share app link page title --> - <string name="main_share_title">"Corona-Warn-App teilen"</string> + <string name="main_share_title">"Corona-Warn-App empfehlen"</string> <!-- XHED: Share app link page subtitle --> <string name="main_share_headline">"Gemeinsam Corona bekämpfen"</string> <!-- YTXT: Share app link page body --> - <string name="main_share_body">"Je mehr Menschen mitmachen, desto besser durchbrechen wir Infektionsketten. Laden Sie Familie, Freunde und Bekannte ein!"</string> + <string name="main_share_body">"Je mehr Menschen mitmachen, desto besser durchbrechen wir Infektionsketten. Verschicken Sie einen Link auf die Corona-Warn-App an Familie, Freunde und Bekannte!"</string> <!-- XBUT: Share app link page button --> <string name="main_share_button">"Download-Link versenden"</string> <!-- YMSG: Message when sharing is executed --> @@ -282,7 +278,7 @@ <!-- XHED: App overview subtitle for glossary risk calculation --> <string name="main_overview_subtitle_glossary_calculation">"Risiko-Ãœberprüfung"</string> <!-- YTXT: App overview body for glossary risk calculation --> - <string name="main_overview_body_glossary_calculation">"Abfrage der Begegnungs-Aufzeichnung und Abgleich mit den gemeldeten Infektionen anderer Nutzerinnen und Nutzer. Die Risiko-Ãœberprüfung erfolgt automatisch ungefähr alle zwei Stunden."</string> + <string name="main_overview_body_glossary_calculation">"Abfrage der Begegnungs-Aufzeichnung und Abgleich mit den gemeldeten Infektionen anderer Nutzerinnen und Nutzer. Ihr Risiko wird mehrmals täglich automatisch überprüft."</string> <!-- XHED: App overview subtitle for glossary contact --> <string name="main_overview_subtitle_glossary_contact">"Risiko-Begegnung"</string> <!-- YTXT: App overview body for glossary contact --> @@ -1299,17 +1295,7 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> diff --git a/Corona-Warn-App/src/main/res/values-en/strings.xml b/Corona-Warn-App/src/main/res/values-en/strings.xml index 568659f0360416649e4005c198a66d174be6ff68..a5e01cdeff99cac3ee50b5509b42354aebefa547 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -20,12 +20,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -109,6 +105,10 @@ <string name="notification_headline">"Corona-Warn-App"</string> <!-- XTXT: Notification body --> <string name="notification_body">"You have new messages from your Corona-Warn-App."</string> + <!-- XHED: Notification title - Reminder to share a positive test result--> + <string name="notification_headline_share_positive_result">"You can help!"</string> + <!-- XTXT: Notification body - Reminder to share a positive test result--> + <string name="notification_body_share_positive_result">"Please share your test result and warn others."</string> <!-- #################################### App Auto Update @@ -150,11 +150,9 @@ <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"Exposure logging permanently active"</string> <!-- XTXT; risk card - no update done yet --> - <string name="risk_card_body_not_yet_fetched">"Exposures have not yet been checked."</string> + <string name="risk_card_body_not_yet_fetched">"Encounters have not yet been checked."</string> <!-- XTXT: risk card - last successful update --> <string name="risk_card_body_time_fetched">"Updated: %1$s"</string> - <!-- XTXT: risk card - next update --> - <string name="risk_card_body_next_update">"Updated daily"</string> <!-- XTXT: risk card - hint to open the app daily --> <string name="risk_card_body_open_daily">"Note: Please open the app daily to update your risk status."</string> <!-- XBUT: risk card - update risk --> @@ -185,13 +183,19 @@ <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"Exposure logging stopped"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure logging:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure check:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"Exposure logging is not possible"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> <string name="risk_card_outdated_risk_body">"Your exposure logging could not be updated for more than 24 hours."</string> <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"Your risk status has not been updated for more than 48 hours. Please update your risk status."</string> + <!-- XHED: risk card - risk check failed headline, no internet connection --> + <string name="risk_card_check_failed_no_internet_headline">"Exposure check failed"</string> + <!-- XTXT: risk card - risk check failed, please check your internet connection --> + <string name="risk_card_check_failed_no_internet_body">"The synchronization of random IDs with the server failed. You can restart the synchronization manually."</string> + <!-- XTXT: risk card - risk check failed, restart button --> + <string name="risk_card_check_failed_no_internet_restart_button">"Restart"</string> <!-- #################################### Risk Card - Progress @@ -247,7 +251,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"Exposure Logging"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"Exposure logging is one of the three central features of the app. When you activate it, encounters with people\'s devices are logged. You don\'t have to do anything else."</string> + <string name="main_overview_body_tracing">"Exposure logging is one of three central features of the app. Once you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Risk of Infection"</string> <!-- YTXT: App overview body text about risk levels --> @@ -357,9 +361,9 @@ <item quantity="many">"You have an increased risk of infection because you were last exposed %1$s days ago over a longer period of time and at close proximity to at least one person diagnosed with COVID-19."</item> </plurals> <!-- YTXT: risk details - risk calculation explanation --> - <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else."</string> + <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else."</string> <!-- YTXT: risk details - risk calculation explanation for increased risk --> - <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string> + <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string> <!-- NOTR --> <string name="risk_details_button_update">@string/risk_card_button_update</string> <!-- NOTR --> @@ -415,7 +419,7 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"More protection for you and for us all. By using the Corona-Warn-App we can break infection chains much quicker."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"Turn your device into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string> + <string name="onboarding_body">"Turn your smartphone into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string> <!-- YTXT: onboarding(together) - explain application --> <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their devices, whereby no personal data whatsoever is accessed."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> @@ -452,7 +456,7 @@ <!-- XBUT: onboarding(tracing) - negative button (right) --> <string name="onboarding_tracing_dialog_button_negative">"Back"</string> <!-- XACT: onboarding(tracing) - dialog about background jobs header text --> - <string name="onboarding_background_fetch_dialog_headline">"Background updates deactivated"</string> + <string name="onboarding_background_fetch_dialog_headline">"Background app refresh deactivated"</string> <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"You have deactivated background updates for the Corona-Warn-App. Please activate background updates to use automatic exposure logging. If you do not activate background updates, you can only start exposure logging manually in the app. You can activate background updates for the app in your device settings."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> @@ -474,7 +478,7 @@ <!-- XBUT: onboarding(tracing) - dialog about manual checking button --> <string name="onboarding_manual_required_dialog_button">"OK"</string> <!-- XACT: onboarding(tracing) - illustraction description, header image --> - <string name="onboarding_tracing_illustration_description">"Three people have activated exposure logging on their devices, which will log their encounters with each other."</string> + <string name="onboarding_tracing_illustration_description">"Three persons have activated exposure logging on their smartphones, which will log their encounters with each other."</string> <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Allow location access"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> @@ -671,7 +675,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) is Germany’s federal public health body. The RKI publishes the Corona-Warn-App on behalf of the Federal Government. The app is intended as a digital complement to public health measures already introduced: social distancing, hygiene, and face masks."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"People who use the app help to trace and break chains of infection. The app saves encounters with other people locally on your device. You are notified if you have encountered people who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> + <string name="information_about_body">"Whoever uses the app helps to trace and break chains of infection. The app saves encounters with other persons locally on your smartphone. You are notified if you have encountered persons who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -703,9 +707,9 @@ <!-- XTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_phone">"Our customer service is here to help."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"Languages: German, English, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string> + <string name="information_contact_body_open">"Languages: English, German, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string> + <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string> <!-- XACT: describes illustration --> <string name="information_contact_illustration_description">"A man wears a headset while making a phone call."</string> <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website --> @@ -788,9 +792,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"Back"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Test has errors"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"QR code no longer valid"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"There was a problem evaluating your test. Your QR code has already expired."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"Your test is more than 21 days old and can no longer be registered in the app. If you are tested again in future, please make sure to scan the QR code as soon as you get it."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> @@ -865,15 +869,15 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Update"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Delete Test"</string> + <string name="submission_test_result_pending_remove_test_button">"Remove test"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Your Test Result"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"The laboratory result indicates no verification that you have coronavirus SARS-CoV-2.\n\nPlease delete the test from the Corona-Warn-App, so that you can save a new test code here if necessary."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Delete Test"</string> + <string name="submission_test_result_negative_remove_test_button">"Remove test"</string> <!-- XHED: Page headline for other warnings screen --> - <string name="submission_test_result_positive_steps_warning_others_heading">"Warning Others"</string> + <string name="submission_test_result_positive_steps_warning_others_heading">"Warn Others"</string> <!-- YTXT: Body text for for other warnings screen--> <string name="submission_test_result_positive_steps_warning_others_body">"Share your random IDs and warn others.\nHelp determine the risk of infection for others more accurately by also indicating when you first noticed any coronavirus symptoms."</string> <!-- XBUT: positive test result : continue button --> @@ -952,13 +956,13 @@ <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"Register your test by scanning the QR code of your test document."</string> <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"Consent"</string> + <string name="submission_dispatcher_qr_privacy_dialog_headline">"Declaration of Consent"</string> <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Acceptâ€, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacyâ€."</string> <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Accept"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Allow"</string> <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Accept"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Allow"</string> <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"TAN"</string> <!-- YTXT: Body text for TAN code dispatcher option --> @@ -972,7 +976,7 @@ <!-- Submission Positive Other Warning --> <!-- XHED: Page title for the positive result additional warning page--> - <string name="submission_positive_other_warning_title">"Warning Others"</string> + <string name="submission_positive_other_warning_title">"Warn Others"</string> <!-- XHED: Page headline for the positive result additional warning page--> <string name="submission_positive_other_warning_headline">"Please help all of us!"</string> <!-- YTXT: Body text for the positive result additional warning page--> @@ -980,7 +984,7 @@ <!-- XBUT: other warning continue button --> <string name="submission_positive_other_warning_button">"Accept"</string> <!-- XACT: other warning - illustration description, explanation image --> - <string name="submission_positive_other_illustration_description">"A device transmits an encrypted positive test diagnosis to the system."</string> + <string name="submission_positive_other_illustration_description">"A smartphone transmits an encrypted positive test diagnosis to the system."</string> <!-- XHED: Title for the interop country list--> <string name="submission_interoperability_list_title">"The following countries currently participate in transnational exposure logging:"</string> @@ -1046,7 +1050,7 @@ <!-- XBUT: symptom initial screen no button --> <string name="submission_symptom_negative_button">"No"</string> <!-- XBUT: symptom initial screen no information button --> - <string name="submission_symptom_no_information_button">"No answer"</string> + <string name="submission_symptom_no_information_button">"No statement"</string> <!-- XBUT: symptom initial screen continue button --> <string name="submission_symptom_further_button">"Next"</string> @@ -1071,9 +1075,9 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"Register the test by entering the TAN in the app."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"Languages: \nGerman, English, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string> + <string name="submission_contact_operating_hours_body">"Languages: \nEnglish, German, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string> + <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string> <!-- XACT: Submission contact page title --> <string name="submission_contact_accessibility_title">"Call the hotline and request a TAN"</string> @@ -1096,7 +1100,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"More than 2 weeks ago"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"No answer"</string> + <string name="submission_symptom_verify">"No statement"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1157,7 +1161,7 @@ <!-- YTXT: invalid status text --> <string name="test_result_card_status_invalid">"Evaluation is not possible"</string> <!-- YTXT: pending status text --> - <string name="test_result_card_status_pending">"Your result is not available yet"</string> + <string name="test_result_card_status_pending">"Your result is not yet available"</string> <!-- XHED: Title for further info of test result negative --> <string name="test_result_card_negative_further_info_title">"Other information:"</string> <!-- YTXT: Content for further info of test result negative --> @@ -1201,7 +1205,10 @@ <string name="errors_google_update_needed">"Your Corona-Warn-App is correctly installed, but the \"COVID-19 Exposure Notifications System\" is not available on your smartphone\'s operating system. This means that you cannot use the Corona-Warn-App. For further information, please see our FAQ page: https://www.coronawarn.app/en/faq/"</string> <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day --> <string name="errors_google_api_error">"The Corona-Warn-App is running correctly, but we cannot update your current risk status. Exposure logging remains active and is working correctly. For further information, please see our FAQ page: https://www.coronawarn.app/en/faq/"</string> - + <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_title">"Limit already reached"</string> + <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_description">"No more exposure checks possible today, as you have reached the maximum number of checks per day defined by your operating system. Please check your risik status again tomorrow."</string> <!-- #################################### Generic Error Messages ###################################### --> @@ -1248,17 +1255,7 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> @@ -1357,7 +1354,7 @@ <!-- YMSG: Onboarding tracing step second section in interoperability after the title --> <string name="interoperability_onboarding_second_section">"When a user submits their random IDs to the exchange server jointly operated by the participating countries, users of the official corona apps in all these countries can be warned of potential exposure."</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, and nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string> + <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string> <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. --> <string name="interoperability_onboarding_list_title">"The following countries currently participate:"</string> diff --git a/Corona-Warn-App/src/main/res/values-pl/strings.xml b/Corona-Warn-App/src/main/res/values-pl/strings.xml index 0f8a8191a508dc0d04aa93af8d0c2aca7ebc8576..eded35c4be1a262535dce655302b0fba532e50e5 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -20,12 +20,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -109,6 +105,10 @@ <string name="notification_headline">"Corona-Warn-App"</string> <!-- XTXT: Notification body --> <string name="notification_body">"Masz nowe wiadomoÅ›ci od Corona-Warn-App."</string> + <!-- XHED: Notification title - Reminder to share a positive test result--> + <string name="notification_headline_share_positive_result">"Możesz pomóc!"</string> + <!-- XTXT: Notification body - Reminder to share a positive test result--> + <string name="notification_body_share_positive_result">"UdostÄ™pnij swój wynik testu i ostrzeż innych."</string> <!-- #################################### App Auto Update @@ -132,7 +132,7 @@ <item quantity="one">"%1$s narażenie z niskim ryzykiem"</item> <item quantity="other">"%1$s narażenia z niskim ryzykiem"</item> <item quantity="zero">"Brak narażenia z niskim ryzykiem do tej pory"</item> - <item quantity="two">"%1$s narażenia z niskim ryzykiem"</item> + <item quantity="two">"%1$s narażeÅ„ z niskim ryzykiem"</item> <item quantity="few">"%1$s narażenia z niskim ryzykiem"</item> <item quantity="many">"%1$s narażeÅ„ z niskim ryzykiem"</item> </plurals> @@ -150,13 +150,11 @@ <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"Rejestrowanie narażenia stale aktywne"</string> <!-- XTXT; risk card - no update done yet --> - <string name="risk_card_body_not_yet_fetched">"Narażenia nie zostaÅ‚y jeszcze sprawdzone."</string> + <string name="risk_card_body_not_yet_fetched">"Kontakty nie zostaÅ‚y jeszcze sprawdzone."</string> <!-- XTXT: risk card - last successful update --> <string name="risk_card_body_time_fetched">"Zaktualizowano: %1$s"</string> - <!-- XTXT: risk card - next update --> - <string name="risk_card_body_next_update">"Aktualizowane codziennie"</string> <!-- XTXT: risk card - hint to open the app daily --> - <string name="risk_card_body_open_daily">"Uwaga: ProszÄ™ codziennie otwierać aplikacjÄ™, aby zaktualizować swój status ryzyka."</string> + <string name="risk_card_body_open_daily">"Uwaga: Otwieraj codziennie aplikacjÄ™, aby aktualizować swój status ryzyka."</string> <!-- XBUT: risk card - update risk --> <string name="risk_card_button_update">"Aktualizuj"</string> <!-- XBUT: risk card - update risk with time display --> @@ -185,13 +183,19 @@ <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"Rejestrowanie narażenia zatrzymane"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"Ostatnie rejestrowanie narażenia:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"Ostatnie sprawdzenie narażenia:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"Rejestrowanie narażenia jest niemożliwe"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> <string name="risk_card_outdated_risk_body">"Rejestrowanie narażenia nie mogÅ‚o zostać zaktualizowane przez okres dÅ‚uższy niż 24 godziny."</string> <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"Twój status ryzyka nie byÅ‚ aktualizowany od ponad 48 godzin. Zaktualizuj swój status ryzyka."</string> + <!-- XHED: risk card - risk check failed headline, no internet connection --> + <string name="risk_card_check_failed_no_internet_headline">"Sprawdzanie narażeÅ„ nie powiodÅ‚o siÄ™"</string> + <!-- XTXT: risk card - risk check failed, please check your internet connection --> + <string name="risk_card_check_failed_no_internet_body">"Synchronizacja losowych identyfikatorów z serwerem nie powiodÅ‚a siÄ™. Możesz ponownie uruchomić synchronizacjÄ™ rÄ™cznie."</string> + <!-- XTXT: risk card - risk check failed, restart button --> + <string name="risk_card_check_failed_no_internet_restart_button">"Uruchom ponownie"</string> <!-- #################################### Risk Card - Progress @@ -247,7 +251,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"Rejestrowanie narażenia"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"Rejestrowanie narażenia jest jednÄ… z trzech głównych funkcji aplikacji. Po jej aktywacji rejestrowane sÄ… kontakty z urzÄ…dzeniami innych osób. Nie musisz robić nic wiÄ™cej."</string> + <string name="main_overview_body_tracing">"Rejestrowanie narażenia jest jednÄ… z trzech głównych funkcji aplikacji. Po jej aktywacji rejestrowane sÄ… kontakty ze smartfonami innych osób. Nie musisz robić nic wiÄ™cej."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Ryzyko zakażenia"</string> <!-- YTXT: App overview body text about risk levels --> @@ -357,9 +361,9 @@ <item quantity="many">"Masz podwyższone ryzyko zakażenia, ponieważ %1$s dni temu byÅ‚eÅ›(-aÅ›) narażony(-a) na dÅ‚uższy, bliski kontakt z co najmniej jednÄ… osobÄ…, u której zdiagnozowano COVID-19."</item> </plurals> <!-- YTXT: risk details - risk calculation explanation --> - <string name="risk_details_information_body_notice">"Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie w urzÄ…dzeniu. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane."</string> + <string name="risk_details_information_body_notice">"Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie w smartfonie. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane."</string> <!-- YTXT: risk details - risk calculation explanation for increased risk --> - <string name="risk_details_information_body_notice_increased">"Dlatego Twoje ryzyko zakażenia zostaÅ‚o ocenione jako podwyższone. Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie w urzÄ…dzeniu. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane. Po powrocie do domu unikaj również bliskiego kontaktu z czÅ‚onkami rodziny lub gospodarstwa domowego."</string> + <string name="risk_details_information_body_notice_increased">"Dlatego Twoje ryzyko zakażenia zostaÅ‚o ocenione jako podwyższone. Ryzyko zakażenia jest obliczane na podstawie danych rejestrowania narażenia (czas trwania i bliskość kontaktu) lokalnie na smartfonie. Twoje ryzyko zakażenia nie jest widoczne dla nikogo ani nikomu przekazywane. Po powrocie do domu unikaj również bliskiego kontaktu z czÅ‚onkami rodziny lub gospodarstwa domowego."</string> <!-- NOTR --> <string name="risk_details_button_update">@string/risk_card_button_update</string> <!-- NOTR --> @@ -415,7 +419,7 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"WiÄ™ksza ochrona dla Ciebie i dla nas wszystkich. KorzystajÄ…c z aplikacji Corona-Warn-App, możemy znacznie szybciej przerwać Å‚aÅ„cuchy zakażeÅ„."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"ZmieÅ„ swoje urzÄ…dzenie w system ostrzegania przed koronawirusem. Zapoznaj siÄ™ ze swoim statusem ryzyka i dowiedz siÄ™, czy miaÅ‚eÅ›(-aÅ›) bliski kontakt z osobÄ…, u której w ciÄ…gu ostatnich 14 dni zdiagnozowano COVID-19."</string> + <string name="onboarding_body">"ZmieÅ„ swój smartfon w system ostrzegania przed koronawirusem. Zapoznaj siÄ™ ze swoim statusem ryzyka i dowiedz siÄ™, czy miaÅ‚eÅ›(-aÅ›) bliski kontakt z osobÄ…, u której w ciÄ…gu ostatnich 14 dni zdiagnozowano COVID-19."</string> <!-- YTXT: onboarding(together) - explain application --> <string name="onboarding_body_emphasized">"Aplikacja rejestruje kontakty miÄ™dzy osobami poprzez wymianÄ™ zaszyfrowanych, losowych identyfikatorów miÄ™dzy ich urzÄ…dzeniami bez uzyskiwania dostÄ™pu do danych osobowych."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> @@ -452,7 +456,7 @@ <!-- XBUT: onboarding(tracing) - negative button (right) --> <string name="onboarding_tracing_dialog_button_negative">"Wstecz"</string> <!-- XACT: onboarding(tracing) - dialog about background jobs header text --> - <string name="onboarding_background_fetch_dialog_headline">"Aktualizacje w tle dezaktywowane"</string> + <string name="onboarding_background_fetch_dialog_headline">"OdÅ›wieżanie aplikacji w tle dezaktywowane"</string> <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"DezaktywowaÅ‚eÅ›(-aÅ›) aktualizacje w tle dla aplikacji Corona-Warn-App. Aktywuj aktualizacje w tle, aby korzystać z automatycznego rejestrowania narażenia. JeÅ›li nie aktywujesz aktualizacji w tle, możliwe bÄ™dzie tylko rÄ™czne uruchomienie rejestrowania narażenia w aplikacji. Możesz aktywować aktualizacje w tle dla aplikacji w ustawieniach swojego urzÄ…dzenia."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> @@ -474,7 +478,7 @@ <!-- XBUT: onboarding(tracing) - dialog about manual checking button --> <string name="onboarding_manual_required_dialog_button">"OK"</string> <!-- XACT: onboarding(tracing) - illustraction description, header image --> - <string name="onboarding_tracing_illustration_description">"Trzy osoby aktywowaÅ‚y rejestrowanie narażenia na swoich urzÄ…dzeniach, które bÄ™dÄ… rejestrować ich wzajemne kontakty."</string> + <string name="onboarding_tracing_illustration_description">"Trzy osoby aktywowaÅ‚y rejestrowanie narażenia na swoich smartfonach, które bÄ™dÄ… rejestrować ich wzajemne kontakty."</string> <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Zezwól na dostÄ™p do lokalizacji"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> @@ -536,7 +540,7 @@ <!-- XTXT: settings(tracing) - shows status under header in home, inactive location --> <string name="settings_tracing_body_inactive_location">"UsÅ‚ugi lokalizacji dezaktywowane"</string> <!-- YTXT: settings(tracing) - explains tracings --> - <string name="settings_tracing_body_text">"Musisz wÅ‚Ä…czyć funkcjÄ™ rejestrowania narażenia, aby aplikacja mogÅ‚a ustalić, czy dotyczy CiÄ™ ryzyko zakażenia po kontakcie z zainfekowanym użytkownikiem aplikacji. Funkcja rejestrowania narażenia dziaÅ‚a we wszystkich uczestniczÄ…cych krajach, co oznacza, że potencjalne narażenie użytkowników jest wykrywane również przez inne oficjalne aplikacje koronawirusowe.\n\nDziaÅ‚anie funkcji rejestrowania narażenia polega na odbieraniu przez Twój smartfon za pomocÄ… Bluetooth zaszyfrowanych, losowych identyfikatorów innych użytkowników i przekazywaniu Twoich wÅ‚asnych, losowych identyfikatorów do ich urzÄ…dzeÅ„. Codziennie aplikacja pobiera listÄ™ losowych identyfikatorów – wraz z wszelkimi opcjonalnie podawanymi informacjami o wystÄ…pieniu objawów – wszystkich użytkowników, którzy mieli pozytywny wynik testu na wirusa i dobrowolnie udostÄ™pnili tÄ™ informacjÄ™ poprzez aplikacjÄ™. Lista jest nastÄ™pnie porównywana z losowymi identyfikatorami innych użytkowników, które zarejestrowaÅ‚ Twój smartfon, w celu obliczenia prawdopodobieÅ„stwa Twojego zakażenia i ostrzeżenia CiÄ™ w razie potrzeby. FunkcjÄ™ tÄ™ można wyÅ‚Ä…czyć w dowolnym momencie..\n\nAplikacja nigdy nie gromadzi danych osobowych takich jak imiÄ™ i nazwisko, adres czy lokalizacja. Takie informacje nie sÄ… też przekazywane innym użytkownikom. Nie jest możliwe wykorzystanie losowych identyfikatorów w celu ustalenia tożsamoÅ›ci poszczególnych osób."</string> + <string name="settings_tracing_body_text">"Musisz wÅ‚Ä…czyć funkcjÄ™ rejestrowania narażenia, aby aplikacja mogÅ‚a ustalić, czy dotyczy CiÄ™ ryzyko zakażenia po kontakcie z zainfekowanym użytkownikiem aplikacji. Funkcja rejestrowania narażenia dziaÅ‚a w skali miÄ™dzynarodowej, co oznacza, że potencjalne narażenie użytkowników jest wykrywane również przez inne oficjalne aplikacje koronawirusowe.\n\nDziaÅ‚anie funkcji rejestrowania narażenia polega na odbieraniu przez Twój smartfon za pomocÄ… Bluetooth zaszyfrowanych, losowych identyfikatorów innych użytkowników i przekazywaniu Twoich wÅ‚asnych, losowych identyfikatorów do ich smartfonów. Codziennie aplikacja pobiera listÄ™ losowych identyfikatorów – wraz z wszelkimi opcjonalnie podawanymi informacjami o wystÄ…pieniu objawów – wszystkich użytkowników, którzy mieli pozytywny wynik testu na koronawirusa i dobrowolnie udostÄ™pnili tÄ™ informacjÄ™ poprzez aplikacjÄ™. Lista jest nastÄ™pnie porównywana z losowymi identyfikatorami innych użytkowników, które zarejestrowaÅ‚ Twój smartfon, w celu obliczenia prawdopodobieÅ„stwa Twojego zakażenia i ostrzeżenia CiÄ™ w razie potrzeby. FunkcjÄ™ tÄ™ można wyÅ‚Ä…czyć w dowolnym momencie.\n\nAplikacja nigdy nie gromadzi danych osobowych, takich jak imiÄ™ i nazwisko, adres czy lokalizacja. Takie informacje nie sÄ… też przekazywane innym użytkownikom. Nie jest możliwe wykorzystanie losowych identyfikatorów w celu ustalenia tożsamoÅ›ci poszczególnych osób."</string> <!-- XTXT: settings(tracing) - status next to switch under title --> <string name="settings_tracing_status_active">"Aktywne"</string> <!-- XTXT: settings(tracing) - status next to switch under title --> @@ -570,7 +574,7 @@ <!-- XTXT: settings(tracing) - explains the circle progress indicator to the right with the current value --> <plurals name="settings_tracing_status_body_active"> <item quantity="one">"Rejestrowanie narażenia jest aktywne od jednego dnia.\nSprawdzanie narażeÅ„ jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na staÅ‚e."</item> - <item quantity="other">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeÅ„ jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na staÅ‚e."</item> + <item quantity="other">"Rejestrowanie narażenia jest aktywne od %1$s dnia.\nSprawdzanie narażeÅ„ jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na staÅ‚e."</item> <item quantity="zero">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeÅ„ jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na staÅ‚e."</item> <item quantity="two">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeÅ„ jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na staÅ‚e."</item> <item quantity="few">"Rejestrowanie narażenia jest aktywne od %1$s dni.\nSprawdzanie narażeÅ„ jest wiarygodne tylko wtedy, gdy rejestrowanie narażenia jest aktywowane na staÅ‚e."</item> @@ -671,7 +675,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Instytut Roberta Kocha (RKI) to niemiecka federalna instytucja zdrowia publicznego. RKI publikuje aplikacjÄ™ Corona-Warn-App w imieniu rzÄ…du federalnego. Aplikacja ta sÅ‚uży jako cyfrowe uzupeÅ‚nienie już wprowadzonych Å›rodków ochrony zdrowia publicznego, takich jak zachowanie dystansu spoÅ‚ecznego, dbanie o higienÄ™ oraz noszenie maseczek."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Osoby korzystajÄ…ce z aplikacji pomagajÄ… w Å›ledzeniu i przerwaniu Å‚aÅ„cuchów zakażeÅ„. Aplikacja zapisuje kontakty z innymi osobami lokalnie na Twoim urzÄ…dzeniu. Otrzymasz powiadomienie, jeÅ›li okaże siÄ™, że u osób, z którymi miaÅ‚eÅ›(-aÅ›) kontakt, zdiagnozowano później COVID-19. Twoja tożsamość i prywatność sÄ… zawsze chronione."</string> + <string name="information_about_body">"Wszystkie osoby korzystajÄ…ce z aplikacji pomagajÄ… w Å›ledzeniu i przerwaniu Å‚aÅ„cuchów zakażeÅ„. Aplikacja zapisuje kontakty z innymi osobami lokalnie na Twoim smartfonie. Otrzymasz powiadomienie, jeÅ›li okaże siÄ™, że u osób, z którymi miaÅ‚eÅ›(-aÅ›) kontakt, zdiagnozowano później COVID-19. Twoja tożsamość i prywatność sÄ… zawsze chronione."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Grupa osób korzysta ze smartfonów na mieÅ›cie."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -703,7 +707,7 @@ <!-- XTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_phone">"Nasz zespół obsÅ‚ugi klienta jest gotowy do pomocy."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"JÄ™zyki: niemiecki, angielski, turecki\nGodziny pracy:"<xliff:g id="line_break">"\n"</xliff:g>"od poniedziaÅ‚ku do soboty: 7:00 - 22:00"<xliff:g id="line_break">"\n(za wyjÄ…tkiem Å›wiÄ…t paÅ„stwowych)"</xliff:g><xliff:g id="line_break">"\nPoÅ‚Ä…czenie jest bezpÅ‚atne."</xliff:g></string> + <string name="information_contact_body_open">"JÄ™zyki: angielski, niemiecki, turecki\nGodziny pracy:"<xliff:g id="line_break">"\n"</xliff:g>"od poniedziaÅ‚ku do soboty: 7:00 - 22:00"<xliff:g id="line_break">"\n(za wyjÄ…tkiem Å›wiÄ…t paÅ„stwowych)"</xliff:g><xliff:g id="line_break">"\nPoÅ‚Ä…czenie jest bezpÅ‚atne."</xliff:g></string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_other">"W razie jakichkolwiek pytaÅ„ zwiÄ…zanych ze zdrowiem skontaktuj siÄ™ z lekarzem rodzinnym lub lekarzem dyżurnym pod numerem: 116 117."</string> <!-- XACT: describes illustration --> @@ -788,9 +792,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"Wstecz"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Test zawiera bÅ‚Ä™dy"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"Kod QR straciÅ‚ ważność"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"Podczas ustalania wyniku testu pojawiÅ‚ siÄ™ bÅ‚Ä…d. Twój kod QR wygasÅ‚."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"Twój test ma wiÄ™cej niż 21 dni i nie można go już zarejestrować w aplikacji. JeÅ›li w przyszÅ‚oÅ›ci bÄ™dziesz ponownie poddawać siÄ™ testowi, zeskanuj kod QR, gdy tylko go otrzymasz."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> @@ -869,11 +873,11 @@ <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Twój wynik testu"</string> <!-- YTXT: Body text for next steps section of test negative result --> - <string name="submission_test_result_negative_steps_negative_body">"Wynik laboratoryjny nie potwierdza zakażenia wirusem SARS-CoV-2.\n\nUsuÅ„ test z aplikacji Corona-Warn-App, aby w razie potrzeby móc zapisać w niej kod nowego testu."</string> + <string name="submission_test_result_negative_steps_negative_body">"Wynik laboratoryjny nie potwierdza zakażenia koronawirusem SARS-CoV-2.\n\nUsuÅ„ test z aplikacji Corona-Warn-App, aby w razie potrzeby móc zapisać w niej kod nowego testu."</string> <!-- XBUT: negative test result : remove the test button --> <string name="submission_test_result_negative_remove_test_button">"UsuÅ„ test"</string> <!-- XHED: Page headline for other warnings screen --> - <string name="submission_test_result_positive_steps_warning_others_heading">"Ostrzeganie innych"</string> + <string name="submission_test_result_positive_steps_warning_others_heading">"Ostrzegaj innych"</string> <!-- YTXT: Body text for for other warnings screen--> <string name="submission_test_result_positive_steps_warning_others_body">"UdostÄ™pnij swoje losowe identyfikatory, aby ostrzegać innych.\nPomóż innym dokÅ‚adniej ocenić ryzyko zakażenia, wysyÅ‚ajÄ…c informacjÄ™, gdy dostrzeżesz u siebie objawy koronawirusa."</string> <!-- XBUT: positive test result : continue button --> @@ -952,13 +956,13 @@ <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"Zarejestruj test poprzez zeskanowanie kodu QR dokumentu testu."</string> <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"Zgoda"</string> + <string name="submission_dispatcher_qr_privacy_dialog_headline">"OÅ›wiadczenia o wyrażeniu zgody"</string> <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Acceptâ€, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacyâ€."</string> <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Akceptuj"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Zezwól"</string> <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nie akceptuj"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nie zezwalaj"</string> <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"TAN"</string> <!-- YTXT: Body text for TAN code dispatcher option --> @@ -972,7 +976,7 @@ <!-- Submission Positive Other Warning --> <!-- XHED: Page title for the positive result additional warning page--> - <string name="submission_positive_other_warning_title">"Ostrzeganie innych"</string> + <string name="submission_positive_other_warning_title">"Ostrzegaj innych"</string> <!-- XHED: Page headline for the positive result additional warning page--> <string name="submission_positive_other_warning_headline">"Pomóż nam wszystkim!"</string> <!-- YTXT: Body text for the positive result additional warning page--> @@ -980,7 +984,7 @@ <!-- XBUT: other warning continue button --> <string name="submission_positive_other_warning_button">"Akceptuj"</string> <!-- XACT: other warning - illustration description, explanation image --> - <string name="submission_positive_other_illustration_description">"UrzÄ…dzenie przesyÅ‚a zaszyfrowanÄ… diagnozÄ™ zakażenia do systemu."</string> + <string name="submission_positive_other_illustration_description">"Zaszyfrowana diagnoza zakażenia jest przesyÅ‚ana do systemu."</string> <!-- XHED: Title for the interop country list--> <string name="submission_interoperability_list_title">"NastÄ™pujÄ…ce kraje uczestniczÄ… obecnie w miÄ™dzynarodowym rejestrowaniu narażenia:"</string> @@ -1046,7 +1050,7 @@ <!-- XBUT: symptom initial screen no button --> <string name="submission_symptom_negative_button">"Nie"</string> <!-- XBUT: symptom initial screen no information button --> - <string name="submission_symptom_no_information_button">"Brak odpowiedzi"</string> + <string name="submission_symptom_no_information_button">"Bez komentarza"</string> <!-- XBUT: symptom initial screen continue button --> <string name="submission_symptom_further_button">"Dalej"</string> @@ -1071,7 +1075,7 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"Zarejestruj test poprzez wpisanie numeru TAN w aplikacji."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"JÄ™zyki: \nniemiecki, angielski, turecki\n\nGodziny pracy:\nod poniedziaÅ‚ku do niedzieli: 24 godziny na dobÄ™\n\nPoÅ‚Ä…czenie jest bezpÅ‚atne."</string> + <string name="submission_contact_operating_hours_body">"JÄ™zyki: \nangielski, niemiecki, turecki\n\nGodziny pracy:\nod poniedziaÅ‚ku do niedzieli: caÅ‚odobowo\n\nPoÅ‚Ä…czenie jest bezpÅ‚atne."</string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="submission_contact_body_other">"W razie jakichkolwiek pytaÅ„ zwiÄ…zanych ze zdrowiem skontaktuj siÄ™ z lekarzem rodzinnym lub lekarzem dyżurnym pod numerem: 116 117."</string> @@ -1086,7 +1090,7 @@ <!-- XHED: Page title for calendar page in submission symptom flow --> <string name="submission_symptom_calendar_title">"PoczÄ…tek wystÄ…pienia objawów"</string> <!-- XHED: Page headline for calendar page in symptom submission flow --> - <string name="submission_symptom_calendar_headline">"Kiedy zaczÄ…Å‚eÅ›(-ęłaÅ›) odczuwać te objawy? "</string> + <string name="submission_symptom_calendar_headline">"Kiedy zaczÄ…Å‚eÅ›(-Å‚aÅ›) odczuwać te objawy? "</string> <!-- YTXT: Body text for calendar page in symptom submission flow--> <string name="submission_symptom_calendar_body">"Wybierz dokÅ‚adnÄ… datÄ™ w kalendarzu lub, jeÅ›li nie pamiÄ™tasz dokÅ‚adnej daty, wybierz jednÄ… z innych opcji."</string> <!-- XBUT: symptom calendar screen less than 7 days button --> @@ -1096,7 +1100,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"Ponad 2 tygodnie temu"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"Brak odpowiedzi"</string> + <string name="submission_symptom_verify">"Bez komentarza"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1157,7 +1161,7 @@ <!-- YTXT: invalid status text --> <string name="test_result_card_status_invalid">"Ustalenie wyniku jest niemożliwe"</string> <!-- YTXT: pending status text --> - <string name="test_result_card_status_pending">"Wynik Twojego testu nie jest jeszcze dostÄ™pny"</string> + <string name="test_result_card_status_pending">"Twój wynik testu nie jest jeszcze dostÄ™pny"</string> <!-- XHED: Title for further info of test result negative --> <string name="test_result_card_negative_further_info_title">"Inne informacje:"</string> <!-- YTXT: Content for further info of test result negative --> @@ -1201,7 +1205,10 @@ <string name="errors_google_update_needed">"Twoja aplikacja Corona-Warn-App jest poprawnie zainstalowana, ale system „Powiadomienia o narażeniu na COVID-19†nie jest dostÄ™pny w systemie operacyjnym Twojego smartfona. Oznacza to, że nie możesz korzystać z aplikacji Corona-Warn-App. WiÄ™cej informacji znajduje siÄ™ na naszej stronie „CzÄ™sto zadawane pytaniaâ€: https://www.coronawarn.app/en/faq/."</string> <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day --> <string name="errors_google_api_error">"Aplikacja Corona-Warn-App dziaÅ‚a prawidÅ‚owo, ale nie możemy zaktualizować Twojego aktualnego statusu ryzyka. Rejestrowanie narażenia pozostaje aktywne i dziaÅ‚a prawidÅ‚owo. WiÄ™cej informacji można znaleźć na naszej stronie „CzÄ™sto zadawane pytaniaâ€: https://www.coronawarn.app/en/faq/"</string> - + <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_title">"Limit zostaÅ‚ już osiÄ…gniÄ™ty"</string> + <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_description">"Sprawdzanie narażeÅ„ nie jest już dzisiaj możliwe, ponieważ osiÄ…gniÄ™to maksymalnÄ… liczbÄ™ takich kontroli na dzieÅ„ okreÅ›lonÄ… przez system operacyjny. Sprawdź ponownie swój status ryzyka jutro."</string> <!-- #################################### Generic Error Messages ###################################### --> @@ -1248,17 +1255,7 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> @@ -1342,7 +1339,7 @@ <!-- XHED: Header of interoperability information/configuration view --> <string name="interoperability_configuration_title">"Rejestrowanie narażenia\nw różnych krajach"</string> <!-- XTXT: First section after the header of the interoperability information/configuration view --> - <string name="interoperability_configuration_first_section">"Wiele krajów współpracuje ze sobÄ… w celu aktywacji transgranicznych alertów wysyÅ‚anych poprzez wspólny serwer wymiany danych. Na przykÅ‚ad podczas rejestrowania narażenia można uwzglÄ™dnić również kontakty z użytkownikami oficjalnych aplikacji koronawirusowych z innych uczestniczÄ…cych krajów."</string> + <string name="interoperability_configuration_first_section">"Wiele krajów współpracuje ze sobÄ… w celu aktywacji transgranicznych alertów wysyÅ‚anych poprzez wspólny serwer wymiany danych. Na przykÅ‚ad przy rejestrowaniu narażenia można wziąć pod uwagÄ™ również kontakty z użytkownikami oficjalnych aplikacji koronawirusowych z innych uczestniczÄ…cych krajów."</string> <!-- XTXT: Second section after the header of the interoperability information/configuration view --> <string name="interoperability_configuration_second_section">"W tym celu aplikacja pobiera listÄ™, która jest aktualizowana codziennie, z losowymi identyfikatorami wszystkich użytkowników, którzy udostÄ™pnili swoje losowe identyfikatory poprzez wÅ‚asnÄ… aplikacjÄ™. Lista jest nastÄ™pnie porównywana z losowymi identyfikatorami zarejestrowanymi przez Twój smartfon. Codzienne pobieranie listy z losowymi identyfikatorami jest z reguÅ‚y bezpÅ‚atne – za dane używane przez aplikacjÄ™ w tym kontekÅ›cie nie bÄ™dÄ… pobierane opÅ‚aty roamingowe w innych krajach UE."</string> <!-- XHED: Header right above the country list in the interoperability information/configuration view --> @@ -1357,7 +1354,7 @@ <!-- YMSG: Onboarding tracing step second section in interoperability after the title --> <string name="interoperability_onboarding_second_section">"Gdy użytkownik przeÅ›le swój losowy identyfikator do serwera wymiany danych obsÅ‚ugiwanego wspólnie przez kraje uczestniczÄ…ce, o potencjalnym narażeniu mogÄ… zostać ostrzeżeni użytkownicy oficjalnych aplikacji koronawirusowych we wszystkich tych krajach."</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_randomid_download_free">"Codzienne pobieranie listy z losowymi identyfikatorami jest z reguÅ‚y bezpÅ‚atne. Oznacza to, że operatorzy sieci mobilnych nie bÄ™dÄ… pobierać opÅ‚at za transmisjÄ™ danych używanych przez aplikacjÄ™ w tym kontekÅ›cie ani też nie bÄ™dÄ… naliczane opÅ‚aty roamingowe w innych krajach UE. Aby uzyskać wiÄ™cej informacji, skontaktuj siÄ™ ze swoim operatorem sieci mobilnej."</string> + <string name="interoperability_onboarding_randomid_download_free">"Codzienne pobieranie listy z losowymi identyfikatorami jest z reguÅ‚y bezpÅ‚atne. Oznacza to, że operatorzy sieci mobilnych nie bÄ™dÄ… pobierać opÅ‚at za transmisjÄ™ danych używanych przez aplikacjÄ™ w tym kontekÅ›cie ani też nie bÄ™dÄ… naliczane opÅ‚aty roamingowe z tego tytuÅ‚u w innych krajach UE. Aby uzyskać wiÄ™cej informacji, skontaktuj siÄ™ ze swoim operatorem sieci mobilnej."</string> <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. --> <string name="interoperability_onboarding_list_title">"Obecnie uczestniczÄ… nastÄ™pujÄ…ce kraje:"</string> @@ -1377,7 +1374,7 @@ <string name="interoperability_onboarding_delta_free_download">"Codzienne pobieranie listy z losowymi identyfikatorami jest z reguÅ‚y bezpÅ‚atne. Oznacza to, że operatorzy sieci mobilnych nie bÄ™dÄ… pobierać opÅ‚at za transmisjÄ™ danych używanych przez aplikacjÄ™ w tym kontekÅ›cie ani też nie bÄ™dÄ… naliczane opÅ‚aty roamingowe z tego tytuÅ‚u w innych krajach UE. Aby uzyskać wiÄ™cej informacji, skontaktuj siÄ™ ze swoim operatorem sieci mobilnej."</string> <!-- XACT: interoperability (eu) - illustraction description, explanation image --> - <string name="interoperability_eu_illustration_description">"RÄ™ka trzyma smartfon. W tle przedstawiona jest Europa i flaga europejska."</string> + <string name="interoperability_eu_illustration_description">"RÄ™ka trzyma smartfon. Europa i flaga europejska sÄ… przedstawione w tle."</string> <!-- XTXT: Title for the interoperability onboarding if country download fails --> <string name="interoperability_onboarding_list_title_failrequest">"Kraje uczestniczÄ…ce"</string> diff --git a/Corona-Warn-App/src/main/res/values-ro/strings.xml b/Corona-Warn-App/src/main/res/values-ro/strings.xml index 6c78674ea18deac47d80a941d4aefde469f79695..0c5a9e4f4e0e8f0252b4a82e4df7ddcaa07dac65 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -20,12 +20,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -109,6 +105,10 @@ <string name="notification_headline">"Corona-Warn-App"</string> <!-- XTXT: Notification body --> <string name="notification_body">"AveÈ›i mesaje noi de la aplicaÈ›ia Corona-Warn."</string> + <!-- XHED: Notification title - Reminder to share a positive test result--> + <string name="notification_headline_share_positive_result">"PuteÈ›i fi de ajutor!"</string> + <!-- XTXT: Notification body - Reminder to share a positive test result--> + <string name="notification_body_share_positive_result">"Vă rugăm să partajaÈ›i rezultatul testului dvs. pentru a-i avertiza pe ceilalÈ›i."</string> <!-- #################################### App Auto Update @@ -150,11 +150,9 @@ <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"ÃŽnregistrarea în jurnal a expunerilor este permanent activă"</string> <!-- XTXT; risk card - no update done yet --> - <string name="risk_card_body_not_yet_fetched">"Expunerile nu au fost încă verificate."</string> + <string name="risk_card_body_not_yet_fetched">"ÃŽntâlnirile nu au fost încă verificate."</string> <!-- XTXT: risk card - last successful update --> <string name="risk_card_body_time_fetched">"Actualizată: %1$s"</string> - <!-- XTXT: risk card - next update --> - <string name="risk_card_body_next_update">"Actualizată zilnic"</string> <!-- XTXT: risk card - hint to open the app daily --> <string name="risk_card_body_open_daily">"ReÈ›ineÈ›i: DeschideÈ›i aplicaÈ›ia zilnic pentru a actualiza starea riscului dvs."</string> <!-- XBUT: risk card - update risk --> @@ -185,13 +183,19 @@ <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"ÃŽnregistrarea în jurnal a expunerilor a fost oprită"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"Ultima înregistrare în jurnal a expunerilor:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"Ultima verificare a expunerilor:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"ÃŽnregistrarea în jurnal a expunerilor nu este posibilă"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> <string name="risk_card_outdated_risk_body">"ÃŽnregistrarea în jurnal a expunerilor dvs. nu a putut fi actualizată timp de peste 24 de ore."</string> <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"Starea riscului dvs. nu a fost actualizată timp de peste 48 de ore. Vă rugăm să activaÈ›i starea riscului dvs."</string> + <!-- XHED: risk card - risk check failed headline, no internet connection --> + <string name="risk_card_check_failed_no_internet_headline">"Verificarea expunerii a eÈ™uat"</string> + <!-- XTXT: risk card - risk check failed, please check your internet connection --> + <string name="risk_card_check_failed_no_internet_body">"Sincronizarea ID-urilor aleatorii cu serverul a eÈ™uat. PuteÈ›i relansa manual sincronizarea."</string> + <!-- XTXT: risk card - risk check failed, restart button --> + <string name="risk_card_check_failed_no_internet_restart_button">"Relansare"</string> <!-- #################################### Risk Card - Progress @@ -247,7 +251,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"ÃŽnregistrarea în jurnal a expunerilor"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"ÃŽnregistrarea în jurnal a expunerilor este una dintre cele trei caracteristici centrale ale aplicaÈ›iei. Când o activaÈ›i, sunt înregistrate întâlnirile cu dispozitivele altor persoane. Nu trebuie să faceÈ›i nimic altceva."</string> + <string name="main_overview_body_tracing">"ÃŽnregistrarea în jurnal a expunerilor este una dintre cele trei caracteristici centrale ale aplicaÈ›iei. Când o activaÈ›i, sunt înregistrate întâlnirile cu smartphone-urile altor persoane. Nu trebuie să faceÈ›i nimic altceva."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Risc de infectare"</string> <!-- YTXT: App overview body text about risk levels --> @@ -285,7 +289,7 @@ <!-- XHED: App overview subtitle for glossary keys --> <string name="main_overview_subtitle_glossary_keys">"ID aleatoriu"</string> <!-- YTXT: App overview body for glossary keys --> - <string name="main_overview_body_glossary_keys">"ID-urile aleatorii sunt combinaÈ›ii de cifre È™i litere generate aleatoriu. Acestea sunt schimbate între dispozitivele aflate în proximitate strânsă. ID-urile aleatorii nu pot fi asociate cu o persoană anume È™i sunt È™terse automat după 14 zile. Persoanele diagnosticate cu COVID-19 pot opta să trimită ID-urile aleatorii din ultimele 14 zile altor utilizatori ai aplicaÈ›iei."</string> + <string name="main_overview_body_glossary_keys">"ID-urile aleatorii sunt combinaÈ›ii de cifre È™i litere generate aleatoriu. Acestea sunt schimbate între dispozitivele aflate în proximitate strânsă. ID-urile aleatorii nu pot fi asociate cu o persoană anume È™i sunt È™terse automat după 14 zile. Persoanele diagnosticate cu COVID-19 pot opta să trimită ID-urile aleatorii din ultimele 14 zile È™i altor utilizatori ai aplicaÈ›iei."</string> <!-- XACT: main (overview) - illustraction description, explanation image --> <string name="main_overview_illustration_description">"Un smartphone afiÈ™ează conÈ›inut variat, numerotat de la 1 la 3."</string> <!-- XACT: App main page title --> @@ -344,7 +348,7 @@ <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> <string name="risk_details_information_body_outdated_risk">"ÃŽnregistrarea în jurnal a expunerilor dvs. nu a putut fi actualizată timp de peste 24 de ore."</string> <!-- YTXT: risk details - low risk explanation text --> - <string name="risk_details_information_body_low_risk">"AveÈ›i un risc redus de infectare deoarece nu a fost înregistrată nicio expunere la persoane diagnosticate ulterior cu COVID-19 sau întâlnirile au fost limitate la o perioadă scurtă È™i la o distanță mai mare."</string> + <string name="risk_details_information_body_low_risk">"AveÈ›i un risc redus de infectare deoarece nu a fost înregistrată nicio expunere la persoane diagnosticate ulterior cu COVID-19 sau întâlnirile dvs. au fost limitate la o perioadă scurtă È™i la o distanță mai mare."</string> <!-- YTXT: risk details - low risk explanation text with encounter with low risk --> <string name="risk_details_information_body_low_risk_with_encounter">"Riscul de infectare este calculat local pe smartphone-ul dvs., utilizând datele de înregistrare în jurnal a expunerilor. Calculul poate È›ine cont È™i de distanÈ›a È™i durata expunerii la persoane diagnosticate cu COVID-19, precum È™i de potenÈ›iala contagiozitate a acestora. Riscul dvs. de infectare nu poate fi observat sau transmis mai departe niciunei alte persoane."</string> <!-- YTXT: risk details - increased risk explanation text with variable for day(s) since last contact --> @@ -357,9 +361,9 @@ <item quantity="many">"AveÈ›i un risc crescut de infectare deoarece aÈ›i fost expus ultima dată acum %1$s zile pe o perioadă mai lungă de timp È™i în strânsă proximitate cu cel puÈ›in o persoană diagnosticată cu COVID-19."</item> </plurals> <!-- YTXT: risk details - risk calculation explanation --> - <string name="risk_details_information_body_notice">"Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata È™i proximitatea) la nivel local pe dispozitivul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane."</string> + <string name="risk_details_information_body_notice">"Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata È™i proximitatea) la nivel local pe smartphone-ul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane."</string> <!-- YTXT: risk details - risk calculation explanation for increased risk --> - <string name="risk_details_information_body_notice_increased">"Prin urmare, riscul dvs. de infectare a fost clasificat ca fiind crescut. Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata È™i proximitatea) la nivel local pe dispozitivul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane. Când ajungeÈ›i acasă, evitaÈ›i contactul strâns cu membrii familiei sau cu cei din gospodăria dvs."</string> + <string name="risk_details_information_body_notice_increased">"Prin urmare, riscul dvs. de infectare a fost clasificat ca fiind crescut. Riscul dvs. de infectare este calculat pe baza datelor de înregistrare în jurnal a expunerilor (durata È™i proximitatea) la nivel local pe smartphone-ul dvs. Riscul dvs. de infectare nu poate fi văzut de o altă persoană sau transmis unei alte persoane. Când ajungeÈ›i acasă, evitaÈ›i contactul strâns cu membrii familiei sau cu cei din gospodăria dvs."</string> <!-- NOTR --> <string name="risk_details_button_update">@string/risk_card_button_update</string> <!-- NOTR --> @@ -415,7 +419,7 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"Mai multă protecÈ›ie pentru dvs. È™i pentru noi toÈ›i. Utilizând Corona-Warn-App putem întrerupe mai uÈ™or lanÈ›ul de infectare."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"TransformaÈ›i-vă dispozitivul într-un sistem de avertizare împotriva coronavirusului. ObÈ›ineÈ›i un sumar al stării de risc È™i aflaÈ›i dacă aÈ›i intrat în contact strâns cu persoane diagnosticate cu COVID-19 în ultimele 14 zile."</string> + <string name="onboarding_body">"TransformaÈ›i-vă smartphone-ul într-un sistem de avertizare împotriva coronavirusului. ObÈ›ineÈ›i un sumar al stării de risc È™i aflaÈ›i dacă aÈ›i intrat în contact strâns cu persoane diagnosticate cu COVID-19 în ultimele 14 zile."</string> <!-- YTXT: onboarding(together) - explain application --> <string name="onboarding_body_emphasized">"AplicaÈ›ia înregistrează în jurnal întâlnirile dintre persoane prin dispozitivele acestora, care schimbă ID-uri aleatorii criptate, fără a accesa niciun fel de date personale."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> @@ -452,7 +456,7 @@ <!-- XBUT: onboarding(tracing) - negative button (right) --> <string name="onboarding_tracing_dialog_button_negative">"ÃŽnapoi"</string> <!-- XACT: onboarding(tracing) - dialog about background jobs header text --> - <string name="onboarding_background_fetch_dialog_headline">"Actualizări în fundal dezactivate"</string> + <string name="onboarding_background_fetch_dialog_headline">"ÃŽmprospătarea aplicaÈ›iei în fundal dezactivată"</string> <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"AÈ›i dezactivat actualizările în fundal pentru aplicaÈ›ia Corona-Warn. ActivaÈ›i actualizările în fundal pentru a utiliza înregistrarea automată în jurnal a expunerilor. Dacă nu activaÈ›i actualizările în fundal, puteÈ›i porni doar manual din aplicaÈ›ie înregistrarea în jurnal a expunerilor. PuteÈ›i activa actualizările în fundal pentru aplicaÈ›ie din setările dispozitivului dvs."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> @@ -474,7 +478,7 @@ <!-- XBUT: onboarding(tracing) - dialog about manual checking button --> <string name="onboarding_manual_required_dialog_button">"OK"</string> <!-- XACT: onboarding(tracing) - illustraction description, header image --> - <string name="onboarding_tracing_illustration_description">"Trei persoane È™i-au activat pe dispozitiv înregistrarea în jurnal a expunerilor, ceea ce va duce la înregistrarea întâlnirilor lor."</string> + <string name="onboarding_tracing_illustration_description">"Trei persoane È™i-au activat pe smartphone înregistrarea în jurnal a expunerilor, ceea ce va duce la înregistrarea întâlnirilor lor."</string> <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"PermiteÈ›i accesul la locaÈ›ie"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> @@ -671,7 +675,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) este un organism federal de sănătate publică din Germania. RKI a publicat aplicaÈ›ia Corona-Warn în numele Guvernului Federal. AplicaÈ›ia are drept scop să completeze sub formă digitală măsurile de sănătate publică deja introduse: distanÈ›area socială, igiena È™i purtarea măștii."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Persoanele care utilizează aplicaÈ›ia ajută la urmărirea È™i întreruperea lanÈ›urilor de infectare. AplicaÈ›ia salvează local, pe dispozitivul dvs., întâlnirile cu alte persoane. SunteÈ›i notificat dacă aÈ›i întâlnit persoane care au fost diagnosticate ulterior cu COVID-19. Identitatea È™i confidenÈ›ialitatea dvs. sunt protejate întotdeauna."</string> + <string name="information_about_body">"Persoanele care utilizează aplicaÈ›ia ajută la urmărirea È™i întreruperea lanÈ›urilor de infectare. AplicaÈ›ia salvează local, pe smartphone-ul dvs., întâlnirile cu alte persoane. SunteÈ›i notificat dacă aÈ›i întâlnit persoane care au fost diagnosticate ulterior cu COVID-19. Identitatea È™i confidenÈ›ialitatea dvs. sunt protejate întotdeauna."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Un grup de persoane își utilizează smartphone-urile prin oraÈ™."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -703,9 +707,9 @@ <!-- XTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_phone">"Serviciul clienÈ›i vă stă la dispoziÈ›ie."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"Limbi: germană, engleză, turcă\nProgram de lucru:"<xliff:g id="line_break">"\n"</xliff:g>"luni - sâmbătă: 07:00 - 22:00"<xliff:g id="line_break">"\n(exceptând sărbătorile legale)"</xliff:g><xliff:g id="line_break">"\nApelul este gratuit."</xliff:g></string> + <string name="information_contact_body_open">"Limbi: engleză, germană, turcă\nProgram de lucru:"<xliff:g id="line_break">"\n"</xliff:g>"luni - sâmbătă: 07:00 - 22:00"<xliff:g id="line_break">"\n(exceptând sărbătorile legale)"</xliff:g><xliff:g id="line_break">"\nApelul este gratuit."</xliff:g></string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_other">"Dacă aveÈ›i întrebări legate de starea de sănătate, vă rugăm să contactaÈ›i medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 112."</string> + <string name="information_contact_body_other">"Dacă aveÈ›i întrebări legate de starea de sănătate, vă rugăm să contactaÈ›i medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 116 117 (Germania) sau 112 (România)."</string> <!-- XACT: describes illustration --> <string name="information_contact_illustration_description">"Un bărbat poartă căști în timpul unei convorbiri telefonice."</string> <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website --> @@ -788,9 +792,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"ÃŽnapoi"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Testul are erori"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"Codul QR nu mai este valabil"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"A apărut o problemă la evaluarea testului dvs. Codul dvs. QR a expirat deja."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"Testul dvs. are o vechime de peste 21 de zile È™i nu mai poate fi înregistrat în aplicaÈ›ie. Dacă sunteÈ›i testat din nou în viitor, nu uitaÈ›i să scanaÈ›i codul QR imediat ce îl primiÈ›i."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> @@ -865,15 +869,15 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Actualizare"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Ștergere test"</string> + <string name="submission_test_result_pending_remove_test_button">"Eliminare test"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Rezultatul testului dvs."</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"Rezultatul de laborator nu indică o confirmare a infecÈ›iei cu coronavirusul SARS-CoV-2.\n\nȘtergeÈ›i testul din Corona-Warn-App pentru a salva un nou cod de test aici dacă este necesar."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Ștergere test"</string> + <string name="submission_test_result_negative_remove_test_button">"Eliminare test"</string> <!-- XHED: Page headline for other warnings screen --> - <string name="submission_test_result_positive_steps_warning_others_heading">"Avertizarea altora"</string> + <string name="submission_test_result_positive_steps_warning_others_heading">"AvertizaÈ›i-i pe ceilalÈ›i"</string> <!-- YTXT: Body text for for other warnings screen--> <string name="submission_test_result_positive_steps_warning_others_body">"PartajaÈ›i ID-urile dvs. aleatorii È™i avertizaÈ›i-i pe ceilalÈ›i.\nAjutaÈ›i la stabilirea riscului de infectare pentru ceilalÈ›i cu mai multă acurateÈ›e, indicând momentul în care aÈ›i observat prima dată simptomele de coronavirus."</string> <!-- XBUT: positive test result : continue button --> @@ -952,13 +956,13 @@ <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"ÃŽnregistraÈ›i-vă testul scanând codul QR al documentului de testare."</string> <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"Consimțământ"</string> + <string name="submission_dispatcher_qr_privacy_dialog_headline">"DeclaraÈ›ie de consimțământ"</string> <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Acceptâ€, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacyâ€."</string> <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Accept"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"PermiteÈ›i"</string> <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nu accept"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nu permiteÈ›i"</string> <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"TAN"</string> <!-- YTXT: Body text for TAN code dispatcher option --> @@ -972,7 +976,7 @@ <!-- Submission Positive Other Warning --> <!-- XHED: Page title for the positive result additional warning page--> - <string name="submission_positive_other_warning_title">"Avertizarea altor persoane"</string> + <string name="submission_positive_other_warning_title">"AvertizaÈ›i-i pe ceilalÈ›i"</string> <!-- XHED: Page headline for the positive result additional warning page--> <string name="submission_positive_other_warning_headline">"Să ne ajutăm împreună!"</string> <!-- YTXT: Body text for the positive result additional warning page--> @@ -980,7 +984,7 @@ <!-- XBUT: other warning continue button --> <string name="submission_positive_other_warning_button">"Accept"</string> <!-- XACT: other warning - illustration description, explanation image --> - <string name="submission_positive_other_illustration_description">"Un dispozitiv transmite un diagnostic de test pozitiv criptat către sistem."</string> + <string name="submission_positive_other_illustration_description">"Un smartphone transmite un diagnostic de test pozitiv criptat către sistem."</string> <!-- XHED: Title for the interop country list--> <string name="submission_interoperability_list_title">"Următoarele țări participă în prezent la înregistrarea în jurnal a expunerilor la nivel transnaÈ›ional:"</string> @@ -1046,7 +1050,7 @@ <!-- XBUT: symptom initial screen no button --> <string name="submission_symptom_negative_button">"Nu"</string> <!-- XBUT: symptom initial screen no information button --> - <string name="submission_symptom_no_information_button">"Nu răspund"</string> + <string name="submission_symptom_no_information_button">"Nu comentez"</string> <!-- XBUT: symptom initial screen continue button --> <string name="submission_symptom_further_button">"ÃŽnainte"</string> @@ -1071,9 +1075,9 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"ÃŽnregistraÈ›i testul introducând codul TAN în aplicaÈ›ie."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"Limbi: \ngermană, engleză, turcă\n\nProgram de lucru:\nluni - duminică: non-stop\n\nApelul este gratuit."</string> + <string name="submission_contact_operating_hours_body">"Limbi: \nengleză, germană, turcă\n\nProgram de lucru:\nluni - duminică: non-stop\n\nApelul este gratuit."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="submission_contact_body_other">"Dacă aveÈ›i întrebări legate de starea de sănătate, vă rugăm să contactaÈ›i medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 112."</string> + <string name="submission_contact_body_other">"Dacă aveÈ›i întrebări legate de starea de sănătate, vă rugăm să contactaÈ›i medicul de familie sau hotline-ul pentru servicii medicale de urgență, la numărul de telefon: 116 117 (Germania) sau 112 (România)."</string> <!-- XACT: Submission contact page title --> <string name="submission_contact_accessibility_title">"SunaÈ›i la hotline È™i solicitaÈ›i un TAN"</string> @@ -1096,7 +1100,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"Cu peste 2 săptămâni în urmă"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"Nu răspund"</string> + <string name="submission_symptom_verify">"Nu comentez"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1201,7 +1205,10 @@ <string name="errors_google_update_needed">"AplicaÈ›ia dvs. Corona-Warn este instalată corect, dar serviciul „Notificări privind expunerea la COVID-19†nu este disponibil în sistemul de operare al smartphone-ului dvs. Aceasta înseamnă că nu puteÈ›i utiliza aplicaÈ›ia Corona-Warn. Pentru mai multe informaÈ›ii, consultaÈ›i pagina noastră de întrebări frecvente: https://www.coronawarn.app/en/faq/"</string> <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day --> <string name="errors_google_api_error">"AplicaÈ›ia Corona-Warn funcÈ›ionează corect, dar nu putem actualiza starea curentă a riscului dvs. ÃŽnregistrarea în jurnal a expunerilor rămâne activă È™i funcÈ›ionează corect. Pentru mai multe informaÈ›ii, consultaÈ›i pagina noastră de întrebări frecvente: https://www.coronawarn.app/en/faq/"</string> - + <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_title">"Limita a fost deja atinsă"</string> + <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_description">"Astăzi nu mai este posibilă verificarea expunerii, deoarece aÈ›i atins numărul maxim de verificări pe zi definit de sistemul dvs. de operare. VerificaÈ›i din nou mâine starea riscului dvs."</string> <!-- #################################### Generic Error Messages ###################################### --> @@ -1248,17 +1255,7 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> @@ -1357,7 +1354,7 @@ <!-- YMSG: Onboarding tracing step second section in interoperability after the title --> <string name="interoperability_onboarding_second_section">"Când un utilizator transmite ID-urile sale aleatorii la serverul de schimb operat în comun de țările participante, utilizatorii aplicaÈ›iilor oficiale anticoronavirus din toate aceste țări pot fi avertizaÈ›i de posibila expunere."</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_randomid_download_free">"Descărcarea zilnică a listei cu ID-urilor aleatorii este, de obicei, gratuită pentru dvs. ÃŽn mod specific, aceasta înseamnă că operatorii de reÈ›ele mobile nu percep costuri pentru datele utilizate de aplicaÈ›ie în acest context È™i nu aplică niciun fel de costuri de roaming pentru această opÈ›iune în alte țări UE. ContactaÈ›i operatorul reÈ›elei mobile pentru mai multe informaÈ›ii."</string> + <string name="interoperability_onboarding_randomid_download_free">"Descărcarea zilnică a listei cu ID-uri aleatorii este, de obicei, gratuită pentru dvs. ÃŽn mod specific, aceasta înseamnă că operatorii de reÈ›ele mobile nu percep costuri pentru datele utilizate de aplicaÈ›ie în acest context È™i nu aplică niciun fel de costuri de roaming pentru această opÈ›iune în alte țări UE. ContactaÈ›i operatorul reÈ›elei mobile pentru mai multe informaÈ›ii."</string> <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. --> <string name="interoperability_onboarding_list_title">"ÃŽn prezent participă următoarele țări:"</string> diff --git a/Corona-Warn-App/src/main/res/values-tr/strings.xml b/Corona-Warn-App/src/main/res/values-tr/strings.xml index 02255db8b322545744f9c3fa76f6a5958bb01296..78dc5a455f852fb5ba62c532be47921025386790 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -20,12 +20,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -109,6 +105,10 @@ <string name="notification_headline">"Corona-Warn-App"</string> <!-- XTXT: Notification body --> <string name="notification_body">"Corona-Warn-App uygulamasından yeni mesajlarınız var."</string> + <!-- XHED: Notification title - Reminder to share a positive test result--> + <string name="notification_headline_share_positive_result">"Yardımcı olabilirsiniz!"</string> + <!-- XTXT: Notification body - Reminder to share a positive test result--> + <string name="notification_body_share_positive_result">"Lütfen test sonucunuzu paylaşın ve diÄŸer kullanıcıları uyarın."</string> <!-- #################################### App Auto Update @@ -150,11 +150,9 @@ <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"Maruz kalma günlüğü sürekli etkin"</string> <!-- XTXT; risk card - no update done yet --> - <string name="risk_card_body_not_yet_fetched">"Maruz kalmalar henüz kontrol edilmedi."</string> + <string name="risk_card_body_not_yet_fetched">"KarşılaÅŸmalar henüz kontrol edilmedi."</string> <!-- XTXT: risk card - last successful update --> <string name="risk_card_body_time_fetched">"Güncelleme: %1$s"</string> - <!-- XTXT: risk card - next update --> - <string name="risk_card_body_next_update">"Günlük olarak güncellenir"</string> <!-- XTXT: risk card - hint to open the app daily --> <string name="risk_card_body_open_daily">"Not: Risk durumunuzu güncellemek için lütfen uygulamayı her gün açın."</string> <!-- XBUT: risk card - update risk --> @@ -185,13 +183,19 @@ <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"Maruz kalma günlüğü durduruldu"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"Son maruz kalma günlüğü:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"Son maruz kalma kontrolü:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"Maruz kalma günlüğü oluÅŸturulamıyor"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> <string name="risk_card_outdated_risk_body">"Maruz kalma günlüğünüz 24 saatten uzun süre için güncellenemedi."</string> <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"Risk durumunuz 48 saatten uzun süredir güncellenmedi. Lütfen risk durumunuzu güncelleyin."</string> + <!-- XHED: risk card - risk check failed headline, no internet connection --> + <string name="risk_card_check_failed_no_internet_headline">"Maruz kalma kontrolü baÅŸarısız oldu"</string> + <!-- XTXT: risk card - risk check failed, please check your internet connection --> + <string name="risk_card_check_failed_no_internet_body">"Rastgele kimliklerin sunucu ile senkronizasyonu baÅŸarısız oldu. Senkronizasyonu manüel olarak baÅŸlatabilirsiniz."</string> + <!-- XTXT: risk card - risk check failed, restart button --> + <string name="risk_card_check_failed_no_internet_restart_button">"Yeniden baÅŸlat"</string> <!-- #################################### Risk Card - Progress @@ -247,7 +251,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"Maruz Kalma Günlüğü"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"Maruz kalma günlüğü, uygulamanın üç temel özelliÄŸinden biridir. Bu özelliÄŸi etkinleÅŸtirdiÄŸinizde, diÄŸer kiÅŸilerin cihazlarıyla karşılaÅŸmalarınız günlüğe kaydedilir. BaÅŸka bir iÅŸlem yapmanız gerekmez."</string> + <string name="main_overview_body_tracing">"Maruz kalma günlüğü, uygulamanın üç temel özelliÄŸinden biridir. Bu özelliÄŸi etkinleÅŸtirdiÄŸinizde, diÄŸer kiÅŸilerin akıllı telefonlarıyla karşılaÅŸmalarınız günlüğe kaydedilir. BaÅŸka bir iÅŸlem yapmanız gerekmez."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Enfeksiyon Riski"</string> <!-- YTXT: App overview body text about risk levels --> @@ -357,9 +361,9 @@ <item quantity="many">"En son %1$s gün önce, COVID-19 tanısı konan en az bir kiÅŸiyle daha uzun süreyle ve yakın mesafeden maruz kalma yaÅŸadığınız için enfeksiyon riskiniz daha yüksektir."</item> </plurals> <!-- YTXT: risk details - risk calculation explanation --> - <string name="risk_details_information_body_notice">"Enfeksiyon riskiniz, cihazınızda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz baÅŸkaları tarafından görüntülenemez veya baÅŸkalarına aktarılmaz."</string> + <string name="risk_details_information_body_notice">"Enfeksiyon riskiniz, akıllı telefonunuzda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz baÅŸkaları tarafından görüntülenemez veya baÅŸkalarına aktarılmaz."</string> <!-- YTXT: risk details - risk calculation explanation for increased risk --> - <string name="risk_details_information_body_notice_increased">"Bu nedenle enfeksiyon riskiniz artmış olarak derecelendirilmiÅŸtir. Enfeksiyon riskiniz, cihazınızda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz baÅŸkaları tarafından görüntülenemez veya baÅŸkalarına aktarılmaz. Eve gittiÄŸinizde lütfen aile fertleriniz veya ev arkadaÅŸlarınızla yakın temastan kaçının."</string> + <string name="risk_details_information_body_notice_increased">"Bu nedenle enfeksiyon riskiniz artmış olarak derecelendirilmiÅŸtir. Enfeksiyon riskiniz, akıllı telefonunuzda yerel olarak bulunan maruz kalma günlüğü verileri (süre ve mesafe) kullanılarak hesaplanır. Enfeksiyon riskiniz baÅŸkaları tarafından görüntülenemez veya baÅŸkalarına aktarılmaz. Eve gittiÄŸinizde lütfen aile fertleriniz veya ev arkadaÅŸlarınızla yakın temastan kaçının."</string> <!-- NOTR --> <string name="risk_details_button_update">@string/risk_card_button_update</string> <!-- NOTR --> @@ -415,7 +419,7 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"Sizin için ve hepimiz için daha fazla koruma. Corona-Warn-App uygulamasını kullanarak enfeksiyon zincirlerini çok daha kısa süre içinde kırabiliriz."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"Cihazınızı koronavirüs uyarı sistemine dönüştürün. Risk durumunuza iliÅŸkin genel bir bakış elde edin ve son 14 gün içinde COVID-19 tanısı konan herhangi biri ile yakın temasa geçip geçmediÄŸinizi öğrenin."</string> + <string name="onboarding_body">"Akıllı telefonunuzu koronavirüs uyarı sistemine dönüştürün. Risk durumunuza iliÅŸkin genel bir bakış elde edin ve son 14 gün içinde COVID-19 tanısı konan herhangi biri ile yakın temasa geçip geçmediÄŸinizi öğrenin."</string> <!-- YTXT: onboarding(together) - explain application --> <string name="onboarding_body_emphasized">"Uygulama kiÅŸilerin cihazları arasında ÅŸifrelenmiÅŸ rastgele kimlikleri paylaÅŸarak karşılaÅŸma günlüğü oluÅŸturur ve bu sırada hiçbir kiÅŸisel veriye eriÅŸim saÄŸlanmaz."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> @@ -452,7 +456,7 @@ <!-- XBUT: onboarding(tracing) - negative button (right) --> <string name="onboarding_tracing_dialog_button_negative">"Geri"</string> <!-- XACT: onboarding(tracing) - dialog about background jobs header text --> - <string name="onboarding_background_fetch_dialog_headline">"Arka plan güncellemeleri devre dışı bırakıldı"</string> + <string name="onboarding_background_fetch_dialog_headline">"Arka planda uygulamayı yenileme devre dışı bırakıldı"</string> <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"Corona-Warn-App için arka plan güncellemelerini devre dışı bıraktınız. Otomatik maruz kalma günlüğü özelliÄŸini kullanmak için lütfen arka plan güncellemelerini etkinleÅŸtirin. Arka plan güncellemelerini etkinleÅŸtirmezseniz maruz kalma günlüğü özelliÄŸini yalnızca uygulamadan manüel olarak baÅŸlatabilirsiniz. Uygulamanın arka plan güncellemelerini cihazınızın ayarlarından etkinleÅŸtirebilirsiniz."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> @@ -474,7 +478,7 @@ <!-- XBUT: onboarding(tracing) - dialog about manual checking button --> <string name="onboarding_manual_required_dialog_button">"Tamam"</string> <!-- XACT: onboarding(tracing) - illustraction description, header image --> - <string name="onboarding_tracing_illustration_description">"Üç kiÅŸi cihazlarında maruz kalma günlüğünü etkinleÅŸtirdi ve birbirleri ile karşılaÅŸmaları günlüğe kaydedilecektir."</string> + <string name="onboarding_tracing_illustration_description">"Üç kiÅŸi akıllı telefonlarında maruz kalma günlüğünü etkinleÅŸtirdi ve birbirleri ile karşılaÅŸmaları günlüğe kaydedilecektir."</string> <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Konum eriÅŸimine izin ver"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> @@ -671,7 +675,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI), Almanya\'nın federal kamu saÄŸlığı kurumudur. RKI, Federal Hükûmet adına Corona-Warn-App uygulamasını yayınlamaktadır. Uygulama, daha önce açıklanan kamu saÄŸlığı önlemlerine iliÅŸkin dijital bir tamamlayıcı niteliÄŸindedir: sosyal mesafe, hijyen uygulamaları ve yüz maskeleri."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"Uygulamayı kullanan kiÅŸiler, enfeksiyon zincirlerinin takip edilmesine ve kırılmasına yardımcı olur. Uygulama, diÄŸer kiÅŸilerle karşılaÅŸmaları cihazınızda yerel olarak kaydeder. Daha sonra COVID-19 tanısı konan kiÅŸilerle karşılaÅŸmışsanız size bildirim gönderilir. KimliÄŸiniz ve gizliliÄŸiniz daima koruma altındadır."</string> + <string name="information_about_body">"Uygulamayı kullanan herkes, enfeksiyon zincirlerinin takip edilmesine ve kırılmasına yardımcı olur. Uygulama, diÄŸer kiÅŸilerle karşılaÅŸmaları akıllı telefonunuzda yerel olarak kaydeder. Daha sonra COVID-19 tanısı konan kiÅŸilerle karşılaÅŸmışsanız size bildirim gönderilir. KimliÄŸiniz ve gizliliÄŸiniz daima koruma altındadır."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"Bölgedeki bir grup insan akıllı telefonlarını kullanıyor."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -703,7 +707,7 @@ <!-- XTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_phone">"Müşteri hizmetlerimiz size yardımcı olmaya hazır."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"Diller: Almanca, Ä°ngilizce, Türkçe\nMesai saatleri:"<xliff:g id="line_break">"\n"</xliff:g>"Pazartesi - Cumartesi: 7.00 - 22.00"<xliff:g id="line_break">"\n(ulusal tatiller hariçtir)"</xliff:g><xliff:g id="line_break">"\nAramalar ücretsizdir."</xliff:g></string> + <string name="information_contact_body_open">"Diller: Ä°ngilizce, Almanca, Türkçe\nMesai saatleri:"<xliff:g id="line_break">"\n"</xliff:g>"Pazartesi - Cumartesi: 7.00 - 22.00"<xliff:g id="line_break">"\n(ulusal tatiller hariçtir)"</xliff:g><xliff:g id="line_break">"\nAramalar ücretsizdir."</xliff:g></string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_other">"SaÄŸlıkla ilgili tüm sorularınız için lütfen aile hekiminizle veya tıbbi acil servis yardım hattı ile iletiÅŸime geçin. Telefon: 116 117."</string> <!-- XACT: describes illustration --> @@ -788,9 +792,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"Geri"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Testte hata var"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"QR kod artık geçerli deÄŸil"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"Testinizi deÄŸerlendirirken bir problem oluÅŸtu. QR kodunuzun süresi dolmuÅŸ."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"Testiniz 21 günden eski ve artık uygulamaya kaydedilemez. Ä°lerleyen zamanlarda yeniden test yaptırırsanız lütfen QR kodu alır almaz tarayın."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"Tamam"</string> @@ -865,15 +869,15 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Güncelle"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Testi Sil"</string> + <string name="submission_test_result_pending_remove_test_button">"Testi kaldır"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Test Sonucunuz"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"Laboratuvar sonucuna göre koronavirüs SARS-CoV-2 olduÄŸunuza dair bir doÄŸrulama yok.\n\nGerekirse yeni bir test kodu kaydedebilmeniz için lütfen testi Corona-Warn-App\'ten silin."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Testi Sil"</string> + <string name="submission_test_result_negative_remove_test_button">"Testi kaldır"</string> <!-- XHED: Page headline for other warnings screen --> - <string name="submission_test_result_positive_steps_warning_others_heading">"DiÄŸer Kullanıcıları Uyarma"</string> + <string name="submission_test_result_positive_steps_warning_others_heading">"DiÄŸer Kullanıcıları Uyarın"</string> <!-- YTXT: Body text for for other warnings screen--> <string name="submission_test_result_positive_steps_warning_others_body">"Rastgele kimliklerinizi paylaşın ve diÄŸer kullanıcıları uyarın.\nHerhangi bir koronavirüs semptomunu ne zaman ilk kez fark ettiÄŸinizi de belirterek diÄŸer kullanıcıların enfeksiyon riskini daha doÄŸru ÅŸekilde belirlemeye yardımcı olun."</string> <!-- XBUT: positive test result : continue button --> @@ -952,17 +956,17 @@ <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"Test belgenizin QR kodunu tarayarak testinizi kaydedin."</string> <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"Onay"</string> + <string name="submission_dispatcher_qr_privacy_dialog_headline">"Kabul Beyanı"</string> <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> <string name="submission_dispatcher_qr_privacy_dialog_body">"\"Kabul Et\" seçeneÄŸine dokunarak Uygulamanın koronavirüs testinizin durumunu sorgulamasına ve Uygulamada görüntülemesine izin verirsiniz. Bu özelliÄŸi, QR kod aldıysanız ve test sonucunuzun Uygulamanın sunucu sistemine aktarılmasına onay verdiyseniz kullanabilirsiniz. Testi yapan laboratuvar test sonucunuzu sunucuya kaydettiÄŸi anda sonucu Uygulamada görüntüleyebilirsiniz. Ayrıca bildirimleri etkinleÅŸtirirseniz Uygulama, kullanmadığınız sırada test sonucunuzun alındığını belirten bir bildirim gönderir. Ancak gizlilik nedenleriyle testin sonucu yalnızca Uygulamada görüntülenecektir. Uygulamada test kaydınızı silerek dilediÄŸiniz zaman bu onayı geri çekebilirsiniz. Onayınızı geri çekmeniz, onayınızı geri çekmeden önce testi iÅŸlemenin hukuki niteliÄŸini etkilemeyecektir. Menüde \"Veri GizliliÄŸi\" baÅŸlığında daha fazla bilgiye eriÅŸebilirsiniz."</string> <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Kabul Et"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Ä°zin Ver"</string> <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Kabul Etme"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Ä°zin Verme"</string> <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"TAN"</string> <!-- YTXT: Body text for TAN code dispatcher option --> - <string name="submission_dispatcher_tan_code_card_text">"TAN\'yi manuel olarak girerek testinizi kaydedin."</string> + <string name="submission_dispatcher_tan_code_card_text">"TAN\'yi manuel olarak girerek kaydedin."</string> <!-- YTXT: Dispatcher text for TELE-TAN option --> <string name="submission_dispatcher_card_tan_tele">"TAN Talebi"</string> <!-- YTXT: Body text for TELE_TAN dispatcher option --> @@ -972,7 +976,7 @@ <!-- Submission Positive Other Warning --> <!-- XHED: Page title for the positive result additional warning page--> - <string name="submission_positive_other_warning_title">"DiÄŸer Kullanıcıları Uyarma"</string> + <string name="submission_positive_other_warning_title">"DiÄŸer Kullanıcıları Uyarın"</string> <!-- XHED: Page headline for the positive result additional warning page--> <string name="submission_positive_other_warning_headline">"Lütfen hepimize yardımcı olun!"</string> <!-- YTXT: Body text for the positive result additional warning page--> @@ -980,7 +984,7 @@ <!-- XBUT: other warning continue button --> <string name="submission_positive_other_warning_button">"Kabul Et"</string> <!-- XACT: other warning - illustration description, explanation image --> - <string name="submission_positive_other_illustration_description">"Bir cihaz ÅŸifrelenmiÅŸ pozitif test tanısını sisteme aktarır."</string> + <string name="submission_positive_other_illustration_description">"Bir akıllı telefon ÅŸifrelenmiÅŸ pozitif test tanısını sisteme aktarır."</string> <!-- XHED: Title for the interop country list--> <string name="submission_interoperability_list_title">"AÅŸağıdaki ülkeler, ülkeler arası maruz kalma günlüğüne katılmaktadır:"</string> @@ -1046,7 +1050,7 @@ <!-- XBUT: symptom initial screen no button --> <string name="submission_symptom_negative_button">"Hayır"</string> <!-- XBUT: symptom initial screen no information button --> - <string name="submission_symptom_no_information_button">"Bilgi yok"</string> + <string name="submission_symptom_no_information_button">"Beyan yok"</string> <!-- XBUT: symptom initial screen continue button --> <string name="submission_symptom_further_button">"Sonraki"</string> @@ -1071,7 +1075,7 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"Uygulamaya TAN girerek testi kaydedin."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"Diller: \nAlmanca, Ä°ngilizce, Türkçe\n\nMesai saatleri:\nPazartesi - Pazar: 24 saat\n\nArama ücretsizdir."</string> + <string name="submission_contact_operating_hours_body">"Diller: \nÄ°ngilizce, Almanca, Türkçe\n\nMesai saatleri:\nPazartesi - Pazar: 24 saat\n\nArama ücretsizdir."</string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="submission_contact_body_other">"SaÄŸlıkla ilgili tüm sorularınız için lütfen aile hekiminizle veya tıbbi acil servis yardım hattı ile iletiÅŸime geçin. Telefon: 116 117."</string> @@ -1096,7 +1100,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"2 haftadan uzun süre önce"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"Bilgi yok"</string> + <string name="submission_symptom_verify">"Beyan yok"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1201,7 +1205,10 @@ <string name="errors_google_update_needed">"Corona-Warn-App uygulamanız doÄŸru ÅŸekilde yüklendi ancak akıllı telefonunuzun iÅŸletim sisteminde \"COVID-19 Maruz Kalma Bildirimleri Sistemi\" yok. Bu, Corona-Warn-App\'i kullanamayacağınız anlamına geliyor. Daha fazla bilgi için lütfen SSS sayfamıza bakın: https://www.coronawarn.app/en/faq/"</string> <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day --> <string name="errors_google_api_error">"Corona-Warn-App doÄŸru ÅŸekilde çalışıyor ancak mevcut risk durumunuzu güncelleyemiyoruz. Maruz kalma günlüğü aktif ve doÄŸru ÅŸekilde çalışıyor. Daha fazla bilgi için lütfen SSS sayfamıza bakın: https://www.coronawarn.app/en/faq/"</string> - + <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_title">"Sınıra ulaşıldı"</string> + <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> + <string name="errors_risk_detection_limit_reached_description">"Ä°ÅŸletim sisteminizce tanımlanan günlük azami kontrol sayısına ulaÅŸtığınız için bugün daha fazla maruz kalma kontrolü yapılamaz. Lütfen risk durumunuzu yarın yeniden kontrol edin."</string> <!-- #################################### Generic Error Messages ###################################### --> @@ -1248,17 +1255,7 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index b396ed0c73d4116aee33669d877b232e7a8b4f9c..09331c944495e4296bb62ebb71f3ec3611eb89e2 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -21,12 +21,8 @@ <!-- NOTR --> <string name="preference_tracing"><xliff:g id="preference">"preference_tracing"</xliff:g></string> <!-- NOTR --> - <string name="preference_timestamp_diagnosis_keys_fetch"><xliff:g id="preference">"preference_timestamp_diagnosis_keys_fetch"</xliff:g></string> - <!-- NOTR --> <string name="preference_timestamp_manual_diagnosis_keys_retrieval"><xliff:g id="preference">"preference_timestamp_manual_diagnosis_keys_retrieval"</xliff:g></string> <!-- NOTR --> - <string name="preference_string_google_api_token"><xliff:g id="preference">"preference_m_string_google_api_token"</xliff:g></string> - <!-- NOTR --> <string name="preference_background_job_allowed"><xliff:g id="preference">"preference_background_job_enabled"</xliff:g></string> <!-- NOTR --> <string name="preference_mobile_data_allowed"><xliff:g id="preference">"preference_mobile_data_enabled"</xliff:g></string> @@ -67,8 +63,6 @@ <!-- NOTR --> <string name="preference_risk_days_explanation_shown"><xliff:g id="preference">"preference_risk_days_explanation_shown"</xliff:g></string> <!-- NOTR --> - <string name="preference_background_notification"><xliff:g id="preference">"preference_background_notification"</xliff:g></string> - <!-- NOTR --> <string name="preference_interoperability_selected_country_codes"><xliff:g id="preference">"preference_interoperability_selected_country_codes"</xliff:g></string> <!-- NOTR --> <string name="preference_interoperability_all_countries_selected">preference_interoperability_all_countries_selected</string> @@ -117,9 +111,9 @@ <!-- XTXT: Notification body --> <string name="notification_body">"You have new messages from your Corona-Warn-App."</string> <!-- XHED: Notification title - Reminder to share a positive test result--> - <string name="notification_headline_share_positive_result">"Helfen Sie mit!"</string> + <string name="notification_headline_share_positive_result">"You can help!"</string> <!-- XTXT: Notification body - Reminder to share a positive test result--> - <string name="notification_body_share_positive_result">"Bitte warnen Sie andere und teilen Sie Ihr Testergebnis."</string> + <string name="notification_body_share_positive_result">"Please share your test result and warn others."</string> <!-- #################################### App Auto Update @@ -161,11 +155,9 @@ <!-- XTXT: risk card- tracing active for 14 out of 14 days --> <string name="risk_card_body_saved_days_full">"Exposure logging permanently active"</string> <!-- XTXT; risk card - no update done yet --> - <string name="risk_card_body_not_yet_fetched">"Exposures have not yet been checked."</string> + <string name="risk_card_body_not_yet_fetched">"Encounters have not yet been checked."</string> <!-- XTXT: risk card - last successful update --> <string name="risk_card_body_time_fetched">"Updated: %1$s"</string> - <!-- XTXT: risk card - next update --> - <string name="risk_card_body_next_update">"Updated daily"</string> <!-- XTXT: risk card - hint to open the app daily --> <string name="risk_card_body_open_daily">"Note: Please open the app daily to update your risk status."</string> <!-- XBUT: risk card - update risk --> @@ -196,7 +188,7 @@ <!-- XHED: risk card - tracing stopped headline, due to no possible calculation --> <string name="risk_card_no_calculation_possible_headline">"Exposure logging stopped"</string> <!-- XTXT: risk card - last successfully calculated risk level --> - <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure logging:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> + <string name="risk_card_no_calculation_possible_body_saved_risk">"Last exposure check:"<xliff:g id="line_break">"\n"</xliff:g>"%1$s"</string> <!-- XHED: risk card - outdated risk headline, calculation isn't possible --> <string name="risk_card_outdated_risk_headline">"Exposure logging is not possible"</string> <!-- XTXT: risk card - outdated risk, calculation couldn't be updated in the last 24 hours --> @@ -204,11 +196,11 @@ <!-- XTXT: risk card - outdated risk manual, calculation couldn't be updated in the last 48 hours --> <string name="risk_card_outdated_manual_risk_body">"Your risk status has not been updated for more than 48 hours. Please update your risk status."</string> <!-- XHED: risk card - risk check failed headline, no internet connection --> - <string name="risk_card_check_failed_no_internet_headline" /> + <string name="risk_card_check_failed_no_internet_headline">"Exposure check failed"</string> <!-- XTXT: risk card - risk check failed, please check your internet connection --> - <string name="risk_card_check_failed_no_internet_body" /> + <string name="risk_card_check_failed_no_internet_body">"The synchronization of random IDs with the server failed. You can restart the synchronization manually."</string> <!-- XTXT: risk card - risk check failed, restart button --> - <string name="risk_card_check_failed_no_internet_restart_button" /> + <string name="risk_card_check_failed_no_internet_restart_button">"Restart"</string> <!-- #################################### Risk Card - Progress @@ -264,7 +256,7 @@ <!-- XHED: App overview subtitle for tracing explanation--> <string name="main_overview_subtitle_tracing">"Exposure Logging"</string> <!-- YTXT: App overview body text about tracing --> - <string name="main_overview_body_tracing">"Exposure logging is one of the three central features of the app. When you activate it, encounters with people\'s devices are logged. You don\'t have to do anything else."</string> + <string name="main_overview_body_tracing">"Exposure logging is one of three central features of the app. Once you activate it, encounters with people\'s smartphones are logged. You don\'t have to do anything else."</string> <!-- XHED: App overview subtitle for risk explanation --> <string name="main_overview_subtitle_risk">"Risk of Infection"</string> <!-- YTXT: App overview body text about risk levels --> @@ -374,9 +366,9 @@ <item quantity="many">"You have an increased risk of infection because you were last exposed %1$s days ago over a longer period of time and at close proximity to at least one person diagnosed with COVID-19."</item> </plurals> <!-- YTXT: risk details - risk calculation explanation --> - <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else."</string> + <string name="risk_details_information_body_notice">"Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else."</string> <!-- YTXT: risk details - risk calculation explanation for increased risk --> - <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your device. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string> + <string name="risk_details_information_body_notice_increased">"Therefore, your risk of infection has been ranked as increased. Your risk of infection is calculated from the exposure logging data (duration and proximity) locally on your smartphone. Your risk of infection cannot be seen by, or passed on to, anyone else. When you get home, please also avoid close contact with members of your family or household."</string> <!-- NOTR --> <string name="risk_details_button_update">@string/risk_card_button_update</string> <!-- NOTR --> @@ -432,7 +424,7 @@ <!-- XHED: onboarding(together) - two/three line headline under an illustration --> <string name="onboarding_subtitle">"More protection for you and for us all. By using the Corona-Warn-App we can break infection chains much quicker."</string> <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"Turn your device into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string> + <string name="onboarding_body">"Turn your smartphone into a coronavirus warning system. Get an overview of your risk status and find out whether you\'ve had close contact with anyone diagnosed with COVID-19 in the last 14 days."</string> <!-- YTXT: onboarding(together) - explain application --> <string name="onboarding_body_emphasized">"The app logs encounters between individuals by exchanging encrypted, random IDs between their devices, whereby no personal data whatsoever is accessed."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> @@ -469,7 +461,7 @@ <!-- XBUT: onboarding(tracing) - negative button (right) --> <string name="onboarding_tracing_dialog_button_negative">"Back"</string> <!-- XACT: onboarding(tracing) - dialog about background jobs header text --> - <string name="onboarding_background_fetch_dialog_headline">"Background updates deactivated"</string> + <string name="onboarding_background_fetch_dialog_headline">"Background app refresh deactivated"</string> <!-- YMSI: onboarding(tracing) - dialog about background jobs --> <string name="onboarding_background_fetch_dialog_body">"You have deactivated background updates for the Corona-Warn-App. Please activate background updates to use automatic exposure logging. If you do not activate background updates, you can only start exposure logging manually in the app. You can activate background updates for the app in your device settings."</string> <!-- XBUT: onboarding(tracing) - dialog about background jobs, open device settings --> @@ -491,7 +483,7 @@ <!-- XBUT: onboarding(tracing) - dialog about manual checking button --> <string name="onboarding_manual_required_dialog_button">"OK"</string> <!-- XACT: onboarding(tracing) - illustraction description, header image --> - <string name="onboarding_tracing_illustration_description">"Three people have activated exposure logging on their devices, which will log their encounters with each other."</string> + <string name="onboarding_tracing_illustration_description">"Three persons have activated exposure logging on their smartphones, which will log their encounters with each other."</string> <!-- XHED: onboarding(tracing) - location explanation for bluetooth headline --> <string name="onboarding_tracing_location_headline">"Allow location access"</string> <!-- XTXT: onboarding(tracing) - location explanation for bluetooth body text --> @@ -688,7 +680,7 @@ <!-- YTXT: Body text for about information page --> <string name="information_about_body_emphasized">"Robert Koch Institute (RKI) is Germany’s federal public health body. The RKI publishes the Corona-Warn-App on behalf of the Federal Government. The app is intended as a digital complement to public health measures already introduced: social distancing, hygiene, and face masks."</string> <!-- YTXT: Body text for about information page --> - <string name="information_about_body">"People who use the app help to trace and break chains of infection. The app saves encounters with other people locally on your device. You are notified if you have encountered people who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> + <string name="information_about_body">"Whoever uses the app helps to trace and break chains of infection. The app saves encounters with other persons locally on your smartphone. You are notified if you have encountered persons who were later diagnosed with COVID-19. Your identity and privacy are always protected."</string> <!-- XACT: describes illustration --> <string name="information_about_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XHED: Page title for privacy information page, also menu item / button text --> @@ -720,9 +712,9 @@ <!-- XTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_phone">"Our customer service is here to help."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"Languages: German, English, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string> + <string name="information_contact_body_open">"Languages: English, German, Turkish\nBusiness hours:"<xliff:g id="line_break">"\n"</xliff:g>"Monday to Saturday: 7am - 10pm"<xliff:g id="line_break">"\n(except national holidays)"</xliff:g><xliff:g id="line_break">"\nThe call is free of charge."</xliff:g></string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string> + <string name="information_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string> <!-- XACT: describes illustration --> <string name="information_contact_illustration_description">"A man wears a headset while making a phone call."</string> <!-- XLNK: Menu item / hyper link / button text for navigation to FAQ website --> @@ -805,9 +797,9 @@ <string name="submission_error_dialog_web_tan_invalid_button_positive">"Back"</string> <!-- XHED: Dialog title for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_title">"Test has errors"</string> + <string name="submission_error_dialog_web_tan_redeemed_title">"QR code no longer valid"</string> <!-- XMSG: Dialog body for submission tan redeemed --> - <string name="submission_error_dialog_web_tan_redeemed_body">"There was a problem evaluating your test. Your QR code has already expired."</string> + <string name="submission_error_dialog_web_tan_redeemed_body">"Your test is more than 21 days old and can no longer be registered in the app. If you are tested again in future, please make sure to scan the QR code as soon as you get it."</string> <!-- XBUT: Positive button for submission tan redeemed --> <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> @@ -919,15 +911,15 @@ <!-- XBUT: test result pending : refresh button --> <string name="submission_test_result_pending_refresh_button">"Update"</string> <!-- XBUT: test result pending : remove the test button --> - <string name="submission_test_result_pending_remove_test_button">"Delete Test"</string> + <string name="submission_test_result_pending_remove_test_button">"Remove test"</string> <!-- XHED: Page headline for negative test result next steps --> <string name="submission_test_result_negative_steps_negative_heading">"Your Test Result"</string> <!-- YTXT: Body text for next steps section of test negative result --> <string name="submission_test_result_negative_steps_negative_body">"The laboratory result indicates no verification that you have coronavirus SARS-CoV-2.\n\nPlease delete the test from the Corona-Warn-App, so that you can save a new test code here if necessary."</string> <!-- XBUT: negative test result : remove the test button --> - <string name="submission_test_result_negative_remove_test_button">"Delete Test"</string> + <string name="submission_test_result_negative_remove_test_button">"Remove Test"</string> <!-- XHED: Page headline for other warnings screen --> - <string name="submission_test_result_positive_steps_warning_others_heading">"Warning Others"</string> + <string name="submission_test_result_positive_steps_warning_others_heading">"Warn Others"</string> <!-- YTXT: Body text for for other warnings screen--> <string name="submission_test_result_positive_steps_warning_others_body">"Share your random IDs and warn others.\nHelp determine the risk of infection for others more accurately by also indicating when you first noticed any coronavirus symptoms."</string> <!-- XBUT: positive test result : continue button --> @@ -1006,13 +998,13 @@ <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"Register your test by scanning the QR code of your test document."</string> <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"Consent"</string> + <string name="submission_dispatcher_qr_privacy_dialog_headline">"Declaration of Consent"</string> <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> <string name="submission_dispatcher_qr_privacy_dialog_body">"By tapping “Acceptâ€, you consent to the App querying the status of your coronavirus test and displaying it in the App. This feature is available to you if you have received a QR code and have consented to your test result being transmitted to the App’s server system. As soon as the testing lab has stored your test result on the server, you will be able to see the result in the App. If you have enabled notifications, you will also receive a notification outside the App telling you that your test result has been received. However, for privacy reasons, the test result itself will only be displayed in the App. You can withdraw this consent at any time by deleting your test registration in the App. Withdrawing your consent will not affect the lawfulness of processing before its withdrawal. Further information can be found in the menu under “Data Privacyâ€."</string> <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Accept"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Allow"</string> <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Accept"</string> + <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Allow"</string> <!-- YTXT: Dispatcher text for TAN code option --> <string name="submission_dispatcher_card_tan_code">"TAN"</string> <!-- YTXT: Body text for TAN code dispatcher option --> @@ -1026,7 +1018,7 @@ <!-- Submission Positive Other Warning --> <!-- XHED: Page title for the positive result additional warning page--> - <string name="submission_positive_other_warning_title">"Warning Others"</string> + <string name="submission_positive_other_warning_title">"Warn Others"</string> <!-- XHED: Page headline for the positive result additional warning page--> <string name="submission_positive_other_warning_headline">"Please help all of us!"</string> <!-- YTXT: Body text for the positive result additional warning page--> @@ -1034,7 +1026,7 @@ <!-- XBUT: other warning continue button --> <string name="submission_positive_other_warning_button">"Accept"</string> <!-- XACT: other warning - illustration description, explanation image --> - <string name="submission_positive_other_illustration_description">"A device transmits an encrypted positive test diagnosis to the system."</string> + <string name="submission_positive_other_illustration_description">"A smartphone transmits an encrypted positive test diagnosis to the system."</string> <!-- XHED: Title for the interop country list--> <string name="submission_interoperability_list_title">"The following countries currently participate in transnational exposure logging:"</string> @@ -1100,7 +1092,7 @@ <!-- XBUT: symptom initial screen no button --> <string name="submission_symptom_negative_button">"No"</string> <!-- XBUT: symptom initial screen no information button --> - <string name="submission_symptom_no_information_button">"No answer"</string> + <string name="submission_symptom_no_information_button">"No statement"</string> <!-- XBUT: symptom initial screen continue button --> <string name="submission_symptom_further_button">"Next"</string> @@ -1125,9 +1117,9 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"Register the test by entering the TAN in the app."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"Languages: \nGerman, English, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string> + <string name="submission_contact_operating_hours_body">"Languages: \nEnglish, German, Turkish\n\nBusiness hours:\nMonday to Sunday: 24 hours\n\nThe call is free of charge."</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the hotline for the medical emergency service, telephone: 116 117."</string> + <string name="submission_contact_body_other">"If you have any health-related questions, please contact your general practitioner or the medical emergency service hotline, telephone: 116 117."</string> <!-- XACT: Submission contact page title --> @@ -1151,7 +1143,7 @@ <!-- XBUT: symptom calendar screen more than 2 weeks button --> <string name="submission_symptom_more_two_weeks">"More than 2 weeks ago"</string> <!-- XBUT: symptom calendar screen verify button --> - <string name="submission_symptom_verify">"No answer"</string> + <string name="submission_symptom_verify">"No statement"</string> <!-- Submission Status Card --> <!-- XHED: Page title for the various submission status: fetching --> @@ -1212,7 +1204,7 @@ <!-- YTXT: invalid status text --> <string name="test_result_card_status_invalid">"Evaluation is not possible"</string> <!-- YTXT: pending status text --> - <string name="test_result_card_status_pending">"Your result is not available yet"</string> + <string name="test_result_card_status_pending">"Your result is not yet available"</string> <!-- XHED: Title for further info of test result negative --> <string name="test_result_card_negative_further_info_title">"Other information:"</string> <!-- YTXT: Content for further info of test result negative --> @@ -1257,9 +1249,9 @@ <!-- XTXT: error dialog - either Google API Error (10) or reached request limit per day --> <string name="errors_google_api_error">"The Corona-Warn-App is running correctly, but we cannot update your current risk status. Exposure logging remains active and is working correctly. For further information, please see our FAQ page: https://www.coronawarn.app/en/faq/"</string> <!-- XTXT: error dialog - Error title when the provideDiagnosisKeys quota limit was reached. --> - <string name="errors_risk_detection_limit_reached_title" /> + <string name="errors_risk_detection_limit_reached_title">"Limit already reached"</string> <!-- XTXT: error dialog - Error description when the provideDiagnosisKeys quota limit was reached. --> - <string name="errors_risk_detection_limit_reached_description" /> + <string name="errors_risk_detection_limit_reached_description">"No more exposure checks possible today, as you have reached the maximum number of checks per day defined by your operating system. Please check your risik status again tomorrow."</string> <!-- #################################### Generic Error Messages @@ -1307,25 +1299,13 @@ <!-- NOTR --> <string name="test_api_button_check_exposure">"Check Exposure Summary"</string> <!-- NOTR --> - <string name="test_api_exposure_summary_headline">"Exposure summary"</string> - <!-- NOTR --> - <string name="test_api_body_daysSinceLastExposure">"Days since last exposure: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_attenuation">"Attenuation Durations in Minutes: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_summation_risk">"Summation Risk Score: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_matchedKeyCount">"Matched key count: %1$s"</string> - <!-- NOTR --> - <string name="test_api_body_maximumRiskScore">"Maximum risk score %1$s"</string> + <string name="test_api_exposure_summary_headline">"Exposure Windows"</string> <!-- NOTR --> <string name="test_api_body_my_keys">"My keys (count: %1$d)"</string> <!-- NOTR --> <string name="test_api_body_other_keys">"Other key"</string> <!-- NOTR --> <string name="test_api_calculate_risk_level">"Calculate Risk Level"</string> - <!-- NOTR --> - <string name="test_api_switch_background_notifications">"Background Notifications"</string> <!-- XHED: Country Entry for Austria --> <string name="country_name_at">"Austria"</string> @@ -1418,7 +1398,7 @@ <!-- YMSG: Onboarding tracing step second section in interoperability after the title --> <string name="interoperability_onboarding_second_section">"When a user submits their random IDs to the exchange server jointly operated by the participating countries, users of the official corona apps in all these countries can be warned of potential exposure."</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, and nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string> + <string name="interoperability_onboarding_randomid_download_free">"The daily download of the list with the random IDs is usually free of charge for you. Specifically, this means that mobile network operators do not charge you for the data used by the app in this context, nor do they apply roaming charges for this in other EU countries. Please contact your mobile network operator for more information."</string> <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. --> <string name="interoperability_onboarding_list_title">"The following countries currently participate:"</string> <!-- XTXT: Description of the expanded terms in delta interopoerability screen part 1 --> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigModuleTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigModuleTest.kt similarity index 92% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigModuleTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigModuleTest.kt index eae101cd8e6281f82ba2658056819e564473fbe0..fdae6dd05d6b673b95c5b49a3a5edf39d76c9124 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigModuleTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigModuleTest.kt @@ -1,7 +1,6 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig import android.content.Context -import de.rki.coronawarnapp.appconfig.AppConfigModule import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt index 8f5817cf7441619cc98c5d9296f7bafa0bbf8718..e5b901e0c00837569a2abf24179a8772019a3067 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigProviderTest.kt @@ -1,5 +1,7 @@ package de.rki.coronawarnapp.appconfig +import de.rki.coronawarnapp.appconfig.internal.AppConfigSource +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -25,7 +27,7 @@ import java.io.File class AppConfigProviderTest : BaseIOTest() { - @MockK lateinit var source: AppConfigSource + @MockK lateinit var appConfigSource: AppConfigSource @MockK lateinit var configData: ConfigData @MockK lateinit var timeStamper: TimeStamper @@ -39,15 +41,16 @@ class AppConfigProviderTest : BaseIOTest() { testDir.mkdirs() testDir.exists() shouldBe true - testConfigDownload = DefaultConfigData( + testConfigDownload = ConfigDataContainer( serverTime = Instant.parse("2020-11-03T05:35:16.000Z"), localOffset = Duration.ZERO, mappedConfig = configData, identifier = "identifier", - configType = ConfigData.Type.FROM_SERVER + configType = ConfigData.Type.FROM_SERVER, + cacheValidity = Duration.standardMinutes(5) ) - coEvery { source.clear() } just Runs - coEvery { source.retrieveConfig() } returns testConfigDownload + coEvery { appConfigSource.clear() } just Runs + coEvery { appConfigSource.getConfigData() } returns testConfigDownload every { timeStamper.nowUTC } returns Instant.parse("2020-11-03T05:35:16.000Z") } @@ -59,7 +62,7 @@ class AppConfigProviderTest : BaseIOTest() { } private fun createInstance(scope: CoroutineScope) = AppConfigProvider( - source = source, + appConfigSource = appConfigSource, dispatcherProvider = TestDispatcherProvider, scope = scope ) @@ -67,13 +70,14 @@ class AppConfigProviderTest : BaseIOTest() { @Test fun `appConfig is observable`() = runBlockingTest2(ignoreActive = true) { var counter = 0 - coEvery { source.retrieveConfig() } answers { - DefaultConfigData( + coEvery { appConfigSource.getConfigData() } answers { + ConfigDataContainer( serverTime = Instant.parse("2020-11-03T05:35:16.000Z"), localOffset = Duration.ZERO, mappedConfig = configData, identifier = "${++counter}", - configType = ConfigData.Type.FROM_SERVER + configType = ConfigData.Type.FROM_SERVER, + cacheValidity = Duration.standardMinutes(5) ) } @@ -90,19 +94,19 @@ class AppConfigProviderTest : BaseIOTest() { advanceUntilIdle() coVerifySequence { - source.retrieveConfig() - source.retrieveConfig() - source.retrieveConfig() - source.retrieveConfig() + appConfigSource.getConfigData() + appConfigSource.getConfigData() + appConfigSource.getConfigData() + appConfigSource.getConfigData() } } @Test - fun `appConfig uses WHILE_SUBSCRIBED mode`() = runBlockingTest2(ignoreActive = true) { + fun `appConfig uses LAZILY mode`() = runBlockingTest2(ignoreActive = true) { val instance = createInstance(this) val testCollector1 = instance.currentConfig.test(startOnScope = this) - coVerify(exactly = 1) { source.retrieveConfig() } + coVerify(exactly = 1) { appConfigSource.getConfigData() } // Was still active val testCollector2 = instance.currentConfig.test(startOnScope = this) @@ -114,7 +118,7 @@ class AppConfigProviderTest : BaseIOTest() { advanceUntilIdle() testCollector3.cancel() - coVerify(exactly = 1) { source.retrieveConfig() } + coVerify(exactly = 1) { appConfigSource.getConfigData() } testCollector1.cancel() // Last subscriber advanceUntilIdle() @@ -123,7 +127,7 @@ class AppConfigProviderTest : BaseIOTest() { advanceUntilIdle() testCollector4.cancel() - coVerify(exactly = 2) { source.retrieveConfig() } + coVerify(exactly = 1) { appConfigSource.getConfigData() } } @Test @@ -133,7 +137,7 @@ class AppConfigProviderTest : BaseIOTest() { instance.clear() coVerifySequence { - source.clear() + appConfigSource.clear() } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e06b40b3ed38a7e8d3a194f27ad2a003749bd293 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/ConfigChangeDetectorTest.kt @@ -0,0 +1,104 @@ +package de.rki.coronawarnapp.appconfig + +import de.rki.coronawarnapp.risk.RiskLevelData +import de.rki.coronawarnapp.task.TaskController +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify +import io.mockk.verifySequence +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestCoroutineScope +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class ConfigChangeDetectorTest : BaseTest() { + + @MockK lateinit var appConfigProvider: AppConfigProvider + @MockK lateinit var taskController: TaskController + @MockK lateinit var riskLevelData: RiskLevelData + + private val currentConfigFake = MutableStateFlow(mockConfigId("initial")) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkObject(ConfigChangeDetector.RiskLevelRepositoryDeferrer) + every { ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() } just Runs + + every { taskController.submit(any()) } just Runs + every { appConfigProvider.currentConfig } returns currentConfigFake + } + + private fun mockConfigId(id: String): ConfigData { + return mockk<ConfigData>().apply { + every { identifier } returns id + } + } + + private fun createInstance() = ConfigChangeDetector( + appConfigProvider = appConfigProvider, + taskController = taskController, + appScope = TestCoroutineScope(), + riskLevelData = riskLevelData + ) + + @Test + fun `new identifier without previous one is ignored`() { + + every { riskLevelData.lastUsedConfigIdentifier } returns null + + createInstance().launch() + + verify(exactly = 0) { + taskController.submit(any()) + ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + } + } + + @Test + fun `new identifier results in new risk level calculation`() { + every { riskLevelData.lastUsedConfigIdentifier } returns "I'm a new identifier" + + createInstance().launch() + + verifySequence { + ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + taskController.submit(any()) + } + } + + @Test + fun `same idetifier results in no op`() { + every { riskLevelData.lastUsedConfigIdentifier } returns "initial" + + createInstance().launch() + + verify(exactly = 0) { + taskController.submit(any()) + ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + } + } + + @Test + fun `new emissions keep triggering the check`() { + every { riskLevelData.lastUsedConfigIdentifier } returns "initial" + + createInstance().launch() + currentConfigFake.value = mockConfigId("Straw") + currentConfigFake.value = mockConfigId("berry") + + verifySequence { + ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + taskController.submit(any()) + ConfigChangeDetector.RiskLevelRepositoryDeferrer.resetRiskLevel() + taskController.submit(any()) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSourceTest.kt deleted file mode 100644 index 2ddfad5332b90522c39a2f0848db1b3eb6342463..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSourceTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -package de.rki.coronawarnapp.appconfig.download - -import android.content.Context -import android.content.res.AssetManager -import de.rki.coronawarnapp.util.HashExtensions.toSHA256 -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.clearAllMocks -import io.mockk.every -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.BaseIOTest -import java.io.File - -class DefaultAppConfigSourceTest : BaseIOTest() { - @MockK private lateinit var context: Context - @MockK private lateinit var assetManager: AssetManager - - private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) - private val configFile = File(testDir, "default_app_config.bin") - private val checksumFile = File(testDir, "default_app_config.sha256") - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - every { context.assets } returns assetManager - - every { assetManager.open("default_app_config.bin") } answers { configFile.inputStream() } - every { assetManager.open("default_app_config.sha256") } answers { checksumFile.inputStream() } - - testDir.mkdirs() - testDir.exists() shouldBe true - } - - @AfterEach - fun teardown() { - clearAllMocks() - testDir.deleteRecursively() - } - - private fun createInstance() = DefaultAppConfigSource(context = context) - - @Test - fun `config loaded from asset`() { - val testData = "The Cake Is A Lie" - configFile.writeText(testData) - checksumFile.writeText(testData.toSHA256()) - - val instance = createInstance() - instance.getRawDefaultConfig() shouldBe testData.toByteArray() - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSourceTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8a87d88edf92f9c547a5a5f52b192e5c5d0752d2 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/internal/AppConfigSourceTest.kt @@ -0,0 +1,137 @@ +package de.rki.coronawarnapp.appconfig.internal + +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.sources.fallback.DefaultAppConfigSource +import de.rki.coronawarnapp.appconfig.sources.local.LocalAppConfigSource +import de.rki.coronawarnapp.appconfig.sources.remote.RemoteAppConfigSource +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class AppConfigSourceTest : BaseTest() { + + @MockK lateinit var remoteSource: RemoteAppConfigSource + @MockK lateinit var localSource: LocalAppConfigSource + @MockK lateinit var defaultSource: DefaultAppConfigSource + @MockK lateinit var timeStamper: TimeStamper + + private val remoteConfig = ConfigDataContainer( + serverTime = Instant.EPOCH, + localOffset = Duration.standardHours(1), + mappedConfig = mockk(), + configType = ConfigData.Type.FROM_SERVER, + identifier = "remoteetag", + cacheValidity = Duration.standardSeconds(42) + ) + + private val localConfig = ConfigDataContainer( + serverTime = Instant.EPOCH, + localOffset = Duration.standardHours(1), + mappedConfig = mockk(), + configType = ConfigData.Type.LAST_RETRIEVED, + identifier = "localetag", + cacheValidity = Duration.standardSeconds(300) + ) + + private val defaultConfig = ConfigDataContainer( + serverTime = Instant.EPOCH, + localOffset = Duration.standardHours(1), + mappedConfig = mockk(), + configType = ConfigData.Type.LOCAL_DEFAULT, + identifier = "fallback.local", + cacheValidity = Duration.ZERO + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + coEvery { remoteSource.getConfigData() } returns remoteConfig + coEvery { localSource.getConfigData() } returns localConfig + coEvery { defaultSource.getConfigData() } returns defaultConfig + + every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1)) + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun createInstance() = AppConfigSource( + remoteAppConfigSource = remoteSource, + localAppConfigSource = localSource, + defaultAppConfigSource = defaultSource, + timeStamper = timeStamper + ) + + @Test + fun `local config is used if available and valid`() = runBlockingTest { + val instance = createInstance() + instance.getConfigData() shouldBe localConfig + + coVerifySequence { + localSource.getConfigData() + timeStamper.nowUTC + } + } + + @Test + fun `remote config is used if local config is not valid`() = runBlockingTest { + every { timeStamper.nowUTC } returns Instant.EPOCH + .plus(Duration.standardHours(1)) + .plus(Duration.standardSeconds(301)) // Local config has 300 seconds validity + + val instance = createInstance() + instance.getConfigData() shouldBe remoteConfig + + coVerifySequence { + localSource.getConfigData() + timeStamper.nowUTC + remoteSource.getConfigData() + } + } + + @Test + fun `local config is used despite being invalid if remote config is unavailable`() = runBlockingTest { + every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(2)) + coEvery { remoteSource.getConfigData() } returns null + + val instance = createInstance() + instance.getConfigData() shouldBe localConfig + + coVerifySequence { + localSource.getConfigData() + timeStamper.nowUTC + remoteSource.getConfigData() + } + } + + @Test + fun `default config is used if remote and local are unavailable`() = runBlockingTest { + coEvery { remoteSource.getConfigData() } returns null + coEvery { localSource.getConfigData() } returns null + + val instance = createInstance() + instance.getConfigData() shouldBe defaultConfig + + coVerifySequence { + localSource.getConfigData() + remoteSource.getConfigData() + defaultSource.getConfigData() + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt index 2ef853f468f8e5b58bf7db38802a617b5c0d76b5..edebc20fd1c278450405d3ca202bd8ebd5721ab9 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/CWAConfigMapperTest.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.appconfig.mapping -import de.rki.coronawarnapp.server.protocols.internal.AppConfig +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Test import testhelpers.BaseTest @@ -11,11 +11,12 @@ class CWAConfigMapperTest : BaseTest() { @Test fun `simple creation`() { - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() .addAllSupportedCountries(listOf("DE", "NL")) .build() createInstance().map(rawConfig).apply { - this.appVersion shouldBe rawConfig.appVersion + this.latestVersionCode shouldBe rawConfig.latestVersionCode + this.minVersionCode shouldBe rawConfig.minVersionCode this.supportedCountries shouldBe listOf("DE", "NL") } } @@ -23,11 +24,12 @@ class CWAConfigMapperTest : BaseTest() { @Test fun `invalid supported countries are filtered out`() { // Could happen due to protobuf scheme missmatch - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() .addAllSupportedCountries(listOf("plausible deniability")) .build() createInstance().map(rawConfig).apply { - this.appVersion shouldBe rawConfig.appVersion + this.latestVersionCode shouldBe rawConfig.latestVersionCode + this.minVersionCode shouldBe rawConfig.minVersionCode this.supportedCountries shouldBe emptyList() } } @@ -35,10 +37,11 @@ class CWAConfigMapperTest : BaseTest() { @Test fun `if supportedCountryList is empty, we do not insert DE as fallback`() { // Because the UI requires this to detect when to show alternative UI elements - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() .build() createInstance().map(rawConfig).apply { - this.appVersion shouldBe rawConfig.appVersion + this.latestVersionCode shouldBe rawConfig.latestVersionCode + this.minVersionCode shouldBe rawConfig.minVersionCode this.supportedCountries shouldBe emptyList() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt index 18ee07f72f83cfdbfccae776f6df5edc21a23083..22f65553bf124132ecd7df87bc67b821afbbbc2e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ConfigParserTest.kt @@ -2,8 +2,8 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.appconfig.CWAConfig import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig +import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig import de.rki.coronawarnapp.appconfig.KeyDownloadConfig -import de.rki.coronawarnapp.appconfig.RiskCalculationConfig import io.mockk.MockKAnnotations import io.mockk.clearAllMocks import io.mockk.every @@ -20,7 +20,7 @@ class ConfigParserTest : BaseTest() { @MockK lateinit var cwaConfigMapper: CWAConfig.Mapper @MockK lateinit var keyDownloadConfigMapper: KeyDownloadConfig.Mapper @MockK lateinit var exposureDetectionConfigMapper: ExposureDetectionConfig.Mapper - @MockK lateinit var riskCalculationConfigMapper: RiskCalculationConfig.Mapper + @MockK lateinit var exposureWindowRiskCalculationConfigMapper: ExposureWindowRiskCalculationConfig.Mapper @BeforeEach fun setup() { @@ -29,7 +29,7 @@ class ConfigParserTest : BaseTest() { every { cwaConfigMapper.map(any()) } returns mockk() every { keyDownloadConfigMapper.map(any()) } returns mockk() every { exposureDetectionConfigMapper.map(any()) } returns mockk() - every { riskCalculationConfigMapper.map(any()) } returns mockk() + every { exposureWindowRiskCalculationConfigMapper.map(any()) } returns mockk() } @AfterEach @@ -41,7 +41,7 @@ class ConfigParserTest : BaseTest() { cwaConfigMapper = cwaConfigMapper, keyDownloadConfigMapper = keyDownloadConfigMapper, exposureDetectionConfigMapper = exposureDetectionConfigMapper, - riskCalculationConfigMapper = riskCalculationConfigMapper + exposureWindowRiskCalculationConfigMapper = exposureWindowRiskCalculationConfigMapper ) @Test @@ -52,19 +52,28 @@ class ConfigParserTest : BaseTest() { cwaConfigMapper.map(any()) keyDownloadConfigMapper.map(any()) exposureDetectionConfigMapper.map(any()) - riskCalculationConfigMapper.map(any()) + exposureWindowRiskCalculationConfigMapper.map(any()) } } } companion object { private val APPCONFIG_RAW = ( - "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + - "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" + - "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" + - "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" + - "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" + - "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804" + "081f101f1a0e0a0c0a0872657365727665641001220244452a061" + + "8c20320e003320508061084073ad4010a0d0a0b1900000000004" + + "0524020010a0d120b190000000000002440200112140a1209000" + + "000000000f03f1900000000000000401a160a0b1900000000008" + + "04b40200111000000000000f03f1a1f0a14090000000000804b4" + + "0190000000000804f40200111000000000000e03f220f0a0b190" + + "000000000002e402001100122160a12090000000000002e40190" + + "00000008087c34010022a0f0a0b190000000000002e402001100" + + "12a160a12090000000000002e4019000000008087c3401002320" + + "a10041804200328023001399a9999999999c93f420c0a0408011" + + "0010a04080210024a750a031e32461220000000000000f03f000" + + "000000000f03f000000000000f03f000000000000f03f220b080" + + "111000000000000f03f220b080211000000000000f03f320b080" + + "111000000000000f03f320b080211000000000000f03f320b080" + + "311000000000000f03f320b080411000000000000f03f" ).decodeHex() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt index 2d0ab66e16af911165826ea1d14c99e651483b62..a048e1468ab76c1ed7717da48d8f423b4cca0a35 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/DownloadConfigMapperTest.kt @@ -1,9 +1,10 @@ package de.rki.coronawarnapp.appconfig.mapping import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import de.rki.coronawarnapp.server.protocols.internal.KeyDownloadParameters +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.KeyDownloadParameters import io.kotest.matchers.shouldBe +import org.joda.time.Duration import org.joda.time.LocalDate import org.joda.time.LocalTime import org.junit.jupiter.api.Test @@ -22,8 +23,8 @@ class DownloadConfigMapperTest : BaseTest() { }.let { addRevokedDayPackages(it) } } - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setAndroidKeyDownloadParameters(builder) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setKeyDownloadParameters(builder) .build() createInstance().map(rawConfig).apply { @@ -46,8 +47,8 @@ class DownloadConfigMapperTest : BaseTest() { }.let { addRevokedHourPackages(it) } } - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setAndroidKeyDownloadParameters(builder) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setKeyDownloadParameters(builder) .build() createInstance().map(rawConfig).apply { @@ -59,4 +60,17 @@ class DownloadConfigMapperTest : BaseTest() { } } } + + @Test + fun `if the protobuf data structures are null we return defaults`() { + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .build() + + createInstance().map(rawConfig).apply { + revokedDayPackages shouldBe emptyList() + revokedHourPackages shouldBe emptyList() + overallDownloadTimeout shouldBe Duration.standardMinutes(8) + individualDownloadTimeout shouldBe Duration.standardSeconds(60) + } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt index 7812c23aba103ae8b16d0b5cbb9d799764fb3776..f39e4f9dee0a89f0d4d073cca27b76c99cabfe1b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/ExposureDetectionConfigMapperTest.kt @@ -1,7 +1,7 @@ package de.rki.coronawarnapp.appconfig.mapping -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import de.rki.coronawarnapp.server.protocols.internal.ExposureDetectionParameters.ExposureDetectionParametersAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.AppConfigAndroid +import de.rki.coronawarnapp.server.protocols.internal.v2.ExposureDetectionParameters.ExposureDetectionParametersAndroid import io.kotest.matchers.shouldBe import org.joda.time.Duration import org.junit.jupiter.api.Test @@ -13,21 +13,18 @@ class ExposureDetectionConfigMapperTest : BaseTest() { @Test fun `simple creation`() { - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setMinRiskScore(1) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() .build() createInstance().map(rawConfig).apply { - exposureDetectionConfiguration shouldBe rawConfig.mapRiskScoreToExposureConfiguration() - exposureDetectionParameters shouldBe rawConfig.androidExposureDetectionParameters + exposureDetectionParameters shouldBe null } } @Test fun `detection interval 0 defaults to almost infinite delay`() { val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder() - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setMinRiskScore(1) - .setAndroidExposureDetectionParameters(exposureDetectionParameters) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setExposureDetectionParameters(exposureDetectionParameters) .build() createInstance().map(rawConfig).apply { minTimeBetweenDetections shouldBe Duration.standardDays(99) @@ -40,9 +37,8 @@ class ExposureDetectionConfigMapperTest : BaseTest() { val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder().apply { maxExposureDetectionsPerInterval = 3 } - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setMinRiskScore(1) - .setAndroidExposureDetectionParameters(exposureDetectionParameters) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setExposureDetectionParameters(exposureDetectionParameters) .build() createInstance().map(rawConfig).apply { minTimeBetweenDetections shouldBe Duration.standardHours(24 / 3) @@ -55,9 +51,8 @@ class ExposureDetectionConfigMapperTest : BaseTest() { val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder().apply { overallTimeoutInSeconds = 10 * 60 } - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setMinRiskScore(1) - .setAndroidExposureDetectionParameters(exposureDetectionParameters) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setExposureDetectionParameters(exposureDetectionParameters) .build() createInstance().map(rawConfig).apply { overallDetectionTimeout shouldBe Duration.standardMinutes(10) @@ -69,12 +64,22 @@ class ExposureDetectionConfigMapperTest : BaseTest() { val exposureDetectionParameters = ExposureDetectionParametersAndroid.newBuilder().apply { overallTimeoutInSeconds = 0 } - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .setMinRiskScore(1) - .setAndroidExposureDetectionParameters(exposureDetectionParameters) + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .setExposureDetectionParameters(exposureDetectionParameters) .build() createInstance().map(rawConfig).apply { overallDetectionTimeout shouldBe Duration.standardMinutes(15) } } + + @Test + fun `if protobuf is missing the datastructure we return defaults`() { + val rawConfig = AppConfigAndroid.ApplicationConfigurationAndroid.newBuilder() + .build() + createInstance().map(rawConfig).apply { + overallDetectionTimeout shouldBe Duration.standardMinutes(15) + minTimeBetweenDetections shouldBe Duration.standardHours(24 / 6) + maxExposureDetectionsPerUTCDay shouldBe 6 + } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapperTest.kt deleted file mode 100644 index e0cf0e3c09a213d3f214cb292efca309a4824e67..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/mapping/RiskCalculationConfigMapperTest.kt +++ /dev/null @@ -1,22 +0,0 @@ -package de.rki.coronawarnapp.appconfig.mapping - -import de.rki.coronawarnapp.server.protocols.internal.AppConfig -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Test -import testhelpers.BaseTest - -class RiskCalculationConfigMapperTest : BaseTest() { - - private fun createInstance() = RiskCalculationConfigMapper() - - @Test - fun `simple creation`() { - val rawConfig = AppConfig.ApplicationConfiguration.newBuilder() - .build() - createInstance().map(rawConfig).apply { - this.attenuationDuration shouldBe rawConfig.attenuationDuration - this.minRiskScore shouldBe rawConfig.minRiskScore - this.riskScoreClasses shouldBe rawConfig.riskScoreClasses - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSanityCheck.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt similarity index 97% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSanityCheck.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt index 9b52302936bc78f31632bf44598fd3f701b9bb82..c9459763b063371cb4d0630ac7b7ff0b483dd090 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/DefaultAppConfigSanityCheck.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSanityCheck.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.fallback import android.content.Context import android.os.Build diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..9aaeca2fdb1435ce41d157ef3b1e97b4e399f86e --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/fallback/DefaultAppConfigSourceTest.kt @@ -0,0 +1,103 @@ +package de.rki.coronawarnapp.appconfig.sources.fallback + +import android.content.Context +import android.content.res.AssetManager +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.appconfig.mapping.ConfigParser +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.decodeHex +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import java.io.File + +class DefaultAppConfigSourceTest : BaseIOTest() { + @MockK private lateinit var context: Context + @MockK private lateinit var assetManager: AssetManager + @MockK lateinit var configParser: ConfigParser + @MockK lateinit var configData: ConfigData + + private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName) + private val configFile = File(testDir, "default_app_config_android.bin") + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + every { context.assets } returns assetManager + + every { assetManager.open("default_app_config_android.bin") } answers { configFile.inputStream() } + + coEvery { configParser.parse(any()) } returns configData + + testDir.mkdirs() + testDir.exists() shouldBe true + } + + @AfterEach + fun teardown() { + clearAllMocks() + testDir.deleteRecursively() + } + + private fun createInstance() = DefaultAppConfigSource( + context = context, + configParser = configParser + ) + + @Test + fun `config loaded from asset`() { + val testData = "The Cake Is A Lie" + configFile.writeText(testData) + + val instance = createInstance() + instance.getRawDefaultConfig() shouldBe testData.toByteArray() + } + + @Test + fun `loading internal config data from assets`() = runBlockingTest { + configFile.writeBytes(APPCONFIG_RAW) + + val instance = createInstance() + + instance.getConfigData() shouldBe ConfigDataContainer( + serverTime = Instant.EPOCH, + localOffset = Duration.ZERO, + mappedConfig = configData, + configType = ConfigData.Type.LOCAL_DEFAULT, + identifier = "fallback.local", + cacheValidity = Duration.ZERO + ) + } + + @Test + fun `exceptions when getting the default config are rethrown`() = runBlockingTest { + val instance = createInstance() + + shouldThrowAny { + instance.getConfigData() + } + } + + companion object { + private val APPCONFIG_RAW = ( + "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + + "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" + + "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" + + "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" + + "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" + + "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804" + ).decodeHex().toByteArray() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt similarity index 67% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorageTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt index 20ddd9ef77f9e81fc8c08cec1a47d0acc9f23bc2..13b19949d1f28e9aee3ea878d73df6cb34e387dd 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/AppConfigStorageTest.kt @@ -1,6 +1,7 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.local import android.content.Context +import de.rki.coronawarnapp.appconfig.internal.InternalConfigData import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.serialization.SerializationModule import io.kotest.matchers.shouldBe @@ -8,6 +9,7 @@ import io.mockk.MockKAnnotations import io.mockk.clearAllMocks import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import kotlinx.coroutines.test.runBlockingTest import okio.ByteString.Companion.decodeHex import okio.ByteString.Companion.toByteString @@ -31,11 +33,12 @@ class AppConfigStorageTest : BaseIOTest() { private val legacyConfigPath = File(storageDir, "appconfig") private val configPath = File(storageDir, "appconfig.json") - private val testConfigDownload = ConfigDownload( + private val testConfigDownload = InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.parse("2020-11-03T05:35:16.000Z"), localOffset = Duration.standardHours(1), - etag = "I am an ETag :)!" + etag = "I am an ETag :)!", + cacheValidity = Duration.standardSeconds(123) ) @BeforeEach @@ -72,13 +75,32 @@ class AppConfigStorageTest : BaseIOTest() { "rawData": "$APPCONFIG_BASE64", "etag": "I am an ETag :)!", "serverTime": 1604381716000, - "localOffset": 3600000 + "localOffset": 3600000, + "cacheValidity": 123000 } """.toComparableJson() storage.getStoredConfig() shouldBe testConfigDownload } + @Test + fun `restoring from storage`() = runBlockingTest { + configPath.parentFile!!.mkdirs() + configPath.writeText( + """ + { + "rawData": "$APPCONFIG_BASE64", + "etag": "I am an ETag :)!", + "serverTime": 1604381716000, + "localOffset": 3600000, + "cacheValidity": 123000 + } + """.trimIndent() + ) + val storage = createStorage() + storage.getStoredConfig() shouldBe testConfigDownload + } + @Test fun `nulling and overwriting`() = runBlockingTest { val storage = createStorage() @@ -98,7 +120,8 @@ class AppConfigStorageTest : BaseIOTest() { "rawData": "$APPCONFIG_BASE64", "etag": "I am an ETag :)!", "serverTime": 1604381716000, - "localOffset": 3600000 + "localOffset": 3600000, + "cacheValidity": 123000 } """.toComparableJson() @@ -117,11 +140,12 @@ class AppConfigStorageTest : BaseIOTest() { val storage = createStorage() - storage.getStoredConfig() shouldBe ConfigDownload( + storage.getStoredConfig() shouldBe InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.ofEpochMilli(1234), localOffset = Duration.ZERO, - etag = "I am an ETag :)!" + etag = "I am an ETag :)!", + cacheValidity = Duration.standardMinutes(5) ) } @@ -138,6 +162,58 @@ class AppConfigStorageTest : BaseIOTest() { configPath.exists() shouldBe true } + @Test + fun `return null on errors`() = runBlockingTest { + every { timeStamper.nowUTC } throws Exception() + + val storage = createStorage() + storage.getStoredConfig() shouldBe null + } + + @Test + fun `return null on invalid json and delete config file`() = runBlockingTest { + configPath.parentFile!!.mkdirs() + configPath.writeText( + """ + { + + } + """.trimIndent() + ) + val storage = createStorage() + storage.getStoredConfig() shouldBe null + + configPath.exists() shouldBe false + } + + @Test + fun `return null on empty file and delete config file`() { + configPath.parentFile!!.mkdirs() + configPath.createNewFile() + + val storage = createStorage() + + runBlockingTest { + storage.getStoredConfig() shouldBe null + } + + configPath.exists() shouldBe false + } + + @Test + fun `catch errors when trying to save the config`() { + configPath.parentFile!!.mkdirs() + configPath.createNewFile() + + val storage = createStorage() + + runBlockingTest { + storage.setStoredConfig(mockk()) + } + + configPath.exists() shouldBe true + } + companion object { private val APPCONFIG_RAW = ( "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSourceTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..155d2f76c5a1293205cf55008a22d9a5308e5869 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/local/LocalAppConfigSourceTest.kt @@ -0,0 +1,139 @@ +package de.rki.coronawarnapp.appconfig.sources.local + +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.appconfig.internal.InternalConfigData +import de.rki.coronawarnapp.appconfig.mapping.ConfigParser +import de.rki.coronawarnapp.util.TimeStamper +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.test.runBlockingTest +import okio.ByteString.Companion.decodeHex +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseIOTest +import testhelpers.TestDispatcherProvider +import testhelpers.coroutines.runBlockingTest2 +import java.io.File + +class LocalAppConfigSourceTest : BaseIOTest() { + + @MockK lateinit var configStorage: AppConfigStorage + @MockK lateinit var configParser: ConfigParser + @MockK lateinit var configData: ConfigData + @MockK lateinit var timeStamper: TimeStamper + + private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) + + private var expectedData = InternalConfigData( + rawData = APPCONFIG_RAW, + serverTime = Instant.parse("2020-11-03T05:35:16.000Z"), + localOffset = Duration.standardHours(1), + etag = "etag", + cacheValidity = Duration.standardMinutes(5) + ) + + private var mockConfigStorage: InternalConfigData? = null + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + testDir.mkdirs() + testDir.exists() shouldBe true + + coEvery { configStorage.getStoredConfig() } answers { mockConfigStorage } + coEvery { configStorage.setStoredConfig(any()) } answers { + mockConfigStorage = arg(0) + } + + every { configParser.parse(APPCONFIG_RAW) } returns configData + + every { timeStamper.nowUTC } returns Instant.parse("2020-11-03T05:35:16.000Z") + } + + @AfterEach + fun teardown() { + clearAllMocks() + testDir.deleteRecursively() + } + + private fun createInstance() = LocalAppConfigSource( + storage = configStorage, + parser = configParser, + dispatcherProvider = TestDispatcherProvider + ) + + @Test + fun `local app config source returns null if storage is empty`() = runBlockingTest { + coEvery { configStorage.getStoredConfig() } returns null + + val instance = createInstance() + + instance.getConfigData() shouldBe null + + coVerifyOrder { configStorage.getStoredConfig() } + } + + @Test + fun `local default config is loaded from storage`() = runBlockingTest { + coEvery { configStorage.getStoredConfig() } returns expectedData + + val instance = createInstance() + + instance.getConfigData() shouldBe ConfigDataContainer( + serverTime = expectedData.serverTime, + localOffset = expectedData.localOffset, + mappedConfig = configData, + configType = ConfigData.Type.LAST_RETRIEVED, + identifier = expectedData.etag, + cacheValidity = Duration.standardMinutes(5) + ) + + coVerifyOrder { configStorage.getStoredConfig() } + } + + @Test + fun `local app config source returns null if there is any exception`() = runBlockingTest { + coEvery { configStorage.getStoredConfig() } returns expectedData.copy( + rawData = "I'm not valid protobuf".toByteArray() + ) + + val instance = createInstance() + + instance.getConfigData() shouldBe null + + coVerifyOrder { configStorage.getStoredConfig() } + } + + @Test + fun `clear clears caches`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance() + + instance.clear() + + advanceUntilIdle() + + coVerifyOrder { + configStorage.setStoredConfig(null) + } + } + + companion object { + private val APPCONFIG_RAW = ( + "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + + "2e636f726f6e617761726e2e6170700a260a0448494748100f1848221a68747470733a2f2f7777772e636f7" + + "26f6e617761726e2e6170701a640a10080110021803200428053006380740081100000000000049401a0a20" + + "0128013001380140012100000000000049402a1008051005180520052805300538054005310000000000003" + + "4403a0e1001180120012801300138014001410000000000004940221c0a040837103f121209000000000000" + + "f03f11000000000000e03f20192a1a0a0a0a041008180212021005120c0a0408011804120408011804" + ).decodeHex().toByteArray() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt similarity index 86% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt index da1ab2f7d7e9dd50d4286a5f55827b76f55e2ad9..f90e0f1b8df65c7bb1de01715375996a488df942 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigApiTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigApiTest.kt @@ -1,7 +1,8 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.remote import android.content.Context import de.rki.coronawarnapp.appconfig.AppConfigModule +import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2 import de.rki.coronawarnapp.environment.download.DownloadCDNModule import de.rki.coronawarnapp.http.HttpModule import io.kotest.matchers.shouldBe @@ -49,7 +50,7 @@ class AppConfigApiTest : BaseIOTest() { testDir.deleteRecursively() } - private fun createAPI(): AppConfigApiV1 { + private fun createAPI(): AppConfigApiV2 { val httpModule = HttpModule() val defaultHttpClient = httpModule.defaultHttpClient() val gsonConverterFactory = httpModule.provideGSONConverter() @@ -76,14 +77,14 @@ class AppConfigApiTest : BaseIOTest() { webServer.enqueue(MockResponse().setBody("~appconfig")) runBlocking { - api.getApplicationConfiguration("DE").apply { + api.getApplicationConfiguration().apply { body()!!.string() shouldBe "~appconfig" } } val request = webServer.takeRequest(5, TimeUnit.SECONDS)!! request.method shouldBe "GET" - request.path shouldBe "/version/v1/configuration/country/DE/app_config" + request.path shouldBe "/version/v1/app_config_android" } @Test @@ -97,7 +98,7 @@ class AppConfigApiTest : BaseIOTest() { webServer.enqueue(configResponse) runBlocking { - api.getApplicationConfiguration("DE").apply { + api.getApplicationConfiguration().apply { body()!!.string() shouldBe "~appconfig" } } @@ -106,12 +107,12 @@ class AppConfigApiTest : BaseIOTest() { webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { method shouldBe "GET" - path shouldBe "/version/v1/configuration/country/DE/app_config" + path shouldBe "/version/v1/app_config_android" } webServer.enqueue(configResponse) runBlocking { - api.getApplicationConfiguration("DE").apply { + api.getApplicationConfiguration().apply { body()!!.string() shouldBe "~appconfig" } } @@ -124,7 +125,7 @@ class AppConfigApiTest : BaseIOTest() { webServer.enqueue(configResponse) runBlocking { - api.getApplicationConfiguration("DE").apply { + api.getApplicationConfiguration().apply { body()!!.string() shouldBe "~appconfig" } } @@ -133,7 +134,7 @@ class AppConfigApiTest : BaseIOTest() { webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { method shouldBe "GET" - path shouldBe "/version/v1/configuration/country/DE/app_config" + path shouldBe "/version/v1/app_config_android" } } @@ -148,7 +149,7 @@ class AppConfigApiTest : BaseIOTest() { webServer.enqueue(configResponse) runBlocking { - api.getApplicationConfiguration("DE").apply { + api.getApplicationConfiguration().apply { body()!!.string() shouldBe "~appconfig" } } @@ -157,13 +158,13 @@ class AppConfigApiTest : BaseIOTest() { webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply { method shouldBe "GET" - path shouldBe "/version/v1/configuration/country/DE/app_config" + path shouldBe "/version/v1/app_config_android" } webServer.enqueue(MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY)) runBlocking { - api.getApplicationConfiguration("DE").apply { + api.getApplicationConfiguration().apply { body()!!.string() shouldBe "~appconfig" } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt similarity index 82% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigServerTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt index db28c6d6d7c8456eac64fc06250071511e1df0d8..9ae6713c24fd62c7f1b6a1636b90d1aaf89b4b24 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/download/AppConfigServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt @@ -1,5 +1,9 @@ -package de.rki.coronawarnapp.appconfig.download +package de.rki.coronawarnapp.appconfig.sources.remote +import de.rki.coronawarnapp.appconfig.download.AppConfigApiV2 +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationCorruptException +import de.rki.coronawarnapp.appconfig.internal.ApplicationConfigurationInvalidException +import de.rki.coronawarnapp.appconfig.internal.InternalConfigData import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.security.VerificationKeys @@ -28,7 +32,7 @@ import java.io.File class AppConfigServerTest : BaseIOTest() { - @MockK lateinit var api: AppConfigApiV1 + @MockK lateinit var api: AppConfigApiV2 @MockK lateinit var verificationKeys: VerificationKeys @MockK lateinit var timeStamper: TimeStamper private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) @@ -54,32 +58,33 @@ class AppConfigServerTest : BaseIOTest() { private fun createInstance(homeCountry: LocationCode = defaultHomeCountry) = AppConfigServer( api = { api }, verificationKeys = verificationKeys, - homeCountry = homeCountry, cache = mockk(), timeStamper = timeStamper ) @Test fun `application config download`() = runBlockingTest { - coEvery { api.getApplicationConfiguration("DE") } returns Response.success( + coEvery { api.getApplicationConfiguration() } returns Response.success( APPCONFIG_BUNDLE.toResponseBody(), Headers.headersOf( "Date", "Tue, 03 Nov 2020 08:46:03 GMT", - "ETag", "I am an ETag :)!" + "ETag", "I am an ETag :)!", + "Cache-Control", "public,max-age=123" ) ) val downloadServer = createInstance() val configDownload = downloadServer.downloadAppConfig() - configDownload shouldBe ConfigDownload( + configDownload shouldBe InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.parse("2020-11-03T08:46:03.000Z"), localOffset = Duration( Instant.parse("2020-11-03T08:46:03.000Z"), Instant.ofEpochMilli(123456789) ), - etag = "I am an ETag :)!" + etag = "I am an ETag :)!", + cacheValidity = Duration.standardSeconds(123) ) verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) } @@ -87,7 +92,7 @@ class AppConfigServerTest : BaseIOTest() { @Test fun `application config data is faulty`() = runBlockingTest { - coEvery { api.getApplicationConfiguration("DE") } returns Response.success( + coEvery { api.getApplicationConfiguration() } returns Response.success( "123ABC".decodeHex().toResponseBody() ) @@ -100,7 +105,7 @@ class AppConfigServerTest : BaseIOTest() { @Test fun `application config verification fails`() = runBlockingTest { - coEvery { api.getApplicationConfiguration("DE") } returns Response.success( + coEvery { api.getApplicationConfiguration() } returns Response.success( APPCONFIG_BUNDLE.toResponseBody() ) every { verificationKeys.hasInvalidSignature(any(), any()) } returns true @@ -114,7 +119,7 @@ class AppConfigServerTest : BaseIOTest() { @Test fun `missing server date leads to local time fallback`() = runBlockingTest { - coEvery { api.getApplicationConfiguration("DE") } returns Response.success( + coEvery { api.getApplicationConfiguration() } returns Response.success( APPCONFIG_BUNDLE.toResponseBody(), Headers.headersOf( "ETag", "I am an ETag :)!" @@ -124,17 +129,18 @@ class AppConfigServerTest : BaseIOTest() { val downloadServer = createInstance() val configDownload = downloadServer.downloadAppConfig() - configDownload shouldBe ConfigDownload( + configDownload shouldBe InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.ofEpochMilli(123456789), localOffset = Duration.ZERO, - etag = "I am an ETag :)!" + etag = "I am an ETag :)!", + cacheValidity = Duration.standardSeconds(300) ) } @Test fun `missing server etag leads to exception`() = runBlockingTest { - coEvery { api.getApplicationConfiguration("DE") } returns Response.success( + coEvery { api.getApplicationConfiguration() } returns Response.success( APPCONFIG_BUNDLE.toResponseBody() ) @@ -147,7 +153,7 @@ class AppConfigServerTest : BaseIOTest() { @Test fun `local offset is the difference between server time and local time`() = runBlockingTest { - coEvery { api.getApplicationConfiguration("DE") } returns Response.success( + coEvery { api.getApplicationConfiguration() } returns Response.success( APPCONFIG_BUNDLE.toResponseBody(), Headers.headersOf( "Date", "Tue, 03 Nov 2020 06:35:16 GMT", @@ -158,11 +164,12 @@ class AppConfigServerTest : BaseIOTest() { val downloadServer = createInstance() - downloadServer.downloadAppConfig() shouldBe ConfigDownload( + downloadServer.downloadAppConfig() shouldBe InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.parse("2020-11-03T06:35:16.000Z"), localOffset = Duration.standardHours(-1), - etag = "I am an ETag :)!" + etag = "I am an ETag :)!", + cacheValidity = Duration.standardSeconds(300) ) } @@ -183,16 +190,17 @@ class AppConfigServerTest : BaseIOTest() { every { mockCacheResponse.sentRequestAtMillis } returns Instant.parse("2020-11-03T04:35:16.000Z").millis every { response.raw().cacheResponse } returns mockCacheResponse - coEvery { api.getApplicationConfiguration("DE") } returns response + coEvery { api.getApplicationConfiguration() } returns response every { timeStamper.nowUTC } returns Instant.parse("2020-11-03T05:35:16.000Z") val downloadServer = createInstance() - downloadServer.downloadAppConfig() shouldBe ConfigDownload( + downloadServer.downloadAppConfig() shouldBe InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.parse("2020-11-03T06:35:16.000Z"), localOffset = Duration.standardHours(-2), - etag = "I am an ETag :)!" + etag = "I am an ETag :)!", + cacheValidity = Duration.standardSeconds(300) ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigSourceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSourceTest.kt similarity index 58% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigSourceTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSourceTest.kt index 2a3bf75048881cb9f6acb22bcb84e7ee8511abcc..162ae3f5c6d6822ba94496f02c3cd3713eadc4c0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/AppConfigSourceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/RemoteAppConfigSourceTest.kt @@ -1,10 +1,10 @@ -package de.rki.coronawarnapp.appconfig +package de.rki.coronawarnapp.appconfig.sources.remote -import de.rki.coronawarnapp.appconfig.download.AppConfigServer -import de.rki.coronawarnapp.appconfig.download.AppConfigStorage -import de.rki.coronawarnapp.appconfig.download.ConfigDownload -import de.rki.coronawarnapp.appconfig.download.DefaultAppConfigSource +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.appconfig.internal.InternalConfigData import de.rki.coronawarnapp.appconfig.mapping.ConfigParser +import de.rki.coronawarnapp.appconfig.sources.local.AppConfigStorage import de.rki.coronawarnapp.util.TimeStamper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -16,8 +16,6 @@ import io.mockk.coVerifyOrder import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just -import io.mockk.verify -import kotlinx.coroutines.test.runBlockingTest import okio.ByteString.Companion.decodeHex import org.joda.time.Duration import org.joda.time.Instant @@ -30,25 +28,25 @@ import testhelpers.coroutines.runBlockingTest2 import java.io.File import java.io.IOException -class AppConfigSourceTest : BaseIOTest() { +class RemoteAppConfigSourceTest : BaseIOTest() { @MockK lateinit var configServer: AppConfigServer @MockK lateinit var configStorage: AppConfigStorage @MockK lateinit var configParser: ConfigParser @MockK lateinit var configData: ConfigData @MockK lateinit var timeStamper: TimeStamper - @MockK lateinit var appConfigDefaultFallback: DefaultAppConfigSource private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) - private var testConfigDownload = ConfigDownload( + private var dataFromServer = InternalConfigData( rawData = APPCONFIG_RAW, serverTime = Instant.parse("2020-11-03T05:35:16.000Z"), localOffset = Duration.standardHours(1), - etag = "etag" + etag = "etag", + cacheValidity = Duration.standardSeconds(420) ) - private var mockConfigStorage: ConfigDownload? = null + private var mockConfigStorage: InternalConfigData? = null @BeforeEach fun setup() { @@ -61,7 +59,7 @@ class AppConfigSourceTest : BaseIOTest() { mockConfigStorage = arg(0) } - coEvery { configServer.downloadAppConfig() } returns testConfigDownload + coEvery { configServer.downloadAppConfig() } returns dataFromServer every { configServer.clearCache() } just Runs every { configParser.parse(APPCONFIG_RAW) } returns configData @@ -75,55 +73,38 @@ class AppConfigSourceTest : BaseIOTest() { testDir.deleteRecursively() } - private fun createInstance() = AppConfigSource( + private fun createInstance() = RemoteAppConfigSource( server = configServer, storage = configStorage, parser = configParser, - defaultAppConfig = appConfigDefaultFallback, dispatcherProvider = TestDispatcherProvider ) @Test fun `successful download stores new config`() = runBlockingTest2(ignoreActive = true) { val source = createInstance() - source.retrieveConfig() shouldBe DefaultConfigData( + source.getConfigData() shouldBe ConfigDataContainer( serverTime = mockConfigStorage!!.serverTime, localOffset = mockConfigStorage!!.localOffset, mappedConfig = configData, configType = ConfigData.Type.FROM_SERVER, - identifier = "etag" + identifier = "etag", + cacheValidity = Duration.standardSeconds(420) ) - mockConfigStorage shouldBe testConfigDownload + mockConfigStorage shouldBe dataFromServer - coVerify { configStorage.setStoredConfig(testConfigDownload) } - verify(exactly = 0) { appConfigDefaultFallback.getRawDefaultConfig() } - } - - @Test - fun `fallback to last config if download fails`() = runBlockingTest2(ignoreActive = true) { - mockConfigStorage = testConfigDownload - coEvery { configServer.downloadAppConfig() } throws Exception() - - createInstance().retrieveConfig() shouldBe DefaultConfigData( - serverTime = mockConfigStorage!!.serverTime, - localOffset = mockConfigStorage!!.localOffset, - mappedConfig = configData, - configType = ConfigData.Type.LAST_RETRIEVED, - identifier = "etag" - ) - - verify(exactly = 0) { appConfigDefaultFallback.getRawDefaultConfig() } + coVerify { configStorage.setStoredConfig(dataFromServer) } } @Test fun `failed download doesn't overwrite valid config`() = runBlockingTest2(ignoreActive = true) { - mockConfigStorage = testConfigDownload + mockConfigStorage = dataFromServer coEvery { configServer.downloadAppConfig() } throws IOException() - createInstance().retrieveConfig() + createInstance().getConfigData() - mockConfigStorage shouldBe testConfigDownload + mockConfigStorage shouldBe dataFromServer coVerify(exactly = 0) { configStorage.setStoredConfig(any()) } } @@ -137,30 +118,10 @@ class AppConfigSourceTest : BaseIOTest() { advanceUntilIdle() coVerifyOrder { - configStorage.setStoredConfig(null) configServer.clearCache() } } - @Test - fun `local default config is used as last resort`() = runBlockingTest { - coEvery { configServer.downloadAppConfig() } throws IOException() - coEvery { configStorage.getStoredConfig() } returns null - every { appConfigDefaultFallback.getRawDefaultConfig() } returns APPCONFIG_RAW - - val instance = createInstance() - - instance.retrieveConfig() shouldBe DefaultConfigData( - serverTime = Instant.EPOCH, - localOffset = Duration.standardHours(12), - mappedConfig = configData, - configType = ConfigData.Type.LOCAL_DEFAULT, - identifier = "fallback.local" - ) - - verify { appConfigDefaultFallback.getRawDefaultConfig() } - } - companion object { private val APPCONFIG_RAW = ( "080b124d0a230a034c4f57180f221a68747470733a2f2f777777" + diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt index 062d1bfb4c1fbf45e9fb29d274fbd97c0c7ee93a..f6dae8e55b255f6e8cb3d71bd3c8a7577cb88466 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/HourPackageSyncToolTest.kt @@ -204,6 +204,20 @@ class HourPackageSyncToolTest : CommonSyncToolTest() { instance.expectNewHourPackages(listOf(cachedKey1, cachedKey2), now) shouldBe true } + @Test + fun `EXPECT_NEW_HOUR_PACKAGES does not get confused by same hour on next day`() = runBlockingTest { + val cachedKey1 = mockk<CachedKey>().apply { + every { info } returns mockk<CachedKeyInfo>().apply { + every { toDateTime() } returns Instant.parse("2020-01-01T00:00:03.000Z").toDateTime(DateTimeZone.UTC) + } + } + + val instance = createInstance() + + val now = Instant.parse("2020-01-02T01:00:03.000Z") + instance.expectNewHourPackages(listOf(cachedKey1), now) shouldBe true + } + @Test fun `if keys were revoked skip the EXPECT packages check`() = runBlockingTest { every { timeStamper.nowUTC } returns Instant.parse("2020-01-04T02:00:00.000Z") diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt index ea7a698c4c4f55f7142598b9982bc197f91a672f..0be9642b7a7afd6310ed242a095c3a2ecdf18ec3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/HomeFragmentViewModelTest.kt @@ -10,12 +10,12 @@ import de.rki.coronawarnapp.ui.main.home.HomeFragmentViewModel import de.rki.coronawarnapp.ui.main.home.SubmissionCardState import de.rki.coronawarnapp.ui.main.home.SubmissionCardsStateProvider import de.rki.coronawarnapp.ui.main.home.TracingHeaderState -import de.rki.coronawarnapp.ui.submission.ApiRequestState.SUCCESS import de.rki.coronawarnapp.ui.tracing.card.TracingCardState import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider import de.rki.coronawarnapp.ui.viewmodel.SettingsViewModel import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE import de.rki.coronawarnapp.util.DeviceUIState.PAIRED_POSITIVE_TELETAN +import de.rki.coronawarnapp.util.NetworkRequestWrapper import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -131,7 +131,7 @@ class HomeFragmentViewModelTest : BaseTest() { @Test fun `positive test result notification is triggered on positive QR code result`() { - val state = SubmissionCardState(PAIRED_POSITIVE, true, SUCCESS) + val state = SubmissionCardState(NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE), true) every { submissionCardsStateProvider.state } returns flowOf(state) every { testResultNotificationService.schedulePositiveTestResultReminder() } returns Unit @@ -145,7 +145,7 @@ class HomeFragmentViewModelTest : BaseTest() { @Test fun `positive test result notification is triggered on positive TeleTan code result`() { - val state = SubmissionCardState(PAIRED_POSITIVE_TELETAN, true, SUCCESS) + val state = SubmissionCardState(NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE_TELETAN), true) every { submissionCardsStateProvider.state } returns flowOf(state) every { testResultNotificationService.schedulePositiveTestResultReminder() } returns Unit diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt index 37d908b103393884b0e18d7776bd7eae0d547377..e661eb23ae7b28ae3ee123f96ed067d076dd4e3e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardStateTest.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.ui.main.home.SubmissionCardState import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.NetworkRequestWrapper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.clearAllMocks @@ -33,12 +34,18 @@ class SubmissionCardStateTest : BaseTest() { private fun instance( deviceUiState: DeviceUIState = mockk(), isDeviceRegistered: Boolean = true, - uiStateState: ApiRequestState = mockk() - ) = SubmissionCardState( - deviceUiState = deviceUiState, - isDeviceRegistered = isDeviceRegistered, - uiStateState = uiStateState - ) + uiStateState: ApiRequestState = ApiRequestState.SUCCESS + ) = + when (uiStateState) { + ApiRequestState.SUCCESS -> + SubmissionCardState(NetworkRequestWrapper.RequestSuccessful(deviceUiState), isDeviceRegistered) + ApiRequestState.FAILED -> + SubmissionCardState(NetworkRequestWrapper.RequestFailed(mockk()), isDeviceRegistered) + ApiRequestState.STARTED -> + SubmissionCardState(NetworkRequestWrapper.RequestStarted, isDeviceRegistered) + ApiRequestState.IDLE -> + SubmissionCardState(NetworkRequestWrapper.RequestIdle, isDeviceRegistered) + } @Test fun `risk card visibility`() { @@ -163,7 +170,7 @@ class SubmissionCardStateTest : BaseTest() { isFetchingCardVisible() shouldBe false } instance(isDeviceRegistered = true, uiStateState = ApiRequestState.FAILED).apply { - isFetchingCardVisible() shouldBe true + isFetchingCardVisible() shouldBe false } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt index 056cb108b466a39887d2365bfb88c226f6fb0514..65e402345e88790e9a18f1cf3d7470ec3c86b2d0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/home/SubmissionCardsStateProviderTest.kt @@ -5,8 +5,8 @@ import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.main.home.SubmissionCardState import de.rki.coronawarnapp.ui.main.home.SubmissionCardsStateProvider -import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.NetworkRequestWrapper import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.clearAllMocks @@ -46,20 +46,19 @@ class SubmissionCardsStateProviderTest : BaseTest() { @Test fun `state is combined correctly`() = runBlockingTest { - every { submissionRepository.deviceUIStateFlow } returns flow { emit(DeviceUIState.PAIRED_POSITIVE) } - every { submissionRepository.uiStateStateFlow } returns flow { emit(ApiRequestState.SUCCESS) } + every { submissionRepository.deviceUIStateFlow } returns flow { + emit(NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>(DeviceUIState.PAIRED_POSITIVE)) + } every { LocalData.registrationToken() } returns "token" createInstance().apply { state.first() shouldBe SubmissionCardState( - deviceUiState = DeviceUIState.PAIRED_POSITIVE, - uiStateState = ApiRequestState.SUCCESS, + deviceUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), isDeviceRegistered = true ) verify { submissionRepository.deviceUIStateFlow - submissionRepository.uiStateStateFlow LocalData.registrationToken() } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt index 4880b66aa8e79d52d909cc7e2ecdc9fc47a5ea96..b24f8fc8964adf921e00389250ed379eb54ea4d1 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/ENFClientTest.kt @@ -1,22 +1,24 @@ package de.rki.coronawarnapp.nearby -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTracker import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider.DiagnosisKeyProvider +import de.rki.coronawarnapp.nearby.modules.exposurewindow.ExposureWindowProvider import de.rki.coronawarnapp.nearby.modules.locationless.ScanningSupport import de.rki.coronawarnapp.nearby.modules.tracing.TracingStatus +import de.rki.coronawarnapp.nearby.modules.version.ENFVersion import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.coVerifySequence import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just -import io.mockk.mockk import io.mockk.verifySequence import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -29,7 +31,6 @@ import org.junit.jupiter.api.Test import testhelpers.BaseTest import java.io.File -@Suppress("DEPRECATION") class ENFClientTest : BaseTest() { @MockK lateinit var googleENFClient: ExposureNotificationClient @@ -37,12 +38,14 @@ class ENFClientTest : BaseTest() { @MockK lateinit var diagnosisKeyProvider: DiagnosisKeyProvider @MockK lateinit var tracingStatus: TracingStatus @MockK lateinit var scanningSupport: ScanningSupport + @MockK lateinit var exposureWindowProvider: ExposureWindowProvider @MockK lateinit var exposureDetectionTracker: ExposureDetectionTracker + @MockK lateinit var enfVersion: ENFVersion @BeforeEach fun setup() { MockKAnnotations.init(this) - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns true + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true every { exposureDetectionTracker.trackNewExposureDetection(any()) } just Runs } @@ -56,6 +59,8 @@ class ENFClientTest : BaseTest() { diagnosisKeyProvider = diagnosisKeyProvider, tracingStatus = tracingStatus, scanningSupport = scanningSupport, + enfVersion = enfVersion, + exposureWindowProvider = exposureWindowProvider, exposureDetectionTracker = exposureDetectionTracker ) @@ -69,24 +74,20 @@ class ENFClientTest : BaseTest() { fun `provide diagnosis key call is forwarded to the right module`() { val client = createClient() val keyFiles = listOf(File("test")) - val configuration = mockk<ExposureConfiguration>() - val token = "123" - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns true + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true runBlocking { - client.provideDiagnosisKeys(keyFiles, configuration, token) shouldBe true + client.provideDiagnosisKeys(keyFiles) shouldBe true } - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns false + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns false runBlocking { - client.provideDiagnosisKeys(keyFiles, configuration, token) shouldBe false + client.provideDiagnosisKeys(keyFiles) shouldBe false } coVerify(exactly = 2) { diagnosisKeyProvider.provideDiagnosisKeys( - keyFiles, - configuration, - token + keyFiles ) } } @@ -95,16 +96,14 @@ class ENFClientTest : BaseTest() { fun `provide diagnosis key call is only forwarded if there are actually key files`() { val client = createClient() val keyFiles = emptyList<File>() - val configuration = mockk<ExposureConfiguration>() - val token = "123" - coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) } returns true + coEvery { diagnosisKeyProvider.provideDiagnosisKeys(any()) } returns true runBlocking { - client.provideDiagnosisKeys(keyFiles, configuration, token) shouldBe true + client.provideDiagnosisKeys(keyFiles) shouldBe true } coVerify(exactly = 0) { - diagnosisKeyProvider.provideDiagnosisKeys(any(), any(), any()) + diagnosisKeyProvider.provideDiagnosisKeys(any()) } } @@ -265,4 +264,26 @@ class ENFClientTest : BaseTest() { createClient().lastSuccessfulTrackedExposureDetection().first()!!.identifier shouldBe "1" } } + + @Test + fun `exposure windows check is forwarded to the right module`() = runBlocking { + val exposureWindowList = emptyList<ExposureWindow>() + coEvery { exposureWindowProvider.exposureWindows() } returns exposureWindowList + + val client = createClient() + client.exposureWindows() shouldBe exposureWindowList + + coVerify(exactly = 1) { + exposureWindowProvider.exposureWindows() + } + } + + @Test + fun `enf version check is forwaded to the right module`() = runBlocking { + coEvery { enfVersion.getENFClientVersion() } returns Long.MAX_VALUE + + createClient().getENFClientVersion() shouldBe Long.MAX_VALUE + + coVerifySequence { enfVersion.getENFClientVersion() } + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt index fee9f63fa4177ddbb0fc6819d6788fe67535c514..702291ceaeaacdea384fe8f930a56caec0590ede 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/detectiontracker/ExposureDetectionTrackerStorageTest.kt @@ -130,4 +130,20 @@ class ExposureDetectionTrackerStorageTest : BaseIOTest() { storedData.getValue("b2b98400-058d-43e6-b952-529a5255248b").isCalculating shouldBe true storedData.getValue("aeb15509-fb34-42ce-8795-7a9ae0c2f389").isCalculating shouldBe false } + + @Test + fun `we catch empty json data and prevent unsafely initialized maps`() = runBlockingTest { + storageDir.mkdirs() + storageFile.writeText("") + + storageFile.exists() shouldBe true + + createStorage().apply { + val value = load() + value.size shouldBe 0 + value shouldBe emptyMap() + + storageFile.exists() shouldBe false + } + } } 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 24fbd423acacfc4c3ba860da038d9a9b4d447e6a..da15e57215243c887afb317a87160ca618a71619 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,37 +1,32 @@ -@file:Suppress("DEPRECATION") - package de.rki.coronawarnapp.nearby.modules.diagnosiskeyprovider -import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient -import de.rki.coronawarnapp.util.GoogleAPIVersion +import de.rki.coronawarnapp.nearby.modules.version.ENFVersion +import de.rki.coronawarnapp.nearby.modules.version.OutdatedENFVersionException +import io.kotest.matchers.shouldBe +import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.coVerifySequence import io.mockk.impl.annotations.MockK import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows import testhelpers.BaseTest import testhelpers.gms.MockGMSTask import java.io.File class DefaultDiagnosisKeyProviderTest : BaseTest() { - @MockK - lateinit var googleENFClient: ExposureNotificationClient - - @MockK - lateinit var googleAPIVersion: GoogleAPIVersion + @MockK lateinit var googleENFClient: ExposureNotificationClient + @MockK lateinit var enfVersion: ENFVersion + @MockK lateinit var submissionQuota: SubmissionQuota - @MockK - lateinit var submissionQuota: SubmissionQuota - - @MockK - lateinit var exampleConfiguration: ExposureConfiguration private val exampleKeyFiles = listOf(File("file1"), File("file2")) - private val exampleToken = "123e4567-e89b-12d3-a456-426655440000" @BeforeEach fun setup() { @@ -39,15 +34,9 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { coEvery { submissionQuota.consumeQuota(any()) } returns true - coEvery { - googleENFClient.provideDiagnosisKeys( - any(), - any(), - any() - ) - } returns MockGMSTask.forValue(null) + coEvery { googleENFClient.provideDiagnosisKeys(any<List<File>>()) } returns MockGMSTask.forValue(null) - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true + coEvery { enfVersion.requireMinimumVersion(any()) } returns Unit } @AfterEach @@ -56,141 +45,65 @@ class DefaultDiagnosisKeyProviderTest : BaseTest() { } private fun createProvider() = DefaultDiagnosisKeyProvider( - googleAPIVersion = googleAPIVersion, + enfVersion = enfVersion, submissionQuota = submissionQuota, enfClient = googleENFClient ) @Test - fun `legacy key provision is used on older ENF versions`() { - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns false + fun `provide diagnosis keys with outdated ENF versions`() { + coEvery { enfVersion.requireMinimumVersion(any()) } throws OutdatedENFVersionException( + current = 9000, + required = 5000 + ) val provider = createProvider() - runBlocking { - provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken) - } - - coVerify(exactly = 0) { - googleENFClient.provideDiagnosisKeys( - exampleKeyFiles, exampleConfiguration, exampleToken - ) + assertThrows<OutdatedENFVersionException> { + runBlockingTest { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe false } - coVerify(exactly = 1) { - googleENFClient.provideDiagnosisKeys( - listOf(exampleKeyFiles[0]), exampleConfiguration, exampleToken - ) - googleENFClient.provideDiagnosisKeys( - listOf(exampleKeyFiles[1]), exampleConfiguration, exampleToken - ) - submissionQuota.consumeQuota(2) + coVerify { + googleENFClient wasNot Called + submissionQuota wasNot Called } } @Test - fun `normal key provision is used on newer ENF versions`() { - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true - + fun `key provision is used on newer ENF versions`() { val provider = createProvider() - runBlocking { - provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken) - } + runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true - coVerify(exactly = 1) { - googleENFClient.provideDiagnosisKeys(any(), any(), any()) - googleENFClient.provideDiagnosisKeys( - exampleKeyFiles, exampleConfiguration, exampleToken - ) + coVerifySequence { submissionQuota.consumeQuota(1) + googleENFClient.provideDiagnosisKeys(exampleKeyFiles) } } @Test - fun `passing an a null configuration leads to constructing a fallback from defaults`() { - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true - - val provider = createProvider() - val fallback = ExposureConfiguration.ExposureConfigurationBuilder().build() - - runBlocking { - provider.provideDiagnosisKeys(exampleKeyFiles, null, exampleToken) - } - - coVerify(exactly = 1) { - googleENFClient.provideDiagnosisKeys(any(), any(), any()) - googleENFClient.provideDiagnosisKeys(exampleKeyFiles, fallback, exampleToken) - } - } - - @Test - fun `passing an a null configuration leads to constructing a fallback from defaults, legacy`() { - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns false - - val provider = createProvider() - val fallback = ExposureConfiguration.ExposureConfigurationBuilder().build() - - runBlocking { - provider.provideDiagnosisKeys(exampleKeyFiles, null, exampleToken) - } - - coVerify(exactly = 1) { - googleENFClient.provideDiagnosisKeys( - listOf(exampleKeyFiles[0]), fallback, exampleToken - ) - googleENFClient.provideDiagnosisKeys( - listOf(exampleKeyFiles[1]), fallback, exampleToken - ) - submissionQuota.consumeQuota(2) - } - } - - @Test - fun `quota is consumed silenently`() { - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns true + fun `quota is just monitored`() { coEvery { submissionQuota.consumeQuota(any()) } returns false val provider = createProvider() - runBlocking { - provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken) - } + runBlocking { provider.provideDiagnosisKeys(exampleKeyFiles) } shouldBe true - coVerify(exactly = 1) { - googleENFClient.provideDiagnosisKeys(any(), any(), any()) - googleENFClient.provideDiagnosisKeys( - exampleKeyFiles, exampleConfiguration, exampleToken - ) + coVerifySequence { submissionQuota.consumeQuota(1) + googleENFClient.provideDiagnosisKeys(exampleKeyFiles) } } @Test - fun `quota is consumed silently, legacy`() { - coEvery { googleAPIVersion.isAtLeast(GoogleAPIVersion.V16) } returns false - coEvery { submissionQuota.consumeQuota(any()) } returns false - + fun `provide empty key list`() { val provider = createProvider() - runBlocking { - provider.provideDiagnosisKeys(exampleKeyFiles, exampleConfiguration, exampleToken) - } - - coVerify(exactly = 0) { - googleENFClient.provideDiagnosisKeys( - exampleKeyFiles, exampleConfiguration, exampleToken - ) - } + runBlocking { provider.provideDiagnosisKeys(emptyList()) } shouldBe true - coVerify(exactly = 1) { - googleENFClient.provideDiagnosisKeys( - listOf(exampleKeyFiles[0]), exampleConfiguration, exampleToken - ) - googleENFClient.provideDiagnosisKeys( - listOf(exampleKeyFiles[1]), exampleConfiguration, exampleToken - ) - submissionQuota.consumeQuota(2) + coVerify { + googleENFClient wasNot Called + submissionQuota wasNot Called } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt index c5d7238a84bd7dea1318173e73841f09ee74bbdc..52c90fc86ad19a59195445262bde726331554c80 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/diagnosiskeyprovider/SubmissionQuotaTest.kt @@ -69,26 +69,25 @@ class SubmissionQuotaTest : BaseTest() { quota.consumeQuota(5) shouldBe true } - coVerify { enfData.currentQuota = 20 } + coVerify { enfData.currentQuota = 6 } // Reset to 20, then consumed 5 - testStorageCurrentQuota shouldBe 15 + testStorageCurrentQuota shouldBe 1 } @Test fun `quota consumption return true if quota was available`() { - testStorageCurrentQuota shouldBe 20 + testStorageCurrentQuota shouldBe 6 val quota = createQuota() runBlocking { - quota.consumeQuota(10) shouldBe true - quota.consumeQuota(10) shouldBe true - quota.consumeQuota(10) shouldBe false + quota.consumeQuota(3) shouldBe true + quota.consumeQuota(3) shouldBe true quota.consumeQuota(1) shouldBe false } - verify(exactly = 4) { timeStamper.nowUTC } + verify(exactly = 3) { timeStamper.nowUTC } } @Test @@ -97,7 +96,7 @@ class SubmissionQuotaTest : BaseTest() { runBlocking { quota.consumeQuota(0) shouldBe true - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true quota.consumeQuota(0) shouldBe true quota.consumeQuota(1) shouldBe false } @@ -105,12 +104,12 @@ class SubmissionQuotaTest : BaseTest() { @Test fun `partial consumption is not possible`() { - testStorageCurrentQuota shouldBe 20 + testStorageCurrentQuota shouldBe 6 val quota = createQuota() runBlocking { - quota.consumeQuota(18) shouldBe true + quota.consumeQuota(4) shouldBe true quota.consumeQuota(1) shouldBe true quota.consumeQuota(2) shouldBe false } @@ -124,23 +123,23 @@ class SubmissionQuotaTest : BaseTest() { val timeTravelTarget = Instant.parse("2020-12-24T00:00:00.001Z") runBlocking { - quota.consumeQuota(20) shouldBe true - quota.consumeQuota(20) shouldBe false + quota.consumeQuota(6) shouldBe true + quota.consumeQuota(6) shouldBe false every { timeStamper.nowUTC } returns timeTravelTarget - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true quota.consumeQuota(1) shouldBe false } - coVerify(exactly = 1) { enfData.currentQuota = 20 } + coVerify(exactly = 1) { enfData.currentQuota = 6 } verify(exactly = 4) { timeStamper.nowUTC } verify(exactly = 1) { enfData.lastQuotaResetAt = timeTravelTarget } } @Test fun `quota fill up is at midnight`() { - testStorageCurrentQuota = 20 + testStorageCurrentQuota = 6 testStorageLastQuotaReset = Instant.parse("2020-12-24T23:00:00.000Z") val startTime = Instant.parse("2020-12-24T23:59:59.998Z") every { timeStamper.nowUTC } returns startTime @@ -148,7 +147,7 @@ class SubmissionQuotaTest : BaseTest() { val quota = createQuota() runBlocking { - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true quota.consumeQuota(1) shouldBe false every { timeStamper.nowUTC } returns startTime.plus(1) @@ -161,10 +160,10 @@ class SubmissionQuotaTest : BaseTest() { quota.consumeQuota(1) shouldBe true every { timeStamper.nowUTC } returns startTime.plus(4) - quota.consumeQuota(20) shouldBe false + quota.consumeQuota(6) shouldBe false every { timeStamper.nowUTC } returns startTime.plus(3).plus(Duration.standardDays(1)) - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true } } @@ -175,26 +174,26 @@ class SubmissionQuotaTest : BaseTest() { runBlocking { every { timeStamper.nowUTC } returns startTime val quota = createQuota() - quota.consumeQuota(17) shouldBe true + quota.consumeQuota(3) shouldBe true } runBlocking { every { timeStamper.nowUTC } returns startTime.plus(Duration.standardDays(365)) val quota = createQuota() - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true quota.consumeQuota(1) shouldBe false } runBlocking { every { timeStamper.nowUTC } returns startTime.plus(Duration.standardDays(365 * 2)) val quota = createQuota() - quota.consumeQuota(17) shouldBe true + quota.consumeQuota(3) shouldBe true } runBlocking { every { timeStamper.nowUTC } returns startTime.plus(Duration.standardDays(365 * 3)) val quota = createQuota() quota.consumeQuota(3) shouldBe true - quota.consumeQuota(17) shouldBe true + quota.consumeQuota(3) shouldBe true quota.consumeQuota(1) shouldBe false } } @@ -208,12 +207,12 @@ class SubmissionQuotaTest : BaseTest() { val quota = createQuota() runBlocking { - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true quota.consumeQuota(1) shouldBe false // Go forward and get a reset every { timeStamper.nowUTC } returns startTime.plus(Duration.standardHours(1)) - quota.consumeQuota(20) shouldBe true + quota.consumeQuota(6) shouldBe true quota.consumeQuota(1) shouldBe false // Go backwards and don't gain a reset diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt index d6c4cc4d37817dc2dfc8023b745f0755f6de0b69..ec0ba8d4a53bec4c79e4629f6a3900fffb1cce48 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/tracing/DefaultTracingStatusTest.kt @@ -8,14 +8,17 @@ import io.mockk.clearAllMocks import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import testhelpers.coroutines.runBlockingTest2 +import testhelpers.coroutines.test import testhelpers.gms.MockGMSTask class DefaultTracingStatusTest : BaseTest() { @@ -26,7 +29,7 @@ class DefaultTracingStatusTest : BaseTest() { fun setup() { MockKAnnotations.init(this) - every { client.isEnabled } returns MockGMSTask.forValue(true) + every { client.isEnabled } answers { MockGMSTask.forValue(true) } } @AfterEach @@ -34,33 +37,60 @@ class DefaultTracingStatusTest : BaseTest() { clearAllMocks() } - private fun createInstance(): DefaultTracingStatus = DefaultTracingStatus( - client = client + private fun createInstance(scope: CoroutineScope): DefaultTracingStatus = DefaultTracingStatus( + client = client, + scope = scope ) @Test - fun `init is sideeffect free and lazy`() { - createInstance() + fun `init is sideeffect free and lazy`() = runBlockingTest2(ignoreActive = true) { + createInstance(scope = this) + + advanceUntilIdle() + verify { client wasNot Called } } @Test - fun `state emission works`() = runBlockingTest { - val instance = createInstance() + fun `state emission works`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) instance.isTracingEnabled.first() shouldBe true } @Test - fun `state is updated and polling stops on collection stop`() = runBlockingTest { + fun `state is updated and polling stops on cancel`() = runBlockingTest2(ignoreActive = true) { every { client.isEnabled } returnsMany listOf( true, false, true, false, true, false, true ).map { MockGMSTask.forValue(it) } - val instance = createInstance() + val instance = createInstance(scope = this) instance.isTracingEnabled.take(6).toList() shouldBe listOf( true, false, true, false, true, false ) verify(exactly = 6) { client.isEnabled } } + + @Test + fun `subscriptions are shared but not cached`() = runBlockingTest2(ignoreActive = true) { + val instance = createInstance(scope = this) + + val collector1 = instance.isTracingEnabled.test(tag = "1", startOnScope = this) + val collector2 = instance.isTracingEnabled.test(tag = "2", startOnScope = this) + + delay(500) + + collector1.latestValue shouldBe true + collector2.latestValue shouldBe true + + collector1.cancel() + collector2.cancel() + + advanceUntilIdle() + + verify(exactly = 1) { client.isEnabled } + + every { client.isEnabled } answers { MockGMSTask.forValue(false) } + instance.isTracingEnabled.first() shouldBe false + } } 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 new file mode 100644 index 0000000000000000000000000000000000000000..6d9e563b93dda599d64478739717998339f72d63 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/modules/version/DefaultENFVersionTest.kt @@ -0,0 +1,112 @@ +package de.rki.coronawarnapp.nearby.modules.version + +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.CommonStatusCodes.API_NOT_CONNECTED +import com.google.android.gms.common.api.CommonStatusCodes.INTERNAL_ERROR +import com.google.android.gms.common.api.Status +import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.every +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.gms.MockGMSTask + +@ExperimentalCoroutinesApi +internal class DefaultENFVersionTest { + + @MockK lateinit var client: ExposureNotificationClient + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + + @AfterEach + fun tearDown() { + clearAllMocks() + } + + fun createInstance() = DefaultENFVersion( + client = client + ) + + @Test + fun `current version is newer than the required version`() { + every { client.version } returns MockGMSTask.forValue(17000000L) + + runBlockingTest { + createInstance().apply { + getENFClientVersion() shouldBe 17000000L + shouldNotThrowAny { + requireMinimumVersion(ENFVersion.V1_6) + } + } + } + } + + @Test + fun `current version is older than the required version`() { + every { client.version } returns MockGMSTask.forValue(15000000L) + + runBlockingTest { + createInstance().apply { + getENFClientVersion() shouldBe 15000000L + + shouldThrow<OutdatedENFVersionException> { + requireMinimumVersion(ENFVersion.V1_6) + } + } + } + } + + @Test + fun `current version is equal to the required version`() { + every { client.version } returns MockGMSTask.forValue(16000000L) + + runBlockingTest { + createInstance().apply { + getENFClientVersion() shouldBe ENFVersion.V1_6 + shouldNotThrowAny { + requireMinimumVersion(ENFVersion.V1_6) + } + } + } + } + + @Test + fun `API_NOT_CONNECTED exceptions are not treated as failures`() { + every { client.version } returns MockGMSTask.forError(ApiException(Status(API_NOT_CONNECTED))) + + runBlockingTest { + createInstance().apply { + getENFClientVersion() shouldBe null + shouldNotThrowAny { + requireMinimumVersion(ENFVersion.V1_6) + } + } + } + } + + @Test + fun `rethrows unexpected exceptions`() { + every { client.version } returns MockGMSTask.forError(ApiException(Status(INTERNAL_ERROR))) + + runBlockingTest { + createInstance().apply { + getENFClientVersion() shouldBe null + + shouldThrow<ApiException> { + requireMinimumVersion(ENFVersion.V1_6) + } + } + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..e56375ebb81f0aa772df5885b9ff5a8436088c19 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/ExposureWindowsCalculationTest.kt @@ -0,0 +1,422 @@ +package de.rki.coronawarnapp.nearby.windows + +import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import com.google.android.gms.nearby.exposurenotification.ScanInstance +import com.google.gson.Gson +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.appconfig.internal.ConfigDataContainer +import de.rki.coronawarnapp.nearby.windows.entities.ExposureWindowsJsonInput +import de.rki.coronawarnapp.nearby.windows.entities.cases.JsonScanInstance +import de.rki.coronawarnapp.nearby.windows.entities.cases.JsonWindow +import de.rki.coronawarnapp.nearby.windows.entities.cases.TestCase +import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCalculationConfiguration +import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationFilter +import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonMinutesAtAttenuationWeight +import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonNormalizedTimeToRiskLevelMapping +import de.rki.coronawarnapp.nearby.windows.entities.configuration.JsonTrlFilter +import de.rki.coronawarnapp.risk.DefaultRiskLevels +import de.rki.coronawarnapp.risk.result.AggregatedRiskResult +import de.rki.coronawarnapp.risk.result.RiskResult +import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.serialization.fromJson +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.MockKAnnotations +import io.mockk.clearAllMocks +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.runBlocking +import org.joda.time.DateTimeConstants +import org.joda.time.Duration +import org.joda.time.Instant +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import timber.log.Timber +import java.io.FileReader +import java.nio.file.Paths + +class ExposureWindowsCalculationTest : BaseTest() { + + @MockK lateinit var appConfigProvider: AppConfigProvider + @MockK lateinit var configData: ConfigData + @MockK lateinit var timeStamper: TimeStamper + + private lateinit var riskLevels: DefaultRiskLevels + private lateinit var testConfig: ConfigData + + // Json file (located in /test/resources/exposure-windows-risk-calculation.json) + private val fileName = "exposure-windows-risk-calculation.json" + + // Debug logs + private enum class LogLevel { + NONE, + ONLY_COMPARISON, + EXTENDED, + ALL + } + + private val logLevel = LogLevel.ONLY_COMPARISON + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + every { timeStamper.nowUTC } returns Instant.now() + } + + @AfterEach + fun teardown() { + clearAllMocks() + } + + private fun debugLog(s: String, toShow: LogLevel = LogLevel.ALL) { + if (logLevel < toShow) + return + Timber.v(s) + } + + @Test + fun `one test to rule them all`(): Unit = runBlocking { + // 1 - Load and parse json file + val jsonFile = Paths.get("src", "test", "resources", fileName).toFile() + jsonFile shouldNotBe null + val jsonString = FileReader(jsonFile).readText() + jsonString.length shouldBeGreaterThan 0 + val json = Gson().fromJson<ExposureWindowsJsonInput>(jsonString) + json shouldNotBe null + + // 2 - Check test cases + for (case: TestCase in json.testCases) { + checkTestCase(case) + } + debugLog("Test cases checked. Total count: ${json.testCases.size}") + + // 3 - Mock calculation configuration and create default risk level with it + setupTestConfiguration(json.defaultRiskCalculationConfiguration) + coEvery { appConfigProvider.getAppConfig() } returns testConfig + every { appConfigProvider.currentConfig } returns flow { testConfig } + logConfiguration(testConfig) + + riskLevels = DefaultRiskLevels() + + val appConfig = appConfigProvider.getAppConfig() + + // 4 - Mock and log exposure windows + val allExposureWindows = mutableListOf<ExposureWindow>() + for (case: TestCase in json.testCases) { + val exposureWindows: List<ExposureWindow> = + case.exposureWindows.map { window -> jsonToExposureWindow(window) } + allExposureWindows.addAll(exposureWindows) + + // 5 - Calculate risk level for test case and aggregate results + val exposureWindowsAndResult = HashMap<ExposureWindow, RiskResult>() + for (exposureWindow: ExposureWindow in exposureWindows) { + + logExposureWindow(exposureWindow, "âž¡âž¡ EXPOSURE WINDOW PASSED âž¡âž¡", LogLevel.EXTENDED) + val riskResult = riskLevels.calculateRisk(appConfig, exposureWindow) ?: continue + exposureWindowsAndResult[exposureWindow] = riskResult + } + debugLog("Exposure windows and result: ${exposureWindowsAndResult.size}") + + val aggregatedRiskResult = riskLevels.aggregateResults(appConfig, exposureWindowsAndResult) + + debugLog( + "\n" + comparisonDebugTable(aggregatedRiskResult, case), + LogLevel.ONLY_COMPARISON + ) + + // 6 - Check with expected result from test case + aggregatedRiskResult.totalRiskLevel.number shouldBe case.expTotalRiskLevel + aggregatedRiskResult.mostRecentDateWithHighRisk shouldBe getTestCaseDate(case.expAgeOfMostRecentDateWithHighRiskInDays) + aggregatedRiskResult.mostRecentDateWithLowRisk shouldBe getTestCaseDate(case.expAgeOfMostRecentDateWithLowRiskInDays) + aggregatedRiskResult.totalMinimumDistinctEncountersWithHighRisk shouldBe case.expTotalMinimumDistinctEncountersWithHighRisk + aggregatedRiskResult.totalMinimumDistinctEncountersWithLowRisk shouldBe case.expTotalMinimumDistinctEncountersWithLowRisk + } + } + + private fun getTestCaseDate(expAge: Long?): Instant? { + if (expAge == null) return null + return timeStamper.nowUTC - expAge * DateTimeConstants.MILLIS_PER_DAY + } + + private fun comparisonDebugTable(aggregated: AggregatedRiskResult, case: TestCase): String { + val result = StringBuilder() + result.append("\n").append(case.description) + result.append("\n").append("+----------------------+--------------------------+--------------------------+") + result.append("\n").append("| Property | Actual | Expected |") + result.append("\n").append("+----------------------+--------------------------+--------------------------+") + result.append( + addPropertyCheckToComparisonDebugTable( + "Total Risk", + aggregated.totalRiskLevel.number, + case.expTotalRiskLevel + ) + ) + result.append( + addPropertyCheckToComparisonDebugTable( + "Date With High Risk", + aggregated.mostRecentDateWithHighRisk, + getTestCaseDate(case.expAgeOfMostRecentDateWithHighRiskInDays) + ) + ) + result.append( + addPropertyCheckToComparisonDebugTable( + "Date With Low Risk", + aggregated.mostRecentDateWithLowRisk, + getTestCaseDate(case.expAgeOfMostRecentDateWithLowRiskInDays) + ) + ) + result.append( + addPropertyCheckToComparisonDebugTable( + "Encounters High Risk", + aggregated.totalMinimumDistinctEncountersWithHighRisk, + case.expTotalMinimumDistinctEncountersWithHighRisk + ) + ) + result.append( + addPropertyCheckToComparisonDebugTable( + "Encounters Low Risk", + aggregated.totalMinimumDistinctEncountersWithLowRisk, + case.expTotalMinimumDistinctEncountersWithLowRisk + ) + ) + result.append("\n") + return result.toString() + } + + private fun addPropertyCheckToComparisonDebugTable(propertyName: String, expected: Any?, actual: Any?): String { + val format = "| %-20s | %-24s | %-24s |" + val result = StringBuilder() + result.append("\n").append(String.format(format, propertyName, expected, actual)) + result.append("\n").append("+----------------------+--------------------------+--------------------------+") + return result.toString() + } + + private fun checkTestCase(case: TestCase) { + debugLog("Checking ${case.description}", LogLevel.ALL) + case.expTotalRiskLevel shouldNotBe null + case.expTotalMinimumDistinctEncountersWithLowRisk shouldNotBe null + case.expTotalMinimumDistinctEncountersWithHighRisk shouldNotBe null + case.exposureWindows.map { exposureWindow -> checkExposureWindow(exposureWindow) } + } + + private fun checkExposureWindow(jsonWindow: JsonWindow) { + jsonWindow.ageInDays shouldNotBe null + jsonWindow.reportType shouldNotBe null + jsonWindow.infectiousness shouldNotBe null + jsonWindow.calibrationConfidence shouldNotBe null + } + + private fun logConfiguration(config: ConfigData) { + val result = StringBuilder() + result.append("\n\n").append("----------------- \uD83D\uDEE0 CONFIGURATION \uD83D\uDEE0 -----------") + + result.append("\n").append("â—¦ Minutes At Attenuation Filters (${config.minutesAtAttenuationFilters.size})") + for (filter: RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter in config.minutesAtAttenuationFilters) { + result.append("\n\t").append("⇥ Filter") + result.append(logRange(filter.attenuationRange, "Attenuation Range")) + result.append(logRange(filter.dropIfMinutesInRange, "Drop If Minutes In Range")) + } + + result.append("\n").append("â—¦ Minutes At Attenuation Weights (${config.minutesAtAttenuationWeights.size})") + for (weight: RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight in config.minutesAtAttenuationWeights) { + result.append("\n\t").append("⇥ Weight") + result.append(logRange(weight.attenuationRange, "Attenuation Range")) + result.append("\n\t\t").append("↳ Weight: ${weight.weight}") + } + + result.append("\n") + .append("â—¦ Normalized Time Per Day To Risk Level Mapping List (${config.normalizedTimePerDayToRiskLevelMappingList.size})") + for (mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping in config.normalizedTimePerDayToRiskLevelMappingList) { + result.append("\n\t").append("⇥ Mapping") + result.append(logRange(mapping.normalizedTimeRange, "Normalized Time Range")) + result.append("\n\t\t").append("↳ Risk Level: ${mapping.riskLevel}") + } + + result.append("\n") + .append("â—¦ Normalized Time Per Exposure Window To Risk Level Mapping (${config.normalizedTimePerExposureWindowToRiskLevelMapping.size})") + for (mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping in config.normalizedTimePerExposureWindowToRiskLevelMapping) { + result.append("\n\t").append("⇥ Mapping") + result.append(logRange(mapping.normalizedTimeRange, "Normalized Time Range")) + result.append("\n\t\t").append("↳ Risk Level: ${mapping.riskLevel}") + } + + result.append("\n").append("â—¦ Transmission Risk Level Encoding:") + result.append("\n\t") + .append("↳ Infectiousness Offset High: ${config.transmissionRiskLevelEncoding.infectiousnessOffsetHigh}") + result.append("\n\t") + .append("↳ Infectiousness Offset Standard: ${config.transmissionRiskLevelEncoding.infectiousnessOffsetStandard}") + result.append("\n\t") + .append("↳ Report Type Offset Confirmed Clinical Diagnosis: ${config.transmissionRiskLevelEncoding.reportTypeOffsetConfirmedClinicalDiagnosis}") + result.append("\n\t") + .append("↳ Report Type Offset Confirmed Test: ${config.transmissionRiskLevelEncoding.reportTypeOffsetConfirmedTest}") + result.append("\n\t") + .append("↳ Report Type Offset Recursive: ${config.transmissionRiskLevelEncoding.reportTypeOffsetRecursive}") + result.append("\n\t") + .append("↳ Report Type Offset Self Report: ${config.transmissionRiskLevelEncoding.reportTypeOffsetSelfReport}") + + result.append("\n").append("â—¦ Transmission Risk Level Filters (${config.transmissionRiskLevelFilters.size})") + for (filter: RiskCalculationParametersOuterClass.TrlFilter in config.transmissionRiskLevelFilters) { + result.append("\n\t").append("⇥ Trl Filter") + result.append(logRange(filter.dropIfTrlInRange, "Drop If Trl In Range")) + } + + result.append("\n").append("â—¦ Transmission Risk Level Multiplier: ${config.transmissionRiskLevelMultiplier}") + result.append("\n").append("-------------------------------------------- âš™ -").append("\n") + debugLog(result.toString(), LogLevel.NONE) + } + + private fun logRange(range: RiskCalculationParametersOuterClass.Range, rangeName: String): String { + val builder = StringBuilder() + builder.append("\n\t\t").append("⇥ $rangeName") + builder.append("\n\t\t\t").append("↳ Min: ${range.min}") + builder.append("\n\t\t\t").append("↳ Max: ${range.max}") + builder.append("\n\t\t\t").append("↳ Min Exclusive: ${range.minExclusive}") + builder.append("\n\t\t\t").append("↳ Max Exclusive: ${range.maxExclusive}") + return builder.toString() + } + + private fun logExposureWindow(exposureWindow: ExposureWindow, title: String, logLevel: LogLevel = LogLevel.ALL) { + val result = StringBuilder() + result.append("\n\n").append("------------ $title -----------") + result.append("\n").append("Mocked Exposure window: #${exposureWindow.hashCode()}") + result.append("\n").append("â—¦ Calibration Confidence: ${exposureWindow.calibrationConfidence}") + result.append("\n").append("â—¦ Date Millis Since Epoch: ${exposureWindow.dateMillisSinceEpoch}") + result.append("\n").append("â—¦ Infectiousness: ${exposureWindow.infectiousness}") + result.append("\n").append("â—¦ Report type: ${exposureWindow.reportType}") + + result.append("\n").append("‣ Scan Instances (${exposureWindow.scanInstances.size}):") + for (scan: ScanInstance in exposureWindow.scanInstances) { + result.append("\n\t").append("⇥ Mocked Scan Instance: #${scan.hashCode()}") + result.append("\n\t\t").append("↳ Min Attenuation: ${scan.minAttenuationDb}") + result.append("\n\t\t").append("↳ Seconds Since Last Scan: ${scan.secondsSinceLastScan}") + result.append("\n\t\t").append("↳ Typical Attenuation: ${scan.typicalAttenuationDb}") + } + result.append("\n").append("-------------------------------------------- ✂ ----").append("\n") + debugLog(result.toString(), logLevel) + } + + private fun setupTestConfiguration(json: DefaultRiskCalculationConfiguration) { + + testConfig = ConfigDataContainer( + serverTime = Instant.now(), + cacheValidity = Duration.standardMinutes(5), + localOffset = Duration.ZERO, + mappedConfig = configData, + identifier = "soup", + configType = ConfigData.Type.FROM_SERVER + ) + + val attenuationFilters = mutableListOf<RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter>() + for (jsonFilter: JsonMinutesAtAttenuationFilter in json.minutesAtAttenuationFilters) { + val filter: RiskCalculationParametersOuterClass.MinutesAtAttenuationFilter = mockk() + every { filter.attenuationRange.min } returns jsonFilter.attenuationRange.min + every { filter.attenuationRange.max } returns jsonFilter.attenuationRange.max + every { filter.attenuationRange.minExclusive } returns jsonFilter.attenuationRange.minExclusive + every { filter.attenuationRange.maxExclusive } returns jsonFilter.attenuationRange.maxExclusive + every { filter.dropIfMinutesInRange.min } returns jsonFilter.dropIfMinutesInRange.min + every { filter.dropIfMinutesInRange.max } returns jsonFilter.dropIfMinutesInRange.max + every { filter.dropIfMinutesInRange.minExclusive } returns jsonFilter.dropIfMinutesInRange.minExclusive + every { filter.dropIfMinutesInRange.maxExclusive } returns jsonFilter.dropIfMinutesInRange.maxExclusive + attenuationFilters.add(filter) + } + every { testConfig.minutesAtAttenuationFilters } returns attenuationFilters + + val attenuationWeights = mutableListOf<RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight>() + for (jsonWeight: JsonMinutesAtAttenuationWeight in json.minutesAtAttenuationWeights) { + val weight: RiskCalculationParametersOuterClass.MinutesAtAttenuationWeight = mockk() + every { weight.attenuationRange.min } returns jsonWeight.attenuationRange.min + every { weight.attenuationRange.max } returns jsonWeight.attenuationRange.max + every { weight.attenuationRange.minExclusive } returns jsonWeight.attenuationRange.minExclusive + every { weight.attenuationRange.maxExclusive } returns jsonWeight.attenuationRange.maxExclusive + every { weight.weight } returns jsonWeight.weight + attenuationWeights.add(weight) + } + every { testConfig.minutesAtAttenuationWeights } returns attenuationWeights + + val normalizedTimePerDayToRiskLevelMapping = + mutableListOf<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>() + for (jsonMapping: JsonNormalizedTimeToRiskLevelMapping in json.normalizedTimePerDayToRiskLevelMapping) { + val mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping = mockk() + every { mapping.riskLevel } returns RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber( + jsonMapping.riskLevel + ) + every { mapping.normalizedTimeRange.min } returns jsonMapping.normalizedTimeRange.min + every { mapping.normalizedTimeRange.max } returns jsonMapping.normalizedTimeRange.max + every { mapping.normalizedTimeRange.minExclusive } returns jsonMapping.normalizedTimeRange.minExclusive + every { mapping.normalizedTimeRange.maxExclusive } returns jsonMapping.normalizedTimeRange.maxExclusive + normalizedTimePerDayToRiskLevelMapping.add(mapping) + } + every { testConfig.normalizedTimePerDayToRiskLevelMappingList } returns normalizedTimePerDayToRiskLevelMapping + + val normalizedTimePerExposureWindowToRiskLevelMapping = + mutableListOf<RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping>() + for (jsonMapping: JsonNormalizedTimeToRiskLevelMapping in json.normalizedTimePerEWToRiskLevelMapping) { + val mapping: RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping = mockk() + every { mapping.riskLevel } returns RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.forNumber( + jsonMapping.riskLevel + ) + every { mapping.normalizedTimeRange.min } returns jsonMapping.normalizedTimeRange.min + every { mapping.normalizedTimeRange.max } returns jsonMapping.normalizedTimeRange.max + every { mapping.normalizedTimeRange.minExclusive } returns jsonMapping.normalizedTimeRange.minExclusive + every { mapping.normalizedTimeRange.maxExclusive } returns jsonMapping.normalizedTimeRange.maxExclusive + normalizedTimePerExposureWindowToRiskLevelMapping.add(mapping) + } + every { testConfig.normalizedTimePerExposureWindowToRiskLevelMapping } returns normalizedTimePerExposureWindowToRiskLevelMapping + + every { testConfig.transmissionRiskLevelMultiplier } returns json.transmissionRiskLevelMultiplier + + val trlEncoding: RiskCalculationParametersOuterClass.TransmissionRiskLevelEncoding = mockk() + every { trlEncoding.infectiousnessOffsetHigh } returns json.trlEncoding.infectiousnessOffsetHigh + every { trlEncoding.infectiousnessOffsetStandard } returns json.trlEncoding.infectiousnessOffsetStandard + every { trlEncoding.reportTypeOffsetConfirmedClinicalDiagnosis } returns json.trlEncoding.reportTypeOffsetConfirmedClinicalDiagnosis + every { trlEncoding.reportTypeOffsetConfirmedTest } returns json.trlEncoding.reportTypeOffsetConfirmedTest + every { trlEncoding.reportTypeOffsetRecursive } returns json.trlEncoding.reportTypeOffsetRecursive + every { trlEncoding.reportTypeOffsetSelfReport } returns json.trlEncoding.reportTypeOffsetSelfReport + every { testConfig.transmissionRiskLevelEncoding } returns trlEncoding + + val trlFilters = mutableListOf<RiskCalculationParametersOuterClass.TrlFilter>() + for (jsonFilter: JsonTrlFilter in json.trlFilters) { + val filter: RiskCalculationParametersOuterClass.TrlFilter = mockk() + every { filter.dropIfTrlInRange.min } returns jsonFilter.dropIfTrlInRange.min + every { filter.dropIfTrlInRange.max } returns jsonFilter.dropIfTrlInRange.max + every { filter.dropIfTrlInRange.minExclusive } returns jsonFilter.dropIfTrlInRange.minExclusive + every { filter.dropIfTrlInRange.maxExclusive } returns jsonFilter.dropIfTrlInRange.maxExclusive + trlFilters.add(filter) + } + every { testConfig.transmissionRiskLevelFilters } returns trlFilters + } + + private fun jsonToExposureWindow(json: JsonWindow): ExposureWindow { + val exposureWindow: ExposureWindow = mockk() + + every { exposureWindow.calibrationConfidence } returns json.calibrationConfidence + every { exposureWindow.dateMillisSinceEpoch } returns timeStamper.nowUTC.millis - (DateTimeConstants.MILLIS_PER_DAY * json.ageInDays).toLong() + every { exposureWindow.infectiousness } returns json.infectiousness + every { exposureWindow.reportType } returns json.reportType + every { exposureWindow.scanInstances } returns json.scanInstances.map { scanInstance -> + jsonToScanInstance( + scanInstance + ) + } + + logExposureWindow(exposureWindow, "⊞ EXPOSURE WINDOW MOCK ⊞") + + return exposureWindow + } + + private fun jsonToScanInstance(json: JsonScanInstance): ScanInstance { + val scanInstance: ScanInstance = mockk() + every { scanInstance.minAttenuationDb } returns json.minAttenuation + every { scanInstance.secondsSinceLastScan } returns json.secondsSinceLastScan + every { scanInstance.typicalAttenuationDb } returns json.typicalAttenuation + return scanInstance + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c0a77a3a27bdc197a2ca75cd885303a3ec587eb --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/ExposureWindowsJsonInput.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.nearby.windows.entities + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.nearby.windows.entities.cases.TestCase +import de.rki.coronawarnapp.nearby.windows.entities.configuration.DefaultRiskCalculationConfiguration + +data class ExposureWindowsJsonInput( + @SerializedName("__comment__") + val comment: String, + @SerializedName("defaultRiskCalculationConfiguration") + val defaultRiskCalculationConfiguration: DefaultRiskCalculationConfiguration, + @SerializedName("testCases") + val testCases: List<TestCase> +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt new file mode 100644 index 0000000000000000000000000000000000000000..a9ea714b7e7f440c4e71c5e27f8f8c912a3c575c --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonScanInstance.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.nearby.windows.entities.cases + +import com.google.gson.annotations.SerializedName + +data class JsonScanInstance( + @SerializedName("minAttenuation") + val minAttenuation: Int, + @SerializedName("secondsSinceLastScan") + val secondsSinceLastScan: Int, + @SerializedName("typicalAttenuation") + val typicalAttenuation: Int +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt new file mode 100644 index 0000000000000000000000000000000000000000..7394b329509f98a10e6d7c368dc3033b62c442ef --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/JsonWindow.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.nearby.windows.entities.cases + +import com.google.gson.annotations.SerializedName + +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/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt new file mode 100644 index 0000000000000000000000000000000000000000..0d28ea6d7b85b963c6e24e08c4e4c906b6a91082 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/cases/TestCase.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.nearby.windows.entities.cases + +import com.google.gson.annotations.SerializedName + +data class TestCase( + @SerializedName("description") + val description: String, + @SerializedName("expAgeOfMostRecentDateWithHighRisk") + val expAgeOfMostRecentDateWithHighRiskInDays: Long?, + @SerializedName("expAgeOfMostRecentDateWithLowRisk") + val expAgeOfMostRecentDateWithLowRiskInDays: Long?, + @SerializedName("expTotalMinimumDistinctEncountersWithHighRisk") + val expTotalMinimumDistinctEncountersWithHighRisk: Int, + @SerializedName("expTotalMinimumDistinctEncountersWithLowRisk") + val expTotalMinimumDistinctEncountersWithLowRisk: Int, + @SerializedName("expTotalRiskLevel") + val expTotalRiskLevel: Int, + @SerializedName("expNumberOfDaysWithLowRisk") + val expNumberOfDaysWithLowRisk: Int, + @SerializedName("expNumberOfDaysWithHighRisk") + val expNumberOfDaysWithHighRisk: Int, + @SerializedName("exposureWindows") + val exposureWindows: List<JsonWindow> +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt new file mode 100644 index 0000000000000000000000000000000000000000..7445d86e786d7250f7cf57dc4c5498eba97fabeb --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/DefaultRiskCalculationConfiguration.kt @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class DefaultRiskCalculationConfiguration( + @SerializedName("minutesAtAttenuationFilters") + val minutesAtAttenuationFilters: List<JsonMinutesAtAttenuationFilter>, + @SerializedName("minutesAtAttenuationWeights") + val minutesAtAttenuationWeights: List<JsonMinutesAtAttenuationWeight>, + @SerializedName("normalizedTimePerDayToRiskLevelMapping") + val normalizedTimePerDayToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>, + @SerializedName("normalizedTimePerEWToRiskLevelMapping") + val normalizedTimePerEWToRiskLevelMapping: List<JsonNormalizedTimeToRiskLevelMapping>, + @SerializedName("transmissionRiskLevelMultiplier") + val transmissionRiskLevelMultiplier: Double, + @SerializedName("trlEncoding") + val trlEncoding: JsonTrlEncoding, + @SerializedName("trlFilters") + val trlFilters: List<JsonTrlFilter> +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..a01efc1f6022a2a68a8a20e25aeefc6320591b2b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationFilter.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class JsonMinutesAtAttenuationFilter( + @SerializedName("attenuationRange") + val attenuationRange: Range, + @SerializedName("dropIfMinutesInRange") + val dropIfMinutesInRange: Range +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt new file mode 100644 index 0000000000000000000000000000000000000000..3af598da7dc5c6d71bf0bbeeac85788f038a90ee --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonMinutesAtAttenuationWeight.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class JsonMinutesAtAttenuationWeight( + @SerializedName("attenuationRange") + val attenuationRange: Range, + @SerializedName("weight") + val weight: Double +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt new file mode 100644 index 0000000000000000000000000000000000000000..4f0fc786998558caf43be1d5cee1e6026f7bb919 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonNormalizedTimeToRiskLevelMapping.kt @@ -0,0 +1,10 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class JsonNormalizedTimeToRiskLevelMapping( + @SerializedName("normalizedTimeRange") + val normalizedTimeRange: Range, + @SerializedName("riskLevel") + val riskLevel: Int +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt new file mode 100644 index 0000000000000000000000000000000000000000..00a3e5af78270ef34fb384909f1f69ddab92f2ba --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlEncoding.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class JsonTrlEncoding( + @SerializedName("infectiousnessOffsetHigh") + val infectiousnessOffsetHigh: Int, + @SerializedName("infectiousnessOffsetStandard") + val infectiousnessOffsetStandard: Int, + @SerializedName("reportTypeOffsetConfirmedClinicalDiagnosis") + val reportTypeOffsetConfirmedClinicalDiagnosis: Int, + @SerializedName("reportTypeOffsetConfirmedTest") + val reportTypeOffsetConfirmedTest: Int, + @SerializedName("reportTypeOffsetRecursive") + val reportTypeOffsetRecursive: Int, + @SerializedName("reportTypeOffsetSelfReport") + val reportTypeOffsetSelfReport: Int +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..6e529bc6af34385922152903db58c5a1521c725e --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/JsonTrlFilter.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class JsonTrlFilter( + @SerializedName("dropIfTrlInRange") + val dropIfTrlInRange: Range +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt new file mode 100644 index 0000000000000000000000000000000000000000..a4d686a74cd8183a8261ec35bf8487b0ad2db005 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/nearby/windows/entities/configuration/Range.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.nearby.windows.entities.configuration + +import com.google.gson.annotations.SerializedName + +data class Range( + @SerializedName("min") + val min: Double, + @SerializedName("minExclusive") + val minExclusive: Boolean, + @SerializedName("max") + val max: Double, + @SerializedName("maxExclusive") + val maxExclusive: Boolean +) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt index fbc170105e025901200d40c8dc3e40d92a95e3c8..4bd4076caffa27abfe8a250730495fbf917eeedc 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/receiver/ExposureStateUpdateReceiverTest.kt @@ -12,12 +12,13 @@ import de.rki.coronawarnapp.nearby.modules.detectiontracker.ExposureDetectionTra import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.util.di.AppInjector import io.mockk.MockKAnnotations +import io.mockk.Runs import io.mockk.clearAllMocks import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.mockkStatic import io.mockk.verifySequence import kotlinx.coroutines.test.TestCoroutineScope import org.junit.jupiter.api.AfterEach @@ -36,6 +37,7 @@ class ExposureStateUpdateReceiverTest : BaseTest() { @MockK private lateinit var intent: Intent @MockK private lateinit var workManager: WorkManager @MockK private lateinit var exposureDetectionTracker: ExposureDetectionTracker + private val scope = TestCoroutineScope() class TestApp : Application(), HasAndroidInjector { @@ -48,23 +50,25 @@ class ExposureStateUpdateReceiverTest : BaseTest() { @BeforeEach fun setUp() { MockKAnnotations.init(this) - mockkStatic(WorkManager::class) every { intent.getStringExtra(ExposureNotificationClient.EXTRA_TOKEN) } returns "token" mockkObject(AppInjector) + every { workManager.enqueue(any<WorkRequest>()) } answers { mockk() } + val application = mockk<TestApp>() every { context.applicationContext } returns application + val broadcastReceiverInjector = AndroidInjector<Any> { it as ExposureStateUpdateReceiver it.exposureDetectionTracker = exposureDetectionTracker it.dispatcherProvider = TestDispatcherProvider it.scope = scope + it.workManager = workManager } every { application.androidInjector() } returns broadcastReceiverInjector - every { WorkManager.getInstance(context) } returns workManager - every { workManager.enqueue(any<WorkRequest>()) } answers { mockk() } + every { exposureDetectionTracker.finishExposureDetection(any(), any()) } just Runs } @AfterEach @@ -77,9 +81,11 @@ class ExposureStateUpdateReceiverTest : BaseTest() { every { intent.action } returns ExposureNotificationClient.ACTION_EXPOSURE_STATE_UPDATED ExposureStateUpdateReceiver().onReceive(context, intent) + scope.advanceUntilIdle() + verifySequence { + exposureDetectionTracker.finishExposureDetection(null, TrackedExposureDetection.Result.UPDATED_STATE) workManager.enqueue(any<WorkRequest>()) - exposureDetectionTracker.finishExposureDetection("token", TrackedExposureDetection.Result.UPDATED_STATE) } } @@ -88,8 +94,11 @@ class ExposureStateUpdateReceiverTest : BaseTest() { every { intent.action } returns ExposureNotificationClient.ACTION_EXPOSURE_NOT_FOUND ExposureStateUpdateReceiver().onReceive(context, intent) + scope.advanceUntilIdle() + verifySequence { - exposureDetectionTracker.finishExposureDetection("token", TrackedExposureDetection.Result.NO_MATCHES) + exposureDetectionTracker.finishExposureDetection(null, TrackedExposureDetection.Result.NO_MATCHES) + workManager.enqueue(any<WorkRequest>()) } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..41a2b35177a6eab0a08331f6337a9ce6f991554d --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelDataTest.kt @@ -0,0 +1,40 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest +import testhelpers.preferences.MockSharedPreferences + +class RiskLevelDataTest : BaseTest() { + + @MockK lateinit var context: Context + lateinit var preferences: MockSharedPreferences + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + preferences = MockSharedPreferences() + every { context.getSharedPreferences("risklevel_localdata", Context.MODE_PRIVATE) } returns preferences + } + + fun createInstance() = RiskLevelData(context = context) + + @Test + fun `update last used config identifier`() { + createInstance().apply { + lastUsedConfigIdentifier shouldBe null + lastUsedConfigIdentifier = "Banana" + lastUsedConfigIdentifier shouldBe "Banana" + preferences.dataMapPeek.containsValue("Banana") shouldBe true + + lastUsedConfigIdentifier = null + lastUsedConfigIdentifier shouldBe null + preferences.dataMapPeek.isEmpty() shouldBe true + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..5e1cbd3839de8c388a5281a9e8084e7b3e1981a3 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskConfigTest.kt @@ -0,0 +1,15 @@ +package de.rki.coronawarnapp.risk + +import io.kotest.matchers.shouldBe +import org.joda.time.Duration +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RiskLevelTaskConfigTest : BaseTest() { + + @Test + fun `risk level task max execution time is not above 9 minutes`() { + val config = RiskLevelTask.Config() + config.executionTimeout.isShorterThan(Duration.standardMinutes(9)) shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f174600cfd181bc4936dbe935c9195c830be8c82 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelTaskTest.kt @@ -0,0 +1,84 @@ +package de.rki.coronawarnapp.risk + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.task.Task +import de.rki.coronawarnapp.util.BackgroundModeStatus +import de.rki.coronawarnapp.util.TimeStamper +import io.mockk.MockKAnnotations +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.mockk +import io.mockk.mockkObject +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runBlockingTest +import org.joda.time.Instant +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RiskLevelTaskTest : BaseTest() { + + @MockK lateinit var riskLevels: RiskLevels + @MockK lateinit var context: Context + @MockK lateinit var enfClient: ENFClient + @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var backgroundModeStatus: BackgroundModeStatus + @MockK lateinit var riskLevelData: RiskLevelData + @MockK lateinit var configData: ConfigData + @MockK lateinit var appConfigProvider: AppConfigProvider + @MockK lateinit var exposureResultStore: ExposureResultStore + + private val arguments: Task.Arguments = object : Task.Arguments {} + + private fun createTask() = RiskLevelTask( + riskLevels = riskLevels, + context = context, + enfClient = enfClient, + timeStamper = timeStamper, + backgroundModeStatus = backgroundModeStatus, + riskLevelData = riskLevelData, + appConfigProvider = appConfigProvider, + exposureResultStore = exposureResultStore + ) + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + + mockkObject(TimeVariables) + every { TimeVariables.getLastTimeDiagnosisKeysFromServerFetch() } returns null + + coEvery { appConfigProvider.getAppConfig() } returns configData + every { configData.identifier } returns "config-identifier" + + every { context.getSystemService(Context.CONNECTIVITY_SERVICE) } returns mockk<ConnectivityManager>().apply { + every { activeNetwork } returns mockk<Network>().apply { + every { getNetworkCapabilities(any()) } returns mockk<NetworkCapabilities>().apply { + every { hasCapability(any()) } returns true + } + } + } + + every { enfClient.isTracingEnabled } returns flowOf(true) + every { timeStamper.nowUTC } returns Instant.EPOCH + + every { riskLevelData.lastUsedConfigIdentifier = any() } just Runs + } + + @Test + fun `last used config ID is set after calculation`() = runBlockingTest { +// val task = createTask() +// task.run(arguments) +// +// verify { riskLevelData.lastUsedConfigIdentifier = "config-identifier" } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt deleted file mode 100644 index 845722a2c23174f01becc23d183dc78053e30226..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelsTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package de.rki.coronawarnapp.risk - -import com.google.android.gms.nearby.exposurenotification.ExposureSummary -import de.rki.coronawarnapp.appconfig.AppConfigProvider -import de.rki.coronawarnapp.server.protocols.internal.AttenuationDurationOuterClass -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK -import junit.framework.TestCase.assertEquals -import org.junit.Before -import org.junit.Test -import testhelpers.BaseTest - -class RiskLevelsTest : BaseTest() { - - @MockK lateinit var appConfigProvider: AppConfigProvider - private lateinit var riskLevels: DefaultRiskLevels - - @Before - fun setUp() { - MockKAnnotations.init(this) - riskLevels = DefaultRiskLevels(appConfigProvider) - } - - @Test - fun `is within defined level threshold`() { - riskLevels.withinDefinedLevelThreshold(2.0, 1, 3) shouldBe true - } - - @Test - fun `is not within defined level threshold`() { - riskLevels.withinDefinedLevelThreshold(4.0, 1, 3) shouldBe false - } - - @Test - fun `is within defined level threshold - edge cases`() { - riskLevels.withinDefinedLevelThreshold(1.0, 1, 3) shouldBe true - riskLevels.withinDefinedLevelThreshold(3.0, 1, 3) shouldBe true - } - - @Test - fun calculateRiskScoreZero() { - val riskScore = - riskLevels.calculateRiskScore( - buildAttenuationDuration(0.5, 0.5, 1.0), - buildSummary(0, 0, 0, 0) - ) - - assertEquals(0.0, riskScore) - } - - @Test - fun calculateRiskScoreLow() { - val riskScore = - riskLevels.calculateRiskScore( - buildAttenuationDuration(0.5, 0.5, 1.0), - buildSummary(156, 10, 10, 10) - ) - - assertEquals(124.8, riskScore) - } - - @Test - fun calculateRiskScoreMid() { - val riskScore = - riskLevels.calculateRiskScore( - buildAttenuationDuration(0.5, 0.5, 1.0), - buildSummary(256, 15, 15, 15) - ) - - assertEquals(307.2, riskScore) - } - - @Test - fun calculateRiskScoreHigh() { - val riskScore = - riskLevels.calculateRiskScore( - buildAttenuationDuration(0.5, 0.5, 1.0), - buildSummary(512, 30, 30, 30) - ) - - assertEquals(1228.8, riskScore) - } - - @Test - fun calculateRiskScoreMax() { - val riskScore = - riskLevels.calculateRiskScore( - buildAttenuationDuration(0.5, 0.5, 1.0), - buildSummary(4096, 30, 30, 30) - ) - - assertEquals(9830.4, riskScore) - } - - @Test - fun calculateRiskScoreCapped() { - val riskScore = - riskLevels.calculateRiskScore( - buildAttenuationDuration(0.5, 0.5, 1.0), - buildSummary(4096, 45, 45, 45) - ) - - assertEquals(9830.4, riskScore) - } - - private fun buildAttenuationDuration( - high: Double, - mid: Double, - low: Double, - norm: Int = 25, - offset: Int = 0 - ): AttenuationDurationOuterClass.AttenuationDuration { - return AttenuationDurationOuterClass.AttenuationDuration - .newBuilder() - .setRiskScoreNormalizationDivisor(norm) - .setDefaultBucketOffset(offset) - .setWeights( - AttenuationDurationOuterClass.Weights - .newBuilder() - .setHigh(high) - .setMid(mid) - .setLow(low) - .build() - ) - .build() - } - - private fun buildSummary( - maxRisk: Int = 0, - lowAttenuation: Int = 0, - midAttenuation: Int = 0, - highAttenuation: Int = 0 - ): ExposureSummary { - val intArray = IntArray(3) - intArray[0] = lowAttenuation - intArray[1] = midAttenuation - intArray[2] = highAttenuation - return ExposureSummary.ExposureSummaryBuilder() - .setMaximumRiskScore(maxRisk) - .setAttenuationDurations(intArray) - .build() - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt deleted file mode 100644 index b4a6ffd64b8a5ba18289f74bd437da2b0d63e769..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/ExposureSummaryRepositoryTest.kt +++ /dev/null @@ -1,112 +0,0 @@ -package de.rki.coronawarnapp.storage - -import com.google.android.gms.nearby.exposurenotification.ExposureSummary -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.unmockkAll -import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Before -import org.junit.Test -import java.util.UUID - -/** - * ExposureSummaryRepository test. - */ -class ExposureSummaryRepositoryTest { - - @MockK - private lateinit var dao: ExposureSummaryDao - private lateinit var repository: ExposureSummaryRepository - - @Before - fun setUp() { - MockKAnnotations.init(this) - repository = ExposureSummaryRepository(dao) - - mockkObject(InternalExposureNotificationClient) - coEvery { InternalExposureNotificationClient.asyncGetExposureSummary(any()) } returns buildSummary() - coEvery { InternalExposureNotificationClient.asyncIsEnabled() } returns true - - coEvery { dao.getExposureSummaryEntities() } returns listOf() - coEvery { dao.getLatestExposureSummary() } returns null - coEvery { dao.insertExposureSummaryEntity(any()) } returns 0 - } - - /** - * Test DAO is called. - */ - @Test - fun testGet() { - runBlocking { - repository.getExposureSummaryEntities() - - coVerify { - dao.getExposureSummaryEntities() - } - } - } - - /** - * Test DAO is called. - */ - @Test - fun testGetLatest() { - runBlocking { - val token = UUID.randomUUID().toString() - repository.getLatestExposureSummary(token) - - coVerify { - InternalExposureNotificationClient.asyncGetExposureSummary(token) - } - } - } - - /** - * Test DAO is called. - */ - @Test - fun testInsert() { - val es = mockk<ExposureSummary>() - every { es.attenuationDurationsInMinutes } returns intArrayOf(0) - every { es.daysSinceLastExposure } returns 1 - every { es.matchedKeyCount } returns 1 - every { es.maximumRiskScore } returns 0 - every { es.summationRiskScore } returns 0 - - runBlocking { - repository.insertExposureSummaryEntity(es) - - coVerify { - dao.insertExposureSummaryEntity(any()) - } - } - } - - private fun buildSummary( - maxRisk: Int = 0, - lowAttenuation: Int = 0, - midAttenuation: Int = 0, - highAttenuation: Int = 0 - ): ExposureSummary { - val intArray = IntArray(3) - intArray[0] = lowAttenuation - intArray[1] = midAttenuation - intArray[2] = highAttenuation - return ExposureSummary.ExposureSummaryBuilder() - .setMaximumRiskScore(maxRisk) - .setAttenuationDurations(intArray) - .build() - } - - @After - fun cleanUp() { - unmockkAll() - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt index 5b92e22d28896a9dd1471402f68e39496062d51d..e904e2159df50ee958e28227563b8832e56fc6d6 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/update/VersionComparatorTest.kt @@ -8,55 +8,55 @@ class VersionComparatorTest { @Test fun testVersionMajorOlder() { - val result = VersionComparator.isVersionOlder("1.0.0", "2.0.0") + val result = VersionComparator.isVersionOlder(1000000, 2000000) assertThat(result, `is`(true)) } @Test fun testVersionMinorOlder() { - val result = VersionComparator.isVersionOlder("1.0.0", "1.1.0") + val result = VersionComparator.isVersionOlder(1000000, 1010000) assertThat(result, `is`(true)) } @Test fun testVersionPatchOlder() { - val result = VersionComparator.isVersionOlder("1.0.1", "1.0.2") + val result = VersionComparator.isVersionOlder(1000100, 1000200) assertThat(result, `is`(true)) } @Test fun testVersionMajorNewer() { - val result = VersionComparator.isVersionOlder("2.0.0", "1.0.0") + val result = VersionComparator.isVersionOlder(2000000, 1000000) assertThat(result, `is`(false)) } @Test fun testVersionMinorNewer() { - val result = VersionComparator.isVersionOlder("1.2.0", "1.1.0") + val result = VersionComparator.isVersionOlder(1020000, 1010000) assertThat(result, `is`(false)) } @Test fun testVersionPatchNewer() { - val result = VersionComparator.isVersionOlder("1.0.3", "1.0.2") + val result = VersionComparator.isVersionOlder(1000300, 1000200) assertThat(result, `is`(false)) } @Test fun testSameVersion() { - val result = VersionComparator.isVersionOlder("1.0.1", "1.0.1") + val result = VersionComparator.isVersionOlder(1000100, 1000100) assertThat(result, `is`(false)) } @Test fun testIfMajorIsNewerButMinorSmallerNumber() { - val result = VersionComparator.isVersionOlder("3.1.0", "1.2.0") + val result = VersionComparator.isVersionOlder(3010000, 1020000) assertThat(result, `is`(false)) } @Test fun testIfMinorIsNewerButPatchSmallerNumber() { - val result = VersionComparator.isVersionOlder("1.3.1", "1.2.4") + val result = VersionComparator.isVersionOlder(1030100, 1020400) assertThat(result, `is`(false)) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt index 003239700b5cac02e2fc7e0bbcb2e0dccf801133..ec97022501f8487302e33279ca3a5cc1baefac4b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/BackgroundModeStatusTest.kt @@ -10,18 +10,18 @@ import io.mockk.impl.annotations.MockK import io.mockk.mockkObject import io.mockk.verify import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest +import testhelpers.coroutines.runBlockingTest2 +import testhelpers.coroutines.test class BackgroundModeStatusTest : BaseTest() { @MockK lateinit var context: Context - private val scope: CoroutineScope = TestCoroutineScope() @BeforeEach fun setup() { @@ -34,23 +34,21 @@ class BackgroundModeStatusTest : BaseTest() { clearAllMocks() } - private fun createInstance(): BackgroundModeStatus = BackgroundModeStatus( + private fun createInstance(scope: CoroutineScope): BackgroundModeStatus = BackgroundModeStatus( context = context, appScope = scope ) @Test - fun `init is sideeffect free and lazy`() { - createInstance() + fun `init is sideeffect free and lazy`() = runBlockingTest2(ignoreActive = true) { + createInstance(scope = this) verify { context wasNot Called } } @Test - fun isAutoModeEnabled() = runBlockingTest { - every { ConnectivityHelper.autoModeEnabled(any()) } returnsMany listOf( - true, false, true, false - ) - createInstance().apply { + fun isAutoModeEnabled() = runBlockingTest2(ignoreActive = true) { + every { ConnectivityHelper.autoModeEnabled(any()) } returnsMany listOf(true, false, true, false) + createInstance(scope = this).apply { isAutoModeEnabled.first() shouldBe true isAutoModeEnabled.first() shouldBe false isAutoModeEnabled.first() shouldBe true @@ -58,14 +56,60 @@ class BackgroundModeStatusTest : BaseTest() { } @Test - fun isBackgroundRestricted() = runBlockingTest { - every { ConnectivityHelper.isBackgroundRestricted(any()) } returnsMany listOf( - false, true, false - ) - createInstance().apply { + fun `isAutoModeEnabled is shared but not cached`() = runBlockingTest2(ignoreActive = true) { + every { ConnectivityHelper.autoModeEnabled(any()) } returnsMany listOf(true, false, true, false) + + val instance = createInstance(scope = this) + + val collector1 = instance.isAutoModeEnabled.test(tag = "1", startOnScope = this) + val collector2 = instance.isAutoModeEnabled.test(tag = "2", startOnScope = this) + + delay(500) + + collector1.latestValue shouldBe true + collector2.latestValue shouldBe true + + collector1.cancel() + collector2.cancel() + + advanceUntilIdle() + + verify(exactly = 1) { ConnectivityHelper.autoModeEnabled(any()) } + + instance.isAutoModeEnabled.first() shouldBe false + } + + @Test + fun isBackgroundRestricted() = runBlockingTest2(ignoreActive = true) { + every { ConnectivityHelper.isBackgroundRestricted(any()) } returnsMany listOf(false, true, false) + createInstance(scope = this).apply { isBackgroundRestricted.first() shouldBe false isBackgroundRestricted.first() shouldBe true isBackgroundRestricted.first() shouldBe false } } + + @Test + fun `isBackgroundRestricted is shared but not cached`() = runBlockingTest2(ignoreActive = true) { + every { ConnectivityHelper.isBackgroundRestricted(any()) } returnsMany listOf(true, false, true, false) + + val instance = createInstance(scope = this) + + val collector1 = instance.isBackgroundRestricted.test(tag = "1", startOnScope = this) + val collector2 = instance.isBackgroundRestricted.test(tag = "2", startOnScope = this) + + delay(500) + + collector1.latestValue shouldBe true + collector2.latestValue shouldBe true + + collector1.cancel() + collector2.cancel() + + advanceUntilIdle() + + verify(exactly = 1) { ConnectivityHelper.isBackgroundRestricted(any()) } + + instance.isBackgroundRestricted.first() shouldBe false + } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt deleted file mode 100644 index 110d8cbeed2aef3b469c0355c3a848025f1faadf..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/GoogleAPIVersionTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package de.rki.coronawarnapp.util - -import com.google.android.gms.common.api.ApiException -import com.google.android.gms.common.api.CommonStatusCodes.API_NOT_CONNECTED -import com.google.android.gms.common.api.Status -import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient -import io.kotest.matchers.shouldBe -import io.mockk.Called -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockkObject -import io.mockk.unmockkObject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runBlockingTest -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows - -@ExperimentalCoroutinesApi -internal class GoogleAPIVersionTest { - - private lateinit var classUnderTest: GoogleAPIVersion - - @BeforeEach - fun setUp() { - mockkObject(InternalExposureNotificationClient) - classUnderTest = GoogleAPIVersion() - } - - @AfterEach - fun tearDown() { - unmockkObject(InternalExposureNotificationClient) - } - - @Test - fun `isAbove API v16 is true for v17`() { - coEvery { InternalExposureNotificationClient.getVersion() } returns 17000000L - - runBlockingTest { - classUnderTest.isAtLeast(GoogleAPIVersion.V16) shouldBe true - } - } - - @Test - fun `isAbove API v16 is false for v15`() { - coEvery { InternalExposureNotificationClient.getVersion() } returns 15000000L - - runBlockingTest { - classUnderTest.isAtLeast(GoogleAPIVersion.V16) shouldBe false - } - } - - @Test - fun `isAbove API v16 throws IllegalArgument for invalid version`() { - assertThrows<IllegalArgumentException> { - runBlockingTest { - classUnderTest.isAtLeast(1L) - } - coVerify { - InternalExposureNotificationClient.getVersion() wasNot Called - } - } - } - - @Test - fun `isAbove API v16 false when APIException for too low version`() { - coEvery { InternalExposureNotificationClient.getVersion() } throws - ApiException(Status(API_NOT_CONNECTED)) - - runBlockingTest { - classUnderTest.isAtLeast(GoogleAPIVersion.V16) shouldBe false - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt index 3de7010f7bf7105aa25a8738dbcd6772f0c140b4..9b23f5eca4035d5da1266bdc020f05dc3bb131fe 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/flow/HotDataFlowTest.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.util.flow +import de.rki.coronawarnapp.util.mutate import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.types.instanceOf @@ -84,7 +85,7 @@ class HotDataFlowTest : BaseTest() { ) testScope.apply { - runBlockingTest2(permanentJobs = true) { + runBlockingTest2(ignoreActive = true) { hotData.data.first() shouldBe "Test" hotData.data.first() shouldBe "Test" } @@ -107,7 +108,7 @@ class HotDataFlowTest : BaseTest() { ) testScope.apply { - runBlockingTest2(permanentJobs = true) { + runBlockingTest2(ignoreActive = true) { hotData.data.first() shouldBe "Test" hotData.data.first() shouldBe "Test" } @@ -150,6 +151,48 @@ class HotDataFlowTest : BaseTest() { coVerify(exactly = 1) { valueProvider.invoke(any()) } } + data class TestData( + val number: Long = 1 + ) + + @Test + fun `check multi threading value updates with more complex data`() { + val testScope = TestCoroutineScope() + val valueProvider = mockk<suspend CoroutineScope.() -> Map<String, TestData>>() + coEvery { valueProvider.invoke(any()) } returns mapOf("data" to TestData()) + + val hotData = HotDataFlow( + loggingTag = "tag", + scope = testScope, + startValueProvider = valueProvider, + sharingBehavior = SharingStarted.Lazily + ) + + val testCollector = hotData.data.test(startOnScope = testScope) + testCollector.silent = true + + (1..10).forEach { _ -> + thread { + (1..400).forEach { _ -> + hotData.updateSafely { + mutate { + this["data"] = getValue("data").copy( + number = getValue("data").number + 1 + ) + } + } + } + } + } + + runBlocking { + testCollector.await { list, l -> list.size == 4001 } + testCollector.latestValues.map { it.values.single().number } shouldBe (1L..4001L).toList() + } + + coVerify(exactly = 1) { valueProvider.invoke(any()) } + } + @Test fun `only emit new values if they actually changed updates`() { val testScope = TestCoroutineScope() @@ -188,10 +231,10 @@ class HotDataFlowTest : BaseTest() { sharingBehavior = SharingStarted.Lazily ) - testScope.runBlockingTest2(permanentJobs = true) { - val sub1 = hotData.data.test().start(scope = this) - val sub2 = hotData.data.test().start(scope = this) - val sub3 = hotData.data.test().start(scope = this) + testScope.runBlockingTest2(ignoreActive = true) { + val sub1 = hotData.data.test(tag = "sub1", startOnScope = this) + val sub2 = hotData.data.test(tag = "sub2", startOnScope = this) + val sub3 = hotData.data.test(tag = "sub3", startOnScope = this) hotData.updateSafely { "A" } hotData.updateSafely { "B" } @@ -208,6 +251,49 @@ class HotDataFlowTest : BaseTest() { coVerify(exactly = 1) { valueProvider.invoke(any()) } } + @Test + fun `update queue is wiped on completion`() = runBlockingTest2(ignoreActive = true) { + val valueProvider = mockk<suspend CoroutineScope.() -> Long>() + coEvery { valueProvider.invoke(any()) } returns 1 + + val hotData = HotDataFlow( + loggingTag = "tag", + scope = this, + coroutineContext = this.coroutineContext, + startValueProvider = valueProvider, + sharingBehavior = SharingStarted.WhileSubscribed(replayExpirationMillis = 0) + ) + + val testCollector1 = hotData.data.test(tag = "collector1", startOnScope = this) + testCollector1.silent = false + + (1..10).forEach { _ -> + hotData.updateSafely { + this + 1L + } + } + + advanceUntilIdle() + + testCollector1.await { list, _ -> list.size == 11 } + testCollector1.latestValues shouldBe (1L..11L).toList() + + testCollector1.cancel() + testCollector1.awaitFinal() + + val testCollector2 = hotData.data.test(tag = "collector2", startOnScope = this) + testCollector2.silent = false + + advanceUntilIdle() + + testCollector2.cancel() + testCollector2.awaitFinal() + + testCollector2.latestValues shouldBe listOf(1L) + + coVerify(exactly = 2) { valueProvider.invoke(any()) } + } + @Test fun `blocking update is actually blocking`() = runBlocking { val testScope = TestCoroutineScope() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt index c8310865349f45752a771fee79b85b6bfa4e74cc..c7f8e7b15f275e2a9b405de92603fa1531f8f7db 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/formatter/FormatterSubmissionHelperTest.kt @@ -8,8 +8,8 @@ import android.text.style.ForegroundColorSpan import android.view.View import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.NetworkRequestWrapper import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK @@ -74,52 +74,76 @@ class FormatterSubmissionHelperTest { every { context.getDrawable(R.drawable.ic_test_result_illustration_negative) } returns drawable } - private fun formatTestResultSpinnerVisibleBase(oUiStateState: ApiRequestState?, iResult: Int) { - val result = formatTestResultSpinnerVisible(uiStateState = oUiStateState) + private fun formatTestResultSpinnerVisibleBase( + oUiStateState: NetworkRequestWrapper<DeviceUIState, Throwable>?, + iResult: Int + ) { + val result = formatTestResultSpinnerVisible(uiState = oUiStateState) assertThat(result, `is`(iResult)) } - private fun formatTestResultVisibleBase(oUiStateState: ApiRequestState?, iResult: Int) { - val result = formatTestResultVisible(uiStateState = oUiStateState) + private fun formatTestResultVisibleBase( + oUiStateState: NetworkRequestWrapper<DeviceUIState, Throwable>?, + iResult: Int + ) { + val result = formatTestResultVisible(uiState = oUiStateState) assertThat(result, `is`(iResult)) } - private fun formatTestResultStatusTextBase(oUiState: DeviceUIState?, iResult: String) { + private fun formatTestResultStatusTextBase( + oUiState: NetworkRequestWrapper<DeviceUIState, Throwable>?, + iResult: String + ) { val result = formatTestResultStatusText(uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestResultStatusColorBase(oUiState: DeviceUIState?, iResult: Int) { + private fun formatTestResultStatusColorBase( + oUiState: NetworkRequestWrapper<DeviceUIState, Throwable>?, + iResult: Int + ) { val result = formatTestResultStatusColor(uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestStatusIconBase(oUiState: DeviceUIState?) { + private fun formatTestStatusIconBase(oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?) { val result = formatTestStatusIcon(uiState = oUiState) assertThat(result, `is`(drawable)) } - private fun formatTestResultPendingStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) { + private fun formatTestResultPendingStepsVisibleBase( + oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?, + iResult: Int + ) { val result = formatTestResultPendingStepsVisible(uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestResultNegativeStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) { + private fun formatTestResultNegativeStepsVisibleBase( + oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?, + iResult: Int + ) { val result = formatTestResultNegativeStepsVisible(uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestResultPositiveStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) { + private fun formatTestResultPositiveStepsVisibleBase( + oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?, + iResult: Int + ) { val result = formatTestResultPositiveStepsVisible(uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestResultInvalidStepsVisibleBase(oUiState: DeviceUIState?, iResult: Int) { + private fun formatTestResultInvalidStepsVisibleBase( + oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?, + iResult: Int + ) { val result = formatTestResultInvalidStepsVisible(uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestResultBase(oUiState: DeviceUIState?) { + private fun formatTestResultBase(oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?) { mockkConstructor(SpannableStringBuilder::class) val spannableStringBuilder1 = @@ -147,19 +171,19 @@ class FormatterSubmissionHelperTest { fun formatTestResultSpinnerVisible() { formatTestResultSpinnerVisibleBase(oUiStateState = null, iResult = View.VISIBLE) formatTestResultSpinnerVisibleBase( - oUiStateState = ApiRequestState.FAILED, + oUiStateState = NetworkRequestWrapper.RequestFailed(mockk()), iResult = View.VISIBLE ) formatTestResultSpinnerVisibleBase( - oUiStateState = ApiRequestState.IDLE, + oUiStateState = NetworkRequestWrapper.RequestIdle, iResult = View.VISIBLE ) formatTestResultSpinnerVisibleBase( - oUiStateState = ApiRequestState.STARTED, + oUiStateState = NetworkRequestWrapper.RequestStarted, iResult = View.VISIBLE ) formatTestResultSpinnerVisibleBase( - oUiStateState = ApiRequestState.SUCCESS, + oUiStateState = NetworkRequestWrapper.RequestSuccessful(mockk()), iResult = View.GONE ) } @@ -167,10 +191,13 @@ class FormatterSubmissionHelperTest { @Test fun formatTestResultVisible() { formatTestResultVisibleBase(oUiStateState = null, iResult = View.GONE) - formatTestResultVisibleBase(oUiStateState = ApiRequestState.FAILED, iResult = View.GONE) - formatTestResultVisibleBase(oUiStateState = ApiRequestState.IDLE, iResult = View.GONE) - formatTestResultVisibleBase(oUiStateState = ApiRequestState.STARTED, iResult = View.GONE) - formatTestResultVisibleBase(oUiStateState = ApiRequestState.SUCCESS, iResult = View.VISIBLE) + formatTestResultVisibleBase(oUiStateState = NetworkRequestWrapper.RequestFailed(mockk()), iResult = View.GONE) + formatTestResultVisibleBase(oUiStateState = NetworkRequestWrapper.RequestIdle, iResult = View.GONE) + formatTestResultVisibleBase(oUiStateState = NetworkRequestWrapper.RequestStarted, iResult = View.GONE) + formatTestResultVisibleBase( + oUiStateState = NetworkRequestWrapper.RequestSuccessful(mockk()), + iResult = View.VISIBLE + ) } @Test @@ -180,35 +207,35 @@ class FormatterSubmissionHelperTest { iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.PAIRED_NEGATIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), iResult = context.getString(R.string.test_result_card_status_negative) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.PAIRED_ERROR, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.PAIRED_NO_RESULT, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.PAIRED_POSITIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), iResult = context.getString(R.string.test_result_card_status_positive) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), iResult = context.getString(R.string.test_result_card_status_positive) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.SUBMITTED_FINAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.SUBMITTED_INITIAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = DeviceUIState.UNPAIRED, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), iResult = context.getString(R.string.test_result_card_status_invalid) ) } @@ -220,35 +247,35 @@ class FormatterSubmissionHelperTest { iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.PAIRED_NEGATIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), iResult = context.getColor(R.color.colorTextSemanticGreen) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.PAIRED_ERROR, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.PAIRED_NO_RESULT, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.PAIRED_POSITIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.SUBMITTED_FINAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.SUBMITTED_INITIAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), iResult = context.getColor(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = DeviceUIState.UNPAIRED, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), iResult = context.getColor(R.color.colorTextSemanticRed) ) } @@ -256,49 +283,49 @@ class FormatterSubmissionHelperTest { @Test fun formatTestStatusIcon() { formatTestStatusIconBase(oUiState = null) - formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_NEGATIVE) - formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_ERROR) - formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_NO_RESULT) - formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_POSITIVE) - formatTestStatusIconBase(oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN) - formatTestStatusIconBase(oUiState = DeviceUIState.SUBMITTED_FINAL) - formatTestStatusIconBase(oUiState = DeviceUIState.SUBMITTED_INITIAL) - formatTestStatusIconBase(oUiState = DeviceUIState.UNPAIRED) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL)) + formatTestStatusIconBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED)) } @Test fun formatTestResultPendingStepsVisible() { formatTestResultPendingStepsVisibleBase(oUiState = null, iResult = View.GONE) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NEGATIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), iResult = View.GONE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_ERROR, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), iResult = View.GONE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NO_RESULT, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), iResult = View.VISIBLE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), iResult = View.GONE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), iResult = View.GONE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_FINAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), iResult = View.GONE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_INITIAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), iResult = View.GONE ) formatTestResultPendingStepsVisibleBase( - oUiState = DeviceUIState.UNPAIRED, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), iResult = View.GONE ) } @@ -307,35 +334,35 @@ class FormatterSubmissionHelperTest { fun formatTestResultNegativeStepsVisible() { formatTestResultNegativeStepsVisibleBase(oUiState = null, iResult = View.GONE) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NEGATIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), iResult = View.VISIBLE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_ERROR, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), iResult = View.GONE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NO_RESULT, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), iResult = View.GONE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), iResult = View.GONE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), iResult = View.GONE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_FINAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), iResult = View.GONE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_INITIAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), iResult = View.GONE ) formatTestResultNegativeStepsVisibleBase( - oUiState = DeviceUIState.UNPAIRED, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), iResult = View.GONE ) } @@ -344,35 +371,35 @@ class FormatterSubmissionHelperTest { fun formatTestResultPositiveStepsVisible() { formatTestResultPositiveStepsVisibleBase(oUiState = null, iResult = View.GONE) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NEGATIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), iResult = View.GONE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_ERROR, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), iResult = View.GONE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NO_RESULT, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), iResult = View.GONE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), iResult = View.VISIBLE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), iResult = View.VISIBLE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_FINAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), iResult = View.GONE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_INITIAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), iResult = View.GONE ) formatTestResultPositiveStepsVisibleBase( - oUiState = DeviceUIState.UNPAIRED, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), iResult = View.GONE ) } @@ -381,35 +408,35 @@ class FormatterSubmissionHelperTest { fun formatTestResultInvalidStepsVisible() { formatTestResultInvalidStepsVisibleBase(oUiState = null, iResult = View.GONE) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NEGATIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), iResult = View.GONE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_ERROR, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), iResult = View.VISIBLE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_NO_RESULT, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), iResult = View.GONE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), iResult = View.GONE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), iResult = View.GONE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_FINAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), iResult = View.GONE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.SUBMITTED_INITIAL, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), iResult = View.GONE ) formatTestResultInvalidStepsVisibleBase( - oUiState = DeviceUIState.UNPAIRED, + oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), iResult = View.GONE ) } @@ -417,14 +444,14 @@ class FormatterSubmissionHelperTest { @Test fun formatTestResult() { formatTestResultBase(oUiState = null) - formatTestResultBase(oUiState = DeviceUIState.PAIRED_NEGATIVE) - formatTestResultBase(oUiState = DeviceUIState.PAIRED_ERROR) - formatTestResultBase(oUiState = DeviceUIState.PAIRED_NO_RESULT) - formatTestResultBase(oUiState = DeviceUIState.PAIRED_POSITIVE) - formatTestResultBase(oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN) - formatTestResultBase(oUiState = DeviceUIState.SUBMITTED_FINAL) - formatTestResultBase(oUiState = DeviceUIState.SUBMITTED_INITIAL) - formatTestResultBase(oUiState = DeviceUIState.UNPAIRED) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL)) + formatTestResultBase(oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED)) } @After diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt index 468877bbaaa72b0dfc9f3177073a15d4b6e7024c..88976e539bab83ed2fb7aa482ee3f35e26557652 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/worker/WorkerBinderTest.kt @@ -6,7 +6,9 @@ import dagger.Module import dagger.Provides import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationSender +import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.playbook.Playbook +import de.rki.coronawarnapp.risk.ExposureResultStore import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.di.AssistedInjectModule import io.github.classgraph.ClassGraph @@ -87,4 +89,11 @@ class MockProvider { @Provides fun taskController(): TaskController = mockk() + + // For ExposureStateUpdateWorker + @Provides + fun enfClient(): ENFClient = mockk() + + @Provides + fun exposureSummaryRepository(): ExposureResultStore = mockk() } diff --git a/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt b/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt index 3fc56ce9a503dcd65c2afb6f91e657557776c2d8..5a19fa308a2f5ec037373528c26e0ea099810aa1 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/coroutines/FlowTest.kt @@ -16,14 +16,17 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.test.TestCoroutineScope +import kotlinx.coroutines.withTimeout +import org.joda.time.Duration import timber.log.Timber fun <T> Flow<T>.test( tag: String? = null, - startOnScope: CoroutineScope -): TestCollector<T> = test(tag ?: "FlowTest").start(scope = startOnScope) + startOnScope: CoroutineScope = TestCoroutineScope() +): TestCollector<T> = createTest(tag ?: "FlowTest").start(scope = startOnScope) -fun <T> Flow<T>.test( +fun <T> Flow<T>.createTest( tag: String? = null ): TestCollector<T> = TestCollector(this, tag ?: "FlowTest") @@ -74,9 +77,14 @@ class TestCollector<T>( val latestValues: List<T> get() = collectedValues - fun await(condition: (List<T>, T) -> Boolean): T = runBlocking { - emissions().first { - condition(collectedValues, it) + fun await( + timeout: Duration = Duration.standardSeconds(10), + condition: (List<T>, T) -> Boolean + ): T = runBlocking { + withTimeout(timeMillis = timeout.millis) { + emissions().first { + condition(collectedValues, it) + } } } @@ -95,6 +103,8 @@ class TestCollector<T>( } fun cancel() { + if (job.isCompleted) throw IllegalStateException("Flow is already canceled.") + runBlocking { job.cancelAndJoin() } diff --git a/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt b/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt index 375adf36e462e42753b985160ba5062cbcdc3397..68d1ba19c174d4bb013eb4bcba609566a31cafdf 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/coroutines/TestExtensions.kt @@ -11,10 +11,10 @@ import kotlin.coroutines.EmptyCoroutineContext @ExperimentalCoroutinesApi // Since 1.2.1, tentatively till 1.3.0 fun TestCoroutineScope.runBlockingTest2( - permanentJobs: Boolean = false, + ignoreActive: Boolean = false, block: suspend TestCoroutineScope.() -> Unit ): Unit = runBlockingTest2( - ignoreActive = permanentJobs, + ignoreActive = ignoreActive, context = coroutineContext, testBody = block ) diff --git a/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt b/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt index d0ed11f4ec929f0ba4ec6ff7e89bfc832fef9f01..321a733ae210c6fa0dd2600912032fdbb4d08159 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/gms/MockGMSTask.kt @@ -8,12 +8,12 @@ import io.mockk.mockk object MockGMSTask { fun <T> forError(error: Exception): Task<T> = mockk<Task<T>>().apply { - every { addOnSuccessListener(any()) } answers { + every { addOnFailureListener(any()) } answers { val listener = arg<OnFailureListener>(0) listener.onFailure(error) this@apply } - every { addOnFailureListener(any()) } returns this + every { addOnSuccessListener(any()) } returns this } fun <T> forValue(value: T): Task<T> = mockk<Task<T>>().apply { diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt index 33d46578e1a956c645d549edc3f793d2fc9b27df..0b855e1b9938ed9d47650a26c85ba5bbe9ba4927 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockFlowPreference.kt @@ -16,5 +16,6 @@ fun <T> mockFlowPreference( val updateCall = arg<(T) -> T>(0) flow.value = updateCall(flow.value) } + return instance } diff --git a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt index a6294b00f54bcdbc96ab6bd18195389fc1a79e51..c094e560e5401f80bbf7bedff36af3537cc8a67e 100644 --- a/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt +++ b/Corona-Warn-App/src/test/java/testhelpers/preferences/MockSharedPreferences.kt @@ -3,6 +3,7 @@ package testhelpers.preferences import android.content.SharedPreferences class MockSharedPreferences : SharedPreferences { + private val listeners = mutableListOf<SharedPreferences.OnSharedPreferenceChangeListener>() private val dataMap = mutableMapOf<String, Any>() val dataMapPeek: Map<String, Any> get() = dataMap.toMap() @@ -36,12 +37,12 @@ class MockSharedPreferences : SharedPreferences { dataMap.putAll(newData) } - override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) { - throw NotImplementedError() + override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + listeners.add(listener) } - override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener?) { - throw NotImplementedError() + override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) { + listeners.remove(listener) } private fun createEditor( diff --git a/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json new file mode 100644 index 0000000000000000000000000000000000000000..405aa42953a5f7a37a9ae6e98748fb95bafc9ee0 --- /dev/null +++ b/Corona-Warn-App/src/test/resources/exposure-windows-risk-calculation.json @@ -0,0 +1,1065 @@ +{ + "__comment__": "JSON has been generated from YAML, see README", + "defaultRiskCalculationConfiguration": { + "minutesAtAttenuationFilters": [ + { + "attenuationRange": { + "min": 0, + "max": 73, + "maxExclusive": true + }, + "dropIfMinutesInRange": { + "min": 0, + "max": 10, + "maxExclusive": true + } + } + ], + "trlFilters": [ + { + "dropIfTrlInRange": { + "min": 1, + "max": 2 + } + } + ], + "minutesAtAttenuationWeights": [ + { + "attenuationRange": { + "min": 0, + "max": 55, + "maxExclusive": true + }, + "weight": 1 + }, + { + "attenuationRange": { + "min": 55, + "max": 63, + "maxExclusive": true + }, + "weight": 0.5 + } + ], + "normalizedTimePerEWToRiskLevelMapping": [ + { + "normalizedTimeRange": { + "min": 0, + "max": 15, + "maxExclusive": true + }, + "riskLevel": 1 + }, + { + "normalizedTimeRange": { + "min": 15, + "max": 9999 + }, + "riskLevel": 2 + } + ], + "normalizedTimePerDayToRiskLevelMapping": [ + { + "normalizedTimeRange": { + "min": 0, + "max": 15, + "maxExclusive": true + }, + "riskLevel": 1 + }, + { + "normalizedTimeRange": { + "min": 15, + "max": 9999 + }, + "riskLevel": 2 + } + ], + "trlEncoding": { + "infectiousnessOffsetStandard": 0, + "infectiousnessOffsetHigh": 4, + "reportTypeOffsetRecursive": 4, + "reportTypeOffsetSelfReport": 3, + "reportTypeOffsetConfirmedClinicalDiagnosis": 2, + "reportTypeOffsetConfirmedTest": 1 + }, + "transmissionRiskLevelMultiplier": 0.2 + }, + "testCases": [ + { + "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (< 10 minutes)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 299 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "keep Exposure Windows that match minutesAtAttenuationFilters (>= 10 minutes)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "drop Exposure Windows that do not match minutesAtAttenuationFilters (>= 73 dB)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 73, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 73, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "keep Exposure Windows that match minutesAtAttenuationFilters (< 73 dB)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 72, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 72, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "drop Exposure Windows that do not match trlFilters (<= 2)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "keep Exposure Windows that match trlFilters (> 2)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "identify Exposure Window as Low Risk based on normalizedTime (< 15)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 1, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 299 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "identify Exposure Window as High Risk based on normalizedTime (>= 15)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 1, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 1, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 1 + }, + { + "description": "identify the most recent date with Low Risk", + "exposureWindows": [ + { + "ageInDays": 3, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 2, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 4, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 3, + "expAgeOfMostRecentDateWithLowRisk": 2, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 3, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with Low Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with Low Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 2, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with Low Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 2, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with Low Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 2, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 2, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 2, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "determine High Risk in total if there are sufficient Exposure Windows with a Low Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 1 + }, + { + "description": "identify the most recent date with High Risk", + "exposureWindows": [ + { + "ageInDays": 3, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + }, + { + "ageInDays": 2, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + }, + { + "ageInDays": 4, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 2, + "expTotalMinimumDistinctEncountersWithHighRisk": 3, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 3 + }, + { + "description": "count Exposure Windows with same Date/TRL/CallibrationConfidence only once towards distinct encounters with High Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + }, + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 1, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 1 + }, + { + "description": "count Exposure Windows with same Date/TRL but different CallibrationConfidence separately towards distinct encounters with High Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + }, + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 1, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 2, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 1 + }, + { + "description": "count Exposure Windows with same Date/CallibrationConfidence but different TRL separately towards distinct encounters with High Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + }, + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 2, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 1 + }, + { + "description": "count Exposure Windows with same TRL/CallibrationConfidence but different Date separately towards distinct encounters with High Risk", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + }, + { + "ageInDays": 2, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 2, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 2 + }, + { + "description": "determine High Risk in total if there is at least one Exposure Window with High Risk", + "exposureWindows": [ + { + "ageInDays": 2, + "reportType": 3, + "infectiousness": 1, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + }, + { + "ageInDays": 1, + "reportType": 4, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + }, + { + "minAttenuation": 30, + "typicalAttenuation": 25, + "secondsSinceLastScan": 420 + } + ] + } + ], + "expTotalRiskLevel": 2, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expAgeOfMostRecentDateWithLowRisk": 2, + "expAgeOfMostRecentDateWithHighRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 1, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 1 + }, + { + "description": "handle empty set of Exposure Windows", + "exposureWindows": [], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": null, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "handle empty set of Scan Instances (should never happen)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 2, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 0, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": null, + "expAgeOfMostRecentDateWithHighRisk": null, + "expNumberOfDaysWithLowRisk": 0, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "handle a typicalAttenuation: of zero (should never happen)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 0, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 70, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + }, + { + "description": "handle secondsSinceLastScan of zero (should never happen)", + "exposureWindows": [ + { + "ageInDays": 1, + "reportType": 3, + "infectiousness": 2, + "calibrationConfidence": 0, + "scanInstances": [ + { + "minAttenuation": 70, + "typicalAttenuation": 25, + "secondsSinceLastScan": 0 + }, + { + "minAttenuation": 70, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + }, + { + "minAttenuation": 70, + "typicalAttenuation": 25, + "secondsSinceLastScan": 300 + } + ] + } + ], + "expTotalRiskLevel": 1, + "expTotalMinimumDistinctEncountersWithLowRisk": 1, + "expTotalMinimumDistinctEncountersWithHighRisk": 0, + "expAgeOfMostRecentDateWithLowRisk": 1, + "expAgeOfMostRecentDateWithHighRisk": null, + "expNumberOfDaysWithLowRisk": 1, + "expNumberOfDaysWithHighRisk": 0 + } + ] +} \ No newline at end of file diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt index 42ad7c156bb1ceff1fb4a3603f1849edb92f444c..41ea40f28f2074b96d99fb6abd733be53881ce85 100644 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt +++ b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/debugoptions/ui/DebugOptionsFragmentViewModelTest.kt @@ -3,8 +3,6 @@ package de.rki.coronawarnapp.test.debugoptions.ui import android.content.Context import androidx.lifecycle.Observer import de.rki.coronawarnapp.environment.EnvironmentSetup -import de.rki.coronawarnapp.storage.TestSettings -import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.test.api.ui.EnvironmentState import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -30,8 +28,6 @@ class DebugOptionsFragmentViewModelTest : BaseTest() { @MockK private lateinit var environmentSetup: EnvironmentSetup @MockK private lateinit var context: Context - @MockK private lateinit var testSettings: TestSettings - @MockK lateinit var taskController: TaskController private var currentEnvironment = EnvironmentSetup.Type.DEV @@ -61,9 +57,7 @@ class DebugOptionsFragmentViewModelTest : BaseTest() { private fun createViewModel(): DebugOptionsFragmentViewModel = DebugOptionsFragmentViewModel( context = context, - taskController = taskController, envSetup = environmentSetup, - testSettings = testSettings, dispatcherProvider = TestDispatcherProvider ) diff --git a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt b/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt deleted file mode 100644 index 0413a8515b6db83ef95f76efb6e8af70b46238e7..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/testDeviceForTesters/java/de/rki/coronawarnapp/test/risklevel/ui/TestRiskLevelCalculationFragmentCWAViewModelTest.kt +++ /dev/null @@ -1,90 +0,0 @@ -package de.rki.coronawarnapp.test.risklevel.ui - -import android.content.Context -import androidx.lifecycle.SavedStateHandle -import com.google.android.gms.nearby.exposurenotification.ExposureNotificationClient -import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository -import de.rki.coronawarnapp.nearby.ENFClient -import de.rki.coronawarnapp.risk.RiskLevels -import de.rki.coronawarnapp.task.TaskController -import de.rki.coronawarnapp.ui.tracing.card.TracingCardStateProvider -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.clearAllMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.mockk -import kotlinx.coroutines.flow.flowOf -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import testhelpers.BaseTest -import testhelpers.TestDispatcherProvider -import testhelpers.extensions.CoroutinesTestExtension -import testhelpers.extensions.InstantExecutorExtension - -@ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class) -class TestRiskLevelCalculationFragmentCWAViewModelTest : BaseTest() { - - @MockK lateinit var context: Context - @MockK lateinit var savedStateHandle: SavedStateHandle - @MockK lateinit var enfClient: ENFClient - @MockK lateinit var exposureNotificationClient: ExposureNotificationClient - @MockK lateinit var keyCacheRepository: KeyCacheRepository - @MockK lateinit var tracingCardStateProvider: TracingCardStateProvider - @MockK lateinit var taskController: TaskController - @MockK lateinit var riskLevels: RiskLevels - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - coEvery { keyCacheRepository.clear() } returns Unit - every { enfClient.internalClient } returns exposureNotificationClient - every { tracingCardStateProvider.state } returns flowOf(mockk()) - every { taskController.submit(any()) } just Runs - } - - @AfterEach - fun teardown() { - clearAllMocks() - } - - private fun createViewModel(exampleArgs: String? = null): TestRiskLevelCalculationFragmentCWAViewModel = - TestRiskLevelCalculationFragmentCWAViewModel( - handle = savedStateHandle, - exampleArg = exampleArgs, - context = context, - enfClient = enfClient, - keyCacheRepository = keyCacheRepository, - tracingCardStateProvider = tracingCardStateProvider, - dispatcherProvider = TestDispatcherProvider, - riskLevels = riskLevels, - taskController = taskController - ) - - @Test - fun `action clearDiagnosisKeys calls the keyCacheRepo`() { - val vm = createViewModel() - - vm.clearKeyCache() - - coVerify(exactly = 1) { keyCacheRepository.clear() } - } - - @Test - fun `action scanLocalQRCodeAndProvide, triggers event`() { - val vm = createViewModel() - - vm.startLocalQRCodeScanEvent.value shouldBe null - - vm.scanLocalQRCodeAndProvide() - - vm.startLocalQRCodeScanEvent.value shouldBe Unit - } -}