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/diagnosiskeys/storage/KeyCacheDatabaseTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabaseTest.kt index 62230915dc95eda085f563425251a52969ffbb52..fc2bfbe622e5377d36ff584d2c3afbf9e4355772 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabaseTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/diagnosiskeys/storage/KeyCacheDatabaseTest.kt @@ -40,7 +40,7 @@ class KeyCacheDatabaseTest { dao.insertEntry(keyDay) dao.insertEntry(keyHour) - dao.getAllEntries() shouldBe listOf(keyDay, keyHour) + dao.allEntries() shouldBe listOf(keyDay, keyHour) dao.getEntriesForType(CachedKeyInfo.Type.LOCATION_DAY.typeValue) shouldBe listOf(keyDay) dao.getEntriesForType(CachedKeyInfo.Type.LOCATION_HOUR.typeValue) shouldBe listOf(keyHour) @@ -65,7 +65,7 @@ class KeyCacheDatabaseTest { } dao.deleteEntry(keyDay) - dao.getAllEntries() shouldBe listOf( + dao.allEntries() shouldBe listOf( keyHour.copy( isDownloadComplete = true, etag = "with milk" @@ -73,7 +73,7 @@ class KeyCacheDatabaseTest { ) dao.clear() - dao.getAllEntries() shouldBe emptyList() + dao.allEntries() shouldBe emptyList<List<CachedKeyInfo>>() } } } 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/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeInfoFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeInfoFragmentTest.kt deleted file mode 100644 index 5e2b2ceaffa72d225586eb2b612a42f985372f14..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionQrCodeInfoFragmentTest.kt +++ /dev/null @@ -1,58 +0,0 @@ -package de.rki.coronawarnapp.ui.submission - -import androidx.fragment.app.testing.launchFragment -import androidx.fragment.app.testing.launchFragmentInContainer -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.ext.junit.runners.AndroidJUnit4 -import dagger.Module -import dagger.android.ContributesAndroidInjector -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.ui.submission.qrcode.info.SubmissionQRCodeInfoFragment -import de.rki.coronawarnapp.ui.submission.qrcode.info.SubmissionQRCodeInfoFragmentViewModel -import io.mockk.MockKAnnotations -import io.mockk.impl.annotations.MockK -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import testhelpers.BaseUITest - -@RunWith(AndroidJUnit4::class) -class SubmissionQrCodeInfoFragmentTest : BaseUITest() { - - @MockK lateinit var viewModel: SubmissionQRCodeInfoFragmentViewModel - - @Before - fun setup() { - MockKAnnotations.init(this, relaxed = true) - setupMockViewModel(object : SubmissionQRCodeInfoFragmentViewModel.Factory { - override fun create(): SubmissionQRCodeInfoFragmentViewModel = viewModel - }) - } - - @After - fun teardown() { - clearAllViewModels() - } - - @Test - fun launch_fragment() { - launchFragment<SubmissionQRCodeInfoFragment>() - } - - @Test fun testQRInfoNextClicked() { - val scenario = launchFragmentInContainer<SubmissionQRCodeInfoFragment>() - onView(withId(R.id.submission_qr_info_button_next)) - .perform(click()) - - // TODO verify result - } -} - -@Module -abstract class SubmissionQRInfoFragmentModule { - @ContributesAndroidInjector - abstract fun submissionQRInfoScreen(): SubmissionQRCodeInfoFragment -} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionSettingsTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionSettingsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa26c51fb8bd4e0b5341722e53909388e8155a11 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionSettingsTest.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.ui.submission + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import de.rki.coronawarnapp.submission.SubmissionSettings +import io.kotest.matchers.shouldBe +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class SubmissionSettingsTest { + + private val appContext: Context + get() = ApplicationProvider.getApplicationContext() + + @Test + fun consentIsPersisted() { + val settings = SubmissionSettings(appContext) + settings.hasGivenConsent.value shouldBe false + settings.hasGivenConsent.update { true } + settings.hasGivenConsent.value shouldBe true + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt index c610d634556fe12b9ecdd9d2a46ed1d308412525..5544f81e8ad0f0f95844f51ac2a9a993b8dac052 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt @@ -8,12 +8,14 @@ import androidx.work.WorkManager import androidx.work.WorkRequest import androidx.work.testing.TestDriver import androidx.work.testing.WorkManagerTestInitHelper +import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds import de.rki.coronawarnapp.util.formatter.TestResult import io.mockk.coEvery import io.mockk.every +import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.mockkObject import io.mockk.slot @@ -35,7 +37,8 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest { private lateinit var context: Context private lateinit var workManager: WorkManager private lateinit var request: WorkRequest - private lateinit var request2: WorkRequest + @MockK lateinit var playbook: Playbook + private val submissionService = SubmissionService(playbook) // small delay because WorkManager does not run work instantly when delay is off private val delay = 500L @@ -44,7 +47,6 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest { LocalData.registrationToken("test token") LocalData.isTestResultNotificationSent(false) mockkObject(LocalData) - mockkObject(SubmissionService) mockkObject(BackgroundWorkScheduler) // do not init Test WorkManager instance again between tests @@ -118,7 +120,6 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest { @Test fun testDiagnosisTestResultRetrievalPeriodicWorkerRetryAndFail() { val past = Date().time - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() - 1).daysToMilliseconds() - coEvery { SubmissionService.asyncRequestTestResult() } throws Exception("test exception") every { LocalData.initialPollingForTestResultTimeStamp() } returns past BackgroundWorkScheduler.startWorkScheduler() @@ -159,7 +160,7 @@ class DiagnosisTestResultRetrievalPeriodicWorkerTest { past: Long, isCancelTest: Boolean = false ) { - coEvery { SubmissionService.asyncRequestTestResult() } returns result + coEvery { submissionService.asyncRequestTestResult(any()) } returns result every { LocalData.initialPollingForTestResultTimeStamp() } returns past BackgroundWorkScheduler.startWorkScheduler() 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/main/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt similarity index 56% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoModule.kt rename to Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt index 437fc64b2353848b749819925a9187cf4fd956cb..f123767b6fb40c678849cb0723ff722422cb95bb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoModule.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.ui.submission.qrcode.info +package de.rki.coronawarnapp.test.submission.ui import dagger.Binds import dagger.Module @@ -8,11 +8,11 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey @Module -abstract class SubmissionQRCodeInfoModule { +abstract class SubmissionTestFragmentModule { @Binds @IntoMap - @CWAViewModelKey(SubmissionQRCodeInfoFragmentViewModel::class) - abstract fun infoQRFragment( - factory: SubmissionQRCodeInfoFragmentViewModel.Factory + @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/playbook/Playbook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt index d11cdbfdf497d0ac3a2df94e31694101e0796ae3..84252ff2da7b38e811a5c61f08df2ced5cedeac2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt @@ -18,14 +18,14 @@ interface Playbook { suspend fun testResult(registrationToken: String): TestResult + suspend fun submit(data: SubmissionData) + + suspend fun dummy() + data class SubmissionData( val registrationToken: String, val temporaryExposureKeys: List<TemporaryExposureKeyExportOuterClass.TemporaryExposureKey>, val consentToFederation: Boolean, val visistedCountries: List<String> ) - - suspend fun submit(data: SubmissionData) - - suspend fun dummy() } 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/service/submission/SubmissionService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt index 0961774da36df652d96f19c151f3332eb883f233..1931b63c3dcf5bed27d07f442c267de620c863d9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt @@ -1,80 +1,38 @@ package de.rki.coronawarnapp.service.submission -import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException -import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.playbook.Playbook -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.SubmissionRepository -import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.verification.server.VerificationKeyType -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler - -object SubmissionService { +import javax.inject.Inject +class SubmissionService @Inject constructor( private val playbook: Playbook - get() = AppInjector.component.playbook +) { - private val timeStamper: TimeStamper - get() = TimeStamper() + suspend fun asyncRequestTestResult(registrationToken: String): TestResult { + return playbook.testResult(registrationToken) + } - suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { + suspend fun asyncRegisterDeviceViaGUID(guid: String): RegistrationData { val (registrationToken, testResult) = playbook.initialRegistration( guid, VerificationKeyType.GUID ) - LocalData.registrationToken(registrationToken) - deleteTestGUID() - SubmissionRepository.updateTestResult(testResult) - LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) - BackgroundNoise.getInstance().scheduleDummyPattern() - return testResult + return RegistrationData(registrationToken, testResult) } - suspend fun asyncRegisterDeviceViaTAN(tan: String) { + suspend fun asyncRegisterDeviceViaTAN(tan: String): RegistrationData { val (registrationToken, testResult) = playbook.initialRegistration( tan, VerificationKeyType.TELETAN ) - LocalData.registrationToken(registrationToken) - deleteTeleTAN() - SubmissionRepository.updateTestResult(testResult) - LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) - BackgroundNoise.getInstance().scheduleDummyPattern() - } - - suspend fun asyncRequestTestResult(): TestResult { - val registrationToken = - LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() - - return playbook.testResult(registrationToken) - } - - fun containsValidGUID(scanResult: String): Boolean { - val scanResult = QRScanResult(scanResult) - return scanResult.isValid + return RegistrationData(registrationToken, testResult) } - fun storeTestGUID(guid: String) = LocalData.testGUID(guid) - - fun deleteTestGUID() { - LocalData.testGUID(null) - } - - fun deleteRegistrationToken() { - LocalData.registrationToken(null) - LocalData.devicePairingSuccessfulTimestamp(0L) - } - - fun submissionSuccessful() { - BackgroundWorkScheduler.stopWorkScheduler() - LocalData.numberOfSuccessfulSubmissions(1) - } - - private fun deleteTeleTAN() { - LocalData.teletan(null) - } + data class RegistrationData( + val registrationToken: String, + val testResult: TestResult + ) } 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 6661469b1b925ccd87655eb7e65b2fc449d64e01..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 = @@ -35,8 +35,6 @@ object RiskLevelRepository { /** * Resets the data in the [RiskLevelRepository] * - * @see de.rki.coronawarnapp.util.DataReset - * */ fun reset() { internalRisklevelScore.value = RiskLevelConstants.UNKNOWN_RISK_INITIAL @@ -80,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 b601e6d77475a9a9a2e1bd665c0f5ba50d8d58ff..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,137 +1,190 @@ package de.rki.coronawarnapp.storage -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.asLiveData +import androidx.annotation.VisibleForTesting import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.service.submission.SubmissionService -import de.rki.coronawarnapp.ui.submission.ApiRequestState +import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.Event -import de.rki.coronawarnapp.util.di.AppInjector +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 import de.rki.coronawarnapp.worker.BackgroundWorkScheduler +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import timber.log.Timber import java.util.Date - -object SubmissionRepository { - - private val appScope by lazy { - AppInjector.component.appScope +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SubmissionRepository @Inject constructor( + private val submissionSettings: SubmissionSettings, + private val submissionService: SubmissionService, + @AppScope private val scope: CoroutineScope, + private val timeStamper: TimeStamper +) { + + companion object { + fun submissionSuccessful() { + BackgroundWorkScheduler.stopWorkScheduler() + LocalData.numberOfSuccessfulSubmissions(1) + } + fun deleteRegistrationToken() { + LocalData.registrationToken(null) + LocalData.devicePairingSuccessfulTimestamp(0L) + } } - val uiStateStateFlowInternal = MutableStateFlow(ApiRequestState.IDLE) - val uiStateStateFlow: Flow<ApiRequestState> = uiStateStateFlowInternal - val uiStateState: LiveData<ApiRequestState> = uiStateStateFlow.asLiveData() - 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 private val testResultFlow = MutableStateFlow<TestResult?>(null) - private suspend fun fetchTestResult(): DeviceUIState = try { - val testResult = SubmissionService.asyncRequestTestResult() - updateTestResult(testResult) - deriveUiState(testResult) - } catch (err: NoRegistrationTokenSetException) { - DeviceUIState.UNPAIRED + fun setTeletan(teletan: String) { + LocalData.teletan(teletan) } - fun updateTestResult(testResult: TestResult) { - this.testResultFlow.value = testResult - - if (testResult == TestResult.POSITIVE) { - LocalData.isAllowedToSubmitDiagnosisKeys(true) - } - - val initialTestResultReceivedTimestamp = LocalData.initialTestResultReceivedTimestamp() - - if (initialTestResultReceivedTimestamp == null) { - val currentTime = System.currentTimeMillis() - LocalData.initialTestResultReceivedTimestamp(currentTime) - testResultReceivedDateFlowInternal.value = Date(currentTime) - if (testResult == TestResult.PENDING) { - BackgroundWorkScheduler.startWorkScheduler() - } - } else { - testResultReceivedDateFlowInternal.value = Date(initialTestResultReceivedTimestamp) - } + fun deleteTestGUID() { + LocalData.testGUID(null) } - private fun deriveUiState(testResult: TestResult?): DeviceUIState = when (testResult) { - TestResult.NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE - TestResult.POSITIVE -> DeviceUIState.PAIRED_POSITIVE - TestResult.PENDING -> DeviceUIState.PAIRED_NO_RESULT - TestResult.REDEEMED -> DeviceUIState.PAIRED_REDEEMED - TestResult.INVALID -> DeviceUIState.PAIRED_ERROR - null -> DeviceUIState.UNPAIRED + // to be used by new submission flow screens + fun giveConsentToSubmission() { + submissionSettings.hasGivenConsent.update { + true + } } - fun setTeletan(teletan: String) { - LocalData.teletan(teletan) + // to be used by new submission flow screens + fun revokeConsentToSubmission() { + submissionSettings.hasGivenConsent.update { + false + } } - private val uiStateErrorInternal = MutableLiveData<Event<CwaWebException>>(null) - val uiStateError: LiveData<Event<CwaWebException>> = uiStateErrorInternal - // TODO this should be more UI agnostic 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 - appScope.launch { + deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestStarted + + scope.launch { try { - refreshUIState(refresh) - uiStateStateFlowInternal.value = ApiRequestState.SUCCESS + deviceUIStateFlowInternal.value = refreshUIState(refresh) } 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) } } } - fun reset() { - uiStateStateFlowInternal.value = ApiRequestState.IDLE - deviceUIStateFlowInternal.value = DeviceUIState.UNPAIRED - } - // TODO this should be more UI agnostic - private suspend fun refreshUIState(refreshTestResult: Boolean) { + suspend fun refreshUIState(refreshTestResult: Boolean): NetworkRequestWrapper<DeviceUIState, Throwable> { var uiState = DeviceUIState.UNPAIRED if (LocalData.submissionWasSuccessful()) { uiState = DeviceUIState.SUBMITTED_FINAL } else { - if (LocalData.registrationToken() != null) { + val registrationToken = LocalData.registrationToken() + if (registrationToken != null) { uiState = when { LocalData.isAllowedToSubmitDiagnosisKeys() == true -> { DeviceUIState.PAIRED_POSITIVE } - refreshTestResult -> fetchTestResult() + refreshTestResult -> fetchTestResult(registrationToken) else -> { deriveUiState(testResultFlow.value) } } } } - deviceUIStateFlowInternal.value = uiState + return NetworkRequestWrapper.RequestSuccessful(uiState) + } + + suspend fun asyncRegisterDeviceViaTAN(tan: String) { + val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan) + LocalData.registrationToken(registrationData.registrationToken) + LocalData.teletan(null) + updateTestResult(registrationData.testResult) + LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) + BackgroundNoise.getInstance().scheduleDummyPattern() + } + + suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { + val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid) + LocalData.registrationToken(registrationData.registrationToken) + LocalData.testGUID(null) + updateTestResult(registrationData.testResult) + LocalData.devicePairingSuccessfulTimestamp(timeStamper.nowUTC.millis) + BackgroundNoise.getInstance().scheduleDummyPattern() + return registrationData.testResult } + + fun reset() { + deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestIdle + revokeConsentToSubmission() + } + + @VisibleForTesting + fun updateTestResult(testResult: TestResult) { + testResultFlow.value = testResult + + if (testResult == TestResult.POSITIVE) { + LocalData.isAllowedToSubmitDiagnosisKeys(true) + } + + val initialTestResultReceivedTimestamp = LocalData.initialTestResultReceivedTimestamp() + + if (initialTestResultReceivedTimestamp == null) { + val currentTime = System.currentTimeMillis() + LocalData.initialTestResultReceivedTimestamp(currentTime) + testResultReceivedDateFlowInternal.value = Date(currentTime) + if (testResult == TestResult.PENDING) { + BackgroundWorkScheduler.startWorkScheduler() + } + } else { + testResultReceivedDateFlowInternal.value = Date(initialTestResultReceivedTimestamp) + } + } + + private suspend fun fetchTestResult(registrationToken: String): DeviceUIState = try { + val testResult = submissionService.asyncRequestTestResult(registrationToken) + updateTestResult(testResult) + deriveUiState(testResult) + } catch (err: NoRegistrationTokenSetException) { + DeviceUIState.UNPAIRED + } +} + +private fun deriveUiState(testResult: TestResult?): DeviceUIState = when (testResult) { + TestResult.NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE + TestResult.POSITIVE -> DeviceUIState.PAIRED_POSITIVE + TestResult.PENDING -> DeviceUIState.PAIRED_NO_RESULT + TestResult.REDEEMED -> DeviceUIState.PAIRED_REDEEMED + TestResult.INVALID -> DeviceUIState.PAIRED_ERROR + null -> DeviceUIState.UNPAIRED } 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/SubmissionSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..8fb7276180cde94a2d30417c55726f85a5224565 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionSettings.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.submission + +import android.content.Context +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.preferences.createFlowPreference +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SubmissionSettings @Inject constructor( + @AppContext val context: Context +) { + + private val prefs by lazy { + context.getSharedPreferences("submission_localdata", Context.MODE_PRIVATE) + } + + val hasGivenConsent = prefs.createFlowPreference( + key = "key_submission_consent", + defaultValue = false + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionTask.kt index 2bff73211488afcd7022423df0d5200583b2406e..9f7350a10e581ecee86ec451748e8cb517738763 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionTask.kt @@ -4,7 +4,7 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass -import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -41,7 +41,7 @@ class SubmissionTask @Inject constructor( .also { checkCancel() } .let { playbook.submit(it) } - SubmissionService.submissionSuccessful() + SubmissionRepository.submissionSuccessful() object : Task.Result {} } catch (error: Exception) { 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/Country.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/Country.kt index 2c22c8b002551f81783c5690ba7636e090db55c7..25c5d3f8ff24dc7536fa7efb0ff6208e59b50f1d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/Country.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/Country.kt @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.ui import androidx.annotation.DrawableRes import androidx.annotation.StringRes import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.ui.CachedString enum class Country( val code: String, @@ -40,5 +41,7 @@ enum class Country( RO("ro", R.string.country_name_ro, R.drawable.ic_country_ro), SE("se", R.string.country_name_se, R.drawable.ic_country_se), SI("si", R.string.country_name_si, R.drawable.ic_country_si), - SK("sk", R.string.country_name_sk, R.drawable.ic_country_sk) + SK("sk", R.string.country_name_sk, R.drawable.ic_country_sk); + + val label = CachedString { it.getString(labelRes) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/calendar/CalendarView.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/calendar/CalendarView.kt index cb545f2fe2f7b6602553e5c2bbb9fb0023b4eff6..f953c70c0c86c625a6dde1d62cd312ad5b2374b2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/calendar/CalendarView.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/calendar/CalendarView.kt @@ -88,14 +88,13 @@ class CalendarView @JvmOverloads constructor( listener?.invoke(updateData.find { it.isSelected }?.date) } - /** - * Unset selection of each date shown - * - * @see CalendarAdapter.update - */ - fun unsetSelection() { - val updateData = days.map { oldDay -> oldDay.copy(isSelected = false) } - updateSelection(false) + fun setSelectedDate(date: LocalDate?) { + val updateData = if (date != null) { + days.map { oldDay -> oldDay.copy(isSelected = oldDay.date == date) } + } else { + days.map { oldDay -> oldDay.copy(isSelected = false) } + } + updateSelection(date != null) adapter.update(updateData) } 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..87280672027294bb39746d42b6fcf752a3def860 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() { @@ -147,11 +145,11 @@ class HomeFragment : Fragment(R.layout.fragment_home), AutoInject { doNavigate(HomeFragmentDirections.actionMainFragmentToSubmissionResultFragment()) } mainTestUnregistered.apply { - val toSubmissionIntro = { - doNavigate(HomeFragmentDirections.actionMainFragmentToSubmissionIntroFragment()) + val toSubmissionDispatcher = { + doNavigate(HomeFragmentDirections.actionMainFragmentToSubmissionDispatcher()) } - submissionStatusCardUnregistered.setOnClickListener { toSubmissionIntro() } - submissionStatusCardUnregisteredButton.setOnClickListener { toSubmissionIntro() } + submissionStatusCardUnregistered.setOnClickListener { toSubmissionDispatcher() } + submissionStatusCardUnregisteredButton.setOnClickListener { toSubmissionDispatcher() } } mainTestDone.submissionStatusCardDone.setOnClickListener { 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 ea111127b91b13506bd3a057672c47a837e4d555..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 @@ -5,7 +5,6 @@ import androidx.lifecycle.asLiveData import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.notification.TestResultNotificationService import de.rki.coronawarnapp.risk.TimeVariables -import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.TracingRepository @@ -34,7 +33,8 @@ class HomeFragmentViewModel @AssistedInject constructor( private val submissionCardsStateProvider: SubmissionCardsStateProvider, val settingsViewModel: SettingsViewModel, private val tracingRepository: TracingRepository, - private val testResultNotificationService: TestResultNotificationService + private val testResultNotificationService: TestResultNotificationService, + private val submissionRepository: SubmissionRepository ) : CWAViewModel( dispatcherProvider = dispatcherProvider, childViewModels = listOf(settingsViewModel) @@ -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 { @@ -100,11 +101,9 @@ class HomeFragmentViewModel @AssistedInject constructor( } fun refreshRequiredData() { - SubmissionRepository.refreshDeviceUIState() + 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() @@ -123,11 +122,11 @@ class HomeFragmentViewModel @AssistedInject constructor( } fun deregisterWarningAccepted() { - SubmissionService.deleteTestGUID() - SubmissionService.deleteRegistrationToken() + submissionRepository.deleteTestGUID() + SubmissionRepository.deleteRegistrationToken() LocalData.isAllowedToSubmitDiagnosisKeys(false) LocalData.initialTestResultReceivedTimestamp(0L) - SubmissionRepository.refreshDeviceUIState() + submissionRepository.refreshDeviceUIState() } fun userHasAcknowledgedTheLoweredRiskLevel() { 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 087c1e1f72a0036b0f460a6c4a5423a2cd8c33b7..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 @@ -14,15 +12,15 @@ import timber.log.Timber import javax.inject.Inject @Reusable -class SubmissionCardsStateProvider @Inject constructor() { +class SubmissionCardsStateProvider @Inject constructor( + submissionRepository: SubmissionRepository +) { 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/settings/SettingsResetViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt index a685bab2c0ea90d6d8ea1de895e38c40f8213928..e6412428b040247edea21f0f041fe1aab6928395 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/settings/SettingsResetViewModel.kt @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.InternalExposureNotificationClient import de.rki.coronawarnapp.notification.TestResultNotificationService +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.SingleLiveEvent import de.rki.coronawarnapp.util.DataReset import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -16,7 +17,8 @@ import de.rki.coronawarnapp.worker.BackgroundWorkScheduler class SettingsResetViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val dataReset: DataReset, - private val testResultNotificationService: TestResultNotificationService + private val testResultNotificationService: TestResultNotificationService, + private val submissionRepository: SubmissionRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val clickEvent: SingleLiveEvent<SettingsEvents> = SingleLiveEvent() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt index 1564dde8f9a367e79893b5aead8b2ec0f3cd1b43..9589db368364ec1473231a5d59164d64aac89784 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionDispatcherFragment.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionDispatcherBinding import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionDispatcherViewModel import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents -import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 @@ -42,10 +41,10 @@ class SubmissionDispatcherFragment : Fragment(R.layout.fragment_submission_dispa SubmissionDispatcherFragmentDirections .actionSubmissionDispatcherFragmentToSubmissionContactFragment() ) - is SubmissionNavigationEvents.NavigateToQRInfo -> + is SubmissionNavigationEvents.NavigateToConsent -> doNavigate( SubmissionDispatcherFragmentDirections - .actionSubmissionDispatcherFragmentToSubmissionQRCodeInfoFragment() + .actionSubmissionDispatcherFragmentToSubmissionConsentFragment() ) } } @@ -61,7 +60,7 @@ class SubmissionDispatcherFragment : Fragment(R.layout.fragment_submission_dispa viewModel.onBackPressed() } binding.submissionDispatcherContent.submissionDispatcherQr.dispatcherCard.setOnClickListener { - checkForDataPrivacyPermission() + viewModel.onQRCodePressed() } binding.submissionDispatcherContent.submissionDispatcherTanCode.dispatcherCard.setOnClickListener { viewModel.onTanPressed() @@ -70,24 +69,4 @@ class SubmissionDispatcherFragment : Fragment(R.layout.fragment_submission_dispa viewModel.onTeleTanPressed() } } - - private fun checkForDataPrivacyPermission() { - val cameraPermissionRationaleDialogInstance = DialogHelper.DialogInstance( - requireActivity(), - R.string.submission_dispatcher_qr_privacy_dialog_headline, - R.string.submission_dispatcher_qr_privacy_dialog_body, - R.string.submission_dispatcher_qr_privacy_dialog_button_positive, - R.string.submission_dispatcher_qr_privacy_dialog_button_negative, - true, - { - privacyPermissionIsGranted() - } - ) - - DialogHelper.showDialog(cameraPermissionRationaleDialogInstance) - } - - private fun privacyPermissionIsGranted() { - viewModel.onQRScanPressed() - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d0b2a7b14a1b729b0880258c23ddd9473b2122c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentFragment.kt @@ -0,0 +1,46 @@ +package de.rki.coronawarnapp.ui.submission.qrcode.consent + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentSubmissionConsentBinding +import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.di.AutoInject +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 javax.inject.Inject + +class SubmissionConsentFragment : Fragment(R.layout.fragment_submission_consent), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: SubmissionConsentViewModel by cwaViewModels { viewModelFactory } + private val binding: FragmentSubmissionConsentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.viewModel = viewModel + binding.submissionConsentHeader.headerButtonBack.buttonIcon.setOnClickListener { + viewModel.onBackButtonClick() + } + viewModel.routeToScreen.observe2(this) { + when (it) { + is SubmissionNavigationEvents.NavigateToQRCodeScan -> doNavigate( + SubmissionConsentFragmentDirections.actionSubmissionConsentFragmentToSubmissionQRCodeScanFragment() + ) + is SubmissionNavigationEvents.NavigateToDispatcher -> doNavigate( + SubmissionConsentFragmentDirections.actionSubmissionConsentFragmentToHomeFragment() + ) + is SubmissionNavigationEvents.NavigateToDataPrivacy -> doNavigate( + SubmissionConsentFragmentDirections.actionSubmissionConsentFragmentToInformationPrivacyFragment() + ) + } + } + viewModel.countries.observe2(this) { + binding.countries = it + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..40064c3b06eb8e49c3dfc17ab1557342d6b48e71 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.ui.submission.qrcode.consent + +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 SubmissionConsentModule { + @Binds + @IntoMap + @CWAViewModelKey(SubmissionConsentViewModel::class) + abstract fun submissionConsentFragment( + factory: SubmissionConsentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..e720c062a86e95c1a3350965ed4992753eb6d84b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModel.kt @@ -0,0 +1,36 @@ +package de.rki.coronawarnapp.ui.submission.qrcode.consent + +import androidx.lifecycle.asLiveData +import com.squareup.inject.assisted.AssistedInject +import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory + +class SubmissionConsentViewModel @AssistedInject constructor( + private val submissionRepository: SubmissionRepository, + interoperabilityRepository: InteroperabilityRepository +) : CWAViewModel() { + + val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() + + val countries = interoperabilityRepository.countryListFlow.asLiveData() + + fun onConsentButtonClick() { + submissionRepository.giveConsentToSubmission() + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRCodeScan) + } + + fun onBackButtonClick() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToDispatcher) + } + + fun onDataPrivacyClick() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToDataPrivacy) + } + + @AssistedInject.Factory + interface Factory : SimpleCWAViewModelFactory<SubmissionConsentViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoFragment.kt deleted file mode 100644 index c98d506a032d742cec2cc6f8a1749d03717f10b1..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoFragment.kt +++ /dev/null @@ -1,45 +0,0 @@ -package de.rki.coronawarnapp.ui.submission.qrcode.info - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.FragmentSubmissionQrCodeInfoBinding -import de.rki.coronawarnapp.util.di.AutoInject -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 javax.inject.Inject - -class SubmissionQRCodeInfoFragment : Fragment(R.layout.fragment_submission_qr_code_info), AutoInject { - - @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val viewModel: SubmissionQRCodeInfoFragmentViewModel by cwaViewModels { viewModelFactory } - private val binding: FragmentSubmissionQrCodeInfoBinding by viewBindingLazy() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.submissionQrCodeInfoHeader.headerButtonBack.buttonIcon.setOnClickListener { - viewModel.onBackPressed() - } - - binding.submissionQrInfoButtonNext.setOnClickListener { - viewModel.onNextPressed() - } - - viewModel.navigateToDispatcher.observe2(this) { - findNavController().popBackStack() - } - - viewModel.navigateToQRScan.observe2(this) { - doNavigate( - SubmissionQRCodeInfoFragmentDirections - .actionSubmissionQRCodeInfoFragmentToSubmissionQRCodeScanFragment() - ) - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt index b9b618c3088a2d80f62d9ac752b85bb911b9a17e..28b89d5b7b97d2bf0a12862e26d4f187743b4413 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanFragment.kt @@ -97,7 +97,7 @@ class SubmissionQRCodeScanFragment : Fragment(R.layout.fragment_submission_qr_co when (it) { is SubmissionNavigationEvents.NavigateToDispatcher -> navigateToDispatchScreen() - is SubmissionNavigationEvents.NavigateToQRInfo -> + is SubmissionNavigationEvents.NavigateToConsent -> goBack() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt index ec4ade17f1b2408ab3fcefd0249b13ec191af7a8..0a282b83e5b17559986880e23ab8875b30d042c1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModel.kt @@ -7,8 +7,8 @@ import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.service.submission.QRScanResult -import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents @@ -18,7 +18,9 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import timber.log.Timber -class SubmissionQRCodeScanViewModel @AssistedInject constructor() : +class SubmissionQRCodeScanViewModel @AssistedInject constructor( + private val submissionRepository: SubmissionRepository +) : CWAViewModel() { val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() val showRedeemedTokenWarning = SingleLiveEvent<Unit>() @@ -42,7 +44,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor() : private fun doDeviceRegistration(scanResult: QRScanResult) = launch { try { registrationState.postValue(ApiRequestState.STARTED) - checkTestResult(SubmissionService.asyncRegisterDeviceViaGUID(scanResult.guid!!)) + checkTestResult(submissionRepository.asyncRegisterDeviceViaGUID(scanResult.guid!!)) registrationState.postValue(ApiRequestState.SUCCESS) } catch (err: CwaWebException) { registrationState.postValue(ApiRequestState.FAILED) @@ -73,8 +75,8 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor() : private fun deregisterTestFromDevice() { launch { Timber.d("deregisterTestFromDevice()") - SubmissionService.deleteTestGUID() - SubmissionService.deleteRegistrationToken() + submissionRepository.deleteTestGUID() + SubmissionRepository.deleteRegistrationToken() LocalData.isAllowedToSubmitDiagnosisKeys(false) LocalData.initialTestResultReceivedTimestamp(0L) @@ -83,7 +85,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor() : } fun onBackPressed() { - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRInfo) + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToConsent) } fun onClosePressed() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt index 2c1653eedd26f0d6df583642e4edd1fbb6eb3263..ceb33fc3aa71426f3895efb6119877192b704760 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarFragment.kt @@ -58,10 +58,12 @@ class SubmissionSymptomCalendarFragment : Fragment(R.layout.fragment_submission_ } viewModel.symptomStart.observe2(this) { - updateButtons(it) - if (it !is Symptoms.StartOf.Date) { - binding.symptomCalendarContainer.unsetSelection() + when (it) { + is Symptoms.StartOf.Date -> binding.symptomCalendarContainer.setSelectedDate(it.date) + else -> binding.symptomCalendarContainer.setSelectedDate(null) } + + updateButtons(it) } binding.apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt index e79975890a20c3d5572801fd7ecf5481f246e367..394a8b3fecf385c1c1eecbe5a03690c1be372336 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/calendar/SubmissionSymptomCalendarViewModel.kt @@ -19,8 +19,7 @@ class SubmissionSymptomCalendarViewModel @AssistedInject constructor( ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { private val symptomStartInternal = MutableStateFlow<Symptoms.StartOf?>(null) - val symptomStart = symptomStartInternal - .asLiveData(context = dispatcherProvider.Default) + val symptomStart = symptomStartInternal.asLiveData() val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt index c2af32534fb433369929cc714279aed588a3a4f1..13e3ab1f7e26879a99204f6945e603b703568fee 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionFragment.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.ui.submission.symptoms.introduction +import android.app.AlertDialog import android.content.res.ColorStateList import android.os.Bundle import android.view.View @@ -10,7 +11,6 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentSubmissionSymptomIntroBinding import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents -import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.formatter.formatBackgroundButtonStyleByState import de.rki.coronawarnapp.util.formatter.formatButtonStyleByState @@ -47,10 +47,17 @@ class SubmissionSymptomIntroductionFragment : Fragment(R.layout.fragment_submiss it.symptoms ) ) - is SubmissionNavigationEvents.NavigateToTestResult -> handleSubmissionCancellation() + is SubmissionNavigationEvents.NavigateToTestResult -> doNavigate( + SubmissionSymptomIntroductionFragmentDirections + .actionSubmissionSymptomIntroductionFragmentToSubmissionResultFragment() + ) } } + viewModel.showCancelDialog.observe2(this) { + showCancelDialog() + } + viewModel.symptomIndication.observe2(this) { updateButtons(it) } @@ -122,26 +129,16 @@ class SubmissionSymptomIntroductionFragment : Fragment(R.layout.fragment_submiss ) } - /** - * Opens a Dialog that warns user - * when they're about to cancel the submission flow - */ - private fun handleSubmissionCancellation() { - DialogHelper.showDialog( - DialogHelper.DialogInstance( - requireActivity(), - R.string.submission_error_dialog_confirm_cancellation_title, - R.string.submission_error_dialog_confirm_cancellation_body, - R.string.submission_error_dialog_confirm_cancellation_button_positive, - R.string.submission_error_dialog_confirm_cancellation_button_negative, - true, - { - doNavigate( - SubmissionSymptomIntroductionFragmentDirections - .actionSubmissionSymptomIntroductionFragmentToSubmissionResultFragment() - ) - } - ) - ) + private fun showCancelDialog() { + AlertDialog.Builder(requireContext()).apply { + setTitle(R.string.submission_error_dialog_confirm_cancellation_title) + setMessage(R.string.submission_error_dialog_confirm_cancellation_body) + setPositiveButton(R.string.submission_error_dialog_confirm_cancellation_button_positive) { _, _ -> + viewModel.cancelSymptomSubmission() + } + setNegativeButton(R.string.submission_error_dialog_confirm_cancellation_button_negative) { _, _ -> + // NOOP + } + }.show() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt index 43f7550b426a28cfee0e10c8360906c685c4aaf2..d5b01524c5a62b01518be7f8198f92db48428a5f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/symptoms/introduction/SubmissionSymptomIntroductionViewModel.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first +import timber.log.Timber class SubmissionSymptomIntroductionViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider @@ -21,6 +22,8 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor( val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() + val showCancelDialog = SingleLiveEvent<Unit>() + fun onNextClicked() { launch { when (symptomIndicationInternal.first()) { @@ -41,7 +44,7 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor( } fun onPreviousClicked() { - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToTestResult) + showCancelDialog.postValue(Unit) } fun onPositiveSymptomIndication() { @@ -56,6 +59,11 @@ class SubmissionSymptomIntroductionViewModel @AssistedInject constructor( symptomIndicationInternal.value = Symptoms.Indication.NO_INFORMATION } + fun cancelSymptomSubmission() { + Timber.d("Symptom submission was cancelled.") + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToTestResult) + } + @AssistedInject.Factory interface Factory : SimpleCWAViewModelFactory<SubmissionSymptomIntroductionViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt index 28dbe39bc10303fb8df48d1c1458be736bde5f4f..1a8472cccb0facfa029fd5c3590348f69981d411 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModel.kt @@ -7,7 +7,6 @@ import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -19,7 +18,8 @@ import kotlinx.coroutines.flow.map import timber.log.Timber class SubmissionTanViewModel @AssistedInject constructor( - dispatcherProvider: DispatcherProvider + dispatcherProvider: DispatcherProvider, + private val submissionRepository: SubmissionRepository ) : CWAViewModel() { private val currentTan = MutableStateFlow(Tan("")) @@ -47,12 +47,12 @@ class SubmissionTanViewModel @AssistedInject constructor( return } Timber.d("Storing teletan $teletan") - SubmissionRepository.setTeletan(teletan.value) + submissionRepository.setTeletan(teletan.value) launch { try { registrationState.postValue(ApiRequestState.STARTED) - SubmissionService.asyncRegisterDeviceViaTAN(teletan.value) + submissionRepository.asyncRegisterDeviceViaTAN(teletan.value) registrationState.postValue(ApiRequestState.SUCCESS) } catch (err: CwaWebException) { registrationState.postValue(ApiRequestState.FAILED) 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 7340018936e2b5c27b5416a6ab756e68e8631c0e..865b97643e45486845b01846f1bf51ca67029ee4 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,23 +6,20 @@ 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 import de.rki.coronawarnapp.exception.http.CwaServerError import de.rki.coronawarnapp.exception.http.CwaWebException -import de.rki.coronawarnapp.storage.SubmissionRepository 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), @@ -80,6 +77,11 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_ viewModel.uiState.observe2(this) { binding.uiState = it binding.submissionTestResultContent.submissionTestResultSection.setTestResultSection(binding.uiState) + it.deviceUiState.withFailure { + if (it is CwaWebException) { + DialogHelper.showDialog(buildErrorDialog(it)) + } + } } // registers callback when the os level back is pressed @@ -100,10 +102,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(), @@ -136,20 +134,20 @@ class SubmissionTestResultFragment : Fragment(R.layout.fragment_submission_test_ } } - lifecycleScope.launch { viewModel.observeTestResultToSchedulePositiveTestResultReminder() } + viewModel.observeTestResultToSchedulePositiveTestResultReminder() } override fun onResume() { super.onResume() binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) - SubmissionRepository.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh) + viewModel.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh) skipInitialTestResultRefresh = false } private fun setButtonOnClickListener() { binding.submissionTestResultButtonPendingRefresh.setOnClickListener { - SubmissionRepository.refreshDeviceUIState() + viewModel.refreshDeviceUIState() binding.submissionTestResultContent.submissionTestResultSection .sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } 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 97586ba507f291498d92a0145b88e1eacedd4e76..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,16 +3,14 @@ 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.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData 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 @@ -26,7 +24,8 @@ import timber.log.Timber class SubmissionTestResultViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val enfClient: ENFClient, - private val testResultNotificationService: TestResultNotificationService + private val testResultNotificationService: TestResultNotificationService, + private val submissionRepository: SubmissionRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() @@ -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 -> + submissionRepository.deviceUIStateFlow, + submissionRepository.testResultReceivedDateFlow + ) { 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() = - SubmissionRepository.deviceUIStateFlow - .first { it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN } + fun observeTestResultToSchedulePositiveTestResultReminder() = launch { + submissionRepository.deviceUIStateFlow + .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) @@ -94,8 +98,8 @@ class SubmissionTestResultViewModel @AssistedInject constructor( fun deregisterTestFromDevice() { launch { Timber.d("deregisterTestFromDevice()") - SubmissionService.deleteTestGUID() - SubmissionService.deleteRegistrationToken() + submissionRepository.deleteTestGUID() + SubmissionRepository.deleteRegistrationToken() LocalData.isAllowedToSubmitDiagnosisKeys(false) LocalData.initialTestResultReceivedTimestamp(0L) @@ -103,6 +107,10 @@ class SubmissionTestResultViewModel @AssistedInject constructor( } } + fun refreshDeviceUIState(refreshTestResult: Boolean = true) { + submissionRepository.refreshDeviceUIState(refreshTestResult) + } + @AssistedInject.Factory interface Factory : SimpleCWAViewModelFactory<SubmissionTestResultViewModel> } 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/submission/viewmodel/SubmissionDispatcherViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt index 6e22fbdbaa865779941730f586cfdbc0354f2198..0bb7e7ff11aa0ce51665e195f36530e4b229375a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionDispatcherViewModel.kt @@ -13,10 +13,6 @@ class SubmissionDispatcherViewModel @AssistedInject constructor() : CWAViewModel routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) } - fun onQRScanPressed() { - routeToScreen.postValue(SubmissionNavigationEvents.NavigateToQRInfo) - } - fun onTanPressed() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToTAN) } @@ -25,6 +21,10 @@ class SubmissionDispatcherViewModel @AssistedInject constructor() : CWAViewModel routeToScreen.postValue(SubmissionNavigationEvents.NavigateToContact) } + fun onQRCodePressed() { + routeToScreen.postValue(SubmissionNavigationEvents.NavigateToConsent) + } + @AssistedInject.Factory interface Factory : SimpleCWAViewModelFactory<SubmissionDispatcherViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt index 07cdfe2f9d8fc00919ab4de58fb2b4d8c4af0592..667755dd48337165a64ecef61d7b6bcc668b5e56 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionFragmentModule.kt @@ -6,8 +6,8 @@ import de.rki.coronawarnapp.ui.submission.fragment.SubmissionContactFragment import de.rki.coronawarnapp.ui.submission.fragment.SubmissionDispatcherFragment import de.rki.coronawarnapp.ui.submission.fragment.SubmissionDoneFragment import de.rki.coronawarnapp.ui.submission.fragment.SubmissionIntroFragment -import de.rki.coronawarnapp.ui.submission.qrcode.info.SubmissionQRCodeInfoFragment -import de.rki.coronawarnapp.ui.submission.qrcode.info.SubmissionQRCodeInfoModule +import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment +import de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentModule import de.rki.coronawarnapp.ui.submission.qrcode.scan.SubmissionQRCodeScanFragment import de.rki.coronawarnapp.ui.submission.qrcode.scan.SubmissionQRCodeScanModule import de.rki.coronawarnapp.ui.submission.symptoms.calendar.SubmissionSymptomCalendarFragment @@ -60,6 +60,6 @@ internal abstract class SubmissionFragmentModule { @ContributesAndroidInjector(modules = [SubmissionSymptomCalendarModule::class]) abstract fun submissionSymptomCalendarScreen(): SubmissionSymptomCalendarFragment - @ContributesAndroidInjector(modules = [SubmissionQRCodeInfoModule::class]) - abstract fun submissionQRCodeInfoScreen(): SubmissionQRCodeInfoFragment + @ContributesAndroidInjector(modules = [SubmissionConsentModule::class]) + abstract fun submissionConsentScreen(): SubmissionConsentFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt index 5a0fe235fefec008d7d167cfe47144d5fd755f95..4db51a3fc4e3cb8f4da0ea58a350e9b043d630e9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionNavigationEvents.kt @@ -8,6 +8,7 @@ sealed class SubmissionNavigationEvents { object NavigateToSubmissionDone : SubmissionNavigationEvents() object NavigateToSubmissionIntro : SubmissionNavigationEvents() object NavigateToQRCodeScan : SubmissionNavigationEvents() + object NavigateToDataPrivacy : SubmissionNavigationEvents() data class NavigateToResultPositiveOtherWarning( val symptoms: Symptoms @@ -21,7 +22,7 @@ sealed class SubmissionNavigationEvents { object NavigateToSymptomIntroduction : SubmissionNavigationEvents() object NavigateToTAN : SubmissionNavigationEvents() object NavigateToTestResult : SubmissionNavigationEvents() - object NavigateToQRInfo : SubmissionNavigationEvents() + object NavigateToConsent : SubmissionNavigationEvents() object NavigateToMainActivity : SubmissionNavigationEvents() object ShowCancelDialog : SubmissionNavigationEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt index 4a11a2ed4da149940286c01abacf67c6b3d8462b..2deb4e445f9a1be7f1711ef41665dd2f9b5e72d0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningViewModel.kt @@ -7,8 +7,8 @@ import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.notification.TestResultNotificationService -import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionTask import de.rki.coronawarnapp.submission.Symptoms @@ -115,7 +115,7 @@ class SubmissionResultPositiveOtherWarningViewModel @AssistedInject constructor( private fun submitWithNoDiagnosisKeys() { Timber.d("submitWithNoDiagnosisKeys()") - SubmissionService.submissionSuccessful() + SubmissionRepository.submissionSuccessful() } @AssistedInject.Factory 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/view/CountryList.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CountryList.kt deleted file mode 100644 index a97986690a52f7aa4223a34e29d7a7c3f33adfb0..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CountryList.kt +++ /dev/null @@ -1,48 +0,0 @@ -package de.rki.coronawarnapp.ui.view - -import android.content.Context -import android.util.AttributeSet -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.ui.Country -import java.text.Collator - -class CountryList(context: Context, attrs: AttributeSet) : - LinearLayout(context, attrs) { - - private var _list: List<Country>? = null - var list: List<Country>? - get() = _list - set(value) { - _list = value - buildList() - } - - init { - orientation = VERTICAL - } - - /** - * Cleans the view and rebuilds the list of countries. Presets already selected countries - */ - private fun buildList() { - this.removeAllViews() - list - ?.map { country -> - context.getString(country.labelRes) to country.iconRes - } - ?.sortedWith { a, b -> - Collator.getInstance().compare(a.first, b.first) - } - ?.forEachIndexed { index, (label, iconRes) -> - inflate(context, R.layout.view_country_list_entry, this) - val child = this.getChildAt(index) - child.apply { - findViewById<ImageView>(R.id.country_list_entry_image).setImageResource(iconRes) - findViewById<TextView>(R.id.country_list_entry_label).text = label - } - } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CountryListView.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CountryListView.kt new file mode 100644 index 0000000000000000000000000000000000000000..38163a84e6719f8272768ea03de51dc5e558453a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/CountryListView.kt @@ -0,0 +1,78 @@ +package de.rki.coronawarnapp.ui.view + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.ViewCountryListEntryFlagItemBinding +import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.ui.lists.BaseAdapter +import de.rki.coronawarnapp.ui.view.CountryFlagsAdapter.CountryFlagViewHolder +import de.rki.coronawarnapp.util.lists.BindableVH + +class CountryListView(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { + + private val adapterCountryFlags = CountryFlagsAdapter() + private val grid: RecyclerView + private val countryNames: TextView + + var countries: List<Country> = emptyList() + set(value) { + field = value.also { countries -> + adapterCountryFlags.countryList = countries + countryNames.text = countries.joinToString(", ") { it.label.get(context) } + } + } + + init { + orientation = HORIZONTAL + inflate(context, R.layout.view_country_list_entry_flag_container, this) + + grid = findViewById<RecyclerView>(R.id.flagGrid).apply { + layoutManager = GridLayoutManager(context, FLAG_COLUMNS) + adapter = adapterCountryFlags + } + countryNames = findViewById(R.id.country_list_entry_label) + } + + // Helper to allow for null in data binding + fun setCountryList(countries: List<Country>?) { + this.countries = countries ?: emptyList() + } + + companion object { + private const val FLAG_COLUMNS = 8 + } +} + +private class CountryFlagsAdapter : BaseAdapter<CountryFlagViewHolder>() { + + var countryList: List<Country> = emptyList() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun getItemCount(): Int = countryList.size + + override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): CountryFlagViewHolder = CountryFlagViewHolder(parent) + + override fun onBindBaseVH(holder: CountryFlagViewHolder, position: Int) = holder.bind(countryList[position]) + + class CountryFlagViewHolder(val parent: ViewGroup) : VH( + R.layout.view_country_list_entry_flag_item, parent + ), BindableVH<Country, ViewCountryListEntryFlagItemBinding> { + + override val viewBinding: Lazy<ViewCountryListEntryFlagItemBinding> = lazy { + ViewCountryListEntryFlagItemBinding.bind(itemView) + } + + override val onBindData: ViewCountryListEntryFlagItemBinding.(key: Country) -> Unit = { item -> + countryListEntryImage.setImageResource(item.iconRes) + } + } +} 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 6b668e6c420a52ee06796f5a4365794fd96452b6..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/viewmodel/SubmissionViewModel.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.rki.coronawarnapp.ui.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData -import de.rki.coronawarnapp.storage.SubmissionRepository -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.viewmodel.CWAViewModel - -class SubmissionViewModel : CWAViewModel() { - - val deviceUiState: LiveData<DeviceUIState> = SubmissionRepository.deviceUIStateFlow.asLiveData() -} 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 86690e22520a4689a14f591cf5e5edab61ef853a..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 @@ -43,7 +46,10 @@ class DataReset @Inject constructor( @AppContext private val context: Context, private val keyCacheRepository: KeyCacheRepository, private val appConfigProvider: AppConfigProvider, - private val interoperabilityRepository: InteroperabilityRepository + private val interoperabilityRepository: InteroperabilityRepository, + private val submissionRepository: SubmissionRepository, + private val exposureDetectionTracker: ExposureDetectionTracker, + private val keyPackageSyncSettings: KeyPackageSyncSettings ) { private val mutex = Mutex() @@ -56,15 +62,19 @@ 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 RiskLevelRepository.reset() // Reset the current states stored in LiveData - SubmissionRepository.reset() + submissionRepository.reset() 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/EncryptedPreferencesFactory.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt index f672b43e13122e7ba39df08200f5b94eb144e375..317e3e765f7dd4fe0c60c33f9761ca9e4e2756fc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/EncryptedPreferencesFactory.kt @@ -16,13 +16,13 @@ class EncryptedPreferencesFactory @Inject constructor( @AppContext private val context: Context ) { - private val masterKeyAlias by lazy { + private val mainKeyAlias by lazy { MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) } private fun createInstance(fileName: String) = EncryptedSharedPreferences.create( fileName, - masterKeyAlias, + mainKeyAlias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM 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 60dc57174abac3d7ed941ad2dd3d7662da4066e6..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 @@ -7,6 +7,7 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.notification.NotificationHelper import de.rki.coronawarnapp.service.submission.SubmissionService import de.rki.coronawarnapp.storage.LocalData @@ -23,13 +24,10 @@ import timber.log.Timber */ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( @Assisted val context: Context, - @Assisted workerParams: WorkerParameters + @Assisted workerParams: WorkerParameters, + private val submissionService: SubmissionService ) : CoroutineWorker(context, workerParams) { - companion object { - private val TAG: String? = DiagnosisTestResultRetrievalPeriodicWorker::class.simpleName - } - /** * Work execution * @@ -44,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") @@ -67,7 +59,8 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( ) < BackgroundConstants.POLLING_VALIDITY_MAX_DAYS ) { Timber.d(" $id maximum days not exceeded") - val testResult = SubmissionService.asyncRequestTestResult() + val registrationToken = LocalData.registrationToken() ?: throw NoRegistrationTokenSetException() + val testResult = submissionService.asyncRequestTestResult(registrationToken) initiateNotification(testResult) Timber.d(" $id Test Result Notification Initiated") } else { @@ -78,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 @@ -130,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/drawable/ic_illustration_consent.xml b/Corona-Warn-App/src/main/res/drawable/ic_illustration_consent.xml new file mode 100644 index 0000000000000000000000000000000000000000..98fac7d4b24a3f910b8df6f7a0a9575df297ce6e --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_illustration_consent.xml @@ -0,0 +1,1479 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="357dp" + android:height="212dp" + android:viewportWidth="357" + android:viewportHeight="212"> + <path + android:pathData="M182.943,186.428C230.263,186.428 268.623,149.171 268.623,103.212C268.623,57.253 230.263,19.995 182.943,19.995C135.624,19.995 97.263,57.253 97.263,103.212C97.263,149.171 135.624,186.428 182.943,186.428Z" + android:fillColor="#F6FBFF" + android:fillType="evenOdd"/> + <group> + <clip-path + android:pathData="M97.264,103.212C97.264,149.171 135.624,186.428 182.944,186.428C230.264,186.428 268.624,149.171 268.624,103.212C268.624,57.253 230.264,19.995 182.944,19.995C135.624,19.995 97.264,57.253 97.264,103.212Z" + android:fillType="evenOdd"/> + <path + android:pathData="M91.589,191.94H274.298V14.484H91.589V191.94Z" + android:fillColor="#F6FBFF" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M116.303,103.212C116.303,138.958 146.14,167.936 182.944,167.936C219.748,167.936 249.584,138.958 249.584,103.212C249.584,67.467 219.748,38.488 182.944,38.488C146.14,38.488 116.303,67.467 116.303,103.212Z" + android:fillType="evenOdd"/> + <path + android:pathData="M111.891,172.222H253.997V34.202H111.891V172.222Z" + android:fillColor="#E9F6FF" + android:fillType="evenOdd"/> + </group> + <path + android:pathData="M110.646,109.217C110.646,110.661 111.506,111.917 112.782,112.591V116.894C112.782,117.46 113.284,117.918 113.905,117.918H115.931C116.551,117.918 117.053,117.46 117.053,116.894V112.591C118.329,111.917 119.191,110.661 119.191,109.217C119.191,107.063 117.278,105.316 114.917,105.316C112.559,105.316 110.646,107.063 110.646,109.217Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <group> + <clip-path + android:pathData="M184.617,46.118L183.215,56.289H192.735L192.366,46.118H184.617Z" + android:fillType="evenOdd"/> + <path + android:pathData="M178.456,60.912H197.496V41.495H178.456V60.912Z" + android:fillColor="#B7846A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M180.398,38.737C180.358,55.023 197.288,52.906 197.779,36.563C197.797,28.954 194.11,25.362 190.127,25.361C185.585,25.36 180.658,30.03 180.398,38.737Z" + android:fillType="evenOdd"/> + <path + android:pathData="M175.637,54.562H202.54V20.738H175.637V54.562Z" + android:fillColor="#B7846A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M176.921,100.962C171.143,120.311 171.573,132.867 170.371,143.455C170.354,143.476 174.236,191.711 174.232,191.723L180.627,191.875C181.407,175.777 182.19,159.678 182.969,143.579C185.013,132.831 188.136,124.146 190.408,116.138L191.11,121.861L195.559,191.458L200.313,191.307C202.338,161.553 210.444,135.866 203.123,100.204L176.921,100.962Z" + android:fillType="evenOdd"/> + <path + android:pathData="M165.611,196.499H210.888V95.581H165.611V196.499Z" + android:fillColor="#DEA8A3" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M169.313,101.365L166.838,98.64C152.845,117.212 172.257,114.582 169.313,101.365Z" + android:fillType="evenOdd"/> + <path + android:pathData="M156.952,116.547H174.382V94.017H156.952V116.547Z" + android:fillColor="#B7846A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M167.675,79.717C166.518,84.96 166.133,92.616 165.413,97.623C165.318,98.279 164.715,100.947 166.319,101.514C168.193,102.15 169.578,102.912 170.123,101.152C171.91,95.384 174.876,83.192 176.537,75.544C176.896,73.886 177.693,70.992 177.693,71.751C177.693,72.599 175.649,98.789 176.921,100.962C177.58,102.087 181.523,102.581 186.191,102.597H186.689C193.246,102.574 201.024,101.638 203.123,100.204C203.64,99.851 203.376,95.006 202.703,89.145C202.588,89.038 202.471,88.932 202.353,88.823C200.521,87.131 196.189,84.27 193.411,83.076C194.152,81.217 195.062,80.438 195.802,78.579C195.802,78.579 200.05,81.543 200.812,81.994C201.157,82.197 201.503,82.388 201.837,82.567C201.635,81.212 201.42,79.873 201.196,78.579C200.165,72.609 200.041,67.198 200.041,67.198C200.041,67.198 201.417,74.072 202.353,77.062C203.894,81.994 204.665,83.89 204.665,83.89C204.665,83.89 203.367,83.379 201.837,82.567C202.167,84.782 202.463,87.03 202.703,89.145C208.672,94.597 212.619,96.702 215.455,92.237C215.645,91.937 215.735,91.496 215.746,90.94V90.729C215.68,86.83 212.092,77.974 210.445,72.129C208.599,65.591 204.665,54.298 192.923,54.258C192.053,54.257 191.079,54.39 190.092,54.523C187.721,54.843 185.278,55.164 184.062,53.593C171.913,56.575 169.601,70.992 167.675,79.717Z" + android:fillType="evenOdd"/> + <path + android:pathData="M160.463,107.22H220.506V48.97H160.463V107.22Z" + android:fillColor="#B2DBF0" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M195.221,203.308C197.055,205.13 198.395,204.256 200.123,203.308C204.509,201.419 200.943,193.023 200.475,191.054L195.821,189.916C195.718,191.509 191.258,200.951 195.221,203.308Z" + android:fillType="evenOdd"/> + <path + android:pathData="M188.822,208.996H207.045V185.293H188.822V208.996Z" + android:fillColor="#4A4A4A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M162.167,201.373C167.671,201.362 173.949,201.352 179.453,201.341C179.643,201.281 180.05,201.123 180.402,200.732C181.47,200.081 180.478,192.884 180.627,191.874C178.495,191.824 176.364,191.774 174.232,191.723C174.489,197.126 159.109,198.063 162.167,201.373Z" + android:fillType="evenOdd"/> + <path + android:pathData="M157.004,205.997H185.664V187.101H157.004V205.997Z" + android:fillColor="#4A4A4A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M181.711,68.119C181.021,68.119 180.51,68.671 180.571,69.35L181.685,81.897C181.745,82.577 182.355,83.128 183.046,83.128H188.304C188.996,83.128 189.506,82.577 189.446,81.897L188.331,69.35C188.27,68.671 187.661,68.119 186.971,68.119H181.711Z" + android:fillType="evenOdd"/> + <path + android:pathData="M175.805,87.751H194.21V63.496H175.805V87.751Z" + android:fillColor="#4A4A4A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M184.456,78.517C186.174,80.656 190.121,82.796 193.713,82.46L194.646,80.761L195.459,79.006C193.453,77.563 191.303,75.505 188.509,74.569C188.161,74.547 187.833,74.538 187.526,74.538C183.19,74.538 182.859,76.528 184.456,78.517Z" + android:fillType="evenOdd"/> + <path + android:pathData="M178.816,87.119H200.218V69.914H178.816V87.119Z" + android:fillColor="#B7846A" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M179.415,13.95C179.13,14.261 178.706,14.408 178.284,14.354C173.412,13.715 172.636,15.174 172.317,16.634C171.999,18.092 172.14,19.549 169.114,18.916C162.918,17.616 163.835,22.276 163.519,24.226C163.205,26.173 161.022,26.478 161.022,26.478C159.168,26.993 161.113,30.087 159.049,31.664C157.374,32.942 157.896,34.506 158.154,35.053C158.209,35.171 158.285,35.278 158.374,35.374C160.874,38.072 159.053,38.989 159.053,38.989C157.944,39.843 157.526,40.645 157.511,41.362V41.446C157.54,42.719 158.821,43.723 159.724,44.273C160.175,44.547 160.4,45.073 160.302,45.588C159.956,47.435 161.801,48.443 163.807,48.227C166.1,47.981 165.963,50.026 165.963,50.026C165.186,53.013 168.156,53.043 169.704,52.865C170.205,52.809 170.702,53.048 170.953,53.478C171.583,54.565 172.389,54.94 173.183,54.959H173.311C174.253,54.937 175.16,54.439 175.713,54.065C176.054,53.833 176.489,53.775 176.877,53.918C178.757,54.613 179.779,53.92 180.242,53.405C180.474,53.145 180.82,53 181.171,53.007C184.719,53.074 186.301,47.034 185.49,45.496L181.806,40.215C181.344,39.181 179.657,34.426 182.074,33.019C183.886,31.965 185.357,33.698 188.179,30.559C190.752,27.697 190.751,27.467 192.021,27.249C192.91,27.094 193.303,29.201 196.011,30.664C196.39,30.869 196.411,31.775 196.458,32.101C197.652,40.192 192.597,44.985 190.979,46.162C190.699,46.365 190.518,46.668 190.467,47.007C189.285,54.758 197.232,54.472 198.869,54.333C199.068,54.317 199.263,54.35 199.45,54.415C201.777,55.23 203.087,53.688 203.664,52.689C203.915,52.255 204.411,52.018 204.915,52.064C207.955,52.341 208.8,50.511 209.034,49.449C209.13,49.015 209.446,48.66 209.872,48.517C212.825,47.534 212.816,45.275 212.645,44.26C212.588,43.92 212.684,43.57 212.895,43.294C213.712,42.23 214.01,41.34 214.015,40.607V40.561C214.007,39.532 213.419,38.824 212.897,38.397C212.487,38.061 212.353,37.519 212.504,37.016C213.176,34.777 210.78,33.216 209.407,32.525C208.932,32.287 208.646,31.776 208.718,31.257C208.997,29.29 207.723,28.23 206.827,27.739C206.365,27.485 206.107,26.979 206.177,26.463C206.774,22.023 203.416,21.17 202.016,21.005C201.651,20.963 201.329,20.764 201.108,20.472C199.413,18.234 196.421,19.665 196.421,19.665C196.421,19.665 195.048,20.424 195.57,18.131C195.975,16.365 194.351,15.865 193.411,15.723C193.024,15.664 192.707,15.417 192.527,15.075C191.041,12.245 187.239,12.688 185.871,12.951C185.553,13.012 185.226,12.948 184.948,12.784C184.221,12.352 183.534,12.183 182.901,12.183C181.29,12.183 180.033,13.281 179.415,13.95Z" + android:fillType="evenOdd"/> + <path + android:pathData="M152.751,59.583H218.776V7.56H152.751V59.583Z" + android:fillColor="#000000" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M192.833,83.895L203.265,94.708L201.617,81.733L195.577,78.489L192.833,83.895Z" + android:fillType="evenOdd"/> + <path + android:pathData="M188.072,99.331H208.025V73.866H188.072V99.331Z" + android:fillColor="#B2DBF0" + android:fillType="evenOdd"/> + </group> + <group> + <clip-path + android:pathData="M0,0.162h357v211.74h-357z"/> + </group> + <path + android:pathData="M31.534,49.894L114.633,31.261A3.974,4.025 121.996,0 1,119.461 34.277L144.808,140.912A3.974,4.025 121.996,0 1,141.83 145.679L58.731,164.312A3.974,4.025 121.996,0 1,53.903 161.296L28.556,54.661A3.974,4.025 121.996,0 1,31.534 49.894z" + android:fillColor="#E7E7E7"/> + <path + android:pathData="M56.347,131.659L130.579,115.014A0.46,0.466 121.993,0 1,131.138 115.363L131.138,115.363A0.46,0.466 121.993,0 1,130.794 115.915L56.561,132.56A0.46,0.466 121.993,0 1,56.003 132.21L56.003,132.21A0.46,0.466 121.993,0 1,56.347 131.659z" + android:fillColor="#657888"/> + <path + android:pathData="M58.197,138.699L112.949,126.422A0.46,0.466 121.996,0 1,113.508 126.771L113.508,126.771A0.46,0.466 121.996,0 1,113.163 127.322L58.411,139.6A0.46,0.466 121.996,0 1,57.852 139.25L57.852,139.25A0.46,0.466 121.996,0 1,58.197 138.699z" + android:fillColor="#657888"/> + <path + android:pathData="M60.247,145.494L126.131,130.721A0.46,0.466 121.993,0 1,126.69 131.07L126.69,131.07A0.46,0.466 121.993,0 1,126.345 131.622L60.461,146.395A0.46,0.466 121.993,0 1,59.902 146.045L59.902,146.045A0.46,0.466 121.993,0 1,60.247 145.494z" + android:fillColor="#657888"/> + <path + android:pathData="M92.269,63.081L91.012,63.358L90.727,62.137L91.985,61.86L92.269,63.081Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.526,62.805L92.27,63.08L91.984,61.861L93.241,61.584L93.526,62.805Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.783,62.528L93.526,62.805L93.241,61.585L94.499,61.308L94.783,62.528Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.04,62.252L94.783,62.528L94.498,61.308L95.755,61.031L96.04,62.252Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.297,61.975L96.04,62.252L95.755,61.031L97.013,60.754L97.297,61.975Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.554,61.699L97.297,61.975L97.012,60.756L98.27,60.479L98.554,61.699Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M99.812,61.422L98.555,61.699L98.27,60.478L99.526,60.201L99.812,61.422Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.096,62.644L98.839,62.919L98.554,61.7L99.812,61.423L100.096,62.644Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.381,63.864L99.124,64.14L98.839,62.919L100.096,62.644L100.381,63.864Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.666,65.085L99.409,65.362L99.123,64.141L100.382,63.864L100.666,65.085Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.95,66.306L99.693,66.581L99.408,65.362L100.666,65.085L100.95,66.306Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M101.235,67.526L99.978,67.803L99.693,66.583L100.95,66.306L101.235,67.526Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M101.519,68.748L100.262,69.024L99.977,67.804L101.235,67.527L101.519,68.748Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.756,68.473L69.013,68.196L68.728,66.976L67.471,67.253L67.756,68.473Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M66.499,68.75L67.756,68.474L67.471,67.252L66.215,67.529L66.499,68.75Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M65.243,69.026L66.5,68.749L66.214,67.528L64.957,67.805L65.243,69.026Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M63.985,69.302L65.242,69.027L64.957,67.805L63.7,68.082L63.985,69.302Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M62.728,69.58L63.985,69.303L63.7,68.082L62.444,68.359L62.728,69.58Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M61.471,69.855L62.728,69.579L62.443,68.357L61.186,68.634L61.471,69.855Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M60.214,70.132L61.471,69.855L61.186,68.635L59.93,68.912L60.214,70.132Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M60.499,71.353L61.756,71.077L61.471,69.855L60.214,70.132L60.499,71.353Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M60.783,72.574L62.04,72.297L61.755,71.076L60.5,71.353L60.783,72.574Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M61.068,73.795L62.325,73.519L62.04,72.297L60.783,72.574L61.068,73.795Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M61.353,75.016L62.61,74.741L62.324,73.519L61.069,73.796L61.353,75.016Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M61.637,76.237L62.894,75.96L62.61,74.739L61.354,75.016L61.637,76.237Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M61.922,77.458L63.179,77.182L62.894,75.96L61.637,76.237L61.922,77.458Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.739,99.403L99.482,99.68L99.766,100.901L101.023,100.624L100.739,99.403Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M101.996,99.127L100.739,99.403L101.024,100.625L102.281,100.348L101.996,99.127Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M103.253,98.85L101.996,99.127L102.281,100.347L103.537,100.07L103.253,98.85Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M104.509,98.573L103.252,98.849L103.538,100.071L104.794,99.794L104.509,98.573Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M105.767,98.297L104.51,98.574L104.795,99.795L106.052,99.518L105.767,98.297Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M107.024,98.021L105.767,98.296L106.052,99.518L107.308,99.241L107.024,98.021Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M108.281,97.744L107.024,98.02L107.309,99.242L108.566,98.965L108.281,97.744Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M107.996,96.523L106.739,96.799L107.024,98.021L108.28,97.744L107.996,96.523Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M107.711,95.302L106.455,95.579L106.74,96.799L107.997,96.522L107.711,95.302Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M107.427,94.082L106.17,94.358L106.455,95.58L107.711,95.303L107.427,94.082Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M107.142,92.86L105.885,93.137L106.17,94.358L107.427,94.081L107.142,92.86Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M106.857,91.64L105.601,91.915L105.886,93.137L107.141,92.86L106.857,91.64Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M106.572,90.419L105.315,90.695L105.601,91.917L106.857,91.64L106.572,90.419Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.226,104.796L77.482,104.519L77.768,105.739L76.511,106.016L76.226,104.796Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.969,105.072L76.226,104.796L76.511,106.017L75.253,106.292L74.969,105.072Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.712,105.348L74.969,105.071L75.254,106.292L73.997,106.569L73.712,105.348Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.455,105.624L73.712,105.349L73.997,106.568L72.739,106.845L72.455,105.624Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.198,105.901L72.455,105.624L72.74,106.845L71.482,107.122L71.198,105.901Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.94,106.177L71.197,105.901L71.482,107.121L70.226,107.398L69.94,106.177Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.684,106.455L69.941,106.178L70.226,107.398L68.968,107.675L68.684,106.455Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.399,105.233L69.656,104.957L69.941,106.176L68.684,106.453L68.399,105.233Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.115,104.012L69.372,103.735L69.657,104.956L68.399,105.233L68.115,104.012Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.83,102.792L69.086,102.515L69.372,103.735L68.115,104.012L67.83,102.792Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.545,101.571L68.802,101.295L69.087,102.514L67.829,102.791L67.545,101.571Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.26,100.35L68.517,100.073L68.802,101.294L67.546,101.571L67.26,100.35Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M66.976,99.128L68.232,98.853L68.518,100.072L67.259,100.349L66.976,99.128Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.831,70.589L76.574,70.866L76.289,69.645L77.547,69.368L77.831,70.589Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.602,69.76L80.345,70.035L80.06,68.816L81.318,68.539L81.602,69.76Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.373,71.533L78.116,71.81L77.831,70.59L79.089,70.313L79.373,71.533Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.144,70.704L81.887,70.981L81.602,69.76L82.86,69.483L83.144,70.704Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.658,70.15L84.401,70.427L84.116,69.207L85.374,68.93L85.658,70.15Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.914,72.479L79.657,72.754L79.372,71.534L80.63,71.258L80.914,72.479Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.943,71.372L84.686,71.648L84.401,70.428L85.658,70.151L85.943,71.372Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.685,74.252L77.428,74.529L77.143,73.308L78.4,73.031L78.685,74.252Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.199,73.698L79.942,73.975L79.657,72.755L80.914,72.478L81.199,73.698Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.713,73.146L82.456,73.423L82.171,72.202L83.429,71.925L83.713,73.146Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M84.97,72.869L83.713,73.145L83.428,71.925L84.685,71.648L84.97,72.869Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.969,75.473L77.713,75.748L77.427,74.529L78.685,74.252L78.969,75.473Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.227,75.196L78.97,75.473L78.685,74.253L79.941,73.976L80.227,75.196Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.511,76.417L79.255,76.693L78.969,75.473L80.227,75.196L80.511,76.417Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.769,76.141L80.512,76.418L80.227,75.197L81.483,74.92L81.769,76.141Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.54,75.311L84.283,75.586L83.998,74.367L85.254,74.09L85.54,75.311Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M86.796,75.035L85.54,75.311L85.254,74.09L86.513,73.814L86.796,75.035Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.539,77.914L78.282,78.19L77.997,76.97L79.255,76.693L79.539,77.914Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.31,77.085L82.053,77.361L81.768,76.14L83.025,75.864L83.31,77.085Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.824,79.136L78.567,79.411L78.282,78.191L79.539,77.915L79.824,79.136Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.081,78.858L79.824,79.135L79.539,77.915L80.797,77.638L81.081,78.858Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.595,78.306L82.338,78.583L82.053,77.362L83.311,77.085L83.595,78.306Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M86.109,77.753L84.853,78.03L84.567,76.809L85.824,76.532L86.109,77.753Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.366,77.477L86.109,77.752L85.824,76.533L87.082,76.256L87.366,77.477Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.887,68.377L86.63,68.653L86.345,67.432L87.603,67.156L87.887,68.377Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.172,69.598L86.916,69.875L86.63,68.654L87.887,68.377L88.172,69.598Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.457,70.819L87.2,71.095L86.915,69.876L88.173,69.599L88.457,70.819Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.971,70.266L89.714,70.541L89.429,69.322L90.687,69.045L90.971,70.266Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.228,69.989L90.971,70.266L90.686,69.046L91.943,68.769L92.228,69.989Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.484,69.713L92.228,69.989L91.942,68.769L93.201,68.492L93.484,69.713Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.742,69.436L93.485,69.713L93.2,68.492L94.458,68.215L94.742,69.436Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.742,72.039L87.485,72.316L87.2,71.095L88.456,70.818L88.742,72.039Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.255,71.486L89.999,71.763L89.713,70.543L90.97,70.266L91.255,71.486Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.512,71.21L91.255,71.486L90.97,70.266L92.228,69.989L92.512,71.21Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.77,70.934L92.513,71.211L92.228,69.99L93.484,69.713L93.77,70.934Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.027,70.657L93.77,70.933L93.485,69.714L94.742,69.437L95.027,70.657Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.026,73.261L87.769,73.536L87.484,72.317L88.742,72.04L89.026,73.261Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.54,72.708L90.283,72.985L89.998,71.764L91.256,71.487L91.54,72.708Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.797,72.432L91.541,72.707L91.255,71.487L92.512,71.211L92.797,72.432Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.054,72.154L92.797,72.431L92.512,71.211L93.77,70.934L94.054,72.154Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.312,71.878L94.055,72.154L93.77,70.933L95.028,70.657L95.312,71.878Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.311,74.481L88.054,74.757L87.769,73.536L89.027,73.261L89.311,74.481Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.825,73.929L90.568,74.204L90.283,72.985L91.54,72.708L91.825,73.929Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.082,73.652L91.825,73.929L91.54,72.709L92.798,72.432L93.082,73.652Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.339,73.376L93.082,73.652L92.797,72.432L94.054,72.155L94.339,73.376Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.596,73.099L94.339,73.376L94.054,72.155L95.312,71.878L95.596,73.099Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.596,75.702L88.339,75.979L88.054,74.758L89.311,74.481L89.596,75.702Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.145,68.101L87.888,68.378L87.603,67.157L88.859,66.88L89.145,68.101Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.401,67.824L89.145,68.1L88.859,66.879L90.118,66.604L90.401,67.824Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.658,67.547L90.401,67.824L90.116,66.603L91.373,66.326L91.658,67.547Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.916,67.271L91.659,67.547L91.373,66.328L92.63,66.051L92.916,67.271Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.172,66.994L92.916,67.271L92.63,66.05L93.888,65.773L94.172,66.994Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.43,66.718L94.173,66.993L93.888,65.774L95.145,65.497L95.43,66.718Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.687,66.441L95.43,66.718L95.145,65.498L96.403,65.221L96.687,66.441Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.971,67.662L95.714,67.938L95.429,66.718L96.686,66.441L96.971,67.662Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.256,68.884L95.999,69.16L95.714,67.939L96.972,67.663L97.256,68.884Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.541,70.104L96.284,70.381L95.999,69.16L97.256,68.883L97.541,70.104Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.825,71.325L96.568,71.601L96.283,70.382L97.541,70.104L97.825,71.325Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.11,72.546L96.854,72.823L96.568,71.602L97.825,71.325L98.11,72.546Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.395,73.767L97.138,74.042L96.853,72.823L98.111,72.546L98.395,73.767Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.88,76.924L88.623,77.2L88.338,75.98L89.596,75.703L89.88,76.924Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.137,76.646L89.88,76.924L89.595,75.703L90.853,75.426L91.137,76.646Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.395,76.37L91.138,76.646L90.853,75.426L92.109,75.149L92.395,76.37Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.651,76.094L92.395,76.371L92.109,75.15L93.368,74.873L93.651,76.094Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.908,75.817L93.651,76.093L93.366,74.874L94.623,74.597L94.908,75.817Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.165,75.54L94.908,75.817L94.623,74.596L95.881,74.319L96.165,75.54Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.422,75.265L96.166,75.54L95.88,74.321L97.138,74.044L97.422,75.265Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.68,74.988L97.423,75.264L97.138,74.043L98.395,73.768L98.68,74.988Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M66.518,73.078L65.261,73.354L64.976,72.133L66.232,71.857L66.518,73.078Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.774,72.801L66.517,73.078L66.232,71.857L67.49,71.58L67.774,72.801Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.031,72.524L67.774,72.8L67.489,71.579L68.747,71.304L69.031,72.524Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M70.289,72.248L69.032,72.525L68.747,71.304L70.003,71.027L70.289,72.248Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.546,71.972L70.289,72.247L70.003,71.027L71.262,70.751L71.546,71.972Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.803,71.695L71.546,71.972L71.261,70.752L72.518,70.475L72.803,71.695Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.06,71.419L72.803,71.695L72.518,70.475L73.776,70.198L74.06,71.419Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.316,71.142L74.06,71.419L73.774,70.198L75.033,69.921L75.316,71.142Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M66.802,74.299L65.545,74.576L65.26,73.355L66.518,73.078L66.802,74.299Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.602,72.363L74.345,72.639L74.06,71.42L75.316,71.143L75.602,72.363Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.087,75.52L65.83,75.795L65.545,74.576L66.802,74.299L67.087,75.52Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.601,74.967L68.344,75.243L68.059,74.023L69.317,73.746L69.601,74.967Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M70.858,74.689L69.601,74.966L69.316,73.746L70.573,73.469L70.858,74.689Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.115,74.413L70.858,74.689L70.573,73.469L71.831,73.192L72.115,74.413Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.372,74.137L72.115,74.414L71.83,73.193L73.087,72.916L73.372,74.137Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.886,73.584L74.63,73.861L74.344,72.64L75.602,72.363L75.886,73.584Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.371,76.74L66.114,77.017L65.829,75.797L67.087,75.52L67.371,76.74Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.885,76.188L68.629,76.465L68.343,75.244L69.601,74.967L69.885,76.188Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.143,75.911L69.886,76.187L69.601,74.967L70.859,74.69L71.143,75.911Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.4,75.634L71.143,75.911L70.858,74.69L72.115,74.413L72.4,75.634Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.656,75.357L72.399,75.633L72.114,74.414L73.372,74.137L73.656,75.357Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.171,74.805L74.914,75.082L74.629,73.861L75.886,73.584L76.171,74.805Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.656,77.961L66.399,78.237L66.114,77.017L67.371,76.74L67.656,77.961Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M70.171,77.408L68.914,77.684L68.628,76.465L69.885,76.188L70.171,77.408Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.428,77.132L70.171,77.408L69.886,76.187L71.143,75.911L71.428,77.132Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.684,76.855L71.427,77.132L71.142,75.912L72.4,75.635L72.684,76.855Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.941,76.579L72.685,76.855L72.399,75.634L73.656,75.358L73.941,76.579Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.456,76.025L75.199,76.301L74.914,75.082L76.172,74.805L76.456,76.025Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M67.94,79.183L66.684,79.458L66.398,78.238L67.657,77.962L67.94,79.183Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M70.455,78.63L69.198,78.906L68.913,77.685L70.171,77.409L70.455,78.63Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.712,78.353L70.455,78.63L70.17,77.409L71.428,77.132L71.712,78.353Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.969,78.076L71.713,78.352L71.427,77.132L72.684,76.855L72.969,78.076Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.226,77.8L72.969,78.077L72.684,76.856L73.942,76.579L74.226,77.8Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.74,77.246L75.483,77.523L75.198,76.302L76.456,76.025L76.74,77.246Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.226,80.403L66.969,80.68L66.684,79.46L67.94,79.183L68.226,80.403Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.025,78.468L75.768,78.743L75.483,77.524L76.74,77.247L77.025,78.468Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.51,81.624L67.254,81.9L66.968,80.68L68.226,80.403L68.51,81.624Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.768,81.348L68.511,81.625L68.226,80.404L69.482,80.127L69.768,81.348Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.024,81.071L69.767,81.347L69.482,80.128L70.74,79.851L71.024,81.071Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.281,80.794L71.024,81.071L70.739,79.85L71.996,79.573L72.281,80.794Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.539,80.518L72.282,80.793L71.997,79.574L73.253,79.297L73.539,80.518Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.796,80.241L73.539,80.518L73.253,79.298L74.512,79.021L74.796,80.241Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.053,79.965L74.796,80.241L74.511,79.021L75.768,78.744L76.053,79.965Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.309,79.688L76.052,79.965L75.767,78.745L77.025,78.468L77.309,79.688Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.357,93.833L70.101,94.109L69.815,92.889L71.072,92.612L71.357,93.833Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.614,93.557L71.357,93.834L71.072,92.613L72.33,92.336L72.614,93.557Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.871,93.28L72.614,93.556L72.329,92.337L73.587,92.06L73.871,93.28Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.129,93.003L73.872,93.28L73.586,92.059L74.843,91.782L75.129,93.003Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.385,92.728L75.129,93.003L74.843,91.784L76.101,91.507L76.385,92.728Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.643,92.45L76.386,92.727L76.101,91.507L77.357,91.229L77.643,92.45Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.899,92.174L77.643,92.45L77.357,91.23L78.616,90.953L78.899,92.174Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.157,91.897L78.9,92.174L78.615,90.954L79.873,90.677L80.157,91.897Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.642,95.054L70.385,95.331L70.1,94.11L71.358,93.833L71.642,95.054Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.441,93.118L79.185,93.395L78.899,92.174L80.156,91.897L80.441,93.118Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.927,96.274L70.67,96.55L70.385,95.331L71.642,95.054L71.927,96.274Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.441,95.722L73.184,95.997L72.899,94.778L74.156,94.501L74.441,95.722Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.698,95.445L74.441,95.721L74.156,94.5L75.413,94.225L75.698,95.445Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.955,95.169L75.698,95.446L75.413,94.225L76.671,93.948L76.955,95.169Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.212,94.893L76.955,95.168L76.67,93.948L77.927,93.672L78.212,94.893Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.726,94.34L79.469,94.616L79.184,93.396L80.442,93.119L80.726,94.34Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.211,97.496L70.954,97.772L70.669,96.551L71.927,96.275L72.211,97.496Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.725,96.943L73.468,97.219L73.183,95.998L74.441,95.723L74.725,96.943Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.982,96.666L74.726,96.943L74.44,95.722L75.699,95.445L75.982,96.666Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.24,96.39L75.983,96.665L75.698,95.446L76.955,95.169L77.24,96.39Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.497,96.113L77.24,96.39L76.955,95.17L78.213,94.893L78.497,96.113Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.01,95.56L79.754,95.837L79.468,94.616L80.726,94.339L81.01,95.56Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.496,98.717L71.239,98.994L70.954,97.773L72.211,97.496L72.496,98.717Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.01,98.163L73.754,98.44L73.468,97.219L74.725,96.942L75.01,98.163Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.267,97.888L75.01,98.163L74.725,96.944L75.983,96.667L76.267,97.888Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.524,97.61L76.267,97.887L75.982,96.667L77.24,96.39L77.524,97.61Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.781,97.334L77.524,97.61L77.239,96.39L78.496,96.113L78.781,97.334Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.296,96.781L80.039,97.057L79.753,95.838L81.01,95.561L81.296,96.781Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.781,99.938L71.524,100.213L71.239,98.994L72.497,98.717L72.781,99.938Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.295,99.385L74.038,99.66L73.752,98.441L75.011,98.164L75.295,99.385Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.552,99.107L75.295,99.384L75.01,98.164L76.267,97.887L76.552,99.107Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.809,98.832L76.552,99.108L76.267,97.888L77.524,97.611L77.809,98.832Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.066,98.555L77.809,98.832L77.524,97.611L78.782,97.334L79.066,98.555Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.58,98.002L80.323,98.279L80.038,97.058L81.296,96.781L81.58,98.002Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.065,101.158L71.809,101.435L71.523,100.215L72.78,99.938L73.065,101.158Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.865,99.223L80.608,99.5L80.323,98.279L81.58,98.002L81.865,99.223Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.35,102.379L72.093,102.655L71.808,101.435L73.066,101.158L73.35,102.379Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.607,102.103L73.351,102.379L73.065,101.159L74.322,100.883L74.607,102.103Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.864,101.826L74.607,102.103L74.322,100.882L75.58,100.605L75.864,101.826Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M77.121,101.55L75.864,101.826L75.579,100.605L76.836,100.329L77.121,101.55Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.378,101.273L77.121,101.55L76.836,100.33L78.094,100.053L78.378,101.273Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.635,100.997L78.379,101.273L78.093,100.052L79.351,99.776L79.635,100.997Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.893,100.721L79.636,100.998L79.351,99.777L80.607,99.5L80.893,100.721Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M82.149,100.444L80.893,100.72L80.607,99.501L81.866,99.224L82.149,100.444Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M82.623,79.803L81.366,80.08L81.081,78.859L82.339,78.582L82.623,79.803Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.879,79.527L82.623,79.803L82.337,78.584L83.594,78.307L83.879,79.527Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.136,79.25L83.88,79.527L83.594,78.306L84.852,78.029L85.136,79.25Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.651,78.697L86.394,78.974L86.109,77.754L87.366,77.477L87.651,78.697Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.08,84.065L67.823,84.341L67.538,83.122L68.796,82.845L69.08,84.065Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.594,83.513L70.337,83.788L70.052,82.569L71.31,82.292L71.594,83.513Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.851,83.236L71.594,83.512L71.309,82.291L72.565,82.016L72.851,83.236Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.107,82.96L72.851,83.237L72.565,82.016L73.824,81.739L74.107,82.96Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.622,82.406L75.365,82.683L75.08,81.463L76.337,81.186L76.622,82.406Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.394,81.577L79.137,81.853L78.852,80.633L80.108,80.356L80.394,81.577Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.65,81.301L80.393,81.578L80.108,80.357L81.366,80.08L81.65,81.301Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M82.907,81.024L81.65,81.3L81.365,80.081L82.622,79.804L82.907,81.024Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.421,80.471L84.165,80.746L83.879,79.527L85.136,79.25L85.421,80.471Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M86.679,80.194L85.422,80.471L85.137,79.251L86.394,78.974L86.679,80.194Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.935,79.918L86.678,80.194L86.393,78.974L87.651,78.697L87.935,79.918Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.192,79.642L87.936,79.919L87.65,78.698L88.907,78.421L89.192,79.642Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.221,78.536L92.964,78.812L92.679,77.591L93.937,77.315L94.221,78.536Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.992,77.706L96.735,77.983L96.45,76.762L97.706,76.485L97.992,77.706Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M99.249,77.43L97.992,77.705L97.707,76.486L98.964,76.209L99.249,77.43Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M70.621,85.01L69.364,85.287L69.079,84.066L70.337,83.789L70.621,85.01Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.135,84.457L71.879,84.734L71.593,83.513L72.851,83.236L73.135,84.457Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.164,83.351L76.907,83.628L76.622,82.407L77.878,82.13L78.164,83.351Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M80.678,82.798L79.421,83.075L79.136,81.854L80.394,81.577L80.678,82.798Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.706,81.691L84.449,81.968L84.164,80.748L85.422,80.471L85.706,81.691Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.22,81.14L86.963,81.415L86.678,80.195L87.935,79.919L88.22,81.14Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.734,80.586L89.478,80.862L89.192,79.641L90.449,79.365L90.734,80.586Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.505,79.757L93.249,80.034L92.963,78.813L94.22,78.536L94.505,79.757Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.762,79.48L94.505,79.756L94.22,78.537L95.478,78.26L95.762,79.48Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.276,78.927L97.02,79.202L96.734,77.983L97.993,77.706L98.276,78.927Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M69.649,86.508L68.392,86.785L68.107,85.564L69.365,85.287L69.649,86.508Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M70.906,86.231L69.649,86.507L69.364,85.288L70.621,85.011L70.906,86.231Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.163,85.954L70.906,86.231L70.621,85.01L71.879,84.733L72.163,85.954Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.677,85.401L73.42,85.678L73.135,84.458L74.393,84.181L74.677,85.401Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.706,84.295L78.449,84.572L78.164,83.351L79.42,83.074L79.706,84.295Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M82.22,83.742L80.963,84.019L80.678,82.799L81.936,82.521L82.22,83.742Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.991,82.913L84.734,83.19L84.449,81.969L85.706,81.692L85.991,82.913Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.019,81.807L89.762,82.084L89.477,80.863L90.735,80.586L91.019,81.807Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.533,81.254L92.276,81.531L91.991,80.31L93.249,80.033L93.533,81.254Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.047,80.701L94.791,80.978L94.505,79.757L95.762,79.48L96.047,80.701Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.448,87.176L71.191,87.451L70.906,86.232L72.163,85.955L72.448,87.176Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.733,85.793L77.477,86.07L77.191,84.849L78.448,84.572L78.733,85.793Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.99,85.517L78.733,85.792L78.448,84.573L79.706,84.296L79.99,85.517Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.247,85.24L79.99,85.516L79.705,84.295L80.962,84.02L81.247,85.24Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.019,84.41L83.762,84.687L83.477,83.466L84.733,83.189L85.019,84.41Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.532,83.857L86.275,84.134L85.99,82.914L87.247,82.637L87.532,83.857Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.79,83.581L87.533,83.857L87.248,82.637L88.504,82.36L88.79,83.581Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.561,82.751L91.304,83.028L91.019,81.807L92.277,81.53L92.561,82.751Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.818,82.475L92.561,82.75L92.276,81.531L93.533,81.254L93.818,82.475Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.103,81.093L98.846,81.368L98.561,80.148L99.818,79.872L100.103,81.093Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M71.476,88.673L70.219,88.949L69.934,87.729L71.19,87.452L71.476,88.673Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.732,88.396L71.476,88.674L71.19,87.453L72.449,87.176L72.732,88.396Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.247,87.843L73.99,88.12L73.705,86.899L74.963,86.622L75.247,87.843Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.018,87.014L77.761,87.291L77.476,86.07L78.734,85.793L79.018,87.014Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M81.532,86.461L80.275,86.738L79.99,85.517L81.248,85.24L81.532,86.461Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M82.789,86.185L81.532,86.46L81.247,85.241L82.505,84.964L82.789,86.185Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.303,85.632L84.046,85.908L83.761,84.688L85.019,84.411L85.303,85.632Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.874,82.866L96.617,83.143L96.331,81.923L97.59,81.646L97.874,82.866Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M99.131,82.59L97.874,82.866L97.589,81.645L98.846,81.369L99.131,82.59Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M73.018,89.617L71.761,89.893L71.476,88.674L72.732,88.396L73.018,89.617Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M76.788,88.788L75.531,89.064L75.246,87.843L76.504,87.567L76.788,88.788Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M78.046,88.511L76.789,88.788L76.503,87.567L77.762,87.29L78.046,88.511Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.073,87.405L81.816,87.682L81.531,86.462L82.789,86.185L83.073,87.405Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.616,85.746L89.359,86.023L89.074,84.802L90.332,84.525L90.616,85.746Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.13,85.193L91.873,85.469L91.588,84.248L92.846,83.973L93.13,85.193Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.387,84.917L93.13,85.194L92.845,83.973L94.102,83.696L94.387,84.917Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.901,84.363L95.644,84.64L95.359,83.42L96.617,83.143L96.901,84.363Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M72.045,91.114L70.788,91.391L70.502,90.171L71.761,89.894L72.045,91.114Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M74.559,90.562L73.302,90.839L73.017,89.618L74.274,89.341L74.559,90.562Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M75.816,90.285L74.559,90.561L74.274,89.341L75.532,89.064L75.816,90.285Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M79.587,89.455L78.33,89.732L78.045,88.511L79.303,88.234L79.587,89.455Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.358,88.626L82.102,88.902L81.816,87.682L83.073,87.405L83.358,88.626Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M84.616,88.35L83.359,88.627L83.074,87.406L84.331,87.129L84.616,88.35Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.129,87.796L85.873,88.073L85.587,86.852L86.844,86.575L87.129,87.796Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.644,87.244L88.387,87.52L88.102,86.299L89.358,86.023L89.644,87.244Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.901,86.967L89.644,87.244L89.359,86.023L90.616,85.746L90.901,86.967Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.929,85.861L94.672,86.138L94.387,84.918L95.644,84.641L95.929,85.861Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M97.186,85.585L95.929,85.861L95.644,84.641L96.901,84.364L97.186,85.585Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M84.9,89.57L83.643,89.846L83.358,88.627L84.616,88.35L84.9,89.57Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.414,89.018L86.157,89.295L85.872,88.074L87.13,87.797L87.414,89.018Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.671,88.741L87.415,89.017L87.129,87.796L88.386,87.521L88.671,88.741Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.442,87.911L91.186,88.188L90.9,86.967L92.157,86.69L92.442,87.911Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.699,87.635L92.442,87.91L92.157,86.691L93.415,86.414L93.699,87.635Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.728,86.529L97.471,86.805L97.186,85.586L98.443,85.309L98.728,86.529Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M101.242,85.976L99.985,86.251L99.7,85.032L100.957,84.755L101.242,85.976Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M82.671,91.345L81.414,91.62L81.128,90.4L82.387,90.124L82.671,91.345Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.928,91.068L82.671,91.345L82.386,90.125L83.643,89.848L83.928,91.068Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.956,89.962L87.699,90.239L87.414,89.018L88.672,88.741L88.956,89.962Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.213,89.686L88.956,89.961L88.671,88.742L89.928,88.465L90.213,89.686Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.727,89.133L91.47,89.409L91.185,88.189L92.443,87.912L92.727,89.133Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.498,88.303L95.241,88.58L94.956,87.359L96.213,87.082L96.498,88.303Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M99.012,87.75L97.755,88.027L97.47,86.806L98.728,86.529L99.012,87.75Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.241,91.183L87.984,91.458L87.699,90.239L88.956,89.962L89.241,91.183Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.24,93.786L81.983,94.062L81.698,92.842L82.956,92.565L83.24,93.786Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M84.497,93.51L83.24,93.787L82.955,92.566L84.212,92.289L84.497,93.51Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.011,92.956L85.755,93.233L85.469,92.012L86.727,91.735L87.011,92.956Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M90.782,92.127L89.525,92.403L89.24,91.183L90.497,90.906L90.782,92.127Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.039,91.851L90.782,92.128L90.497,90.907L91.755,90.63L92.039,91.851Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.296,91.574L92.04,91.85L91.754,90.631L93.013,90.354L93.296,91.574Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.811,91.021L94.554,91.299L94.269,90.078L95.527,89.801L95.811,91.021Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M99.582,90.191L98.325,90.467L98.04,89.248L99.298,88.971L99.582,90.191Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M102.096,89.639L100.839,89.914L100.554,88.695L101.812,88.418L102.096,89.639Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M83.525,95.007L82.268,95.284L81.983,94.063L83.24,93.786L83.525,95.007Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.553,93.9L87.296,94.177L87.011,92.957L88.269,92.68L88.553,93.9Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.81,93.625L88.553,93.901L88.268,92.681L89.525,92.404L89.81,93.625Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M93.581,92.795L92.324,93.071L92.039,91.85L93.297,91.574L93.581,92.795Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.066,95.951L83.81,96.228L83.524,95.007L84.781,94.73L85.066,95.951Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.581,95.398L86.324,95.674L86.039,94.453L87.295,94.178L87.581,95.398Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M91.352,94.568L90.095,94.845L89.81,93.625L91.067,93.348L91.352,94.568Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M92.608,94.293L91.352,94.569L91.066,93.349L92.325,93.072L92.608,94.293Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M96.38,93.463L95.123,93.74L94.838,92.519L96.096,92.242L96.38,93.463Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M98.894,92.91L97.637,93.187L97.352,91.966L98.61,91.689L98.894,92.91Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M95.408,94.96L94.151,95.237L93.866,94.016L95.124,93.739L95.408,94.96Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.436,93.854L99.179,94.132L98.894,92.911L100.152,92.634L100.436,93.854Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M101.693,93.578L100.436,93.854L100.151,92.634L101.409,92.357L101.693,93.578Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M102.95,93.302L101.693,93.577L101.408,92.357L102.665,92.081L102.95,93.302Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M84.379,98.67L83.122,98.947L82.837,97.726L84.095,97.449L84.379,98.67Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M85.636,98.394L84.38,98.669L84.094,97.45L85.351,97.173L85.636,98.394Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M88.15,97.841L86.893,98.117L86.608,96.897L87.865,96.62L88.15,97.841Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M99.463,95.352L98.207,95.627L97.921,94.407L99.179,94.131L99.463,95.352Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M100.721,95.075L99.464,95.352L99.179,94.132L100.436,93.854L100.721,95.075Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M87.178,99.338L85.921,99.614L85.636,98.394L86.893,98.117L87.178,99.338Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M89.692,98.785L88.435,99.061L88.15,97.841L89.407,97.564L89.692,98.785Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M94.72,97.679L93.463,97.954L93.178,96.735L94.436,96.458L94.72,97.679Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M103.52,95.743L102.263,96.019L101.978,94.799L103.234,94.522L103.52,95.743Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M146.138,91.139L144.138,91.13L146.138,91.139Z" + android:fillColor="#FF395A" + android:fillType="evenOdd"/> +</vector> 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_consent.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml new file mode 100644 index 0000000000000000000000000000000000000000..e21c93b44e060114a24fe6da76246af0489f3c2b --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml @@ -0,0 +1,147 @@ +<?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> + <variable + name="countries" + type="java.util.List<de.rki.coronawarnapp.ui.Country>" /> + + <variable + name="viewModel" + type="de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentViewModel" /> + + </data> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/submission_consent_header" + layout="@layout/include_header" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + app:icon="@{@drawable/ic_close}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:title="@{@string/submission_consent_main_headline}" /> + + <ScrollView + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/match_constraint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/submission_consent_header" + app:layout_constraintBottom_toTopOf="@+id/guideline_action"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="@dimen/spacing_normal"> + + <ImageView + android:id="@+id/submission_consent_illustration" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:src="@drawable/ic_illustration_consent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:importantForAccessibility="no"/> + + <include layout="@layout/include_submission_consent_intro" + android:id="@+id/include_submission_consent_intro" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/submission_consent_illustration"/> + + <de.rki.coronawarnapp.ui.view.CountryListView + android:id="@+id/countryList" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginHorizontal="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@+id/include_submission_consent_intro" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:countryList="@{countries}"/> + + <include layout="@layout/include_submission_consent_body" + android:id="@+id/include_submission_consent_body" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginHorizontal="@dimen/guideline_card" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/countryList"/> + + <FrameLayout + android:id="@+id/divider" + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/card_divider" + android:layout_marginTop="@dimen/spacing_normal" + android:background="@color/colorHairline" + app:layout_constraintTop_toBottomOf="@id/include_submission_consent_body" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end"/> + + <TextView + android:id="@+id/submission_consent_main_bottom_body" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/spacing_tiny" + android:text="@string/submission_consent_main_bottom_body" + android:focusable="true" + android:clickable="true" + android:onClick="@{ () -> viewModel.onDataPrivacyClick() }" + android:background="?selectableItemBackground" + app:layout_constraintTop_toBottomOf="@id/divider" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + style="@style/subtitle"/> + + <FrameLayout + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/card_divider" + android:background="@color/colorHairline" + app:layout_constraintTop_toBottomOf="@id/submission_consent_main_bottom_body" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end"/> + + <include layout="@layout/merge_guidelines_side" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </ScrollView> + + <Button + android:id="@+id/submission_consent_button" + style="@style/buttonPrimary" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:text="@string/submission_accept_button" + android:textAllCaps="true" + android:onClick="@{ () -> viewModel.onConsentButtonClick()}" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/guideline_action" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline_action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/guideline_action" /> + + <include layout="@layout/merge_guidelines_side" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_dispatcher.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_dispatcher.xml index bd6ecad1b415a80f45ccf644a904b8037f539bde..63a28f062d471569e401582f95995b546f2e41b6 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_dispatcher.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_dispatcher.xml @@ -16,7 +16,7 @@ layout="@layout/include_header" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - app:icon="@{@drawable/ic_back}" + app:icon="@{@drawable/ic_close}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -34,4 +34,4 @@ </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_positive_other_warning.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_positive_other_warning.xml index fcf4c4a8300f153e0b8e3adc5bf5ee8544990ede..b09583be3dc7083c1eb2d8a5959addee8765f7f2 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_positive_other_warning.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_positive_other_warning.xml @@ -47,7 +47,7 @@ android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" android:enabled="@{uiState != null && uiState.isSubmitButtonEnabled()}" - android:text="@string/submission_positive_other_warning_button" + android:text="@string/submission_accept_button" android:textAllCaps="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/guideline_end" @@ -78,4 +78,4 @@ </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file +</layout> 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 deleted file mode 100644 index d5a9a1cd10884ea60e9a489a467157ee8a0f1ccb..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_qr_code_info.xml +++ /dev/null @@ -1,265 +0,0 @@ -<?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" - xmlns:tools="http://schemas.android.com/tools"> - - <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:fillViewport="true" - tools:context=".ui.submission.fragment.SubmissionQRCodeInfo"> - - <include - android:id="@+id/submission_qr_code_info_header" - layout="@layout/include_header" - 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" /> - - <ScrollView - android:id="@+id/submission_qr_code_info_scrollview" - android:layout_width="match_parent" - android:layout_height="wrap_content" - 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> - - <Button - android:id="@+id/submission_qr_info_button_next" - style="@style/buttonPrimary" - android:layout_width="@dimen/match_constraint" - android:layout_height="wrap_content" - android:layout_marginVertical="@dimen/spacing_normal" - android:text="@string/submission_intro_button_next" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="@id/guideline_end" - app:layout_constraintStart_toStartOf="@id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/guideline_action" /> - - <androidx.constraintlayout.widget.Guideline - android:id="@+id/guideline_action" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_action" /> - - <include layout="@layout/merge_guidelines_side" /> - - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file 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_bullet_point.xml b/Corona-Warn-App/src/main/res/layout/include_bullet_point.xml new file mode 100644 index 0000000000000000000000000000000000000000..099185c7ef900b417cb1b5382f195da98da6e0dc --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_bullet_point.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:tools="http://schemas.android.com/tools" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <ImageView + android:id="@+id/bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:baseline="@dimen/bullet_point_baseline_offset" + android:src="@drawable/bullet_point" + android:importantForAccessibility="no" + app:layout_constraintStart_toStartOf="parent" + tools:showIn="@layout/include_submission_consent_body" /> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/include_interop_list_participating_countries_overview.xml b/Corona-Warn-App/src/main/res/layout/include_interop_list_participating_countries_overview.xml index f6142d91d2b25a4ddeb6c664fbf4d346b6f046dc..6ecf73f518ab7120bf5876447faf66fc9bd0052a 100644 --- a/Corona-Warn-App/src/main/res/layout/include_interop_list_participating_countries_overview.xml +++ b/Corona-Warn-App/src/main/res/layout/include_interop_list_participating_countries_overview.xml @@ -1,6 +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"> + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> <data> @@ -36,17 +37,18 @@ android:visibility="@{FormatterHelper.formatVisibilityText(countryListTitle)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/interoperability_onboarding_list_title" /> - <de.rki.coronawarnapp.ui.view.CountryList + <de.rki.coronawarnapp.ui.view.CountryListView android:id="@+id/countryList" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" + app:countryList="@{countryData}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/country_header_description" - app:list="@{countryData}" /> + app:layout_constraintTop_toBottomOf="@+id/country_header_description" /> <TextView android:id="@+id/label_country_selection_info" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_consent_body.xml b/Corona-Warn-App/src/main/res/layout/include_submission_consent_body.xml new file mode 100644 index 0000000000000000000000000000000000000000..2531e8dc114346c9b70f5a4645f29ad03b37f8e9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_submission_consent_body.xml @@ -0,0 +1,152 @@ +<?xml version="1.0" encoding="utf-8"?> +<layout xmlns:tools="http://schemas.android.com/tools" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <data/> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <FrameLayout + android:layout_width="@dimen/match_constraint" + android:layout_height="@dimen/match_constraint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/submission_consent_your_consent_subsection_headline" + app:layout_constraintBottom_toBottomOf="@id/submission_consent_main_fourth_point" + style="@style/cardGrey"/> + + <TextView + android:id="@+id/submission_consent_your_consent_subsection_headline" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:text="@string/submission_consent_your_consent_subsection_headline" + android:paddingTop="@dimen/spacing_normal" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toTopOf="parent" + style="@style/headline6"/> + + <TextView + android:id="@+id/submission_consent_your_consent_subsection_tapping_agree" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_your_consent_subsection_headline" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + android:text="@string/submission_consent_your_consent_subsection_tapping_agree" + style="@style/subtitle" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/submission_consent_your_consent_subsection_first_point" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:paddingVertical="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_tapping_agree" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start"> + + <include layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/submission_consent_your_consent_subsection_first_point_text"/> + + <TextView + android:id="@+id/submission_consent_your_consent_subsection_first_point_text" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bullet_point_spacing_after" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/bullet_point" + app:layout_constraintTop_toTopOf="parent" + android:text="@string/submission_consent_your_consent_subsection_first_point" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/submission_consent_your_consent_subsection_second_point" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:paddingBottom="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_first_point" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start"> + + <include layout="@layout/include_bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + app:layout_constraintBaseline_toBaselineOf="@id/submission_consent_your_consent_subsection_second_point_text"/> + + <TextView + android:id="@+id/submission_consent_your_consent_subsection_second_point_text" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bullet_point_spacing_after" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/bullet_point" + app:layout_constraintTop_toTopOf="parent" + android:text="@string/submission_consent_your_consent_subsection_second_point" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + android:id="@+id/submission_consent_your_consent_subsection_third_point" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bullet_point_size" + android:paddingStart="@dimen/spacing_normal" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_second_point" + android:text="@string/submission_consent_your_consent_subsection_third_point" + tools:ignore="RtlSymmetry" /> + + <include layout="@layout/view_bullet_point_text" + android:id="@+id/submission_consent_main_first_point" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_third_point" + app:itemText="@{@string/submission_consent_main_first_point}" /> + + <include layout="@layout/view_bullet_point_text" + android:id="@+id/submission_consent_main_second_point" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/submission_consent_main_first_point" + app:itemText="@{@string/submission_consent_main_second_point}" /> + + <include layout="@layout/view_bullet_point_text" + android:id="@+id/submission_consent_main_third_point" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/submission_consent_main_second_point" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:itemText="@{@string/submission_consent_main_third_point}" + style="@style/subtitle" /> + + <include layout="@layout/view_bullet_point_text" + android:id="@+id/submission_consent_main_fourth_point" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/submission_consent_main_third_point" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:itemText="@{@string/submission_consent_main_fourth_point}" + style="@style/subtitle" /> + + <include layout="@layout/merge_guidelines_side" /> + +</androidx.constraintlayout.widget.ConstraintLayout> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_consent_intro.xml b/Corona-Warn-App/src/main/res/layout/include_submission_consent_intro.xml new file mode 100644 index 0000000000000000000000000000000000000000..dc5097c2cc698720f3e3787e5b9727adc02a9f95 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/include_submission_consent_intro.xml @@ -0,0 +1,116 @@ +<?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"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/submission_consent_main_headline_body" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_illustration" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_main_headline_body" + style="@style/subtitle" /> + + <TextView + android:id="@+id/submission_consent_call_test_result" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_main_headline_body" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_call_test_result" + style="@style/headline6" /> + + <TextView + android:id="@+id/submission_consent_call_test_result_body" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_call_test_result_body" + style="@style/subtitle" /> + + <ImageView + android:id="@+id/submission_consent_icon_scan" + android:layout_width="@dimen/circle_icon" + android:layout_height="@dimen/circle_icon" + android:layout_marginTop="@dimen/spacing_normal" + android:src="@drawable/ic_qr_icon_personal_result" + android:background="@drawable/circle" + android:backgroundTint="@color/card_dark" + android:padding="@dimen/circle_icon_padding" + android:importantForAccessibility="no" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result_body" + app:layout_constraintStart_toStartOf="@id/guideline_start"/> + + <TextView + android:id="@+id/submission_consent_call_test_result_scan_your_test_only" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginStart="@dimen/spacing_tiny" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result_body" + app:layout_constraintStart_toEndOf="@id/submission_consent_icon_scan" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_call_test_result_scan_your_test_only" + style="@style/subtitle" /> + + <ImageView + android:id="@+id/submission_consent_icon_single_test" + android:layout_width="@dimen/circle_icon" + android:layout_height="@dimen/circle_icon" + android:layout_marginTop="@dimen/spacing_normal" + android:background="@drawable/circle" + android:backgroundTint="@color/card_dark" + android:src="@drawable/ic_qr_icon_test_scan" + android:importantForAccessibility="no" + android:padding="@dimen/circle_icon_padding" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result_scan_your_test_only" + app:layout_constraintStart_toStartOf="@id/guideline_start"/> + + <TextView + android:id="@+id/submission_consent_call_test_result_scan_test_only_once" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginStart="@dimen/spacing_tiny" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result_scan_your_test_only" + app:layout_constraintStart_toEndOf="@id/submission_consent_icon_single_test" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_call_test_result_scan_test_only_once" + style="@style/subtitle" /> + + <TextView + android:id="@+id/submission_consent_help_by_warning_others_headline" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result_scan_test_only_once" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_help_by_warning_others_headline" + style="@style/headline6" /> + + <TextView + android:id="@+id/submission_consent_help_by_warning_others_body" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@+id/submission_consent_help_by_warning_others_headline" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + android:text="@string/submission_consent_help_by_warning_others_body" + style="@style/subtitle" /> + + <include layout="@layout/merge_guidelines_side" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_dispatcher.xml b/Corona-Warn-App/src/main/res/layout/include_submission_dispatcher.xml index d251147351bdf7af7d9402b9ee0b9a934ab3a923..7e73de12a5791920cca6f4ab31059f260ed61704 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_dispatcher.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_dispatcher.xml @@ -12,6 +12,15 @@ android:focusable="true" android:paddingBottom="@dimen/spacing_normal"> + <ImageView + android:id="@+id/submission_dispatcher_illustration" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:src="@drawable/ic_illustration_test" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + <TextView android:id="@+id/submission_dispatcher_text" style="@style/subtitle" @@ -22,7 +31,19 @@ android:focusable="true" app:layout_constraintEnd_toStartOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_illustration" /> + + <TextView + android:id="@+id/submission_dispatcher_needs_testing_text" + style="@style/headline6" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:text="@string/submission_dispatcher_needs_testing_subheadline" + android:focusable="true" + app:layout_constraintEnd_toStartOf="@+id/guideline_end" + app:layout_constraintStart_toStartOf="@+id/guideline_start" + app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_text" /> <include android:id="@+id/submission_dispatcher_qr" @@ -37,7 +58,7 @@ app:illustration="@{@drawable/ic_submission_illustration_qr_code_card}" app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" app:layout_constraintStart_toStartOf="@+id/guideline_card_start" - app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_text" /> + app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_needs_testing_text" /> <include android:id="@+id/submission_dispatcher_tan_code" @@ -54,12 +75,24 @@ app:layout_constraintStart_toStartOf="@+id/guideline_card_start" app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_qr" /> + <TextView + android:id="@+id/submission_dispatcher_already_positive_text" + style="@style/headline6" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:text="@string/submission_dispatcher_already_positive_subheadline" + android:focusable="true" + app:layout_constraintEnd_toStartOf="@+id/guideline_end" + app:layout_constraintStart_toStartOf="@+id/guideline_start" + app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_tan_code" /> + <include android:id="@+id/submission_dispatcher_tan_tele" layout="@layout/include_dispatcher_card" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" + android:layout_marginTop="@dimen/spacing_normal" android:clickable="true" android:focusable="true" app:body="@{@string/submission_dispatcher_tan_tele_card_text}" @@ -67,7 +100,7 @@ app:illustration="@{@drawable/ic_submission_illustration_tan_hotline_card}" app:layout_constraintEnd_toStartOf="@+id/guideline_card_end" app:layout_constraintStart_toStartOf="@+id/guideline_card_start" - app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_tan_code" /> + app:layout_constraintTop_toBottomOf="@+id/submission_dispatcher_already_positive_text" /> <include layout="@layout/merge_guidelines_side" /> diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml b/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml index a3cc144f4363bdab7d456c558f828c175a901f8b..0dc277c240812c260c786037fe7ae97ac6bc2eb9 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_positive_other_warning.xml @@ -67,22 +67,22 @@ app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintTop_toBottomOf="@+id/submission_positive_other_warning_text" /> - <de.rki.coronawarnapp.ui.view.CountryList + <de.rki.coronawarnapp.ui.view.CountryListView android:id="@+id/countryList" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" app:layout_constraintEnd_toEndOf="@+id/submission_country_header_description" app:layout_constraintStart_toStartOf="@+id/submission_country_header_description" app:layout_constraintTop_toBottomOf="@+id/submission_country_header_description" - app:list="@{countryData}" /> + app:countryList="@{countryData}" /> <include android:id="@+id/submission_positive_location_card_16_years" layout="@layout/include_16_years" android:layout_width="@dimen/match_constraint" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_large" + android:layout_marginTop="@dimen/spacing_normal" android:focusable="true" app:body="@{@string/sixteen_description_text}" app:headline="@{@string/sixteen_title_text}" 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 8e95f455005d3dee6af89fef7fdf6ee99be23191..a55bff2725d226ff13def243c9238e5718276169 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 @@ -11,7 +11,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/layout/merge_guidelines_side.xml b/Corona-Warn-App/src/main/res/layout/merge_guidelines_side.xml index ffeeb8a4e73017cb8177acaff321ce5c12a221e4..de836a048ea3dc6b85acc4e42e1854032e650187 100644 --- a/Corona-Warn-App/src/main/res/layout/merge_guidelines_side.xml +++ b/Corona-Warn-App/src/main/res/layout/merge_guidelines_side.xml @@ -4,7 +4,7 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> - <androidx.constraintlayout.widget.Guideline + <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline_start" android:layout_width="wrap_content" android:layout_height="wrap_content" diff --git a/Corona-Warn-App/src/main/res/layout/view_bullet_point_text.xml b/Corona-Warn-App/src/main/res/layout/view_bullet_point_text.xml new file mode 100644 index 0000000000000000000000000000000000000000..1a679cd56d0e36b364d4a0cb7c95176d9094e104 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/view_bullet_point_text.xml @@ -0,0 +1,40 @@ +<layout 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"> + + <data> + <variable + name="itemText" + type="String" /> + + </data> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="@dimen/spacing_normal"> + + <ImageView + android:id="@+id/bullet_point" + android:layout_width="@dimen/bullet_point_size" + android:layout_height="@dimen/bullet_point_size" + android:baseline="@dimen/bullet_point_baseline_offset" + android:src="@drawable/bullet_point" + android:importantForAccessibility="no" + app:layout_constraintBaseline_toBaselineOf="@+id/bullet_point_content" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/bullet_point_content" + style="@style/subtitle" + android:layout_width="@dimen/match_constraint" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bullet_point_spacing_after" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/bullet_point" + app:layout_constraintTop_toTopOf="parent" + android:text="@{itemText}" + tools:text="@tools:sample/lorem" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/view_country_list_entry_flag_container.xml b/Corona-Warn-App/src/main/res/layout/view_country_list_entry_flag_container.xml new file mode 100644 index 0000000000000000000000000000000000000000..1d1e7b16c128d1e66548a715fd870d90e1e4c9bd --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/view_country_list_entry_flag_container.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:orientation="vertical"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/flagGrid" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + <View + android:layout_width="match_parent" + android:layout_height="3dp" + android:layout_marginTop="@dimen/spacing_small" + android:background="@color/colorHairline" /> + + <TextView + android:id="@+id/country_list_entry_label" + style="@style/subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + tools:text="Deutschland, Frankreich" /> + + <View + android:layout_width="match_parent" + android:layout_height="3dp" + android:background="@color/colorHairline" /> +</LinearLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/view_country_list_entry_flag_item.xml b/Corona-Warn-App/src/main/res/layout/view_country_list_entry_flag_item.xml new file mode 100644 index 0000000000000000000000000000000000000000..1d37e33e4f6ddd1119ebceeb98496fa4bef67ed5 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/view_country_list_entry_flag_item.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/country_list_entry_image" + android:layout_width="28dp" + android:layout_height="28dp" + android:layout_margin="4dp" + app:srcCompat="@drawable/ic_country_eu" /> diff --git a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index 44aea7e8c24b13c4aedcd91e535e6309c8f828b1..6504b4320f74afcef0cf088db168861b14f5d35d 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -38,8 +38,8 @@ android:id="@+id/action_mainFragment_to_mainOverviewFragment" app:destination="@id/mainOverviewFragment" /> <action - android:id="@+id/action_mainFragment_to_submissionIntroFragment" - app:destination="@id/submissionIntroFragment" /> + android:id="@+id/action_mainFragment_to_submissionDispatcher" + app:destination="@id/submissionDispatcherFragment" /> <action android:id="@+id/action_mainFragment_to_onboardingDeltaInteroperabilityFragment" app:destination="@id/onboardingDeltaInteroperabilityFragment" /> @@ -219,8 +219,8 @@ android:id="@+id/action_submissionDispatcherFragment_to_submissionContactFragment" app:destination="@id/submissionContactFragment" /> <action - android:id="@+id/action_submissionDispatcherFragment_to_submissionQRCodeInfoFragment" - app:destination="@id/submissionQRCodeInfoFragment" /> + android:id="@+id/action_submissionDispatcherFragment_to_submissionConsentFragment" + app:destination="@id/submissionConsentFragment" /> </fragment> <fragment android:id="@+id/submissionResultPositiveOtherWarningFragment" @@ -312,14 +312,6 @@ android:id="@+id/deepLink" app:uri="coronawarnapp://launch" /> </activity> - <fragment - android:id="@+id/submissionQRCodeInfoFragment" - android:name="de.rki.coronawarnapp.ui.submission.qrcode.info.SubmissionQRCodeInfoFragment" - android:label="SubmissionQRCodeInfoFragment"> - <action - android:id="@+id/action_submissionQRCodeInfoFragment_to_submissionQRCodeScanFragment" - app:destination="@id/submissionQRCodeScanFragment" /> - </fragment> <fragment android:id="@+id/submissionQRCodeScanFragment" @@ -387,6 +379,20 @@ android:name="symptomIndication" app:argType="de.rki.coronawarnapp.submission.Symptoms$Indication" /> </fragment> - + <fragment + android:id="@+id/submissionConsentFragment" + android:name="de.rki.coronawarnapp.ui.submission.qrcode.consent.SubmissionConsentFragment" + android:label="SubmissionConsentFragment" > + <action + android:id="@+id/action_submissionConsentFragment_to_submissionQRCodeScanFragment" + app:destination="@id/submissionQRCodeScanFragment" /> + <action + android:id="@+id/action_submissionConsentFragment_to_homeFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false"/> + <action + android:id="@+id/action_submissionConsentFragment_to_informationPrivacyFragment" + app:destination="@id/informationPrivacyFragment" /> + </fragment> </navigation> 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..6a1be54319c193dc1927124d7268036aaf732ecd 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> @@ -831,19 +835,6 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Отказ"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"Сканиране на QR код"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"Сканирайте Ñамо Ð’Ð°ÑˆÐ¸Ñ ÑобÑтвен теÑÑ‚."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"ТеÑÑ‚ÑŠÑ‚ може да Ñе Ñканира Ñамо "<b>"веднъж"</b>"."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"Ð’ приложението "<b>"не може"</b>" да Ñе обработват нÑколко теÑта едновременно."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"Ðко ÑъщеÑтвува нов теÑÑ‚, изтрийте ÑъщеÑÑ‚Ð²ÑƒÐ²Ð°Ñ‰Ð¸Ñ Ð¸ Ñканирайте QR кода на най-Ð½Ð¾Ð²Ð¸Ñ Ñ‚ÐµÑÑ‚."</string> - - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"Позиционирайте QR кода в рамката."</string> <!-- YTXT: instruction text for QR code scanning --> @@ -851,7 +842,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 +856,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 --> @@ -947,18 +938,14 @@ <string name="submission_dispatcher_headline">"Избор"</string> <!-- XHED: Page subheadline for dispatcher menu --> <string name="submission_dispatcher_subheadline">"С каква Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ñ€Ð°Ð·Ð¿Ð¾Ð»Ð°Ð³Ð°Ñ‚Ðµ?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">""</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">""</string> <!-- YTXT: Dispatcher text for QR code option --> <string name="submission_dispatcher_card_qr">"Документ Ñ QR код"</string> <!-- 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> - <!-- 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> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <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,15 +959,15 @@ <!-- 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--> <string name="submission_positive_other_warning_body">"Ðко желаете, вече можете да помогнете и околните да бъдат предупредени за възможно заразÑване.\n\nЗа целта Ñ‚Ñ€Ñбва да Ñподелите Ñвоите Ñлучайни идентификатори от поÑледните 14 дни на Ñървъра, използван ÑъвмеÑтно от учаÑтващите държави. По желание може да Ñподелите Ñъщо кога за пръв път Ñте забелÑзали Ñимптомите на коронавируÑната инфекциÑ. От там Вашите уникални ИД и вÑÑка допълнителна Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ñе разпращат до потребителите на Ñъответните официални Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð·Ð° борба Ñ ÐºÐ¾Ñ€Ð¾Ð½Ð°Ð²Ð¸Ñ€ÑƒÑа. По този начин и чуждеÑтранните потребители, Ñ ÐºÐ¾Ð¸Ñ‚Ð¾ Ñте били в контакт, ще бъдат предупредени за възможно заразÑване.\n\nСподелÑÑ‚ Ñе единÑтвено Вашите Ñлучайни идентификатори и информациÑта за развитието на Ñимптомите, коÑто доброволно Ñте Ñподелили. Ðе Ñе разкриват лични данни като име, Ð°Ð´Ñ€ÐµÑ Ð¸Ð»Ð¸ меÑтоположение."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Приемам"</string> + <string name="submission_accept_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 +1058,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 +1087,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 +1188,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 +1238,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 +1339,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 +1369,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 +</resources> 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 e2eb4cebe4ba6da24b7ef14d76ae4b65e4e2bb22..d2c64ea7ff2c56b1995fa8b076c3d70d97a5ad32 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 @@ -3,13 +3,31 @@ <!-- XHED: Header of private data in the delta onboarding interoperability view --> <string name="interoperability_onboarding_delta_footerTitle">Hinweis zur Datenverarbeitung</string> <!-- XTXT: Description of private data in the delta onboarding interoperability view. Below interoperability_onboarding_delta_footerTitle --> - <string name="interoperability_onboarding_delta_footerDescription">Um zu erfahren, ob Sie Risiko-Begegnungen mit App-Nutzern der teilnehmenden Länder hatten und deshalb ein Infektionsrisiko besteht, müssen Sie nichts ändern. Es muss nur weiterhin die Risiko-Ermittlung aktiviert sein. Die Risiko-Ermittlung warnt Sie automatisch bei allen Risiko-Begegnungen, die Sie mit einem Nutzer der Corona-Warn-App oder einer anderen offiziellen Corona-App hatten.\n\nBei aktiviertem COVID-19-Benachrichtigungssystem erzeugt Ihr Android-Smartphone kontinuierlich Zufalls-IDs und versendet diese per Bluetooth, damit sie 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\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="interoperability_onboarding_delta_footerDescription">Um zu erfahren, ob Sie Risiko-Begegnungen mit App-Nutzern der teilnehmenden Länder hatten und deshalb ein Ansteckungsrisiko besteht, müssen Sie nichts ändern. Es muss nur weiterhin die Risiko-Ermittlung aktiviert sein. Die Risiko-Ermittlung warnt Sie automatisch bei allen Risiko-Begegnungen, die Sie mit einem Nutzer der Corona-Warn-App oder einer anderen offiziellen Corona-App hatten.\n\nWenn Sie in Ihrem Android-Smartphone die Funktion „COVID-19-Benachrichtigungssystem“ aktiviert haben, erzeugt Ihr Android-Smartphone kontinuierlich Zufalls-IDs und versendet diese per Bluetooth, damit sie 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\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> <!-- XHED: Title for the privacy card--> <string name="submission_positive_other_warning_privacy_title">"Einwilligungserklärung"</string> <!-- YTXT: Body text for the privacy card--> <string name="submission_positive_other_warning_privacy_body">"<b>Indem Sie „Einverstanden“ antippen, willigen Sie ein, dass Ihre Zufalls-IDs der letzten 14 Tage und eventuelle Angaben zum Symptombeginn an das Serversystem der App übermittelt werden.</b> Diese Informationen werden verwendet, um das Infektionsrisiko für Nutzer der App, mit denen Sie Kontakt hatten, zu bewerten und diese im Falle eines bestehenden Infektionsrisikos zu warnen. Damit auch die Nutzer von offiziellen Corona-Apps der anderen teilnehmenden Länder gewarnt werden können, werden diese Informationen vom Serversystem der App auch auf dem von den teilnehmenden Ländern gemeinsam betriebenen Austausch-Server bereitgestellt.\n\nWeder andere Nutzer noch das RKI oder die Gesundheitsbehörden teilnehmender Länder können anhand der übermittelten Daten auf Ihre Identität, Ihren Namen oder andere persönliche Angaben schließen.\n\nDie Ãœbermittlung der Zufalls-IDs und der eventuellen Angaben zum Symptombeginn zur länderübergreifenden Warnung ist freiwillig. Wenn Sie die Daten nicht übermitteln, entstehen Ihnen keine Nachteile. Da weder nachvollzogen noch kontrolliert werden kann, ob und wie Sie die App verwenden, erfährt außer Ihnen auch niemand, ob Sie Ihre Zufalls-IDs zur Verfügung stellen.\n\nSie können Ihre Einwilligung jederzeit widerrufen, indem Sie die App löschen. Durch den Widerruf der Einwilligung wird die Rechtmäßigkeit der aufgrund der Einwilligung bis zum Widerruf erfolgten Verarbeitung nicht berührt.\n\nWeitere Informationen – auch zu den teilnehmenden Ländern und datenschutzrechtlich verantwortlichen Gesundheitsbehörden – finden Sie unter dem Menüpunkt „App-Informationen“ > „Datenschutz“."</string> <!-- XHED: onboarding(tracing) - headline for consent information --> - <string name="onboarding_tracing_headline_consent">"Einwilligungserklärung"</string> + <string name="onboarding_tracing_headline_consent">"Einverständnis"</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 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> + <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> + <!-- XHED: Page subheadline for consent sub section your consent --> + <string name="submission_consent_your_consent_subsection_headline">"Ihr Einverständnis"</string> + <!-- YTXT: Body for consent sub section your consent subtext --> + <string name="submission_consent_your_consent_subsection_tapping_agree">"Durch Antippen von „Einverstanden“ willigen Sie wie folgt ein:"</string> + <!-- YTXT: Body for consent sub section your consent subtext first point --> + <string name="submission_consent_your_consent_subsection_first_point">"<b>Die App ruft Ihr Testergebnis ab.</b> Wenn Sie es sich später anders überlegen, können Sie den Test in der App entfernen."</string> + <!-- YTXT: Body for consent sub section your consent subtext second point --> + <string name="submission_consent_your_consent_subsection_second_point">"<b>Wenn Sie positiv auf Corona getestet wurden, teilt die App Ihr Testergebnis, um Nutzer, denen Sie begegnet sind, zu warnen. Dies betrifft Nutzer von offiziellen Corona-Apps der oben genannten Länder. Wenn Sie zusätzlich Angaben zum Beginn Ihrer Symptome machen, werden auch diese geteilt.</b>"</string> + <!-- YTXT: Body for consent sub section your consent subtext third point --> + <string name="submission_consent_your_consent_subsection_third_point">"Sie können Ihr Einverständnis jederzeit zurücknehmen. Die Einstellung hierfür finden Sie unter „Test anzeigen“. Vor dem Teilen werden Sie nochmal auf Ihr Einverständnis hingewiesen und um Freigabe Ihres Testergebnisses gebeten."</string> + <!-- YTXT: Body for consent main section first point --> + <string name="submission_consent_main_first_point">"Ihr Einverständnis ist freiwillig."</string> + <!-- YTXT: Body for consent main section second point --> + <string name="submission_consent_main_second_point">"Sie können Ihr Testergebnis auch abrufen, wenn Sie dies nicht teilen. Wenn Sie ihr Testergebnis teilen, helfen Sie jedoch mit, Ihre Mitmenschen vor Ansteckungen zu schützen."</string> + <!-- YTXT: Body for consent main section third point --> + <string name="submission_consent_main_third_point">"Ihre Identität bleibt geheim. Andere Nutzer erfahren nicht, wer sein Testergebnis geteilt hat."</string> + <!-- YTXT: Body for consent main section fourth point --> + <string name="submission_consent_main_fourth_point">"Sie können Ihr Einverständnis abgeben, wenn Sie mindestens 16 Jahre alt sind."</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 87461b73591a622af77aeff2de16d911a487e6dc..1477754f6ade6f3a392076a97165ad3b5d973cc7 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 --> @@ -315,7 +311,7 @@ <!-- XHED: risk details - headline, how a user should act --> <string name="risk_details_headline_behavior">"Verhalten"</string> <!-- XHED: risk details - multiline headline, bold, how to act correct --> - <string name="risk_details_subtitle_behavior">"So verhalten Sie sich richtig"</string> + <string name="risk_details_subtitle_behavior">"So verhalten Sie sich richtig:"</string> <!-- XMSG: risk details - go/stay home, something like a bullet point --> <string name="risk_details_behavior_body_stay_home">"Begeben Sie sich, wenn möglich, nach Hause bzw. bleiben Sie zu Hause."</string> <!-- XMSG: risk details - get in touch with the corresponding people, something like a bullet point --> @@ -345,9 +341,9 @@ <!-- XHED: risk details - infection period logged information body, below behaviors --> <string name="risk_details_information_body_period_logged_assessment">"Für Ihre Risiko-Ermittlung wird nur der Zeitraum der letzten 14 Tage betrachtet. In diesem Zeitraum war Ihre Risiko-Ermittlung für eine Gesamtdauer von %1$s Tagen aktiv. Ältere Tage werden automatisch gelöscht, da sie aus Sicht des Infektionsschutzes nicht mehr relevant sind."</string> <!-- XHED: risk details - how your risk level was calculated, below behaviors --> - <string name="risk_details_subtitle_infection_risk_past">"So wurde Ihr Risiko ermittelt"</string> + <string name="risk_details_subtitle_infection_risk_past">"So wurde Ihr Risiko ermittelt."</string> <!-- XHED: risk details - how your risk level will be calculated, below behaviors --> - <string name="risk_details_subtitle_infection_risk">"So wird Ihr Risiko ermittelt"</string> + <string name="risk_details_subtitle_infection_risk">"So wird Ihr Risiko ermittelt."</string> <!-- XMSG: risk details - risk couldn't be calculated tracing wasn't enabled long enough, below behaviors --> <string name="risk_details_information_body_unknown_risk">"Da Sie die Risiko-Ermittlung noch nicht lange genug aktiviert haben, konnten wir für Sie kein Infektionsrisiko berechnen."</string> <!-- XMSG: risk details - risk calculation wasn't possible for 24h, below behaviors --> @@ -441,9 +437,9 @@ <!-- XHED: onboarding(tracing) - how to enable tracing --> <string name="onboarding_tracing_headline">"Wie Sie die Risiko-Ermittlung ermöglichen"</string> <!-- XHED: onboarding(tracing) - two/three line headline under an illustration --> - <string name="onboarding_tracing_subtitle">"Um zu erkennen, ob für Sie ein Infektionsrisiko vorliegt, müssen Sie die Risiko-Ermittlung aktivieren."</string> + <string name="onboarding_tracing_subtitle">"Um zu erkennen, ob für Sie ein Ansteckungsrisiko vorliegt, müssen Sie die Risiko-Ermittlung aktivieren."</string> <!-- YTXT: onboarding(tracing) - explain tracing --> - <string name="onboarding_tracing_body">"Die Risiko-Ermittlung funktioniert, indem Ihr Smartphone per Bluetooth verschlüsselte Zufalls-IDs anderer Nutzer empfängt und Ihre eigenen Zufalls-IDs an deren Smartphones weitergibt. Die Risiko-Ermittlung lässt sich jederzeit deaktivieren. "</string> + <string name="onboarding_tracing_body">"Die Risiko-Ermittlung funktioniert, indem Ihr Android-Smartphone per Bluetooth verschlüsselte Zufalls-IDs anderer Nutzer empfängt und Ihre eigenen Zufalls-IDs an deren Smartphones weitergibt. Die Risiko-Ermittlung lässt sich jederzeit deaktivieren. "</string> <!-- YTXT: onboarding(tracing) - explain tracing --> <string name="onboarding_tracing_body_emphasized">"Die verschlüsselten Zufalls-IDs geben nur Auskunft über das Datum, die Dauer und die anhand der Signalstärke berechnete Entfernung zu Ihren Mitmenschen. Rückschlüsse auf einzelne Personen sind anhand der Zufalls-IDs nicht möglich."</string> <!-- YTXT: onboarding(tracing) - easy language explain tracing link--> @@ -545,7 +541,7 @@ <!-- XTXT: settings(tracing) - shows status under header in home, inactive location --> <string name="settings_tracing_body_inactive_location">"Standortdienste deaktiviert"</string> <!-- YTXT: settings(tracing) - explains tracings --> - <string name="settings_tracing_body_text">"Um zu erkennen, ob für Sie ein Infektionsrisiko vorliegt, müssen Sie die Risiko-Ermittlung aktivieren. Die Risiko-Ermittlung funktioniert länderübergreifend, so dass auch Risiko-Begegnungen mit Nutzern von anderen offiziellen Corona-Apps erkannt werden.\n\nDie Risiko-Ermittlung funktioniert, indem Ihr Smartphone per Bluetooth verschlüsselte Zufalls-IDs anderer Nutzer empfängt und Ihre eigenen Zufalls-IDs an deren Smartphone weitergibt. Die App lädt täglich eine Liste mit den Zufalls-IDs und eventuellen Angaben zum Symptombeginn aller Corona-positiv getesteten Nutzer herunter, die diese freiwillig über ihre App geteilt haben. Diese Liste wird dann mit den von Ihrem Smartphone aufgezeichneten Zufalls-IDs anderer Nutzer verglichen, um Ihr Infektionsrisiko zu berechnen und Sie zu warnen. Die Risiko-Ermittlung lässt sich jederzeit über den Schieberegler deaktivieren.\n\nPersönliche Daten wie Ihr Name, Ihre Adresse oder Ihr Aufenthaltsort werden über die App zu keiner Zeit erfasst oder an andere Nutzer übermittelt. Rückschlüsse auf Ihre Identität, Ihren Namen oder andere persönliche Angaben sind anhand der Zufalls-IDs nicht möglich."</string> + <string name="settings_tracing_body_text">"Um zu erkennen, ob für Sie ein Ansteckungsrisiko vorliegt, müssen Sie die Risiko-Ermittlung aktivieren. Die Risiko-Ermittlung funktioniert länderübergreifend, so dass auch Risiko-Begegnungen mit Nutzern von anderen offiziellen Corona-Apps erkannt werden.\n\nDie Risiko-Ermittlung funktioniert, indem Ihr Android-Smartphone per Bluetooth verschlüsselte Zufalls-IDs anderer Nutzer empfängt und Ihre eigenen Zufalls-IDs an deren Smartphone weitergibt. Die App lädt täglich eine Liste mit den Zufalls-IDs und eventuellen Angaben zum Symptombeginn aller Corona-positiv getesteten Nutzer herunter, die ihr Testergebnis (genauer gesagt: ihre Zufalls-IDs) freiwillig über ihre App geteilt haben. Diese Liste wird dann mit den von Ihrem Android-Smartphone aufgezeichneten Zufalls-IDs anderer Nutzer, denen Sie begegnet sind, verglichen, um Ihr Ansteckungsrisiko zu berechnen und Sie zu warnen. Die Risiko-Ermittlung lässt sich jederzeit über den Schieberegler deaktivieren.\n\nPersönliche Daten wie Ihr Name, Ihre Adresse oder Ihr Aufenthaltsort werden über die App zu keiner Zeit erfasst oder an andere Nutzer übermittelt. Rückschlüsse auf Ihre Identität, Ihren Namen oder andere persönliche Angaben sind anhand der Zufalls-IDs nicht möglich."</string> <!-- XTXT: settings(tracing) - status next to switch under title --> <string name="settings_tracing_status_active">"Aktiv"</string> <!-- XTXT: settings(tracing) - status next to switch under title --> @@ -624,7 +620,7 @@ <string name="settings_notifications_illustration_description_inactive">"Eine Frau hat die Mitteilungen ihrer Corona-Warn-App ausgeschaltet."</string> <!-- XBUT: settings - go to reset application --> <string name="settings_reset_title">"Anwendung zurücksetzen"</string> - <!-- XTXT: settings(reset) - explains the user what do expect when he navigates to reset --> + <!-- XTXT: settings(reset) - explains the user what do expect when navigating to reset --> <string name="settings_reset_body_description">"Löschen Sie alle Ihre Daten in der App."</string> <!-- XHED: settings(reset) - multiline headline below illustration --> <string name="settings_reset_headline">"Sind Sie sicher, dass Sie die Anwendung zurücksetzen wollen?"</string> @@ -804,9 +800,9 @@ <string name="submission_error_dialog_web_tan_redeemed_button_positive">"OK"</string> <!-- XHED: Dialog title for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_title">"Wollen Sie wirklich abbrechen?"</string> + <string name="submission_error_dialog_confirm_cancellation_title">"Wollen Sie die Symptom-Erfassung abbrechen?"</string> <!-- XMSG: Dialog body for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_body">"Ihre bisherigen Angaben werden nicht gespeichert."</string> + <string name="submission_error_dialog_confirm_cancellation_body">"Wenn Sie Angaben zu Ihren Symptomen machen, können Sie andere noch genauer warnen."</string> <!-- XBUT: Positive button for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_button_positive">"Ja"</string> <!-- XBUT: Negative button for keys submission process cancellation --> @@ -840,24 +836,31 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Abbrechen"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"QR-Code Scan"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"Scannen Sie nur Ihren eigenen Test."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"Jeder Test kann <b>nur einmal</b> gescannt werden."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"Die App kann <b>nicht</b> gleichzeitig mehrere Tests verwalten."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"Wenn Ihnen ein aktuellerer Test vorliegt, löschen Sie den vorhandenen Test und scannen Sie den QR-Code des aktuellen Tests ein."</string> - - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"Positionieren Sie den QR-Code in den Rahmen."</string> <!-- YTXT: instruction text for QR code scanning --> <string name="submission_qr_code_scan_body">"Positionieren Sie den QR-Code in den Rahmen."</string> + <!-- QR Code Consent Screen --> + <!-- XHED: Page headline for Submission consent --> + <string name="submission_consent_main_headline">"Ihr Einverständnis"</string> + <!-- YTXT: Body for Submissionconsent --> + <string name="submission_consent_main_headline_body">"Bevor Sie Ihr Testergebnis abrufen und andere warnen können, ist Ihr Einverständnis erforderlich."</string> + <!-- XHED: Page subheadline for consent call test result --> + <string name="submission_consent_call_test_result">"Testergebnis abrufen"</string> + <!-- YTXT: Body for Submission Consent call test result body --> + <string name="submission_consent_call_test_result_body">"Scannen Sie im nächsten Schritt den QR-Code auf Ihrem Test und rufen Sie Ihr Testergebnis ab."</string> + <!-- YTXT: Body sub text 1 for Submission Consent call test result --> + <string name="submission_consent_call_test_result_scan_your_test_only">"Scannen Sie nur Ihren eigenen Test."</string> + <!-- YTXT: Body sub text 2 for Submission Consent call test result --> + <string name="submission_consent_call_test_result_scan_test_only_once">"Ihr Test kann nur einmal gescannt werden. Die App kann nicht gleichzeitig mehrere Tests verwalten."</string> + <!-- XHED: Page subheadline for consent help by warning others --> + <string name="submission_consent_help_by_warning_others_headline">"Helfen Sie mit, indem Sie andere warnen, denen Sie begegnet sind!"</string> + <!-- YTXT: Body for consent help by warning others --> + <string name="submission_consent_help_by_warning_others_body">"Wenn Sie positiv auf Corona getestet wurden, können Sie Ihre Mitmenschen über die App warnen. Die Warnung funktioniert in mehreren Ländern. Derzeit nehmen folgende Länder teil:"</string> + <!-- YTXT: Page bottom text for consent screen --> + <string name="submission_consent_main_bottom_body">"Ausführliche Informationen zur Datenverarbeitung und Ihrem Einverständnis"</string> + <!-- Submission Test Result --> <!-- XHED: Page headline for test result --> <string name="submission_test_result_headline">"Testergebnis"</string> @@ -953,29 +956,25 @@ <!-- Dispatcher --> <!-- XHED: Page headline for dispatcher menu --> - <string name="submission_dispatcher_headline">"Auswahl"</string> + <string name="submission_dispatcher_headline">"Testergebnis abrufen"</string> <!-- XHED: Page subheadline for dispatcher menu --> - <string name="submission_dispatcher_subheadline">"Welche Informationen liegen Ihnen vor?"</string> + <string name="submission_dispatcher_subheadline">"Rufen Sie über die App Ihr Testergebnis ab und warnen Sie anschließend Ihre Mitmenschen. So schützen Sie sich und andere und helfen mit, die Ausbreitung von Corona zu verhindern."</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">"Sie haben sich bereits testen lassen?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">"Ihr Test ist positiv?"</string> <!-- YTXT: Dispatcher text for QR code option --> - <string name="submission_dispatcher_card_qr">"Dokument mit QR-Code"</string> + <string name="submission_dispatcher_card_qr">"Test mit QR-Code"</string> <!-- YTXT: Body text for QR code dispatcher option --> <string name="submission_dispatcher_qr_card_text">"Registrieren Sie Ihren Test, indem Sie den QR-Code ihres Test-Dokuments scannen."</string> - <!-- XHED: Dialog headline for dispatcher QR prviacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_headline">"Einwilligungserklärung"</string> - <!-- YTXT: Dialog Body text for dispatcher QR privacy dialog --> - <string name="submission_dispatcher_qr_privacy_dialog_body">"Durch Antippen von „Erlauben“ willigen Sie ein, dass die App den Status Ihres Corona-Virus-Tests abfragen und in der App anzeigen darf. Diese Funktion steht Ihnen zur Verfügung, wenn Sie einen QR-Code erhalten und eingewilligt haben, dass Ihr Testergebnis an das Serversystem der App übermittelt werden darf. Sobald das Testlabor Ihr Testergebnis auf dem Server hinterlegt hat, können Sie das Ergebnis in der App sehen. Falls Sie Mitteilungen aktiviert haben, werden Sie auch außerhalb der App über den Eingang des Testergebnis informiert. Das Testergebnis selbst wird aus Datenschutzgründen jedoch nur in der App angezeigt. Sie können diese Einwilligung jederzeit widerrufen, indem Sie die Testregistrierung in der App löschen. Durch den Widerruf der Einwilligung wird die Rechtmäßigkeit der bis zum Widerruf erfolgten Verarbeitung nicht berührt. Weitere Informationen finden Sie unter dem Menüpunkt „Datenschutz“."</string> - <!-- XBUT: submission(dispatcher QR Dialog) - positive button (right) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_positive">"Erlauben"</string> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nicht erlauben"</string> <!-- YTXT: Dispatcher text for TAN code option --> - <string name="submission_dispatcher_card_tan_code">"TAN"</string> + <string name="submission_dispatcher_card_tan_code">"TAN-Eingabe"</string> <!-- YTXT: Body text for TAN code dispatcher option --> - <string name="submission_dispatcher_tan_code_card_text">"Registrieren Sie Ihren Test per manueller TAN-Eingabe."</string> + <string name="submission_dispatcher_tan_code_card_text">"Ihnen liegt eine TAN vor? Weiter zur TAN-Eingabe, um andere zu warnen. "</string> <!-- YTXT: Dispatcher text for TELE-TAN option --> - <string name="submission_dispatcher_card_tan_tele">"TAN anfragen"</string> + <string name="submission_dispatcher_card_tan_tele">"Noch keine TAN?"</string> <!-- YTXT: Body text for TELE_TAN dispatcher option --> - <string name="submission_dispatcher_tan_tele_card_text">"Bitte rufen Sie uns an, falls Sie Corona-positiv getestet wurden."</string> + <string name="submission_dispatcher_tan_tele_card_text">"Rufen Sie uns an und erhalten Sie eine TAN."</string> <!-- XACT: Dispatcher Tan page title --> <string name="submission_dispatcher_accessibility_title">"Welche Informationen liegen Ihnen vor?"</string> @@ -988,7 +987,7 @@ <string name="submission_positive_other_warning_body">"Als Nächstes können Sie dafür sorgen, dass Ihre Mitmenschen vor einer möglichen Infektion gewarnt werden.\n\nHierfür können Sie Ihre eigenen Zufalls-IDs der letzten 14 Tage und optional auch Angaben zum ersten Auftreten von eventuellen Corona-Symptomen an den von den teilnehmenden Ländern gemeinsam betriebenen Server übertragen. Von dort werden Ihre Zufalls-IDs und eventuelle weitere Angaben an die Nutzer der jeweiligen offiziellen Corona-Apps verteilt. So können die anderen Nutzer, mit denen Sie Kontakt hatten, vor einer eventuellen Ansteckung gewarnt werden.\n\nEs werden nur Zufalls-IDs und eventuelle Angaben zum Symptombeginn übertragen. Es werden keine persönlichen Daten wie Ihr Name, Ihre Adresse oder Ihr Aufenthaltsort mitgeteilt."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Einverstanden"</string> + <string name="submission_accept_button">"Einverstanden"</string> <!-- XACT: other warning - illustration description, explanation image --> <string name="submission_positive_other_illustration_description">"Ein Smartphone übermittelt einen positiven Testbefund verschlüsselt ins System."</string> <!-- XHED: Title for the interop country list--> @@ -1261,17 +1260,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 +1346,7 @@ <!-- XTXT: First section after the header of the interoperability information/configuration view --> <string name="interoperability_configuration_first_section">Mehrere Länder arbeiten zusammen, um über den gemeinsam betriebenen Austausch-Server länderübergreifende Warnungen zu ermöglichen. So können bei der Risiko-Ermittlung auch die Kontakte mit Nutzern einer offiziellen Corona-App anderer teilnehmender Länder berücksichtigt werden.</string> <!-- XTXT: Second section after the header of the interoperability information/configuration view --> - <string name="interoperability_configuration_second_section">Hierfür lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die diese über ihre App geteilt haben. Diese Liste wird dann mit den von Ihrem Smartphone aufgezeichneten Zufalls-IDs verglichen. Der tägliche Download der Liste mit den Zufalls-IDs ist für Sie kostenlos – Ihr Datenvolumen wird nicht belastet und es fallen im europäischen Ausland keine Roaming-Gebühren an.</string> + <string name="interoperability_configuration_second_section">Hierfür lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die ihr Testergebnis zur Warnung anderer über ihre App geteilt haben. Diese Liste wird dann mit den von Ihrem Android-Smartphone aufgezeichneten Zufalls-IDs verglichen. Die Downloads der Listen sind für Sie in der Regel kostenlos. Das heißt: Das von der App verursachte Datenvolumen wird von den Mobilfunk-Betreibern nicht angerechnet und im EU-Ausland werden Ihnen keine Roaming-Gebühren berechnet. Näheres erfahren Sie von Ihrem Mobilfunk-Betreiber.</string> <!-- XHED: Header right above the country list in the interoperability information/configuration view --> <string name="interoperability_configuration_list_title">Derzeit nehmen die folgenden Länder an der länderübergreifenden Risiko-Ermittlung teil:</string> <!-- XTXT: Text right under the country list in the interoperability information/configuration view --> @@ -1368,9 +1357,9 @@ <!-- YMSG: Onboarding tracing step first section in interoperability after the title --> <string name="interoperability_onboarding_first_section">Mehrere Länder arbeiten zusammen, um länderübergreifende Warnungen zu ermöglichen. Das heißt es können die Kontakte mit Nutzern der offiziellen Corona-Apps aller teilnehmenden Länder berücksichtigt werden.</string> <!-- YMSG: Onboarding tracing step second section in interoperability after the title --> - <string name="interoperability_onboarding_second_section">Hat ein Nutzer seine Zufalls-IDs über den von den teilnehmenden Ländern gemeinsam betriebenen Austausch-Server zur Verfügung gestellt, können Nutzer der offiziellen Corona-Apps der teilnehmenden Länder gewarnt werden.</string> + <string name="interoperability_onboarding_second_section">Hat ein Nutzer sein positives Testergebnis (genauer gesagt: seine Zufalls-IDs) zur Warnung anderer über den von den teilnehmenden Ländern gemeinsam betriebenen Austausch-Server zur Verfügung gestellt, können alle Nutzer der offiziellen Corona-Apps der teilnehmenden Länder gewarnt werden.</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_randomid_download_free">Der tägliche Download der Liste mit den Zufalls-IDs ist für Sie in der Regel kostenlos. Das heißt: Das von der App verursachte Datenvolumen wird von den Mobilfunk-Betreibern nicht angerechnet und im EU-Ausland werden Ihnen keine Roaming-Gebühren berechnet. Näheres erfahren Sie von Ihrem Mobilfunk-Betreiber.</string> + <string name="interoperability_onboarding_randomid_download_free">Die täglichen Downloads der Listen mit den Zufalls-IDs der Nutzer, die ein positives Testergebnis geteilt haben, sind für Sie in der Regel kostenlos. Das heißt: Das von der App verursachte Datenvolumen wird von den Mobilfunk-Betreibern nicht angerechnet und im EU-Ausland werden Ihnen keine Roaming-Gebühren berechnet. Näheres erfahren Sie von Ihrem Mobilfunk-Betreiber.</string> <!-- XTXT: Small header above the country list in the onboarding screen for interoperability. --> <string name="interoperability_onboarding_list_title">Derzeit nehmen die folgenden Länder teil:</string> @@ -1385,9 +1374,9 @@ <!-- XTXT: Description of the interoperability extension of the app. Below interoperability_onboarding_delta_title --> <string name="interoperability_onboarding_delta_subtitle">Die Funktion der Corona-Warn-App wurde erweitert. Es arbeiten nun mehrere Länder zusammen, um über einen gemeinsam betriebenen Austausch-Server länderübergreifende Warnungen zu ermöglichen. So können bei der Risiko-Ermittlung jetzt auch die Kontakte mit Nutzern einer offiziellen Corona-App anderer teilnehmender Länder berücksichtigt werden.</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_delta_randomid">Hierfür lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die diese über ihre App geteilt haben. Diese Liste wird dann mit den von Ihrem Smartphone aufgezeichneten Zufalls-IDs verglichen.</string> + <string name="interoperability_onboarding_delta_randomid">Hierfür lädt die App täglich eine aktuelle Liste mit den Zufalls-IDs aller Nutzer herunter, die ihr Testergebnis zur Warnung anderer über ihre App geteilt haben. Diese Liste wird dann mit den von Ihrem Android-Smartphone aufgezeichneten Zufalls-IDs verglichen.</string> <!-- YMSG: Onboarding tracing step third section in interoperability after the title. --> - <string name="interoperability_onboarding_delta_free_download">Der tägliche Download der Liste mit den Zufalls-IDs ist für Sie in der Regel kostenlos. Das heißt: Das von der App verursachte Datenvolumen wird von den Mobilfunk-Betreibern nicht angerechnet und im EU-Ausland werden Ihnen keine Roaming-Gebühren berechnet. Näheres erfahren Sie von Ihrem Mobilfunk-Betreiber.</string> + <string name="interoperability_onboarding_delta_free_download">Die Downloads der Listen sind für Sie in der Regel kostenlos. Das heißt: Das von der App verursachte Datenvolumen wird von den Mobilfunk-Betreibern nicht angerechnet und im EU-Ausland werden Ihnen keine Roaming-Gebühren berechnet. Näheres erfahren Sie von Ihrem Mobilfunk-Betreiber.</string> <!-- XACT: interoperability (eu) - illustraction description, explanation image --> <string name="interoperability_eu_illustration_description">Eine Hand hält ein Smartphone. Im Hintergrund ist Europa und die europäische Flagge illustriert</string> 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..1b5a8d634f8186e447677db303334ef3e98ace06 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> @@ -831,19 +835,6 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Cancel"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"QR Code Scan"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"Only scan your own test."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"The test can only be scanned "<b>"once"</b>"."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"The app "<b>"cannot"</b>" manage multiple tests at the same time."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"If a more current test is available, delete the existing test and scan the QR code of the current test."</string> - - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"Position the QR code in the frame."</string> <!-- YTXT: instruction text for QR code scanning --> @@ -865,15 +856,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 --> @@ -947,18 +938,14 @@ <string name="submission_dispatcher_headline">"Selection"</string> <!-- XHED: Page subheadline for dispatcher menu --> <string name="submission_dispatcher_subheadline">"What information do you have?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">""</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">""</string> <!-- YTXT: Dispatcher text for QR code option --> <string name="submission_dispatcher_card_qr">"Document with QR code"</string> <!-- 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> - <!-- 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> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Accept"</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,15 +959,15 @@ <!-- 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--> <string name="submission_positive_other_warning_body">"If you wish, you can now ensure that others are warned of possible infection.\n\nTo do so, you can transmit your own random IDs from the last 14 days – and, optionally, information about when you first noticed coronavirus symptoms – to the server operated jointly by the participating countries. From there, your random IDs and any additional information will be distributed to the users of the relevant official coronavirus apps. In this way, any other users with whom you have had contact can be warned of a possible infection.\n\nThe only information transmitted will be your random IDs and any optional information you provide about the onset of your symptoms. No personal data such as your name, address or location will be disclosed."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Accept"</string> + <string name="submission_accept_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 +1033,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 +1058,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 +1083,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 +1144,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 +1188,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 +1238,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 +1337,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> @@ -1391,4 +1371,4 @@ <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available --> <string name="interoperability_onboarding_list_button_title_no_network">"Open Device Settings"</string> -</resources> \ No newline at end of file +</resources> 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..dca95af4a27fa1f824a0cff2c376f2993b5e3bd4 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> @@ -831,19 +835,6 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Anuluj"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"Skan kodu QR"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"Zeskanuj tylko swój wÅ‚asny test."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"Test można zeskanować tylko "<b>"raz"</b>"."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"Aplikacja "<b>"nie może"</b>" zarzÄ…dzać wieloma testami jednoczeÅ›nie."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"JeÅ›li dostÄ™pny jest bardziej aktualny test, usuÅ„ dotychczasowy test i zeskanuj kod QR aktualnego testu."</string> - - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"Ustaw kod QR w ramce."</string> <!-- YTXT: instruction text for QR code scanning --> @@ -869,11 +860,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 --> @@ -947,18 +938,14 @@ <string name="submission_dispatcher_headline">"Wybór"</string> <!-- XHED: Page subheadline for dispatcher menu --> <string name="submission_dispatcher_subheadline">"Jakie informacje posiadasz?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">""</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">""</string> <!-- YTXT: Dispatcher text for QR code option --> <string name="submission_dispatcher_card_qr">"Dokument z kodem QR"</string> <!-- 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> - <!-- 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> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nie akceptuj"</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,15 +959,15 @@ <!-- 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--> <string name="submission_positive_other_warning_body">"JeÅ›li chcesz, możesz teraz zapewnić, by inni otrzymali ostrzeżenie dotyczÄ…ce potencjalnego zakażenia.\n\nW tym celu możesz wysÅ‚ać losowe identyfikatory z ostatnich 14 dni – oraz opcjonalnie informacjÄ™ o dostrzeżeniu objawów koronawirusa u siebie – na serwer obsÅ‚ugiwany wspólnie przez uczestniczÄ…ce kraje. Z tego serwera Twoje losowe identyfikatory i wszelkie dodatkowe informacje bÄ™dÄ… przesyÅ‚ane do użytkowników odpowiednich oficjalnych aplikacji koronawirusowych. DziÄ™ki temu inni użytkownicy, z którymi miaÅ‚eÅ›(-Å‚aÅ›) kontakt, otrzymajÄ… ostrzeżenie dotyczÄ…ce ewentualnego zakażenia.\n\nJedynymi przesyÅ‚anymi informacjami sÄ… Twoje losowe identyfikatory oraz wszelkie opcjonalnie podane przez Ciebie informacje o wystÄ…pieniu u Ciebie objawów. Nie sÄ… udostÄ™pniane żadne dane osobowe, takie jak imiÄ™ i nazwisko, adres czy lokalizacja."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Akceptuj"</string> + <string name="submission_accept_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 +1033,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 +1058,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 +1073,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 +1083,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 +1144,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 +1188,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 +1238,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 +1322,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 +1337,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 +1357,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> @@ -1391,4 +1371,4 @@ <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available --> <string name="interoperability_onboarding_list_button_title_no_network">"Otwórz ustawienia urzÄ…dzenia"</string> -</resources> \ No newline at end of file +</resources> 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..1ac7c15afb801f7cd3bda858dfe719f1ebcfd94b 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> @@ -831,19 +835,6 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Anulare"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"Scanare cod QR"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"ScanaÈ›i doar codul propriului dvs. test."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"Testul poate fi scanat "<b>"o singură dată"</b>"."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"AplicaÈ›ia "<b>"nu poate"</b>" gestiona mai multe teste simultan.."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"Dacă este disponibil un test mai recent, È™tergeÈ›i testul existent È™i scanaÈ›i codul QR al testului curent."</string> - - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"PoziÈ›ionaÈ›i codul QR în cadru."</string> <!-- YTXT: instruction text for QR code scanning --> @@ -865,15 +856,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 --> @@ -947,18 +938,14 @@ <string name="submission_dispatcher_headline">"SelecÈ›ie"</string> <!-- XHED: Page subheadline for dispatcher menu --> <string name="submission_dispatcher_subheadline">"Ce informaÈ›ii aveÈ›i?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">""</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">""</string> <!-- YTXT: Dispatcher text for QR code option --> <string name="submission_dispatcher_card_qr">"Document cu cod QR"</string> <!-- 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> - <!-- 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> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Nu accept"</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,15 +959,15 @@ <!-- 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--> <string name="submission_positive_other_warning_body">"Dacă doriÈ›i, acum puteÈ›i să vă asiguraÈ›i că alte persoane sunt avertizate de posibila infectare.\n\nPentru aceasta, puteÈ›i transmite ID-urile dvs. aleatorii din ultimele 14 zile – È™i, opÈ›ional, informaÈ›ii despre momentul în care aÈ›i observat prima dată simptomele de coronavirus – către serverul operat în comun de țările participante. De aici, ID-urile aleatorii È™i informaÈ›iile suplimentare vor fi distribuite către utilizatorii aplicaÈ›iilor oficiale relevante împotriva coronavirusului. ÃŽn acest mod, orice alt utilizator cu care aÈ›i avut contact poate fi avertizat de o posibilă infectare.\n\nSingurele informaÈ›ii transmise vor fi doar ID-urile aleatorii È™i orice informaÈ›ii opÈ›ionale pe care le furnizaÈ›i despre debutul simptomelor. Nu vor fi dezvăluite niciun fel de date personale, cum ar fi numele, adresa sau locaÈ›ia dvs."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Accept"</string> + <string name="submission_accept_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 +1033,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 +1058,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 +1083,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 +1188,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 +1238,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 +1337,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> @@ -1391,4 +1371,4 @@ <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available --> <string name="interoperability_onboarding_list_button_title_no_network">"DeschideÈ›i setările dispozitivului"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values-tr/legal_strings.xml b/Corona-Warn-App/src/main/res/values-tr/legal_strings.xml index d8dac17e2b2846ba27fe7c30aced01a2b5c32a27..87ba63dbe1939759baa3dd18105bc8190ef8f660 100644 --- a/Corona-Warn-App/src/main/res/values-tr/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/legal_strings.xml @@ -12,4 +12,22 @@ <string name="onboarding_tracing_headline_consent">"Onay beyanı"</string> <!-- YTXT: onboarding(tracing) - body for consent information --> <string name="onboarding_tracing_body_consent">"DiÄŸer katılımcı ülkelerdeki uygulama kullanıcılarıyla bir riskli temasa maruz kalıp kalmadığınızı ve bu nedenle bir enfeksiyon riski olup olmadığını öğrenmek için, maruz kalma günlüğünü etkinleÅŸtirmeniz gerekmektedir. “Maruz kalma günlüğünü etkinleÅŸtir†tuÅŸuna tıkladığınızda, uygulamanızdaki maruz kalma günlüğünü ve ilgili veri iÅŸlemenin etkinleÅŸtirilmesini kabul etmiÅŸ olursunuz.\n\n Maruz kalma günlüğünü kullanabilmek için, ayrıca Android akıllı telefonunuzdaki Google tarafından saÄŸlanan “COVID-19 bildirimleri†iÅŸlevini de etkinleÅŸtirmeniz ve Corona-Warn-App’ı kullanıma açmanız gerekir.\n\n COVID-19 bildirim sistemi etkinleÅŸtirildiÄŸinde, Android akıllı telefonunuz sürekli olarak rastgele kimlik no’ları oluÅŸturur ve bunları Bluetooth aracılığıyla göndererek çevrenizdeki akıllı telefonlar tarafından alınabilmelerini saÄŸlar. Öte yandan, kendi Android akıllı telefonunuz da diÄŸer akıllı telefonlardan rastgele kimlik no’ları alır. Kendi rastgele kimlik no’larınız ve diÄŸer akıllı telefonlardan alınan kimlik no’ları, Android akıllı telefonunuz tarafından kaydedilir ve orada 14 gün boyunca saklanır.\n\nMaruz kalma günlüğü için uygulama, böyle bir teması bu uygulama üzerinden paylaÅŸan tüm kullanıcıların rastgele kimlik no'larını içeren ve günlük olarak güncellenen bir liste indirir. Daha sonra bu liste, akıllı telefonunuz tarafından kaydedilen rastgele kimlik no’ları ile karşılaÅŸtırılır.\n\nBir riske maruz kalma tespit edilirse, Korona uygulaması sizi bu konuda bilgilendirir. Böyle bir durumda uygulama, akıllı telefonunuz tarafından kaydedilen maruz kalma verilerine (temasın tarihi, süresi ve Bluetooth sinyal gücü) eriÅŸir. DiÄŸer kullanıcıya olan mekânsal mesafe, Bluetooth sinyal gücü üzerinden elde edilir (sinyal ne kadar güçlüyse, mesafe o kadar kısadır). Bu veriler, enfeksiyon riskinizi hesaplamak ve nasıl davranmanız gerektiÄŸi konusunda size önerilerde bulunmak üzere uygulama tarafından deÄŸerlendirilir. Bu deÄŸerlendirme iÅŸlemi, sadece kendi akıllı telefonunuzda gerçekleÅŸtirilir.\n\nSizden baÅŸka hiç kimse (RKI (Robert Koch Enstitüsü) veya katılımcı ülkelerin saÄŸlık kurumu yetkilileri bile), enfeksiyon riskine maruz kalıp kalmadığınızı ve sizin için nasıl bir enfeksiyon riskinin saptandığını öğrenemez.\n\nMaruz kalma günlüğüne vermiÅŸ olduÄŸunuz onayı iptal etmek için, uygulamadaki kaydırıcıyı kullanarak, iÅŸlevi devre dışı bırakabilir veya uygulamayı silebilirsiniz. Maruz kalma günlüğünü tekrar kullanmak isterseniz, kaydırıcıyı yeniden etkinleÅŸtirebilir veya uygulamayı yeniden yükleyebilirsiniz. Maruz kalma günlüğünü devre dışı bırakırsanız, uygulama, artık riskle karşılaşıp karşılaÅŸmadığınızı denetleyemez. Rastgele kimlik no’larının gönderilmesini ve tarafınızdan alınmasını durdurmak için, Android akıllı telefonunuzdaki COVID-19 bildirim sistemini devre dışı bırakmanız gerekir. Android akıllı telefonunuzun COVID-19 bildirim sistemi tarafından kaydedilen kendinize ait ve dışarıdan gelen rastgele kimlik no’larının uygulama tarafından silinmediÄŸini unutmayın. Bunları, sadece Android akıllı telefonunuzun ayarlarından kalıcı olarak silebilirsiniz.\n\nUygulamanın veri gizliliÄŸi beyanını (sınır ötesi maruz kalma günlüğü için veri iÅŸlemeye iliÅŸkin bilgiler de dahil) „Uygulama bilgileri“ > „Veri gizliliÄŸi“ menü öğeleri altında bulabilirsiniz."</string> + <!-- XHED: Page subheadline for consent sub section your consent --> + <string name="submission_consent_your_consent_subsection_headline"></string> + <!-- YTXT: Body for consent sub section your consent subtext --> + <string name="submission_consent_your_consent_subsection_tapping_agree"></string> + <!-- YTXT: Body for consent sub section your consent subtext first point --> + <string name="submission_consent_your_consent_subsection_first_point"></string> + <!-- YTXT: Body for consent sub section your consent subtext second point --> + <string name="submission_consent_your_consent_subsection_second_point"></string> + <!-- YTXT: Body for consent sub section your consent subtext third point --> + <string name="submission_consent_your_consent_subsection_third_point"></string> + <!-- YTXT: Body for consent main section first point --> + <string name="submission_consent_main_first_point"></string> + <!-- YTXT: Body for consent main section second point --> + <string name="submission_consent_main_second_point"></string> + <!-- YTXT: Body for consent main section third point --> + <string name="submission_consent_main_third_point"></string> + <!-- YTXT: Body for consent main section fourth point --> + <string name="submission_consent_main_fourth_point"></string> </resources> 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..a21fb29d30dd9872683cc23d0b5f40d6c09b7b8d 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> @@ -831,19 +835,6 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Ä°ptal"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"QR Kod Tarama"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"Yalnızca kendi testinizi tarayın."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"Test yalnızca "<b>"bir kez"</b>" taranabilir."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"Uygulama aynı anda birden çok testi "<b>"yönetemez"</b>"."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"Daha yakın zamanlı bir test varsa mevcut testi silin ve yakın zamanlı testin QR kodunu tarayın."</string> - - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"QR kodu çerçeveye sığdırın."</string> <!-- YTXT: instruction text for QR code scanning --> @@ -865,15 +856,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 --> @@ -947,22 +938,18 @@ <string name="submission_dispatcher_headline">"Seçim"</string> <!-- XHED: Page subheadline for dispatcher menu --> <string name="submission_dispatcher_subheadline">"Hangi bilgilere sahipsiniz?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">""</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">""</string> <!-- YTXT: Dispatcher text for QR code option --> <string name="submission_dispatcher_card_qr">"QR kod ile belgeleyin"</string> <!-- 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> - <!-- 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> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Kabul Etme"</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,15 +959,15 @@ <!-- 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--> <string name="submission_positive_other_warning_body">"Dilerseniz diÄŸer kullanıcıların olası enfeksiyonlar konusunda uyarılmasını saÄŸlayabilirsiniz.\n\nBunun için son 14 güne ait rastgele kimliklerinizi ve isteÄŸe baÄŸlı olarak koronavirüs semptomlarını ne zaman ilk kez fark ettiÄŸinizi katılımcı ülkeler tarafından ortak olarak iÅŸletilen sunucuya aktarabilirsiniz. Rastgele kimlikleriniz ve tüm ek bilgiler, bu sunucudan ilgili resmi koronavirüs uygulamalarının kullanıcılarına dağıtılacaktır. Bu sayede, temasta bulunduÄŸunuz diÄŸer tüm kullanıcılar olası enfeksiyon konusunda uyarılabilir.\n\nYalnızca rastgele kimlikleriniz ve semptomlarınızın baÅŸlangıcına iliÅŸkin verdiÄŸiniz isteÄŸe baÄŸlı bilgiler aktarılacaktır. Adınız, adresiniz veya konumunuz gibi hiçbir kiÅŸisel veri açıklanmayacaktır."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Kabul Et"</string> + <string name="submission_accept_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 +1033,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 +1058,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 +1083,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 +1188,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 +1238,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 --> @@ -1391,4 +1371,4 @@ <!-- XBUT: Title for the interoperability onboarding Settings-Button if no network is available --> <string name="interoperability_onboarding_list_button_title_no_network">"Cihaz Ayarlarını Aç"</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values/dimens.xml b/Corona-Warn-App/src/main/res/values/dimens.xml index a411731f9aa6b3d473a92cb90143b7858f813c4e..cc98ddb783168f029c6c1925c75c89de5b9feba5 100644 --- a/Corona-Warn-App/src/main/res/values/dimens.xml +++ b/Corona-Warn-App/src/main/res/values/dimens.xml @@ -76,6 +76,8 @@ <dimen name="circle_small">23dp</dimen> <dimen name="circle_large_width">10</dimen> <dimen name="circle_small_width">5</dimen> + <dimen name="circle_icon">32dp</dimen> + <dimen name="circle_icon_padding">6dp</dimen> <!-- todo illustration sizes --> diff --git a/Corona-Warn-App/src/main/res/values/legal_strings.xml b/Corona-Warn-App/src/main/res/values/legal_strings.xml index 244c58b9a2d4988dad72bd47697c3bd55be146e0..60b16674745135b6bcb0f7d570488291b8a8d1ab 100644 --- a/Corona-Warn-App/src/main/res/values/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values/legal_strings.xml @@ -13,4 +13,23 @@ <string name="onboarding_tracing_headline_consent" translatable="false">"Declaration of consent"</string> <!-- YTXT: onboarding(tracing) - body for consent information --> <string name="onboarding_tracing_body_consent" translatable="false">"You need to enable exposure logging to find out whether you have had possible exposures involving app users in the participating countries and are therefore at risk of infection yourself. By tapping on the “Activate exposure logging†button, you agree to enabling the exposure logging feature and to the associated data processing by the app.\n\nIn order to use the exposure logging feature, you will also have to enable the “COVID-19 Exposure Notifications†functionality provided by Google on your Android smartphone and grant the Corona-Warn-App permission to use this.\n\nWhen COVID-19 Exposure Notifications are enabled, your Android smartphone continuously generates random IDs and sends them via Bluetooth so that they can be received by other smartphones near you. Your Android smartphone, in turn, receives the random IDs of other smartphones. Your own random IDs and those received from other smartphones are recorded by your Android smartphone and stored there for 14 days.\n\nFor exposure logging, the app downloads a list, which is updated daily, of the random IDs of all users who have shared their random IDs via their official coronavirus app. This list is then compared with the random IDs of other users which have been recorded by your smartphone.\n\nThe app will inform you if it detects a possible exposure. In this case, the app gains access to the data recorded by your smartphone about the possible exposure (date, duration and Bluetooth signal strength of the contact). The Bluetooth signal strength is used to derive the physical distance to the other user (the stronger the signal, the smaller the distance). The app analyses this information in order to calculate your risk of infection and to give you recommendations for what to do next. This analysis is only performed locally on your smartphone.\n\nApart from you, nobody (not even the RKI or the health authorities of participating countries) will know whether a possible exposure has been detected and what risk of infection has been identified for you.\n\nTo withdraw your consent to the exposure logging feature, you can disable the feature by using the toggle switch in the app or delete the app. If you would like to use the exposure logging feature again, you can toggle the feature back on or reinstall the app. If you disable the exposure logging feature, the app will no longer check for possible exposures. If you also wish to stop your device sending and receiving random IDs, you will need to disable COVID-19 Exposure Notifications in your Android smartphone settings. Please note that your own random IDs and those received from other smartphones which are stored by your Android smartphone’s COVID-19 Exposure Notification functionality will not be deleted by the app. You can only permanently delete these in your Android smartphone settings.\n\nThe app’s privacy notice (including information about the data processing carried out for the transnational exposure logging feature) can be found in the menu under „App Information“ > „Data Privacy“."</string> + <!-- XHED: Page subheadline for consent sub section your consent --> + <string name="submission_consent_your_consent_subsection_headline" translatable="false"></string> + <!-- YTXT: Body for consent sub section your consent subtext --> + <string name="submission_consent_your_consent_subsection_tapping_agree" translatable="false"></string> + <!-- YTXT: Body for consent sub section your consent subtext first point --> + <string name="submission_consent_your_consent_subsection_first_point" translatable="false"></string> + <!-- YTXT: Body for consent sub section your consent subtext second point --> + <string name="submission_consent_your_consent_subsection_second_point" translatable="false"></string> + <!-- YTXT: Body for consent sub section your consent subtext third point --> + <string name="submission_consent_your_consent_subsection_third_point" translatable="false"></string> + <!-- YTXT: Body for consent main section first point --> + <string name="submission_consent_main_first_point" translatable="false"></string> + <!-- YTXT: Body for consent main section second point --> + <string name="submission_consent_main_second_point" translatable="false"></string> + <!-- YTXT: Body for consent main section third point --> + <string name="submission_consent_main_third_point" translatable="false"></string> + <!-- YTXT: Body for consent main section fourth point --> + <string name="submission_consent_main_fourth_point" translatable="false"></string> + </resources> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 266628ab2b4af25556ecb991a8911cff5a40eea2..c880b947c4ccf4885aa592ad84080de14366960e 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> @@ -848,23 +840,31 @@ <!-- XBUT: Dialog(Invalid QR code) - negative button (left) --> <string name="submission_qr_code_scan_invalid_dialog_button_negative">"Cancel"</string> - <!-- QR Code Scan Info Screen --> - <!-- XHED: Page headline for QR Scan info screen --> - <string name="submission_qr_info_headline">"QR Code Scan"</string> - <!-- YTXT: Body text for for QR Scan info point 1 --> - <string name="submission_qr_info_point_1_body">"Only scan your own test."</string> - <!-- YTXT: Body text for for QR Scan info point 2 --> - <string name="submission_qr_info_point_2_body">"The test can only be scanned "<b>"once"</b>"."</string> - <!-- YTXT: Body text for for QR Scan info point 3 --> - <string name="submission_qr_info_point_3_body">"The app "<b>"cannot"</b>" manage multiple tests at the same time."</string> - <!-- YTXT:Body text for for QR Scan info point 4 --> - <string name="submission_qr_info_point_4_body">"If a more current test is available, delete the existing test and scan the QR code of the current test."</string> - <!-- QR Code Scan Screen --> <string name="submission_qr_code_scan_title">"Position the QR code in the frame."</string> <!-- YTXT: instruction text for QR code scanning --> <string name="submission_qr_code_scan_body">"Position the QR code in the frame."</string> + <!-- QR Code Consent Screen --> + <!-- XHED: Page headline for Submission consent --> + <string name="submission_consent_main_headline"></string> + <!-- YTXT: Body for Submissionconsent --> + <string name="submission_consent_main_headline_body"></string> + <!-- XHED: Page subheadline for consent call test result --> + <string name="submission_consent_call_test_result"></string> + <!-- YTXT: Body for Submission Consent call test result body --> + <string name="submission_consent_call_test_result_body"></string> + <!-- YTXT: Body sub text 1 for Submission Consent call test result --> + <string name="submission_consent_call_test_result_scan_your_test_only"></string> + <!-- YTXT: Body sub text 2 for Submission Consent call test result --> + <string name="submission_consent_call_test_result_scan_test_only_once"></string> + <!-- XHED: Page subheadline for consent help by warning others --> + <string name="submission_consent_help_by_warning_others_headline"></string> + <!-- YTXT: Body for consent help by warning others --> + <string name="submission_consent_help_by_warning_others_body"></string> + <!-- YTXT: Page bottom text for consent screen --> + <string name="submission_consent_main_bottom_body"></string> + <!-- Submission Test Result --> <!-- XHED: Page headline for test result --> <string name="submission_test_result_headline">"Test Result"</string> @@ -881,15 +881,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 --> @@ -963,18 +963,14 @@ <string name="submission_dispatcher_headline">"Selection"</string> <!-- XHED: Page subheadline for dispatcher menu --> <string name="submission_dispatcher_subheadline">"What information do you have?"</string> + <!-- XHED: Page subheadline for dispatcher options asking if they have already been tested: QR and TAN --> + <string name="submission_dispatcher_needs_testing_subheadline">""</string> + <!-- XHED: Page subheadline for dispatcher options asking if they already have a positive test: tele-TAN --> + <string name="submission_dispatcher_already_positive_subheadline">""</string> <!-- YTXT: Dispatcher text for QR code option --> <string name="submission_dispatcher_card_qr">"Document with QR code"</string> <!-- 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> - <!-- 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> - <!-- XBUT: submission(dispatcher QR Dialog) - negative button (left) --> - <string name="submission_dispatcher_qr_privacy_dialog_button_negative">"Do Not Accept"</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 --> @@ -988,15 +984,15 @@ <!-- 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--> <string name="submission_positive_other_warning_body">"If you wish, you can now ensure that others are warned of possible infection.\n\nTo do so, you can transmit your own random IDs from the last 14 days – and, optionally, information about when you first noticed coronavirus symptoms – to the server operated jointly by the participating countries. From there, your random IDs and any additional information will be distributed to the users of the relevant official coronavirus apps. In this way, any other users with whom you have had contact can be warned of a possible infection.\n\nThe only information transmitted will be your random IDs and any optional information you provide about the onset of your symptoms. No personal data such as your name, address or location will be disclosed."</string> <!-- XBUT: other warning continue button --> - <string name="submission_positive_other_warning_button">"Accept"</string> + <string name="submission_accept_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> @@ -1062,7 +1058,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> @@ -1087,9 +1083,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 --> @@ -1113,7 +1109,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 --> @@ -1174,7 +1170,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 --> @@ -1219,9 +1215,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 @@ -1269,25 +1265,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> @@ -1380,7 +1364,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 --> @@ -1414,4 +1398,4 @@ <string name="interoperability_onboarding_list_button_title_no_network">"Open Device Settings"</string> -</resources> \ No newline at end of file +</resources> 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 44a4980d7826e632fd792e724dc1c8d8c807d308..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 @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.main.home import android.content.Context import de.rki.coronawarnapp.notification.TestResultNotificationService +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status @@ -9,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 @@ -47,6 +48,7 @@ class HomeFragmentViewModelTest : BaseTest() { @MockK lateinit var submissionCardsStateProvider: SubmissionCardsStateProvider @MockK lateinit var tracingRepository: TracingRepository @MockK lateinit var testResultNotificationService: TestResultNotificationService + @MockK lateinit var submissionRepository: SubmissionRepository @BeforeEach fun setup() { @@ -70,7 +72,8 @@ class HomeFragmentViewModelTest : BaseTest() { tracingCardStateProvider = tracingCardStateProvider, submissionCardsStateProvider = submissionCardsStateProvider, tracingRepository = tracingRepository, - testResultNotificationService = testResultNotificationService + testResultNotificationService = testResultNotificationService, + submissionRepository = submissionRepository ) @Test @@ -128,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 @@ -142,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 2eaac14f64ca80055b87cbfef053e197a6927306..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 @@ -29,11 +29,11 @@ import testhelpers.extensions.InstantExecutorExtension class SubmissionCardsStateProviderTest : BaseTest() { @MockK lateinit var context: Context + @MockK lateinit var submissionRepository: SubmissionRepository @BeforeEach fun setup() { MockKAnnotations.init(this) - mockkObject(SubmissionRepository) mockkObject(LocalData) } @@ -42,24 +42,23 @@ class SubmissionCardsStateProviderTest : BaseTest() { clearAllMocks() } - private fun createInstance() = SubmissionCardsStateProvider() + private fun createInstance() = SubmissionCardsStateProvider(submissionRepository) @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 + submissionRepository.deviceUIStateFlow 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/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt index 5645c8f4921f046181893fb317d88e457de7b114..e56a9d2e0cc587c44462086143c161c7a904e5f8 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt @@ -1,26 +1,18 @@ package de.rki.coronawarnapp.service.submission -import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException -import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.playbook.Playbook -import de.rki.coronawarnapp.storage.LocalData -import de.rki.coronawarnapp.storage.SubmissionRepository -import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.verification.server.VerificationKeyType -import io.kotest.assertions.throwables.shouldThrow 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.mockkObject -import io.mockk.verify import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -28,36 +20,23 @@ import org.junit.jupiter.api.Test class SubmissionServiceTest { + private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064" private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" private val registrationToken = "asdjnskjfdniuewbheboqudnsojdff" - private val testResult = TestResult.PENDING - @MockK lateinit var backgroundNoise: BackgroundNoise @MockK lateinit var mockPlaybook: Playbook @MockK lateinit var appComponent: ApplicationComponent - private val symptoms = Symptoms(Symptoms.StartOf.OneToTwoWeeksAgo, Symptoms.Indication.POSITIVE) + lateinit var submissionService: SubmissionService @BeforeEach fun setUp() { MockKAnnotations.init(this) - mockkObject(AppInjector) every { AppInjector.component } returns appComponent - every { appComponent.playbook } returns mockPlaybook - mockkObject(BackgroundNoise.Companion) - every { BackgroundNoise.getInstance() } returns backgroundNoise - - mockkObject(LocalData) - - mockkObject(SubmissionRepository) - every { SubmissionRepository.updateTestResult(any()) } just Runs - - every { LocalData.teletan() } returns null - every { LocalData.testGUID() } returns null - every { LocalData.registrationToken() } returns null + submissionService = SubmissionService(mockPlaybook) } @AfterEach @@ -67,87 +46,43 @@ class SubmissionServiceTest { @Test fun registrationWithGUIDSucceeds() { - every { LocalData.testGUID() } returns guid - - every { LocalData.testGUID(any()) } just Runs - every { LocalData.registrationToken(any()) } just Runs - every { LocalData.devicePairingSuccessfulTimestamp(any()) } just Runs - coEvery { - mockPlaybook.initialRegistration(any(), VerificationKeyType.GUID) + mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID) } returns (registrationToken to TestResult.PENDING) - coEvery { mockPlaybook.testResult(registrationToken) } returns testResult - - every { backgroundNoise.scheduleDummyPattern() } just Runs runBlocking { - SubmissionService.asyncRegisterDeviceViaGUID(guid) + submissionService.asyncRegisterDeviceViaGUID(guid) } - verify(exactly = 1) { - LocalData.registrationToken(registrationToken) - LocalData.devicePairingSuccessfulTimestamp(any()) - LocalData.testGUID(null) - backgroundNoise.scheduleDummyPattern() - SubmissionRepository.updateTestResult(testResult) + coVerify(exactly = 1) { + mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID) } } @Test fun registrationWithTeleTANSucceeds() { - every { LocalData.teletan() } returns guid - - every { LocalData.teletan(any()) } just Runs - every { LocalData.registrationToken(any()) } just Runs - every { LocalData.devicePairingSuccessfulTimestamp(any()) } just Runs - coEvery { mockPlaybook.initialRegistration(any(), VerificationKeyType.TELETAN) } returns (registrationToken to TestResult.PENDING) - coEvery { mockPlaybook.testResult(registrationToken) } returns testResult - - every { backgroundNoise.scheduleDummyPattern() } just Runs runBlocking { - SubmissionService.asyncRegisterDeviceViaTAN(guid) + submissionService.asyncRegisterDeviceViaTAN(tan) } - verify(exactly = 1) { - LocalData.registrationToken(registrationToken) - LocalData.devicePairingSuccessfulTimestamp(any()) - LocalData.teletan(null) - backgroundNoise.scheduleDummyPattern() - SubmissionRepository.updateTestResult(testResult) - } - } - - @Test - fun requestTestResultWithoutRegistrationTokenFails(): Unit = runBlocking { - shouldThrow<NoRegistrationTokenSetException> { - SubmissionService.asyncRequestTestResult() + coVerify(exactly = 1) { + mockPlaybook.initialRegistration(tan, VerificationKeyType.TELETAN) } } @Test fun requestTestResultSucceeds() { - every { LocalData.registrationToken() } returns registrationToken coEvery { mockPlaybook.testResult(registrationToken) } returns TestResult.NEGATIVE runBlocking { - SubmissionService.asyncRequestTestResult() shouldBe TestResult.NEGATIVE + submissionService.asyncRequestTestResult(registrationToken) shouldBe TestResult.NEGATIVE } - } - - @Test - fun deleteRegistrationTokenSucceeds() { - every { LocalData.registrationToken(null) } just Runs - every { LocalData.devicePairingSuccessfulTimestamp(0L) } just Runs - - SubmissionService.deleteRegistrationToken() - - verify(exactly = 1) { - LocalData.registrationToken(null) - LocalData.devicePairingSuccessfulTimestamp(0L) + coVerify(exactly = 1) { + mockPlaybook.testResult(registrationToken) } } } 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/storage/SubmissionRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..67d81ff5cc319c27fb85005d4b9720b214325994 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/storage/SubmissionRepositoryTest.kt @@ -0,0 +1,116 @@ +package de.rki.coronawarnapp.storage + +import de.rki.coronawarnapp.playbook.BackgroundNoise +import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.coroutine.AppCoroutineScope +import de.rki.coronawarnapp.util.di.AppInjector +import de.rki.coronawarnapp.util.di.ApplicationComponent +import de.rki.coronawarnapp.util.formatter.TestResult +import de.rki.coronawarnapp.util.security.EncryptedPreferencesFactory +import de.rki.coronawarnapp.util.security.EncryptionErrorResetTool +import io.mockk.MockKAnnotations +import io.mockk.Runs +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.mockkObject +import io.mockk.verify +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import testhelpers.preferences.mockFlowPreference + +class SubmissionRepositoryTest { + + @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var submissionService: SubmissionService + + @MockK lateinit var backgroundNoise: BackgroundNoise + @MockK lateinit var appComponent: ApplicationComponent + + @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory + @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool + + private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" + private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064" + private val registrationToken = "asdjnskjfdniuewbheboqudnsojdff" + private val testResult = TestResult.PENDING + private val registrationData = SubmissionService.RegistrationData(registrationToken, testResult) + + lateinit var submissionRepository: SubmissionRepository + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + + mockkObject(AppInjector) + every { AppInjector.component } returns appComponent + every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory + every { appComponent.errorResetTool } returns encryptionErrorResetTool + + mockkObject(BackgroundNoise.Companion) + every { BackgroundNoise.getInstance() } returns backgroundNoise + every { backgroundNoise.scheduleDummyPattern() } just Runs + + mockkObject(LocalData) + every { LocalData.devicePairingSuccessfulTimestamp(0L) } just Runs + every { LocalData.initialTestResultReceivedTimestamp() } returns 1L + every { LocalData.registrationToken(any()) } just Runs + every { LocalData.devicePairingSuccessfulTimestamp(any()) } just Runs + + every { submissionSettings.hasGivenConsent } returns mockFlowPreference(false) + + val appScope = AppCoroutineScope() + submissionRepository = SubmissionRepository(submissionSettings, submissionService, appScope, TimeStamper()) + } + + @Test + fun deleteRegistrationTokenSucceeds() { + SubmissionRepository.deleteRegistrationToken() + + verify(exactly = 1) { + LocalData.registrationToken(null) + LocalData.devicePairingSuccessfulTimestamp(0L) + } + } + + @Test + fun registrationWithGUIDSucceeds() { + every { LocalData.testGUID(any()) } just Runs + coEvery { submissionService.asyncRegisterDeviceViaGUID(guid) } returns registrationData + + runBlocking { + submissionRepository.asyncRegisterDeviceViaGUID(guid) + } + + verify(exactly = 1) { + LocalData.devicePairingSuccessfulTimestamp(any()) + LocalData.registrationToken(registrationToken) + LocalData.testGUID(null) + backgroundNoise.scheduleDummyPattern() + submissionRepository.updateTestResult(testResult) + } + } + + @Test + fun registrationWithTeleTANSucceeds() { + every { LocalData.teletan(any()) } just Runs + coEvery { submissionService.asyncRegisterDeviceViaTAN(tan) } returns registrationData + + runBlocking { + submissionRepository.asyncRegisterDeviceViaTAN(tan) + } + + coVerify(exactly = 1) { + LocalData.devicePairingSuccessfulTimestamp(any()) + LocalData.registrationToken(registrationToken) + LocalData.teletan(null) + backgroundNoise.scheduleDummyPattern() + submissionRepository.updateTestResult(testResult) + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..4ee0455ff6a1cb8d06177bc7a16b2d3946b100cc --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/consent/SubmissionConsentViewModelTest.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.ui.submission.qrcode.consent + +import de.rki.coronawarnapp.storage.SubmissionRepository +import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository +import de.rki.coronawarnapp.ui.Country +import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents +import io.kotest.matchers.shouldBe +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.verify +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import testhelpers.extensions.InstantExecutorExtension + +@ExtendWith(InstantExecutorExtension::class) +class SubmissionConsentViewModelTest { + + @MockK lateinit var submissionRepository: SubmissionRepository + @MockK lateinit var interoperabilityRepository: InteroperabilityRepository + + lateinit var viewModel: SubmissionConsentViewModel + + private val countryList = Country.values().toList() + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + every { interoperabilityRepository.countryListFlow } returns MutableStateFlow(countryList) + every { submissionRepository.giveConsentToSubmission() } just Runs + viewModel = SubmissionConsentViewModel(submissionRepository, interoperabilityRepository) + } + + @Test + fun testOnConsentButtonClick() { + viewModel.onConsentButtonClick() + verify(exactly = 1) { submissionRepository.giveConsentToSubmission() } + } + + @Test + fun testOnDataPrivacyClick() { + viewModel.onDataPrivacyClick() + viewModel.routeToScreen.value shouldBe SubmissionNavigationEvents.NavigateToDataPrivacy + } + + @Test + fun testOnBackButtonClick() { + viewModel.onBackButtonClick() + viewModel.routeToScreen.value shouldBe SubmissionNavigationEvents.NavigateToDispatcher + } + + @Test + fun testCountryList() { + viewModel.countries.observeForever { } + viewModel.countries.value shouldBe countryList + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoFragmentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoFragmentViewModelTest.kt deleted file mode 100644 index d42ae7c1d4307f20c66c0c795901e657fb8a97a9..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/info/SubmissionQRCodeInfoFragmentViewModelTest.kt +++ /dev/null @@ -1,30 +0,0 @@ -package de.rki.coronawarnapp.ui.submission.qrcode.info - -import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import testhelpers.extensions.InstantExecutorExtension -import testhelpers.extensions.getOrAwaitValue - -@ExtendWith(InstantExecutorExtension::class) -class SubmissionQRCodeInfoFragmentViewModelTest { - - private fun createViewModel() = - SubmissionQRCodeInfoFragmentViewModel() - - @Test - fun testBackPressButton() { - val vm = createViewModel() - vm.onBackPressed() - - vm.navigateToDispatcher.getOrAwaitValue() shouldBe Unit - } - - @Test - fun testNextButton() { - val vm = createViewModel() - vm.onNextPressed() - - vm.navigateToQRScan.getOrAwaitValue() shouldBe Unit - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt index 05edd8a920a6bdf01c7796b68d25d38d59c05243..04fe119d41cb91ad44d58fb74b5739db8c3696ec 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/qrcode/scan/SubmissionQRCodeScanViewModelTest.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.storage.LocalData +import de.rki.coronawarnapp.storage.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ScanStatus import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -21,6 +22,7 @@ import testhelpers.extensions.InstantExecutorExtension class SubmissionQRCodeScanViewModelTest : BaseTest() { @MockK lateinit var backgroundNoise: BackgroundNoise + @MockK lateinit var submissionRepository: SubmissionRepository @BeforeEach fun setUp() { @@ -33,7 +35,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { every { BackgroundNoise.getInstance() } returns backgroundNoise } - private fun createViewModel() = SubmissionQRCodeScanViewModel() + private fun createViewModel() = SubmissionQRCodeScanViewModel(submissionRepository) @Test fun scanStatusValid() { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModelTest.kt index ee57585af900af56835c3bf36089e72e85ece59b..4a86498921e9208a55110e9ed906fe4a925a8ebc 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/tan/SubmissionTanViewModelTest.kt @@ -2,11 +2,14 @@ package de.rki.coronawarnapp.ui.submission.tan import de.rki.coronawarnapp.storage.SubmissionRepository import io.kotest.matchers.shouldBe +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.verify +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import testhelpers.BaseTest @@ -17,10 +20,18 @@ import testhelpers.extensions.InstantExecutorExtension @ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class) class SubmissionTanViewModelTest : BaseTest() { + @MockK lateinit var submissionRepository: SubmissionRepository + private fun createInstance() = SubmissionTanViewModel( - dispatcherProvider = TestDispatcherProvider + dispatcherProvider = TestDispatcherProvider, + submissionRepository = submissionRepository ) + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + } + @Test fun tanFormatValid() { val viewModel = createInstance() 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 - } -}