diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5b75aebd94871c4b9fd4b57f986af43e1fafc09e..5c4e109e9de73e96914a91b6df63463b321f4edf 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -25,8 +25,8 @@ __Thank you for this this PR! Please consider the following:__ * Short step by step instructions help the reviewer test your changes, e.g. how to navigate to a new UI element you added. * The PR _won't be reviewed_ if CircleCi is failing or if there are merge conflicts. If Circle CI is still failing mark the PR as a draft and write a little comment on your status. * Provide at least a few unit and/or instrumentation tests. - * Use a meaning full branch name. Use either `fix` or `feature` as prefix for your branch, e.g. `fix/prevent-npe-on-device-rotation-issue_123` - * Test your changes thoroughly. Only open PRs which you think is ready to be merged. If you explicitly need feedback mark the PR as `DRAFT` on Github. + * Use a meaningful branch name. Use either `fix` or `feature` as prefix for your branch, e.g. `fix/prevent-npe-on-device-rotation-issue_123` + * Test your changes thoroughly. Only open PRs which you think is ready to be merged. If you explicitly need feedback mark the PR as `DRAFT` on GitHub. * Don't introduce unrelated code reformatting (e.g., on-save hooks in your IDE) * Remove this checklist before creating your pull request. diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 25be009f467df291bb66c886d5c653ec3b92bff2..338f6bcecb65f8da9a4a404026964624ee57b44d 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -68,6 +68,8 @@ android { buildConfigField "int", "VERSION_MAJOR", VERSION_MAJOR buildConfigField "int", "VERSION_MINOR", VERSION_MINOR buildConfigField "int", "VERSION_PATCH", VERSION_PATCH + + vectorDrawables.useSupportLibrary = true } def signingPropFile = file("../keystore.properties") diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.storage.TraceLocationDatabase/2.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.storage.TraceLocationDatabase/2.json new file mode 100644 index 0000000000000000000000000000000000000000..006ae7f97f5b769e222fb4347857b4b64e070653 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.presencetracing.storage.TraceLocationDatabase/2.json @@ -0,0 +1,204 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "3950e8c7f3123a41f0960bc30b4f07f4", + "entities": [ + { + "tableName": "checkin", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `traceLocationIdBase64` TEXT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `traceLocationStart` TEXT, `traceLocationEnd` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL, `checkInStart` TEXT NOT NULL, `checkInEnd` TEXT NOT NULL, `completed` INTEGER NOT NULL, `createJournalEntry` INTEGER NOT NULL, `submitted` INTEGER NOT NULL, `submissionConsent` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "traceLocationIdBase64", + "columnName": "traceLocationIdBase64", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "traceLocationStart", + "columnName": "traceLocationStart", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "traceLocationEnd", + "columnName": "traceLocationEnd", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultCheckInLengthInMinutes", + "columnName": "defaultCheckInLengthInMinutes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cryptographicSeedBase64", + "columnName": "cryptographicSeedBase64", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cnPublicKey", + "columnName": "cnPublicKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checkInStart", + "columnName": "checkInStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "checkInEnd", + "columnName": "checkInEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "completed", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createJournalEntry", + "columnName": "createJournalEntry", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isSubmitted", + "columnName": "submitted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasSubmissionConsent", + "columnName": "submissionConsent", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "traceLocations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `version` INTEGER NOT NULL, `type` INTEGER NOT NULL, `description` TEXT NOT NULL, `address` TEXT NOT NULL, `startDate` TEXT, `endDate` TEXT, `defaultCheckInLengthInMinutes` INTEGER, `cryptographicSeedBase64` TEXT NOT NULL, `cnPublicKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startDate", + "columnName": "startDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "endDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "defaultCheckInLengthInMinutes", + "columnName": "defaultCheckInLengthInMinutes", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "cryptographicSeedBase64", + "columnName": "cryptographicSeedBase64", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "cnPublicKey", + "columnName": "cnPublicKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3950e8c7f3123a41f0960bc30b4f07f4')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..d38ce5b4023491d3073d20a6e27d4fca2c85f483 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt @@ -0,0 +1,133 @@ +package de.rki.coronawarnapp.presencetracing.migration + +import androidx.room.testing.MigrationTestHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import de.rki.coronawarnapp.presencetracing.storage.TraceLocationDatabase +import de.rki.coronawarnapp.presencetracing.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.presencetracing.storage.migration.PresenceTracingDatabaseMigration1To2 +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import okio.ByteString.Companion.encode +import org.joda.time.Instant +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseTestInstrumentation +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class PresenceTracingDatabaseMigrationTest : BaseTestInstrumentation() { + private val DB_NAME = "TraceLocations_test_db" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + TraceLocationDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + @Test + fun migrate1To2() { + val locationIdBase64 = "41da2115-eba2-49bd-bf17-adb3d635ddaf".encode().base64() + val cryptoGraphicSeed = "cryptographicSeed".encode().base64() + val locationStart = Instant.parse("2021-01-01T12:30:00.000Z") + val locationEnd = Instant.parse("2021-01-01T18:30:00.000Z") + val checkInStart = Instant.parse("2021-01-01T14:30:00.000Z") + val checkInEnd = Instant.parse("2021-01-01T16:30:00.000Z") + helper.createDatabase(DB_NAME, 1).apply { + execSQL( + """ + INSERT INTO "checkin" ( + "id", + "traceLocationIdBase64", + "version", + "type", + "description", + "address", + "traceLocationStart", + "traceLocationEnd", + "defaultCheckInLengthInMinutes", + "cryptographicSeedBase64", + "cnPublicKey", + "checkInStart", + "checkInEnd", + "completed", + "createJournalEntry", + "submitted" + ) VALUES ( + '1', + '$locationIdBase64', + '1', + '2', + 'brothers birthday', + 'Malibu', + '$locationStart', + '$locationEnd', + '42', + '$cryptoGraphicSeed', + 'cnPublicKey', + '$checkInStart', + '$checkInEnd', + '0', + '1', + '0' + ); + """.trimIndent() + ) + close() + } + + // Run migration + helper.runMigrationsAndValidate( + DB_NAME, + 2, + true, + PresenceTracingDatabaseMigration1To2 + ) + + val daoDb = TraceLocationDatabase.Factory( + context = ApplicationProvider.getApplicationContext() + ).create(databaseName = DB_NAME) + + val checkin = TraceLocationCheckInEntity( + id = 1L, + traceLocationIdBase64 = locationIdBase64, + version = 1, + type = 2, + description = "brothers birthday", + address = "Malibu", + traceLocationStart = locationStart, + traceLocationEnd = locationEnd, + defaultCheckInLengthInMinutes = 42, + cryptographicSeedBase64 = cryptoGraphicSeed, + cnPublicKey = "cnPublicKey", + checkInStart = checkInStart, + checkInEnd = checkInEnd, + completed = false, + createJournalEntry = true, + isSubmitted = false, + hasSubmissionConsent = false, + ) + runBlocking { daoDb.checkInDao().allEntries().first() }.single() shouldBe checkin + } + + @Test + @Throws(IOException::class) + fun migrateAll() { + helper.createDatabase(DB_NAME, 1).apply { + close() + } + + // Open latest version of the database. Room will validate the schema once all migrations execute. + TraceLocationDatabase.Factory( + context = ApplicationProvider.getApplicationContext() + ).create(databaseName = DB_NAME).apply { + openHelper.writableDatabase + close() + } + } +} diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/storage/CheckInDatabaseData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/storage/CheckInDatabaseData.kt index 1be7e8f1a81516ffdbd331e55ae209a6a4bb71c4..11601d4bfe7ca3dedb917489015d652d076acf3c 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/storage/CheckInDatabaseData.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/storage/CheckInDatabaseData.kt @@ -23,6 +23,7 @@ object CheckInDatabaseData { completed = false, createJournalEntry = true, isSubmitted = false, + hasSubmissionConsent = false, ) val testCheckInWithoutCheckOutTime = TraceLocationCheckInEntity( @@ -41,5 +42,6 @@ object CheckInDatabaseData { completed = false, createJournalEntry = true, isSubmitted = false, + hasSubmissionConsent = false, ) } diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeData.kt index 2a8bc98f65164640d0f1ce0c63292ebb7390d435..d610fb4905df02d3f210878cee1c6ad16b09b225 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeData.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeData.kt @@ -1,21 +1,21 @@ package de.rki.coronawarnapp.ui.main.home +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.FetchingResult +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.NoTest +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.SubmissionDone +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestError +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestInvalid +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestNegative +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestPending +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR.TestPositive import de.rki.coronawarnapp.risk.RiskState -import de.rki.coronawarnapp.submission.ui.homecards.FetchingResult -import de.rki.coronawarnapp.submission.ui.homecards.NoTest -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionDone -import de.rki.coronawarnapp.submission.ui.homecards.TestError -import de.rki.coronawarnapp.submission.ui.homecards.TestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestFetchingCard -import de.rki.coronawarnapp.submission.ui.homecards.TestInvalid -import de.rki.coronawarnapp.submission.ui.homecards.TestInvalidCard -import de.rki.coronawarnapp.submission.ui.homecards.TestNegative -import de.rki.coronawarnapp.submission.ui.homecards.TestNegativeCard -import de.rki.coronawarnapp.submission.ui.homecards.TestPending -import de.rki.coronawarnapp.submission.ui.homecards.TestPendingCard -import de.rki.coronawarnapp.submission.ui.homecards.TestPositive -import de.rki.coronawarnapp.submission.ui.homecards.TestPositiveCard -import de.rki.coronawarnapp.submission.ui.homecards.TestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestUnregisteredCard import de.rki.coronawarnapp.tracing.TracingProgress import de.rki.coronawarnapp.tracing.states.IncreasedRisk @@ -30,7 +30,6 @@ import de.rki.coronawarnapp.tracing.ui.homecards.TracingFailedCard import de.rki.coronawarnapp.tracing.ui.homecards.TracingProgressCard import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import org.joda.time.Instant -import java.util.Date object HomeData { @@ -134,34 +133,33 @@ object HomeData { state = FetchingResult ) - val TEST_POSITIVE_ITEM = TestPositiveCard.Item( + val TEST_POSITIVE_ITEM = PcrTestPositiveCard.Item( state = TestPositive, onClickAction = {} ) - val TEST_NEGATIVE_ITEM = TestNegativeCard.Item( - state = TestNegative, - onClickAction = {} + val TEST_NEGATIVE_ITEM = PcrTestNegativeCard.Item( + state = TestNegative ) - val TEST_INVALID_ITEM = TestInvalidCard.Item( + val TEST_INVALID_ITEM = PcrTestInvalidCard.Item( state = TestInvalid, onDeleteTest = {} ) - val TEST_ERROR_ITEM = TestErrorCard.Item( + val TEST_ERROR_ITEM = PcrTestErrorCard.Item( state = TestError, onDeleteTest = {} ) - val TEST_PENDING_ITEM = TestPendingCard.Item( + val TEST_PENDING_ITEM = PcrTestPendingCard.Item( state = TestPending, onClickAction = {} ) - val TEST_SUBMISSION_DONE_ITEM = TestSubmissionDoneCard.Item( + val TEST_SUBMISSION_DONE_ITEM = PcrTestSubmissionDoneCard.Item( state = SubmissionDone( - testRegisteredOn = Date() + testRegisteredAt = Instant.now() ) ) } diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt index b2c8c041d1d768c817a92586dc4abf3440a38fc8..db6e15d1b787f3a1ff5f73150390ecd0b5c85096 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/main/home/HomeFragmentTest.kt @@ -11,6 +11,7 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.notification.ShareTestResultNotificationService @@ -19,17 +20,16 @@ import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider -import de.rki.coronawarnapp.submission.ui.homecards.TestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestResultItem -import de.rki.coronawarnapp.submission.ui.homecards.TestSubmissionDoneCard import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.states.TracingStateProvider import de.rki.coronawarnapp.tracing.ui.homecards.TracingStateItem import de.rki.coronawarnapp.tracing.ui.statusbar.TracingHeaderState -import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.ui.main.home.items.FAQCard import de.rki.coronawarnapp.ui.main.home.items.HomeItem +import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.ui.statistics.Statistics import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper @@ -62,7 +62,7 @@ class HomeFragmentTest : BaseUITest() { @MockK lateinit var errorResetTool: EncryptionErrorResetTool @MockK lateinit var tracingStatus: GeneralTracingStatus @MockK lateinit var tracingStateProviderFactory: TracingStateProvider.Factory - @MockK lateinit var submissionStateProvider: SubmissionStateProvider + @MockK lateinit var coronaTestRepository: CoronaTestRepository @MockK lateinit var tracingRepository: TracingRepository @MockK lateinit var shareTestResultNotificationService: ShareTestResultNotificationService @MockK lateinit var submissionRepository: SubmissionRepository @@ -270,7 +270,7 @@ class HomeFragmentTest : BaseUITest() { appConfigProvider = appConfigProvider, tracingStatus = tracingStatus, submissionRepository = submissionRepository, - submissionStateProvider = submissionStateProvider, + coronaTestRepository = coronaTestRepository, cwaSettings = cwaSettings, statisticsProvider = statisticsProvider, deadmanNotificationScheduler = deadmanNotificationScheduler, @@ -288,8 +288,8 @@ class HomeFragmentTest : BaseUITest() { MutableLiveData( mutableListOf<HomeItem>().apply { when (submissionTestResultItem) { - is TestSubmissionDoneCard.Item, - is TestPositiveCard.Item -> { + is PcrTestSubmissionDoneCard.Item, + is PcrTestPositiveCard.Item -> { Timber.d("Tracing item is not added, submission:$submissionTestResultItem") } else -> add(tracingStateItem) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragmentTest.kt index 2fc4006c280e2badf61984db0df2613ff33dd788..ac04a0a70d5c7cfea2dc3b466b939b4aeefa3053 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionContactFragmentTest.kt @@ -59,13 +59,6 @@ class SubmissionContactFragmentTest : BaseUITest() { launchFragment2<SubmissionContactFragment>() } - @Test - fun testContactCallClicked() { - launchFragmentInContainer2<SubmissionContactFragment>() - onView(withId(R.id.submission_contact_button_call)) - .perform(click()) - } - @Test fun testContactEnterTanClicked() { val navController = TestNavHostController(ApplicationProvider.getApplicationContext()) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt index 33dc537e4cc00d5866850b2411f9bd2c90c86ad9..20941db024c95eaf479e2c8afa8b113bb6a49377 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt @@ -5,6 +5,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater_Factory_Impl @@ -39,6 +40,7 @@ class SubmissionTestResultAvailableFragmentTest : BaseUITest() { @MockK lateinit var autoSubmission: AutoSubmission @MockK lateinit var appShortcutsHelper: AppShortcutsHelper @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var checkInRepository: CheckInRepository @Rule @JvmField @@ -51,17 +53,17 @@ class SubmissionTestResultAvailableFragmentTest : BaseUITest() { fun setup() { MockKAnnotations.init(this, relaxed = true) - every { submissionRepository.deviceUIStateFlow } returns flowOf() - every { submissionRepository.testResultReceivedDateFlow } returns flowOf() + every { submissionRepository.testForType(any()) } returns flowOf() every { appShortcutsHelper.removeAppShortcut() } just Runs viewModel = spyk( SubmissionTestResultAvailableViewModel( - TestDispatcherProvider(), - tekHistoryUpdaterFactory, - submissionRepository, - autoSubmission, - analyticsKeySubmissionCollector + dispatcherProvider = TestDispatcherProvider(), + tekHistoryUpdaterFactory = tekHistoryUpdaterFactory, + submissionRepository = submissionRepository, + autoSubmission = autoSubmission, + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + checkInRepository = checkInRepository ) ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt index 352a9a57e08923857aa906ed603bbea20aa37b17..5e48790a6dbfd987b0a88beb50c7607a5cf8d63b 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultConsentGivenFragmentTest.kt @@ -12,6 +12,8 @@ import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiT import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository @@ -19,12 +21,12 @@ import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenFragment import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultConsentGivenViewModel -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 +import io.mockk.mockk import io.mockk.spyk +import org.joda.time.Instant import org.junit.After import org.junit.Before import org.junit.Rule @@ -38,7 +40,6 @@ import testhelpers.captureScreenshot import testhelpers.launchFragment2 import testhelpers.launchFragmentInContainer2 import tools.fastlane.screengrab.locale.LocaleTestRule -import java.util.Date @RunWith(AndroidJUnit4::class) class SubmissionTestResultConsentGivenFragmentTest : BaseUITest() { @@ -107,10 +108,10 @@ class SubmissionTestResultConsentGivenFragmentTest : BaseUITest() { fun capture_fragment() { every { viewModel.uiState } returns MutableLiveData( TestResultUIState( - NetworkRequestWrapper.RequestSuccessful( - DeviceUIState.PAIRED_POSITIVE - ), - Date() + coronaTest = mockk<CoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_POSITIVE + every { registeredAt } returns Instant.now() + } ) ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragmentTest.kt index dbc5de25ebf30886fe62f36034c11d4f55ca9d12..4b970b3a999263b3f99687dc97b6d4355939cfa7 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultFragmentTest.kt @@ -8,20 +8,22 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingFragment import de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper 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.spyk import kotlinx.coroutines.flow.flowOf +import org.joda.time.Instant import org.junit.After import org.junit.Before import org.junit.Rule @@ -35,7 +37,6 @@ import testhelpers.captureScreenshot import testhelpers.launchFragment2 import testhelpers.launchFragmentInContainer2 import tools.fastlane.screengrab.locale.LocaleTestRule -import java.util.Date @RunWith(AndroidJUnit4::class) class SubmissionTestResultFragmentTest : BaseUITest() { @@ -55,8 +56,7 @@ class SubmissionTestResultFragmentTest : BaseUITest() { fun setup() { MockKAnnotations.init(this, relaxed = true) - every { submissionRepository.deviceUIStateFlow } returns flowOf() - every { submissionRepository.testResultReceivedDateFlow } returns flowOf() + every { submissionRepository.testForType(any()) } returns flowOf() viewModel = spyk( SubmissionTestResultPendingViewModel( @@ -71,8 +71,11 @@ class SubmissionTestResultFragmentTest : BaseUITest() { every { consentGiven } returns MutableLiveData(true) every { testState } returns MutableLiveData( TestResultUIState( - deviceUiState = NetworkRequestWrapper.RequestSuccessful(data = DeviceUIState.PAIRED_POSITIVE), - testResultReceivedDate = Date() + coronaTest = mockk<CoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_POSITIVE + every { registeredAt } returns Instant.now() + every { isProcessing } returns false + } ) ) } @@ -113,10 +116,11 @@ class SubmissionTestResultFragmentTest : BaseUITest() { fun capture_fragment() { every { viewModel.testState } returns MutableLiveData( TestResultUIState( - NetworkRequestWrapper.RequestSuccessful( - DeviceUIState.PAIRED_NO_RESULT - ), - Date() + coronaTest = mockk<CoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_OR_RAT_PENDING + every { registeredAt } returns Instant.now() + every { isProcessing } returns false + } ) ) captureScreenshot<SubmissionTestResultPendingFragment>() diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt index d18ae41de25400d3c51c441904c6c07098995b03..5eba253d54bb986fbd8c95f225c8c3e7eeb4876e 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNegativeFragmentTest.kt @@ -4,18 +4,20 @@ import androidx.lifecycle.MutableLiveData import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.ui.submission.testresult.negative.SubmissionTestResultNegativeFragment import de.rki.coronawarnapp.ui.submission.testresult.negative.SubmissionTestResultNegativeViewModel -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 +import io.mockk.mockk import io.mockk.spyk import kotlinx.coroutines.flow.flowOf +import org.joda.time.Instant import org.junit.After import org.junit.Before import org.junit.Rule @@ -27,7 +29,6 @@ import testhelpers.SystemUIDemoModeRule import testhelpers.TestDispatcherProvider import testhelpers.captureScreenshot import tools.fastlane.screengrab.locale.LocaleTestRule -import java.util.Date @RunWith(AndroidJUnit4::class) class SubmissionTestResultNegativeFragmentTest : BaseUITest() { @@ -47,8 +48,7 @@ class SubmissionTestResultNegativeFragmentTest : BaseUITest() { fun setup() { MockKAnnotations.init(this, relaxed = true) - every { submissionRepository.deviceUIStateFlow } returns flowOf() - every { submissionRepository.testResultReceivedDateFlow } returns flowOf() + every { submissionRepository.testForType(any()) } returns flowOf() viewModel = spyk( SubmissionTestResultNegativeViewModel( @@ -75,10 +75,10 @@ class SubmissionTestResultNegativeFragmentTest : BaseUITest() { fun capture_fragment() { every { viewModel.testResult } returns MutableLiveData( TestResultUIState( - NetworkRequestWrapper.RequestSuccessful( - DeviceUIState.PAIRED_NEGATIVE - ), - Date() + coronaTest = mockk<CoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_NEGATIVE + every { registeredAt } returns Instant.now() + } ) ) captureScreenshot<SubmissionTestResultNegativeFragment>() diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt index 8c640d2568570c263e634009b2f896b8d8de3fde..55d8144158bfc191c60d729b26af79204b5ea397 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultNoConsentGivenFragmentTest.kt @@ -4,18 +4,20 @@ import androidx.lifecycle.MutableLiveData import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultNoConsentFragment import de.rki.coronawarnapp.ui.submission.testresult.positive.SubmissionTestResultNoConsentViewModel -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 +import io.mockk.mockk import io.mockk.spyk +import org.joda.time.Instant import org.junit.After import org.junit.Before import org.junit.Rule @@ -26,7 +28,6 @@ import testhelpers.Screenshot import testhelpers.SystemUIDemoModeRule import testhelpers.captureScreenshot import tools.fastlane.screengrab.locale.LocaleTestRule -import java.util.Date @RunWith(AndroidJUnit4::class) class SubmissionTestResultNoConsentGivenFragmentTest : BaseUITest() { @@ -72,10 +73,10 @@ class SubmissionTestResultNoConsentGivenFragmentTest : BaseUITest() { fun capture_fragment() { every { viewModel.uiState } returns MutableLiveData( TestResultUIState( - NetworkRequestWrapper.RequestSuccessful( - DeviceUIState.PAIRED_POSITIVE - ), - Date() + coronaTest = mockk<CoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_POSITIVE + every { registeredAt } returns Instant.now() + } ) ) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionYourConsentFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionYourConsentFragmentTest.kt index 3084dc54851102f60d6703230fd87df7a4a768f9..0fd406215f89d46298b82483eec683e8142cdae7 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionYourConsentFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionYourConsentFragmentTest.kt @@ -42,7 +42,7 @@ class SubmissionYourConsentFragmentTest : BaseUITest() { @Before fun setup() { MockKAnnotations.init(this, relaxed = true) - every { submissionRepository.hasGivenConsentToSubmission } returns flowOf() + every { submissionRepository.testForType(any()) } returns flowOf() viewModel = SubmissionYourConsentViewModel(TestDispatcherProvider(), interoperabilityRepository, submissionRepository) setupMockViewModel( diff --git a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt index 92111156ce7451c297cd779ec6333f70f9d2ee0d..0ab2b436cb66ae70971a996d43e7695c6eb9704a 100644 --- a/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt +++ b/Corona-Warn-App/src/device/java/de/rki/coronawarnapp/risk/storage/DefaultRiskLevelStorage.kt @@ -27,7 +27,7 @@ class DefaultRiskLevelStorage @Inject constructor( // Taken from TimeVariables.MAX_STALE_EXPOSURE_RISK_RANGE override val storedResultLimit: Int = 2 * 6 - override suspend fun storeExposureWindows(storedResultId: String, result: EwRiskLevelResult) { + override suspend fun storeExposureWindows(storedResultId: String, resultEw: EwRiskLevelResult) { Timber.d("storeExposureWindows(): NOOP") // NOOP } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt index 051c3aa5ac77ddc14bb2637e38ef480f868b058c..1fa7e54d4fa91340b2b93f0cd7e5cc449a1811a8 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt @@ -5,10 +5,10 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.ui.durationpicker.DurationPicker -import de.rki.coronawarnapp.ui.durationpicker.toContactDiaryFormat import de.rki.coronawarnapp.databinding.FragmentTestContactDiaryBinding import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.ui.durationpicker.DurationPicker +import de.rki.coronawarnapp.ui.durationpicker.toContactDiaryFormat import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy @@ -96,7 +96,7 @@ class ContactDiaryTestFragment : val TAG: String = ContactDiaryTestFragment::class.simpleName!! val MENU_ITEM = TestMenuItem( title = "Contact Diary Test Options", - description = "Contact Diary related test options..", + description = "Contact Diary related test options.", targetId = R.id.test_contact_diary_fragment ) } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..db311e72acf102f8facc9498d1fb09c92ae2a9bb --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt @@ -0,0 +1,97 @@ +package de.rki.coronawarnapp.test.coronatest.ui + +import android.Manifest +import android.annotation.SuppressLint +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import com.google.zxing.BarcodeFormat +import com.journeyapps.barcodescanner.DefaultDecoderFactory +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestCoronatestBinding +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.permission.CameraPermissionHelper +import de.rki.coronawarnapp.util.tryHumanReadableError +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 CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val vm: CoronaTestTestFragmentViewModel by cwaViewModels { viewModelFactory } + + private val binding: FragmentTestCoronatestBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.apply { + qrcodeScanAction.setOnClickListener { + if (!CameraPermissionHelper.hasCameraPermission(requireActivity())) { + requestPermissions(arrayOf(Manifest.permission.CAMERA), 99) + return@setOnClickListener + } + + val stop = { + qrcodeScanPreview.stopDecoding() + qrcodeScanPreview.pause() + qrcodeScanContainer.isGone = true + } + + val start = { + qrcodeScanContainer.isVisible = true + qrcodeScanPreview.resume() + qrcodeScanPreview.decodeSingle { result -> + vm.onQRCodeScanner(result) + stop() + } + } + + if (qrcodeScanContainer.isVisible) { + stop() + } else { + start() + } + } + qrcodeScanPreview.decoderFactory = DefaultDecoderFactory(listOf(BarcodeFormat.QR_CODE)) + qrcodeScanViewfinder.setCameraPreview(binding.qrcodeScanPreview) + } + + vm.pcrtState.observe2(this) { + binding.pcrtData.text = it.getNiceTextForHumans(requireContext()) + } + binding.apply { + pcrtDeleteAction.setOnClickListener { vm.deletePCRT() } + pcrtRefreshAction.setOnClickListener { vm.refreshPCRT() } + } + + vm.ratState.observe2(this) { + binding.ratData.text = it.getNiceTextForHumans(requireContext()) + } + binding.apply { + ratDeleteAction.setOnClickListener { vm.deleteRAT() } + ratRefreshAction.setOnClickListener { vm.refreshRAT() } + } + + vm.errorEvents.observe2(this) { + val error = it.tryHumanReadableError(requireContext()) + Toast.makeText(requireContext(), error.description, Toast.LENGTH_LONG).show() + } + } + + companion object { + val MENU_ITEM = TestMenuItem( + title = "Corona Tests", + description = "PCR / RapidAntigen Test Options", + targetId = R.id.coronaTestTestFragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..3aa8c30aaf1b5428ac2d5b1a10516c78fcbccb77 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.test.coronatest.ui + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class CoronaTestTestFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(CoronaTestTestFragmentViewModel::class) + abstract fun coronaTest( + factory: CoronaTestTestFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..1f5a05014104c4351e03b1e35f2bd50ebfe035a3 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt @@ -0,0 +1,128 @@ +package de.rki.coronawarnapp.test.coronatest.ui + +import android.content.Context +import androidx.lifecycle.asLiveData +import com.journeyapps.barcodescanner.BarcodeResult +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.latestPCRT +import de.rki.coronawarnapp.coronatest.latestRAT +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber + +class CoronaTestTestFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider, + private val coronaTestRepository: CoronaTestRepository, + private val coronaTestQrCodeValidator: CoronaTestQrCodeValidator, +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + val errorEvents = SingleLiveEvent<Throwable>() + val pcrtState = coronaTestRepository.latestPCRT.map { + PCRTState( + coronaTest = it + ) + }.asLiveData(context = dispatcherProvider.Default) + + val ratState = coronaTestRepository.latestRAT.map { + RATState( + coronaTest = it + ) + }.asLiveData(context = dispatcherProvider.Default) + + fun refreshPCRT() = launch { + try { + Timber.d("Refreshing PCR") + coronaTestRepository.refresh(type = CoronaTest.Type.PCR) + } catch (e: Exception) { + Timber.e(e, "Failed to refresh PCR test.") + errorEvents.postValue(e) + } + } + + fun deletePCRT() = launch { + try { + val pcrTest = coronaTestRepository.latestPCRT.first() + if (pcrTest == null) { + Timber.d("No PCR test to delete") + return@launch + } + coronaTestRepository.removeTest(pcrTest.identifier) + } catch (e: Exception) { + Timber.e(e, "Failed to delete PCR test.") + errorEvents.postValue(e) + } + } + + fun refreshRAT() = launch { + try { + Timber.d("Refreshing RAT") + coronaTestRepository.refresh(type = CoronaTest.Type.RAPID_ANTIGEN) + } catch (e: Exception) { + Timber.e(e, "Failed to refresh RAT test.") + errorEvents.postValue(e) + } + } + + fun deleteRAT() = launch { + try { + val raTest = coronaTestRepository.latestRAT.first() + if (raTest == null) { + Timber.d("No RA test to delete") + return@launch + } + coronaTestRepository.removeTest(raTest.identifier) + } catch (e: Exception) { + Timber.e(e, "Failed to delete RA test.") + errorEvents.postValue(e) + } + } + + fun onQRCodeScanner(result: BarcodeResult) = launch { + try { + val qrCode = coronaTestQrCodeValidator.validate(result.text) + coronaTestRepository.registerTest(qrCode) + } catch (e: Exception) { + Timber.e(e, "Failed to decode qrcode.") + errorEvents.postValue(e) + } + } + + data class PCRTState( + val coronaTest: PCRCoronaTest? + ) { + fun getNiceTextForHumans(context: Context): String { + return coronaTest + ?.toString() + ?.replace("PCRCoronaTest(", "") + ?.replace(",", ",\n") + ?.trimEnd { it == ')' } + ?: "No PCR test registered." + } + } + + data class RATState( + val coronaTest: RACoronaTest? + ) { + fun getNiceTextForHumans(context: Context): String { + return coronaTest + ?.toString() + ?.replace("RACoronaTest(", "") + ?.replace(",", ",\n") + ?.trimEnd { it == ')' } + ?: "No rapid antigen test registered." + } + } + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<CoronaTestTestFragmentViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..7983ba7decb18272f7b0f5266c72786a13ec113c --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragment.kt @@ -0,0 +1,58 @@ +package de.rki.coronawarnapp.test.hometestcards.ui + +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityEvent +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DefaultItemAnimator +import androidx.recyclerview.widget.LinearLayoutManager +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.FragmentTestHomeTestCardsLayoutBinding +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.lists.decorations.TopBottomPaddingDecorator +import de.rki.coronawarnapp.util.lists.diffutil.update +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 HomeTestCardsFragment : Fragment(R.layout.fragment_test_home_test_cards_layout), AutoInject { + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: HomeTestCardsFragmentViewModel by cwaViewModels { viewModelFactory } + + val binding: FragmentTestHomeTestCardsLayoutBinding by viewBindingLazy() + + private val homeAdapter = HomeAdapter() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.recyclerView.apply { + layoutManager = LinearLayoutManager(requireContext()) + itemAnimator = DefaultItemAnimator() + addItemDecoration(TopBottomPaddingDecorator(topPadding = R.dimen.spacing_tiny)) + adapter = homeAdapter + } + + viewModel.homeItems.observe2(this) { + homeAdapter.update(it) + } + } + + override fun onResume() { + super.onResume() + binding.container.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) + } + + companion object { + val MENU_ITEM = TestMenuItem( + title = "Home Cards", + description = "View all possible test result cards (PCR and Antigen).", + targetId = R.id.homeTestCardsFragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragmentModule.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..93450056161753d0dc239d17c55653e4083e30dd --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragmentModule.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.test.hometestcards.ui + +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey + +@Module +abstract class HomeTestCardsFragmentModule { + @Binds + @IntoMap + @CWAViewModelKey(HomeTestCardsFragmentViewModel::class) + abstract fun testHomeTestCardsFragment( + factory: HomeTestCardsFragmentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragmentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..8d43b3595cdcbfd7dda641e9bc9d043f7fb9dc70 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/hometestcards/ui/HomeTestCardsFragmentViewModel.kt @@ -0,0 +1,65 @@ +package de.rki.coronawarnapp.test.hometestcards.ui + +import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestReadyCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestSubmissionDoneCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestOutdatedCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestReadyCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestSubmissionDoneCard +import de.rki.coronawarnapp.submission.ui.homecards.TestFetchingCard +import de.rki.coronawarnapp.submission.ui.homecards.TestResultItem +import de.rki.coronawarnapp.submission.ui.homecards.TestUnregisteredCard +import de.rki.coronawarnapp.ui.main.home.items.HomeItem +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.Flow +import kotlinx.coroutines.flow.flowOf +import org.joda.time.Instant + +class HomeTestCardsFragmentViewModel @AssistedInject constructor( + dispatcherProvider: DispatcherProvider +) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + + private val cards: Flow<List<TestResultItem>> = flowOf( + listOf( + TestUnregisteredCard.Item(SubmissionStatePCR.NoTest) {}, + TestFetchingCard.Item(SubmissionStatePCR.FetchingResult), + PcrTestPendingCard.Item(SubmissionStatePCR.TestPending) {}, + PcrTestReadyCard.Item(SubmissionStatePCR.TestResultReady) {}, + PcrTestInvalidCard.Item(SubmissionStatePCR.TestInvalid) {}, + PcrTestErrorCard.Item(SubmissionStatePCR.TestError) {}, + PcrTestNegativeCard.Item(SubmissionStatePCR.TestNegative), + PcrTestPositiveCard.Item(SubmissionStatePCR.TestPositive) {}, + PcrTestSubmissionDoneCard.Item(SubmissionStatePCR.SubmissionDone(Instant.now())), + RapidTestPendingCard.Item(SubmissionStateRAT.TestPending) {}, + RapidTestReadyCard.Item(SubmissionStateRAT.TestResultReady) {}, + RapidTestInvalidCard.Item(SubmissionStateRAT.TestInvalid) {}, + RapidTestOutdatedCard.Item(SubmissionStateRAT.TestInvalid) {}, + RapidTestErrorCard.Item(SubmissionStateRAT.TestError) {}, + RapidTestNegativeCard.Item(SubmissionStateRAT.TestNegative), + RapidTestPositiveCard.Item(SubmissionStateRAT.TestPositive) {}, + RapidTestSubmissionDoneCard.Item(SubmissionStateRAT.SubmissionDone(Instant.now())) + ) + ) + + val homeItems: LiveData<List<HomeItem>> = cards.asLiveData(dispatcherProvider.Default) + + @AssistedFactory + interface Factory : SimpleCWAViewModelFactory<HomeTestCardsFragmentViewModel> +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/keydownload/ui/KeyDownloadTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/keydownload/ui/KeyDownloadTestFragment.kt index 60233a4eaee37dfb957b7aef30ecf52be4186d2d..4ef7851fb2c31975621c3e299f37caf508bab83a 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/keydownload/ui/KeyDownloadTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/keydownload/ui/KeyDownloadTestFragment.kt @@ -72,7 +72,7 @@ class KeyDownloadTestFragment : Fragment(R.layout.fragment_test_keydownload), Au companion object { val MENU_ITEM = TestMenuItem( title = "Key Packages", - description = "View & Control the downloaded key pkgs..", + description = "View & Control the downloaded key pkgs.", targetId = R.id.test_keydownload_fragment ) } 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 b4200bc33c96ba2d8132bc803a3657f3831a69f5..5481b8257082656430d2bc3b06d3bab62401cc68 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 @@ -6,13 +6,15 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.miscinfo.MiscInfoFragment import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragment import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment +import de.rki.coronawarnapp.test.coronatest.ui.CoronaTestTestFragment import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment -import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragment +import de.rki.coronawarnapp.test.hometestcards.ui.HomeTestCardsFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment +import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragment import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment import de.rki.coronawarnapp.test.submission.ui.SubmissionTestFragment import de.rki.coronawarnapp.test.tasks.ui.TestTaskControllerFragment @@ -25,18 +27,20 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { val testMenuData by lazy { listOf( DebugOptionsFragment.MENU_ITEM, + SettingsCrashReportFragment.MENU_ITEM, AppConfigTestFragment.MENU_ITEM, TestRiskLevelCalculationFragment.MENU_ITEM, + MiscInfoFragment.MENU_ITEM, KeyDownloadTestFragment.MENU_ITEM, TestTaskControllerFragment.MENU_ITEM, SubmissionTestFragment.MENU_ITEM, - SettingsCrashReportFragment.MENU_ITEM, - MiscInfoFragment.MENU_ITEM, ContactDiaryTestFragment.MENU_ITEM, PlaygroundFragment.MENU_ITEM, DataDonationTestFragment.MENU_ITEM, DeltaonboardingFragment.MENU_ITEM, PresenceTracingTestFragment.MENU_ITEM, + HomeTestCardsFragment.MENU_ITEM, + CoronaTestTestFragment.MENU_ITEM, ).let { MutableLiveData(it) } } val showTestScreenEvent = SingleLiveEvent<TestMenuItem>() diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt index 0fc0a95a98eceef10b89513943d36696945192cd..df4137e7ea3f37172547d6fce752f39beb97ff1a 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/presencetracing/ui/PresenceTracingTestFragment.kt @@ -10,6 +10,7 @@ import androidx.core.text.color import androidx.core.text.scale import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.FragmentTestPresenceTracingBinding import de.rki.coronawarnapp.presencetracing.checkins.qrcode.TraceLocation @@ -62,6 +63,10 @@ class PresenceTracingTestFragment : Fragment(R.layout.fragment_test_presence_tra viewModel.runPresenceTracingWarningTask() } + binding.openConsent.setOnClickListener { + findNavController().navigate(R.id.checkInsConsentFragment) + } + viewModel.lastOrganiserLocation.observe(viewLifecycleOwner) { binding.lastOrganiserLocationCard.isVisible = it != null it?.let { traceLocation -> 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 index ce982c5e0300a707a6a3a449916433cef0dafdb7..5281b569b4fe3d20f13c0f176603b50d21f8ba83 100644 --- 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 @@ -28,15 +28,6 @@ class SubmissionTestFragment : Fragment(R.layout.fragment_test_submission), Auto 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() } - } - val tekHistoryAdapter = TEKHistoryAdapter() binding.tekHistoryList.apply { adapter = tekHistoryAdapter @@ -85,7 +76,7 @@ class SubmissionTestFragment : Fragment(R.layout.fragment_test_submission), Auto val TAG: String = SubmissionTestFragment::class.simpleName!! val MENU_ITEM = TestMenuItem( title = "Submission Test Options", - description = "Submission related test options..", + description = "Submission related test options.", targetId = R.id.test_submission_fragment ) } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/submission/ui/SubmissionTestFragmentViewModel.kt index 0218b306014d78f70d9cebd057cc2978cfbd7d66..bd02ec31ee3b2315e569d919de86ca3bfba02907 100644 --- 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 @@ -8,7 +8,6 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import com.google.gson.Gson import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -19,12 +18,10 @@ import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import timber.log.Timber -import java.util.UUID class SubmissionTestFragmentViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val tekHistoryStorage: TEKHistoryStorage, - private val submissionSettings: SubmissionSettings, tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory, @BaseGson baseGson: Gson ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -58,7 +55,6 @@ class SubmissionTestFragmentViewModel @AssistedInject constructor( ) val errorEvents = SingleLiveEvent<Throwable>() - val currentTestId = submissionSettings.registrationToken.flow.asLiveData() val shareTEKsEvent = SingleLiveEvent<TEKExport>() @@ -81,18 +77,6 @@ class SubmissionTestFragmentViewModel @AssistedInject constructor( .map { historyItems -> historyItems.sortedBy { it.obtainedAt } } .asLiveData(context = dispatcherProvider.Default) - fun scrambleRegistrationToken() { - submissionSettings.registrationToken.update { - UUID.randomUUID().toString() - } - } - - fun deleteRegistrationToken() { - submissionSettings.registrationToken.update { - null - } - } - fun updateStorage() { tekHistoryUpdater.updateTEKHistoryOrRequestPermission() } 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 1ed880647cd7385d13cc4b49a23f18850d05d1bd..65aa2e6524a9f0a56a2ae9c9dff8d3e590c2e4fe 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 @@ -8,20 +8,24 @@ import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragment import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragmentModule import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragmentModule +import de.rki.coronawarnapp.test.coronatest.ui.CoronaTestTestFragment +import de.rki.coronawarnapp.test.coronatest.ui.CoronaTestTestFragmentModule import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragmentModule import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragment import de.rki.coronawarnapp.test.debugoptions.ui.DebugOptionsFragmentModule import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaOnboardingFragmentModule import de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment -import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragment -import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragmentModule +import de.rki.coronawarnapp.test.hometestcards.ui.HomeTestCardsFragment +import de.rki.coronawarnapp.test.hometestcards.ui.HomeTestCardsFragmentModule import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragment import de.rki.coronawarnapp.test.keydownload.ui.KeyDownloadTestFragmentModule import de.rki.coronawarnapp.test.menu.ui.TestMenuFragment import de.rki.coronawarnapp.test.menu.ui.TestMenuFragmentModule import de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment import de.rki.coronawarnapp.test.playground.ui.PlaygroundModule +import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragment +import de.rki.coronawarnapp.test.presencetracing.ui.PresenceTracingTestFragmentModule import de.rki.coronawarnapp.test.presencetracing.ui.poster.QrCodePosterTestFragment import de.rki.coronawarnapp.test.presencetracing.ui.poster.QrCodePosterTestFragmentModule import de.rki.coronawarnapp.test.risklevel.ui.TestRiskLevelCalculationFragment @@ -75,4 +79,10 @@ abstract class MainActivityTestModule { @ContributesAndroidInjector(modules = [QrCodePosterTestFragmentModule::class]) abstract fun qrCodePosterTestFragment(): QrCodePosterTestFragment + + @ContributesAndroidInjector(modules = [HomeTestCardsFragmentModule::class]) + abstract fun homeTestCards(): HomeTestCardsFragment + + @ContributesAndroidInjector(modules = [CoronaTestTestFragmentModule::class]) + abstract fun coronaTest(): CoronaTestTestFragment } diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml new file mode 100644 index 0000000000000000000000000000000000000000..3d0c992179a6ae0aea58323db3146fab379c57b5 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml @@ -0,0 +1,172 @@ +<?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" + android:fillViewport="true" + tools:ignore="HardcodedText"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny" + android:orientation="vertical"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/qrcode_container" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny"> + + <TextView + android:id="@+id/qrcode_title" + style="@style/headline6Sixteen" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:text="QRCodes (ɔ◔‿◔)É” ♥" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/qrcode_scan_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Scan using CWA" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/qrcode_title" /> + + <FrameLayout + android:id="@+id/qrcode_scan_container" + android:layout_width="match_parent" + android:layout_height="@dimen/scan_qr_code_viewfinder_size" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/qrcode_scan_action" + tools:visibility="visible"> + + <com.journeyapps.barcodescanner.BarcodeView + android:id="@+id/qrcode_scan_preview" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:zxing_framing_rect_height="@dimen/scan_qr_code_viewfinder_size" + app:zxing_framing_rect_width="@dimen/scan_qr_code_viewfinder_size" /> + + <com.journeyapps.barcodescanner.ViewfinderView + android:id="@+id/qrcode_scan_viewfinder" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:zxing_viewfinder_laser_visibility="false" + app:zxing_viewfinder_mask="@color/colorQrCodeScanMask" /> + + </FrameLayout> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/pcrt_container" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny"> + + <TextView + android:id="@+id/pcrt_title" + style="@style/headline6Sixteen" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:text="Polymerase chain reaction test" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/pcrt_delete_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Delete" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/pcrt_title" /> + + <Button + android:id="@+id/pcrt_refresh_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Refresh" + app:layout_constraintStart_toEndOf="@id/pcrt_delete_action" + app:layout_constraintTop_toBottomOf="@id/pcrt_title" /> + + <TextView + android:id="@+id/pcrt_data" + style="@style/TextAppearance.MaterialComponents.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="No data." + app:layout_constraintTop_toBottomOf="@id/pcrt_delete_action" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/rat_container" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny"> + + <TextView + android:id="@+id/rat_title" + style="@style/headline6Sixteen" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:text="Rapid antigen test" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/rat_delete_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Delete" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/rat_title" /> + + <Button + android:id="@+id/rat_refresh_action" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="Refresh" + app:layout_constraintStart_toEndOf="@id/rat_delete_action" + app:layout_constraintTop_toBottomOf="@id/rat_title" /> + + <TextView + android:id="@+id/rat_data" + style="@style/TextAppearance.MaterialComponents.Caption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:text="No data." + app:layout_constraintTop_toBottomOf="@id/rat_delete_action" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </LinearLayout> +</androidx.core.widget.NestedScrollView> diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml index dba2acc1b028e69527c54e05b6eb93271f194782..19c37f7ec85bd30bdafebae56ee1b811c71ad37c 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml @@ -16,8 +16,8 @@ style="@style/Card" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="10dp" android:layout_marginHorizontal="@dimen/spacing_tiny" + android:layout_marginTop="10dp" android:orientation="vertical"> <TextView @@ -168,5 +168,14 @@ tools:text="ID" /> </LinearLayout> + + <com.google.android.material.button.MaterialButton + android:id="@+id/open_consent" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginTop="16dp" + android:text="Consent" /> + </LinearLayout> </androidx.core.widget.NestedScrollView> 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 index 2bcf0b827e69d49351dce960a513fc6e08de3661..df2c61f6063b2746edb906e2ad0b0405535402f9 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_submission.xml @@ -14,57 +14,6 @@ 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> - - <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/tek_history" style="@style/Card" 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 a8cdc52cfa9795a8acb3cced1acab554f81ed1d9..fcb6ad2366b5adc63ef6954a1d6f37083b4163c6 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 @@ -49,6 +49,12 @@ <action android:id="@+id/action_test_menu_fragment_to_presenceTracingTestFragment" app:destination="@id/presenceTracingTestFragment" /> + <action + android:id="@+id/action_test_menu_fragment_to_homeTestCardsFragment" + app:destination="@id/homeTestCardsFragment" /> + <action + android:id="@+id/action_test_menu_fragment_to_coronaTestTestFragment" + app:destination="@id/coronaTestTestFragment" /> </fragment> <fragment @@ -146,4 +152,14 @@ android:name="traceLocationId" app:argType="long" /> </fragment> + <fragment + android:id="@+id/homeTestCardsFragment" + android:name="de.rki.coronawarnapp.test.hometestcards.ui.HomeTestCardsFragment" + android:label="HomeTestCards" + tools:layout="@layout/fragment_test_home_test_cards_layout" /> + <fragment + android:id="@+id/coronaTestTestFragment" + android:name="de.rki.coronawarnapp.test.coronatest.ui.CoronaTestTestFragment" + tools:layout="@layout/fragment_test_coronatest" + android:label="CoronaTestTestFragment" /> </navigation> 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 0e1a49d5cee914765e1b923d7504324298e03b09..d10c520260fb53b3cc45d2457c95d168d37a8329 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 @@ -15,6 +15,7 @@ import de.rki.coronawarnapp.appconfig.ConfigChangeDetector import de.rki.coronawarnapp.appconfig.devicetime.DeviceTimeHandler import de.rki.coronawarnapp.bugreporting.loghistory.LogHistoryTree import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpScheduler @@ -24,7 +25,6 @@ import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut import de.rki.coronawarnapp.risk.RiskLevelChangeDetector import de.rki.coronawarnapp.storage.OnboardingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.CWADebug @@ -34,8 +34,10 @@ import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import org.conscrypt.Conscrypt import timber.log.Timber import java.security.Security @@ -61,7 +63,7 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { @Inject lateinit var notificationHelper: GeneralNotifications @Inject lateinit var deviceTimeHandler: DeviceTimeHandler @Inject lateinit var autoSubmission: AutoSubmission - @Inject lateinit var submissionSettings: SubmissionSettings + @Inject lateinit var coronaTestRepository: CoronaTestRepository @Inject lateinit var onboardingSettings: OnboardingSettings @Inject lateinit var autoCheckOut: AutoCheckOut @Inject lateinit var traceLocationDbCleanupScheduler: TraceLocationDbCleanUpScheduler @@ -100,9 +102,14 @@ class CoronaWarnApplication : Application(), HasAndroidInjector { .launchIn(GlobalScope) if (onboardingSettings.isOnboarded) { - if (!submissionSettings.isAllowedToSubmitKeys) { - deadmanNotificationScheduler.schedulePeriodic() + // TODO this is on the main thread, not very nice... + runBlocking { + val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } + if (!isAllowedToSubmitKeys) { + deadmanNotificationScheduler.schedulePeriodic() + } } + contactDiaryWorkScheduler.schedulePeriodic() } 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 index 9ba3aef2945746ad3eb62b44777b806aff810454..4e51b2cbb71c755166aa94419ec1e3579947c582 100644 --- 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 @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.appconfig import androidx.annotation.VisibleForTesting +import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningTask import de.rki.coronawarnapp.risk.RiskLevelSettings import de.rki.coronawarnapp.risk.RiskLevelTask import de.rki.coronawarnapp.risk.storage.RiskLevelStorage @@ -46,8 +47,16 @@ class ConfigChangeDetector @Inject constructor( val oldConfigId = riskLevelSettings.lastUsedConfigIdentifier if (newIdentifier != oldConfigId) { Timber.tag(TAG).i("New config id ($newIdentifier) differs from last one ($oldConfigId), resetting.") - riskLevelStorage.clear() - taskController.submit(DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector")) + riskLevelStorage.clearResults() + taskController.submit( + DefaultTaskRequest(RiskLevelTask::class, originTag = "ConfigChangeDetector") + ) + taskController.submit( + DefaultTaskRequest( + PresenceTracingWarningTask::class, + originTag = "ConfigChangeDetector" + ) + ) } else { Timber.tag(TAG).v("Config identifier ($oldConfigId) didn't change, NOOP.") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt index 81f2bece36227c378fb5c212b3a27171c2598a64..b3c47171c9024dcd36ff61c6ff86d1d5417bd0ec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensor.kt @@ -3,22 +3,33 @@ package de.rki.coronawarnapp.bugreporting.censors import dagger.Reusable import de.rki.coronawarnapp.bugreporting.censors.BugCensor.Companion.toNewLogLineIfDifferent import de.rki.coronawarnapp.bugreporting.debuglog.LogLine -import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.util.CWADebug +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import javax.inject.Inject @Reusable class RegistrationTokenCensor @Inject constructor( - private val submissionSettings: SubmissionSettings + private val coronaTestRepository: CoronaTestRepository, ) : BugCensor { override suspend fun checkLog(entry: LogLine): LogLine? { - val token = submissionSettings.registrationToken.value ?: return null - if (!entry.message.contains(token)) return null + val tokens = coronaTestRepository.coronaTests.map { tests -> + tests.map { it.registrationToken } + }.first() - val newMessage = if (CWADebug.isDeviceForTestersBuild) { - entry.message.replace(token, PLACEHOLDER_TESTER + token.takeLast(27)) - } else { - entry.message.replace(token, PLACEHOLDER + token.takeLast(4)) + if (tokens.isEmpty()) return null + + var newMessage = entry.message + + for (token in tokens) { + if (!entry.message.contains(token)) continue + + newMessage = if (CWADebug.isDeviceForTestersBuild) { + newMessage.replace(token, PLACEHOLDER_TESTER + token.takeLast(27)) + } else { + newMessage.replace(token, PLACEHOLDER + token.takeLast(4)) + } } return entry.toNewLogLineIfDifferent(newMessage) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt index 33987d166b62141a4f7cd36c646b2fedafc8375e..554211b35ee9bf856825979deef234f7b3bf62dc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt @@ -17,6 +17,7 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @@ -43,45 +44,50 @@ class ContactDiaryLocationListViewModel @AssistedInject constructor( private val dayElement = contactDiaryRepository.locationVisitsForDate(localDate) private val selectableLocations = contactDiaryRepository.locations - val uiList = selectableLocations.combine(dayElement) { locations, encounters -> - locations.map { location -> - val visit = encounters.singleOrNull { - it.contactDiaryLocation.locationId == location.locationId - } - DiaryLocationListItem( - item = location, - visit = visit, - onItemClick = { onLocationSelectionChanged(it as DiaryLocationListItem) }, - onDurationChanged = { item, duration -> - onDurationChanged(item, duration) - }, - onCircumstancesChanged = { item, circumstances -> - onCircumstancesChanged(item, circumstances) - }, - onCircumStanceInfoClicked = { - openCommentInfo.postValue(Unit) - }, - onDurationDialog = { item, durationString -> - onDurationDialog(item, durationString) + private val diaryLocationListItems: Flow<List<DiaryLocationListItem>> = + selectableLocations.combine(dayElement) { locations, encounters -> + locations.map { location -> + val visit = encounters.singleOrNull { + it.contactDiaryLocation.locationId == location.locationId } - ) - } - }.asLiveData() - - private fun onLocationSelectionChanged(item: DiaryLocationListItem) = launchOnAppScope { - if (!item.selected) { - contactDiaryRepository.addLocationVisit( - DefaultContactDiaryLocationVisit( - date = localDate, - contactDiaryLocation = item.item + DiaryLocationListItem( + item = location, + visit = visit, + onItemClick = { onLocationSelectionChanged(it.stableId) }, + onDurationChanged = { item, duration -> + onDurationChanged(item, duration) + }, + onCircumstancesChanged = { item, circumstances -> + onCircumstancesChanged(item, circumstances) + }, + onCircumStanceInfoClicked = { + openCommentInfo.postValue(Unit) + }, + onDurationDialog = { item, durationString -> + onDurationDialog(item, durationString) + } ) - ) - } else { - val visit = dayElement - .first() - .find { it.contactDiaryLocation.locationId == item.item.locationId } - visit?.let { contactDiaryRepository.deleteLocationVisit(it) } + } } + + val uiList = diaryLocationListItems.asLiveData(dispatcherProvider.Default) + + private fun onLocationSelectionChanged(itemId: Long) = launchOnAppScope { + diaryLocationListItems.first().find { it.stableId == itemId }?.let { item -> + if (!item.selected) { + contactDiaryRepository.addLocationVisit( + DefaultContactDiaryLocationVisit( + date = localDate, + contactDiaryLocation = item.item + ) + ) + } else { + val visit = dayElement + .first() + .find { it.contactDiaryLocation.locationId == item.item.locationId } + visit?.let { contactDiaryRepository.deleteLocationVisit(it) } + } + } ?: run { Timber.d("No item found for id $itemId") } } private fun onDurationDialog( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt index e9c93ba31002468d8431ec261184f6b5845d9d64..01cc23cd02002644647a976484e5c4e289dd6f1c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt @@ -9,7 +9,6 @@ import de.rki.coronawarnapp.databinding.ContactDiaryLocationListItemBinding import de.rki.coronawarnapp.ui.durationpicker.toContactDiaryFormat import de.rki.coronawarnapp.ui.lists.BaseAdapter import de.rki.coronawarnapp.util.lists.BindableVH -import de.rki.coronawarnapp.util.ui.setOnClickListenerThrottled class DiaryLocationViewHolder( parent: ViewGroup @@ -25,7 +24,7 @@ class DiaryLocationViewHolder( val item = changes.firstOrNull() as? DiaryLocationListItem ?: initial mainBox.apply { - header.setOnClickListenerThrottled { + header.setOnClickListener { it.contentDescription = item.onClickDescription.get(context) it.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) item.onItemClick(item) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt index e2c86625dd5934658828fc1238e1f0068bd85222..e9124ed2aa41daa571ffa2eb8da94dc2e5e47eb4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt @@ -20,6 +20,7 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.joda.time.LocalDate @@ -42,7 +43,7 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor( private val dayEncounters = contactDiaryRepository.personEncountersForDate(localDate) private val selectablePersons = contactDiaryRepository.people - val uiList: LiveData<List<DiaryPersonListItem>> = combine( + private val diaryPersonListItems: Flow<List<DiaryPersonListItem>> = combine( selectablePersons, dayEncounters ) { persons, encounters -> @@ -53,7 +54,7 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor( DiaryPersonListItem( item = person, personEncounter = encounter, - onItemClick = { onPersonSelectionChanged(it as DiaryPersonListItem) }, + onItemClick = { onPersonSelectionChanged(it.stableId) }, onDurationChanged = { item, duration -> onDurationChanged(item, duration) }, @@ -71,23 +72,28 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor( } ) } - }.asLiveData(context = dispatcherProvider.Default) + } + + val uiList: LiveData<List<DiaryPersonListItem>> = diaryPersonListItems + .asLiveData(context = dispatcherProvider.Default) private fun onPersonSelectionChanged( - item: DiaryPersonListItem + itemId: Long ) = launchOnAppScope { - if (!item.selected) { - contactDiaryRepository.addPersonEncounter( - DefaultContactDiaryPersonEncounter( - date = localDate, - contactDiaryPerson = item.item + diaryPersonListItems.first().find { it.stableId == itemId }?.let { item -> + if (!item.selected) { + contactDiaryRepository.addPersonEncounter( + DefaultContactDiaryPersonEncounter( + date = localDate, + contactDiaryPerson = item.item + ) ) - ) - } else { - val visit = dayEncounters.first() - .find { it.contactDiaryPerson.personId == item.item.personId } - visit?.let { contactDiaryRepository.deletePersonEncounter(it) } - } + } else { + val visit = dayEncounters.first() + .find { it.contactDiaryPerson.personId == item.item.personId } + visit?.let { contactDiaryRepository.deletePersonEncounter(it) } + } + } ?: run { Timber.d("No item found for id $itemId") } } private fun onDurationChanged( diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt index 1e6fe5c83e73ccd8113c9246185f749f1e0b0ebb..d7d4c9fba3835f5c3fed9b5af01ce529cf091001 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt @@ -10,7 +10,6 @@ import de.rki.coronawarnapp.contactdiary.util.setClickLabel import de.rki.coronawarnapp.databinding.ContactDiaryPersonListItemBinding import de.rki.coronawarnapp.ui.lists.BaseAdapter import de.rki.coronawarnapp.util.lists.BindableVH -import de.rki.coronawarnapp.util.ui.setOnClickListenerThrottled class DiaryPersonViewHolder( parent: ViewGroup @@ -26,7 +25,7 @@ class DiaryPersonViewHolder( val item = changes.firstOrNull() as? DiaryPersonListItem ?: initial mainBox.apply { - header.setOnClickListenerThrottled { + header.setOnClickListener { hideKeyboard() it.contentDescription = item.onClickDescription.get(context) it.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt index b5d208955534aa06fac622a0eea94ddc05a8ead4..74dff70083d075cca4827a6ef246a94810b66db0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt @@ -28,24 +28,26 @@ import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.task.common.DefaultTaskRequest -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent 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 kotlinx.coroutines.flow.flowOf +import org.joda.time.Days import org.joda.time.LocalDate import timber.log.Timber +import kotlin.concurrent.fixedRateTimer class ContactDiaryOverviewViewModel @AssistedInject constructor( taskController: TaskController, dispatcherProvider: DispatcherProvider, contactDiaryRepository: ContactDiaryRepository, riskLevelStorage: RiskLevelStorage, - timeStamper: TimeStamper, + private val timeStamper: TimeStamper, checkInRepository: CheckInRepository, private val exporter: ContactDiaryExporter ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -53,7 +55,18 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( val routeToScreen: SingleLiveEvent<ContactDiaryOverviewNavigationEvents> = SingleLiveEvent() val exportLocationsAndPersons: SingleLiveEvent<String> = SingleLiveEvent() - private val dates = (0 until DAY_COUNT).map { timeStamper.nowUTC.toLocalDateUtc().minusDays(it) } + private fun TimeStamper.localDate(): LocalDate = nowUTC.toUserTimeZone().toLocalDate() + + private fun dates() = (0 until DAY_COUNT).map { timeStamper.localDate().minusDays(it) } + private val datesFlow = MutableStateFlow(dates()) + + private val reloadDatesMidnightTimer = fixedRateTimer( + name = "Reload-contact-journal-dates-timer-thread", + daemon = true, + startAt = timeStamper.localDate().plusDays(1).toDate(), + period = Days.ONE.toStandardDuration().millis, + action = { datesFlow.value = dates() } + ) private val locationVisitsFlow = contactDiaryRepository.locationVisits private val personEncountersFlow = contactDiaryRepository.personEncounters @@ -63,7 +76,7 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( private val checkInsWithinRetentionFlow = checkInRepository.checkInsWithinRetention val listItems = combine( - flowOf(dates), + datesFlow, locationVisitsFlow, personEncountersFlow, riskLevelPerDateFlow, @@ -303,6 +316,11 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( } } + override fun onCleared() { + super.onCleared() + reloadDatesMidnightTimer.cancel() + } + @AssistedFactory interface Factory : SimpleCWAViewModelFactory<ContactDiaryOverviewViewModel> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestModule.kt index 1a6c81a94c34a9caf1ce566111e10c30f5c6098b..3fc92f6ac584cc36b0ca7079a53d52afeeba7254 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestModule.kt @@ -1,6 +1,27 @@ package de.rki.coronawarnapp.coronatest +import dagger.Binds import dagger.Module +import dagger.multibindings.IntoSet +import de.rki.coronawarnapp.coronatest.server.VerificationModule +import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor +import de.rki.coronawarnapp.coronatest.type.pcr.PCRProcessor +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RapidAntigenProcessor -@Module -class CoronaTestModule +@Module( + includes = [VerificationModule::class] +) +abstract class CoronaTestModule { + + @Binds + @IntoSet + abstract fun pcrProcessor( + processor: PCRProcessor + ): CoronaTestProcessor + + @Binds + @IntoSet + abstract fun ratProcessor( + processor: RapidAntigenProcessor + ): CoronaTestProcessor +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt index 832198cf3a59fa31bbda8f0bb55a31fc896f3a6c..2921477f4d0bbddf6ae3ea9a7041d0df92055cd8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt @@ -1,50 +1,253 @@ package de.rki.coronawarnapp.coronatest +import androidx.annotation.VisibleForTesting +import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.coronatest.migration.PCRTestMigration import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode -import de.rki.coronawarnapp.coronatest.server.CoronaTestServer import de.rki.coronawarnapp.coronatest.storage.CoronaTestStorage +import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor +import de.rki.coronawarnapp.coronatest.type.TestIdentifier +import de.rki.coronawarnapp.util.coroutine.AppScope +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.flow.HotDataFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.plus +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton class CoronaTestRepository @Inject constructor( + @AppScope private val appScope: CoroutineScope, + private val dispatcherProvider: DispatcherProvider, private val storage: CoronaTestStorage, - private val server: CoronaTestServer, + private val processors: Set<@JvmSuppressWildcards CoronaTestProcessor>, + private val legacyMigration: PCRTestMigration, ) { - val coronaTests: Flow<Set<CoronaTest>> = emptyFlow() + private val internalData: HotDataFlow<Map<CoronaTestGUID, CoronaTest>> = HotDataFlow( + loggingTag = TAG, + scope = appScope + dispatcherProvider.IO, + sharingBehavior = SharingStarted.Eagerly, + ) { + val legacyTests = legacyMigration.startMigration() + val persistedTests = storage.coronaTests + (legacyTests + persistedTests).map { it.identifier to it }.toMap().also { + Timber.tag(TAG).v("Restored CoronaTest data: %s", it) + } + } - /** - * When this returns and there was no exception, the test was registered and a valid registrationToken obtained. - * Your new test should be available via **coronaTests**. - */ - suspend fun registerTest(request: CoronaTestQRCode) { - Timber.tag(TAG).i("registerTest(request=%s)", request) + val coronaTests: Flow<Set<CoronaTest>> = internalData.data.map { it.values.toSet() } + + init { + internalData.data + .onStart { Timber.tag(TAG).d("Observing test data.") } + .onEach { + Timber.tag(TAG).v("CoronaTest data changed: %s", it) + storage.coronaTests = it.values.toSet() + legacyMigration.finishMigration() + } + .catch { + it.reportProblem(TAG, "Failed to snapshot CoronaTest data to storage.") + throw it + } + .launchIn(appScope + dispatcherProvider.IO) } - suspend fun removeTest(guid: CoronaTestGUID): CoronaTest { - Timber.tag(TAG).i("removeTest(guid=%s)", guid) + private fun getProcessor(type: CoronaTest.Type) = processors.single { it.type == type } - throw NotImplementedError() + suspend fun registerTest(registrationRequest: TestRegistrationRequest): CoronaTest = when (registrationRequest) { + is CoronaTestQRCode -> registerTestByQRCode(registrationRequest) + is CoronaTestTAN -> registerTestByTAN(registrationRequest) + else -> throw IllegalArgumentException("Unknown test request: $registrationRequest") } - suspend fun markAsSubmitted(guid: CoronaTestGUID) { - Timber.tag(TAG).i("markAsSubmitted(guid=%s)", guid) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun registerTestByTAN(request: CoronaTestTAN): CoronaTest { + Timber.tag(TAG).i("registerTestByQRCode(request=%s)", request) + // We check early, if there is no processor, crash early, "should" never happen though... + val processor = getProcessor(request.type) + + val currentTests = internalData.updateBlocking { + if (values.any { it.type == request.type }) { + throw IllegalStateException("There is already a test of this type: ${request.type}.") + } + + val test = processor.create(request) + Timber.tag(TAG).i("Adding new test: %s", test) + + toMutableMap().apply { this[test.identifier] = test } + } + + return currentTests[request.identifier]!! + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal suspend fun registerTestByQRCode(request: CoronaTestQRCode): CoronaTest { + Timber.tag(TAG).i("registerTestByQRCode(request=%s)", request) + // We check early, if there is no processor, crash early, "should" never happen though... + val processor = getProcessor(request.type) + + val currentTests = internalData.updateBlocking { + if (values.any { it.type == request.type }) { + throw IllegalStateException("There is already a test of this type: ${request.type}.") + } + + val test = processor.create(request) + Timber.tag(TAG).i("Adding new test: %s", test) + + toMutableMap().apply { this[test.identifier] = test } + } + + return currentTests[request.identifier]!! + } + + suspend fun removeTest(identifier: TestIdentifier): CoronaTest { + Timber.tag(TAG).i("removeTest(identifier=%s)", identifier) + + var removedTest: CoronaTest? = null + + internalData.updateBlocking { + val toBeRemoved = values.singleOrNull { it.identifier == identifier } + ?: throw IllegalArgumentException("No found for $identifier") + + getProcessor(toBeRemoved.type).onRemove(toBeRemoved) + + toMutableMap().apply { + removedTest = remove(toBeRemoved.identifier) + Timber.tag(TAG).d("Removed: %s", removedTest) + } + } + + return removedTest!! } /** * Passing **null** will refresh all test types. */ - fun refresh(type: CoronaTest.Type? = null) { + suspend fun refresh(type: CoronaTest.Type? = null): Set<CoronaTest> { Timber.tag(TAG).d("refresh(type=%s)", type) + + val toRefresh = internalData.data + .first().values + .filter { if (type == null) true else it.type == type } + .map { it.identifier } + + Timber.tag(TAG).d("Will refresh %s", toRefresh) + + toRefresh.forEach { + modifyTest(it) { processor, test -> + processor.markProcessing(test, true) + } + } + + val refreshedData = internalData.updateBlocking { + val polling = values + .filter { if (type == null) true else it.type == type } + .filter { toRefresh.contains(it.identifier) } + .map { coronaTest -> + + withContext(context = dispatcherProvider.IO) { + async { + Timber.tag(TAG).v("Polling for %s", coronaTest) + // This will not throw an exception + // Any error encountered during polling will be in CoronaTest.lastError + getProcessor(coronaTest.type).pollServer(coronaTest) + } + } + } + + Timber.tag(TAG).d("Waiting for test status polling: %s", polling) + val pollingResults = polling.awaitAll() + + this.toMutableMap().apply { + for (updatedResult in pollingResults) { + this[updatedResult.identifier] = updatedResult + } + } + } + + toRefresh.forEach { + modifyTest(it) { processor, test -> + processor.markProcessing(test, false) + } + } + + return refreshedData.values.toSet() + } + + suspend fun clear() { + Timber.tag(TAG).i("clear()") + internalData.updateBlocking { + Timber.tag(TAG).d("Clearing %s", this) + emptyMap() + } + } + + suspend fun markAsSubmitted(identifier: TestIdentifier) { + Timber.tag(TAG).i("markAsSubmitted(identifier=%s)", identifier) + + modifyTest(identifier) { processor, before -> + processor.markSubmitted(before) + } + } + + suspend fun markAsViewed(identifier: TestIdentifier) { + Timber.tag(TAG).i("markAsViewed(identifier=%s)", identifier) + + modifyTest(identifier) { processor, before -> + processor.markViewed(before) + } + } + + suspend fun updateConsent(identifier: TestIdentifier, consented: Boolean) { + Timber.tag(TAG).i("updateConsent(identifier=%s, consented=%b)", identifier, consented) + + modifyTest(identifier) { processor, before -> + processor.updateConsent(before, consented) + } + } + + suspend fun updateResultNotification(identifier: TestIdentifier, sent: Boolean) { + Timber.tag(TAG).i("updateResultNotification(identifier=%s, sent=%b)", identifier, sent) + + modifyTest(identifier) { processor, before -> + processor.updateResultNotification(before, sent) + } + } + + private suspend fun modifyTest( + identifier: TestIdentifier, + update: suspend (CoronaTestProcessor, CoronaTest) -> CoronaTest + ) { + internalData.updateBlocking { + val original = values.singleOrNull { it.identifier == identifier } + ?: throw IllegalArgumentException("No found for $identifier") + + val processor = getProcessor(original.type) + + val updated = update(processor, original) + Timber.tag(TAG).d("Updated %s to %s", original, updated) + + toMutableMap().apply { this[original.identifier] = updated } + } } companion object { - const val TAG = "CoronaTestRepo" + const val TAG = "CoronaTestRepository" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..8237875c2262584de2afbacaaf9c6e735221b9ef --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepositoryExtensions.kt @@ -0,0 +1,26 @@ +package de.rki.coronawarnapp.coronatest + +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map + +val CoronaTestRepository.latestPCRT: Flow<PCRCoronaTest?> + get() = this.coronaTests + .map { allTests -> + allTests.singleOrNull { + it.type == CoronaTest.Type.PCR + } as? PCRCoronaTest + } + .distinctUntilChanged() + +val CoronaTestRepository.latestRAT: Flow<RACoronaTest?> + get() = this.coronaTests + .map { allTests -> + allTests.singleOrNull { + it.type == CoronaTest.Type.RAPID_ANTIGEN + } as? RACoronaTest + } + .distinctUntilChanged() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt new file mode 100644 index 0000000000000000000000000000000000000000..27542f0fd03527fd95ec339e5b4b68d18538efcd --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestRegistrationRequest.kt @@ -0,0 +1,8 @@ +package de.rki.coronawarnapp.coronatest + +import de.rki.coronawarnapp.coronatest.type.CoronaTest + +interface TestRegistrationRequest { + val type: CoronaTest.Type + val identifier: String +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/execution/TestResultScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/execution/TestResultScheduler.kt new file mode 100644 index 0000000000000000000000000000000000000000..a1c7ba36bb4f42702e665b6e0231b356ef35f3a4 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/execution/TestResultScheduler.kt @@ -0,0 +1,85 @@ +package de.rki.coronawarnapp.coronatest.execution + +import androidx.work.BackoffPolicy +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkInfo +import androidx.work.WorkManager +import dagger.Reusable +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.util.TimeStamper +import de.rki.coronawarnapp.util.coroutine.await +import de.rki.coronawarnapp.worker.BackgroundConstants +import de.rki.coronawarnapp.worker.BackgroundWorkHelper +import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Reusable +class TestResultScheduler @Inject constructor( + private val tracingSettings: TracingSettings, + private val workManager: WorkManager, + private val timeStamper: TimeStamper, +) { + + private suspend fun isScheduled(): Boolean { + val workerInfos = workManager.getWorkInfosForUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME).await() + + return workerInfos.any { it.isScheduled } + } + + fun setPeriodicTestPolling(enabled: Boolean) { + if (enabled) { + // TODO Refactor runBlocking away + val isScheduled = runBlocking { isScheduled() } + if (isScheduled) { + Timber.tag(TAG).w("Already scheduled, skipping") + return + } + Timber.tag(TAG).i("Queueing test result worker (DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER)") + workManager.enqueueUniquePeriodicWork( + PCR_TESTRESULT_WORKER_UNIQUEUNAME, + ExistingPeriodicWorkPolicy.REPLACE, + buildDiagnosisTestResultRetrievalPeriodicWork() + ) + } else { + Timber.tag(TAG).d("cancelWorker()") + workManager.cancelUniqueWork(PCR_TESTRESULT_WORKER_UNIQUEUNAME) + } + } + + private fun buildDiagnosisTestResultRetrievalPeriodicWork() = + PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( + BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), + TimeUnit.MINUTES + ) + .addTag(PCR_TESTRESULT_WORKER_TAG) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() + + private val WorkInfo.isScheduled: Boolean + get() = state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED + + companion object { + /** + * Kind initial delay in minutes for periodic work for accessibility reason + * + * @see TimeUnit.SECONDS + */ + private const val DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY = 10L + private const val PCR_TESTRESULT_WORKER_TAG = "DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER" + private const val PCR_TESTRESULT_WORKER_UNIQUEUNAME = "DiagnosisTestResultBackgroundPeriodicWork" + + private const val TAG = "TestResultScheduler" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt new file mode 100644 index 0000000000000000000000000000000000000000..851c81cd75815948ad67746b31df4bc4e98d6d71 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/migration/PCRTestMigration.kt @@ -0,0 +1,100 @@ +package de.rki.coronawarnapp.coronatest.migration + +import dagger.Reusable +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest +import de.rki.coronawarnapp.storage.TracingSettings +import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.util.CWADebug +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class PCRTestMigration @Inject constructor( + private val submissionSettings: SubmissionSettings, + private val tracingSettings: TracingSettings, +) { + + private val mutex = Mutex() + private var isMigrating: Boolean = false + + @Suppress("DEPRECATION") + suspend fun startMigration(): Set<CoronaTest> = mutex.withLock { + if (isMigrating) throw IllegalStateException("Migration already in progress") + isMigrating = true + Timber.tag(TAG).i("startMigration()") + + val token: RegistrationToken? = submissionSettings.registrationTokenMigration + if (token == null) { + Timber.tag(TAG).d("Nothing to migrate, token was null.") + return emptySet() + } else { + Timber.tag(TAG).i("Migrating token %s", token) + } + + val devicePairingSuccessfulAt = submissionSettings.devicePairingSuccessfulAtMigration + Timber.tag(TAG).v("devicePairingSuccessfulAt=%s", devicePairingSuccessfulAt) + if (devicePairingSuccessfulAt == null) { + if (CWADebug.isDeviceForTestersBuild) { + throw IllegalStateException("Can't have registration token without device pairing timestamp !?") + } + return emptySet() + } + + val isAllowedToSubmitKeys = submissionSettings.isAllowedToSubmitKeysMigration + Timber.tag(TAG).v("isAllowedToSubmitKeys=%s", isAllowedToSubmitKeys) + + val isSubmissionSuccessful = submissionSettings.isSubmissionSuccessfulMigration + Timber.tag(TAG).v("isSubmissionSuccessful=%s", isSubmissionSuccessful) + + val hasViewedTestResult = submissionSettings.hasViewedTestResultMigration + Timber.tag(TAG).v("hasViewedTestResult=%s", hasViewedTestResult) + + val hasGivenConsent = submissionSettings.hasGivenConsentMigration + Timber.tag(TAG).v("hasGivenConsent=%s", hasGivenConsent) + + // TODO per test ? + val testResultNotificationSent = tracingSettings.isTestResultAvailableNotificationSentMigration + Timber.tag(TAG).v("testResultNotificationSent=%s", testResultNotificationSent) + + val legacyPCRTest = PCRCoronaTest( + identifier = LEGACY_IDENTIFIER, + registrationToken = token, + registeredAt = devicePairingSuccessfulAt, + testResult = when (isAllowedToSubmitKeys) { + true -> CoronaTestResult.PCR_POSITIVE + else -> CoronaTestResult.PCR_OR_RAT_PENDING + }, + testResultReceivedAt = devicePairingSuccessfulAt, + isSubmitted = isSubmissionSuccessful, + isViewed = hasViewedTestResult, + isAdvancedConsentGiven = hasGivenConsent, + isResultAvailableNotificationSent = testResultNotificationSent, + ) + return setOf(legacyPCRTest).also { + Timber.tag(TAG).d("Offering converted legacy CoronaTest: %s", it) + } + } + + suspend fun finishMigration() = mutex.withLock { + if (!isMigrating) return@withLock + isMigrating = false + Timber.tag(TAG).i("finishMigration()") + + submissionSettings.deleteLegacyTestData() + tracingSettings.deleteLegacyTestData() + } + + companion object { + private const val TAG = "CoronaTestMigration" + + /** + * We only use this for identification, needs to be guaranteed different from any non-legacy identifiers. + */ + private const val LEGACY_IDENTIFIER = "qrcode-pcr-legacy" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt index 36eda1768e6b139014ba46e35fc13b3fb68d4f0c..0db2d0ed6f9343596216423d14c64f380f662583 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCode.kt @@ -1,30 +1,57 @@ package de.rki.coronawarnapp.coronatest.qrcode import android.os.Parcelable +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.util.HashExtensions.toSHA256 +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.joda.time.Instant +import org.joda.time.LocalDate -sealed class CoronaTestQRCode : Parcelable { +sealed class CoronaTestQRCode : Parcelable, TestRegistrationRequest { - abstract val type: CoronaTest.Type - abstract val guid: CoronaTestGUID + abstract override val type: CoronaTest.Type + abstract val registrationIdentifier: String @Parcelize data class PCR( - override val type: CoronaTest.Type, - override val guid: CoronaTestGUID, - ) : CoronaTestQRCode() + val qrCodeGUID: CoronaTestGUID, + ) : CoronaTestQRCode() { + + @IgnoredOnParcel + override val type: CoronaTest.Type = CoronaTest.Type.PCR + + @IgnoredOnParcel + override val identifier: String + get() = "qrcode-${type.raw}-$qrCodeGUID" + + @IgnoredOnParcel + override val registrationIdentifier: String + get() = qrCodeGUID + } @Parcelize data class RapidAntigen( - override val type: CoronaTest.Type, - override val guid: CoronaTestGUID, + val hash: RapidAntigenHash, val createdAt: Instant, val firstName: String?, val lastName: String?, - val dateOfBirth: String?, - ) : CoronaTestQRCode() + val dateOfBirth: LocalDate?, + ) : CoronaTestQRCode() { + + @IgnoredOnParcel + override val type: CoronaTest.Type = CoronaTest.Type.RAPID_ANTIGEN + + @IgnoredOnParcel + override val identifier: String + get() = "qrcode-${type.raw}-$hash" + + @IgnoredOnParcel + override val registrationIdentifier: String + get() = hash.toSHA256() + } } typealias CoronaTestGUID = String +typealias RapidAntigenHash = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidation.kt deleted file mode 100644 index 717aaf78317c7822d4fc7225da3d76ea5a746839..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidation.kt +++ /dev/null @@ -1,12 +0,0 @@ -package de.rki.coronawarnapp.coronatest.qrcode - -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class CoronaTestQRCodeValidation @Inject constructor() { - - suspend fun validate(qrCode: String): CoronaTestQRCode { - throw NotImplementedError() - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3a54f940680e7457a3b57ade7ce668adc51f251 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt @@ -0,0 +1,27 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +import dagger.Reusable +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class CoronaTestQrCodeValidator @Inject constructor( + ratExtractor: RapidAntigenQrCodeExtractor, + pcrExtractor: PcrQrCodeExtractor +) { + + private val extractors = setOf(ratExtractor, pcrExtractor) + + fun validate(rawString: String): CoronaTestQRCode { + return extractors + .find { it.canHandle(rawString) } + ?.extract(rawString) + ?.also { Timber.i("Extracted data from QR code is $it") } + ?: throw InvalidQRCodeException() + } +} + +interface QrCodeExtractor { + fun canHandle(rawString: String): Boolean + fun extract(rawString: String): CoronaTestQRCode +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/InvalidQRCodeException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/InvalidQRCodeException.kt new file mode 100644 index 0000000000000000000000000000000000000000..24dd420a35009d94e55b06ece7c80802cd6db8eb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/InvalidQRCodeException.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +open class InvalidQRCodeException( + message: String = "An error occurred while parsing the qr code" +) : Exception(message) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..924bd4e89f6b0bef3ca0d7f09cd15539c300797c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt @@ -0,0 +1,34 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +import java.util.regex.Pattern +import javax.inject.Inject + +class PcrQrCodeExtractor @Inject constructor() : QrCodeExtractor { + + override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix, ignoreCase = true) + + override fun extract(rawString: String): CoronaTestQRCode.PCR { + return CoronaTestQRCode.PCR( + extractGUID(rawString) + ) + } + + private fun extractGUID(rawString: String): CoronaTestGUID { + if (!pattern.toRegex().matches(rawString)) throw InvalidQRCodeException() + + val matcher = pattern.matcher(rawString) + return if (matcher.matches()) { + matcher.group(1) as CoronaTestGUID + } else throw InvalidQRCodeException() + } + + private val prefix: String = "https://localhost" + + private val pattern: Pattern = ( + "^" + // Match start of string + "(?:https:\\/{2}localhost)" + // Match `https://localhost` + "(?:\\/{1}\\?)" + // Match the query param `/?` + "([a-f\\d]{6}[-][a-f\\d]{8}[-](?:[a-f\\d]{4}[-]){3}[a-f\\d]{12})" + // Match the UUID + "\$" + ).toPattern(Pattern.CASE_INSENSITIVE) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..deffb83ef479b45aa257c30868fd1c0e468c6395 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt @@ -0,0 +1,92 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +import com.google.common.io.BaseEncoding +import com.google.gson.Gson +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.util.serialization.fromJson +import okio.internal.commonToUtf8String +import org.joda.time.Instant +import org.joda.time.LocalDate +import timber.log.Timber +import java.util.regex.Matcher +import java.util.regex.Pattern +import javax.inject.Inject + +class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor { + + private val prefix: String = "https://s.coronawarn.app?v=1#" + private val prefix2: String = "https://s.coronawarn.app/?v=1#" + private val hexPattern: Pattern = Pattern.compile("\\p{XDigit}+") + + override fun canHandle(rawString: String): Boolean { + return rawString.startsWith(prefix, ignoreCase = true) || rawString.startsWith(prefix2, ignoreCase = true) + } + + override fun extract(rawString: String): CoronaTestQRCode.RapidAntigen { + val data = extractData(rawString).validate() + return CoronaTestQRCode.RapidAntigen( + hash = data.hash!!, + createdAt = data.createdAt!!, + firstName = data.firstName, + lastName = data.lastName, + dateOfBirth = data.dateOfBirth + ) + } + + private fun Payload.validate(): Payload { + if (hash == null || !hash.isSha256Hash()) throw InvalidQRCodeException("Hash is invalid") + if (timestamp == null || timestamp <= 0) throw InvalidQRCodeException("Timestamp is invalid") + createdAt = Instant.ofEpochSecond(timestamp) + dateOfBirth = dob?.let { + try { + LocalDate.parse(it) + } catch (e: Exception) { + Timber.e("Invalid date format") + throw InvalidQRCodeException("Date of birth has wrong format: $it. It should be YYYY-MM-DD") + } + } + return this + } + + private fun String.isSha256Hash(): Boolean { + return length == 64 && isHexadecimal() + } + + private fun String.isHexadecimal(): Boolean { + val matcher: Matcher = hexPattern.matcher(this) + return matcher.matches() + } + + private fun extractData(rawString: String): Payload { + return rawString + .removePrefix(prefix) + .removePrefix(prefix2) + .decode() + } + + private fun String.decode(): Payload { + val decoded = if ( + this.contains("+") || + this.contains("/") || + this.contains("=") + ) { + BaseEncoding.base64().decode(this) + } else { + BaseEncoding.base64Url().decode(this) + } + return Gson().fromJson(decoded.commonToUtf8String()) + } + + private data class Payload( + val hash: String?, + val timestamp: Long?, + @SerializedName("fn") + val firstName: String?, + @SerializedName("ln") + val lastName: String?, + val dob: String? + ) { + var dateOfBirth: LocalDate? = null + var createdAt: Instant? = null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/CoronaTestResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/CoronaTestResult.kt new file mode 100644 index 0000000000000000000000000000000000000000..6807bf6701e021461901e28736c8d2e2e7cd4e59 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/CoronaTestResult.kt @@ -0,0 +1,76 @@ +package de.rki.coronawarnapp.coronatest.server + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import org.json.JSONObject + +enum class CoronaTestResult(val value: Int) { + /** + * Pending (PCR test) or Pending (rapid antigen test) + */ + PCR_OR_RAT_PENDING(0), + + /** + * Negative (PCR test) + */ + PCR_NEGATIVE(1), + + /** + * Positive (PCR test) + */ + PCR_POSITIVE(2), + + /** + * Invalid (PCR test) + */ + PCR_INVALID(3), + + /** + * Redeemed (PCR test; locally referred to as Expired) + */ + PCR_REDEEMED(4), + + /** + * Pending (rapid antigen test) + */ + RAT_PENDING(5), + + /** + * Negative (rapid antigen test) + */ + RAT_NEGATIVE(6), + + /** + * Positive (rapid antigen test) + */ + RAT_POSITIVE(7), + + /** + * Invalid (rapid antigen test) + */ + RAT_INVALID(8), + + /** + * Redeemed (rapid antigen test; locally referred to as Expired)) + */ + RAT_REDEEMED(9); + + override fun toString(): String = "$name($value)" + + companion object { + fun fromInt(value: Int) = values().single { it.value == value } + } + + class GsonAdapter : TypeAdapter<CoronaTestResult>() { + override fun write(out: JsonWriter, value: CoronaTestResult?) { + if (value == null) out.nullValue() + else out.value(value.value) + } + + override fun read(reader: JsonReader): CoronaTestResult? = when (reader.peek()) { + JSONObject.NULL -> reader.nextNull().let { null } + else -> fromInt(reader.nextInt()) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/CoronaTestServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/CoronaTestServer.kt deleted file mode 100644 index 7319755a4eb97a49def0752e2daf003d3afd0b39..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/CoronaTestServer.kt +++ /dev/null @@ -1,7 +0,0 @@ -package de.rki.coronawarnapp.coronatest.server - -import dagger.Reusable -import javax.inject.Inject - -@Reusable -class CoronaTestServer @Inject constructor() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt similarity index 97% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationApiV1.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt index a45f6617de515e732f22c1e769bc37a788d7e301..3a11d242906c2835821f7e7e241d39e54d5b332c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationApiV1.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.verification.server +package de.rki.coronawarnapp.coronatest.server import com.google.gson.annotations.SerializedName import retrofit2.http.Body diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationHttpClient.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationHttpClient.kt similarity index 74% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationHttpClient.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationHttpClient.kt index 1c244ae61732a1e8ab53ca6acc6ec5303c5b203b..160a6fd7a52dd45a31215ff43e31fb0975a69615 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationHttpClient.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationHttpClient.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.verification.server +package de.rki.coronawarnapp.coronatest.server import javax.inject.Qualifier diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationKeyType.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt similarity index 52% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationKeyType.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt index f8112264c1a6c5491e6c5f0c7483c02044582f38..c64cfe628a9c28c3ec05437ba20f5cf81fc82a8b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationKeyType.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.verification.server +package de.rki.coronawarnapp.coronatest.server enum class VerificationKeyType { GUID, TELETAN; diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationModule.kt similarity index 90% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationModule.kt index 85d2bea5611ab57d9b81613fa80d065c1d6e461e..24dba8c5d9506837dc158fb1f2f0676b78787f9e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/VerificationModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.verification +package de.rki.coronawarnapp.coronatest.server import android.content.Context import dagger.Module @@ -8,8 +8,6 @@ import de.rki.coronawarnapp.environment.verification.VerificationCDNServerUrl import de.rki.coronawarnapp.http.HttpClientDefault import de.rki.coronawarnapp.http.RestrictedConnectionSpecs import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.verification.server.VerificationApiV1 -import de.rki.coronawarnapp.verification.server.VerificationHttpClient import okhttp3.Cache import okhttp3.ConnectionSpec import okhttp3.OkHttpClient diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt similarity index 82% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationServer.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt index 1411ca60db6ce02dcdbfb997bbe617345ee3b568..5f6463236d1f9aec470e66832b9ca42ab554d007 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/verification/server/VerificationServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt @@ -1,10 +1,12 @@ -package de.rki.coronawarnapp.verification.server +package de.rki.coronawarnapp.coronatest.server import dagger.Lazy +import de.rki.coronawarnapp.coronatest.type.RegistrationToken import de.rki.coronawarnapp.util.PaddingTool.requestPadding import de.rki.coronawarnapp.util.security.HashHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -19,7 +21,7 @@ class VerificationServer @Inject constructor( suspend fun retrieveRegistrationToken( key: String, keyType: VerificationKeyType - ): String = withContext(Dispatchers.IO) { + ): RegistrationToken = withContext(Dispatchers.IO) { val keyStr = if (keyType == VerificationKeyType.GUID) { HashHelper.hash256(key) } else { @@ -42,21 +44,25 @@ class VerificationServer @Inject constructor( ).registrationToken } - suspend fun retrieveTestResults( - registrationToken: String - ): Int = withContext(Dispatchers.IO) { - api.getTestResult( + suspend fun pollTestResult( + token: RegistrationToken + ): CoronaTestResult = withContext(Dispatchers.IO) { + Timber.tag(TAG).d("retrieveTestResults(token=%s)", token) + val response = api.getTestResult( fake = "0", headerPadding = requestPadding(PADDING_LENGTH_HEADER_TEST_RESULT), request = VerificationApiV1.RegistrationRequest( - registrationToken, + token, requestPadding(PADDING_LENGTH_BODY_TEST_RESULT) ) - ).testResult + ) + + Timber.tag(TAG).d("retrieveTestResults(token=%s) -> %s", token, response) + CoronaTestResult.fromInt(response.testResult) } suspend fun retrieveTan( - registrationToken: String + registrationToken: RegistrationToken ): String = withContext(Dispatchers.IO) { api.getTAN( fake = "0", @@ -96,5 +102,7 @@ class VerificationServer @Inject constructor( const val PADDING_LENGTH_BODY_TAN = 31 + VERIFICATION_BODY_FILL const val PADDING_LENGTH_BODY_TAN_FAKE = 31 + VERIFICATION_BODY_FILL const val DUMMY_REGISTRATION_TOKEN = "11111111-2222-4444-8888-161616161616" + + private const val TAG = "VerificationServer" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorage.kt index 771397027a879b3817c8c516904865cfcf79b088..0fc19f03bbef50d2fd228b96103ece548998c863 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorage.kt @@ -1,23 +1,96 @@ package de.rki.coronawarnapp.coronatest.storage +import android.content.Context +import androidx.core.content.edit +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest +import de.rki.coronawarnapp.util.di.AppContext +import de.rki.coronawarnapp.util.serialization.BaseGson import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @Singleton -class CoronaTestStorage @Inject constructor() { +class CoronaTestStorage @Inject constructor( + @AppContext val context: Context, + @BaseGson val baseGson: Gson +) { - suspend fun load(): Set<CoronaTest> { - Timber.tag(TAG).d("load()") - throw NotImplementedError() + private val prefs by lazy { + context.getSharedPreferences("coronatest_localdata", Context.MODE_PRIVATE) } - suspend fun save(tests: Set<CoronaTest>) { - Timber.tag(TAG).d("save(tests=%s)", tests) + private val gson by lazy { + baseGson.newBuilder().apply { + registerTypeAdapter(CoronaTestResult::class.java, CoronaTestResult.GsonAdapter()) + }.create() } + private val typeTokenPCR by lazy { + object : TypeToken<Set<PCRCoronaTest>>() {}.type + } + + private val typeTokenRA by lazy { + object : TypeToken<Set<RACoronaTest>>() {}.type + } + + var coronaTests: Collection<CoronaTest> + get() { + Timber.tag(TAG).d("load()") + + val pcrTests: Set<PCRCoronaTest> = run { + val raw = prefs.getString(PKEY_DATA_PCR, null) ?: return@run emptySet() + gson.fromJson<Set<PCRCoronaTest>>(raw, typeTokenPCR).onEach { + Timber.tag(TAG).v("PCR loaded: %s", it) + requireNotNull(it.identifier) + } + } + + val raTests: Set<RACoronaTest> = run { + val raw = prefs.getString(PKEY_DATA_RA, null) ?: return@run emptySet() + gson.fromJson<Set<RACoronaTest>>(raw, typeTokenRA).onEach { + Timber.tag(TAG).v("PCR loaded: %s", it) + requireNotNull(it.identifier) + } + } + + val tests = pcrTests + raTests + Timber.tag(TAG).v("Loaded %d tests.", tests.size) + return tests + } + set(value) { + Timber.tag(TAG).d("save(tests=%s)", value) + prefs.edit { + value.filter { it.type == CoronaTest.Type.PCR }.run { + if (isNotEmpty()) { + val raw = gson.toJson(this, typeTokenPCR) + Timber.tag(TAG).v("PCR storing: %s", raw) + putString(PKEY_DATA_PCR, raw) + } else { + Timber.tag(TAG).v("No PCR tests available, clearing.") + remove(PKEY_DATA_PCR) + } + } + value.filter { it.type == CoronaTest.Type.RAPID_ANTIGEN }.run { + if (isNotEmpty()) { + val raw = gson.toJson(this, typeTokenRA) + Timber.tag(TAG).v("RA storing: %s", raw) + putString(PKEY_DATA_RA, raw) + } else { + Timber.tag(TAG).v("No RA tests available, clearing.") + remove(PKEY_DATA_RA) + } + } + } + } + companion object { private const val TAG = "CoronaTestStorage" + private const val PKEY_DATA_RA = "coronatest.data.ra" + private const val PKEY_DATA_PCR = "coronatest.data.pcr" } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/tan/CoronaTestTAN.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/tan/CoronaTestTAN.kt new file mode 100644 index 0000000000000000000000000000000000000000..62200ac43ba6e549b36fe4246c201c209ef88451 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/tan/CoronaTestTAN.kt @@ -0,0 +1,35 @@ +package de.rki.coronawarnapp.coronatest.tan + +import android.os.Parcelable +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +sealed class CoronaTestTAN : Parcelable, TestRegistrationRequest { + + abstract override val type: CoronaTest.Type + abstract val tan: TestTAN + + @IgnoredOnParcel + override val identifier: String + get() = "tan-${type.raw}-$tan" + + @Parcelize + data class PCR( + override val tan: TestTAN, + ) : CoronaTestTAN() { + + @IgnoredOnParcel override val type: CoronaTest.Type = CoronaTest.Type.PCR + } + + @Parcelize + data class RapidAntigen( + override val tan: TestTAN, + ) : CoronaTestTAN() { + + @IgnoredOnParcel override val type: CoronaTest.Type = CoronaTest.Type.RAPID_ANTIGEN + } +} + +typealias TestTAN = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CommonSubmissionStates.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CommonSubmissionStates.kt new file mode 100644 index 0000000000000000000000000000000000000000..1867629ee401e40b7d8ed2f2ec6c62d74b40465b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CommonSubmissionStates.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.coronatest.type + +import org.joda.time.Instant + +interface CommonSubmissionStates { + interface TestUnregistered : CommonSubmissionStates + + interface TestFetching : CommonSubmissionStates + + interface SubmissionDone : CommonSubmissionStates { + val testRegisteredAt: Instant + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt index 08041c76e154830db21c59e6595e416067c038ca..6a3e9b1142e5c68ea4bc995906d3a9b66565b1d1 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt @@ -1,23 +1,42 @@ package de.rki.coronawarnapp.coronatest.type import com.google.gson.annotations.SerializedName -import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import org.joda.time.Instant interface CoronaTest { - val testGUID: CoronaTestGUID - val registrationToken: RegistrationToken + val identifier: TestIdentifier val type: Type - val isRefreshing: Boolean + + val registeredAt: Instant + val registrationToken: RegistrationToken + + val isProcessing: Boolean + val lastError: Throwable? + val isSubmissionAllowed: Boolean val isSubmitted: Boolean + val isViewed: Boolean + + val testResultReceivedAt: Instant? + val testResult: CoronaTestResult + + // TODO why do we need this PER test + val isAdvancedConsentGiven: Boolean + + // TODO Why do we need to store this? + val isJournalEntryCreated: Boolean + + val isResultAvailableNotificationSent: Boolean - enum class Type { + enum class Type(val raw: String) { @SerializedName("PCR") - PCR, + PCR("PCR"), @SerializedName("RAPID_ANTIGEN") - RAPID_ANTIGEN, + RAPID_ANTIGEN("RAPID_ANTIGEN"), } } typealias RegistrationToken = String +typealias TestIdentifier = String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..0605ddfc98541a337d7d2ebfb4c47a55f04a3495 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestProcessor.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.coronatest.type + +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN + +interface CoronaTestProcessor { + + val type: CoronaTest.Type + + suspend fun create(request: CoronaTestQRCode): CoronaTest + + suspend fun create(request: CoronaTestTAN): CoronaTest + + suspend fun pollServer(test: CoronaTest): CoronaTest + + /** + * Called before a test is removed. + */ + suspend fun onRemove(toBeRemoved: CoronaTest) + + suspend fun markSubmitted(test: CoronaTest): CoronaTest + + suspend fun markProcessing(test: CoronaTest, isProcessing: Boolean): CoronaTest + + suspend fun markViewed(test: CoronaTest): CoronaTest + + suspend fun updateConsent(test: CoronaTest, consented: Boolean): CoronaTest + + suspend fun updateResultNotification(test: CoronaTest, sent: Boolean): CoronaTest +} 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/coronatest/type/CoronaTestService.kt similarity index 51% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/SubmissionService.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt index 1931b63c3dcf5bed27d07f442c267de620c863d9..f47d859b5a06c9eacb1531b4c1bf331d1c6b279b 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/coronatest/type/CoronaTestService.kt @@ -1,15 +1,19 @@ -package de.rki.coronawarnapp.service.submission +package de.rki.coronawarnapp.coronatest.type +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.deniability.NoiseScheduler import de.rki.coronawarnapp.playbook.Playbook -import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.verification.server.VerificationKeyType +import de.rki.coronawarnapp.worker.BackgroundConstants +import timber.log.Timber import javax.inject.Inject -class SubmissionService @Inject constructor( - private val playbook: Playbook +class CoronaTestService @Inject constructor( + private val playbook: Playbook, + private val noiseScheduler: NoiseScheduler, ) { - suspend fun asyncRequestTestResult(registrationToken: String): TestResult { + suspend fun asyncRequestTestResult(registrationToken: String): CoronaTestResult { return playbook.testResult(registrationToken) } @@ -19,6 +23,10 @@ class SubmissionService @Inject constructor( guid, VerificationKeyType.GUID ) + + Timber.d("Scheduling background noise.") + scheduleDummyPattern() + return RegistrationData(registrationToken, testResult) } @@ -28,11 +36,21 @@ class SubmissionService @Inject constructor( tan, VerificationKeyType.TELETAN ) + + Timber.d("Scheduling background noise.") + scheduleDummyPattern() + return RegistrationData(registrationToken, testResult) } + private fun scheduleDummyPattern() { + if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0) { + noiseScheduler.setPeriodicNoise(enabled = true) + } + } + data class RegistrationData( val registrationToken: String, - val testResult: TestResult + val testResult: CoronaTestResult ) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/antigen/RapidAntigenCoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/antigen/RapidAntigenCoronaTest.kt deleted file mode 100644 index 654a4cdb5701e51401ca60f12af99afd45011a5e..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/antigen/RapidAntigenCoronaTest.kt +++ /dev/null @@ -1,26 +0,0 @@ -package de.rki.coronawarnapp.coronatest.type.antigen - -import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID -import de.rki.coronawarnapp.coronatest.type.CoronaTest -import de.rki.coronawarnapp.coronatest.type.RegistrationToken - -data class RapidAntigenCoronaTest( - override val testGUID: CoronaTestGUID, - override val registrationToken: RegistrationToken, - override val isRefreshing: Boolean, - override val isSubmissionAllowed: Boolean, - override val isSubmitted: Boolean, - val state: State, -) : CoronaTest { - - override val type: CoronaTest.Type = CoronaTest.Type.RAPID_ANTIGEN - - enum class State { - PENDING, - INVALID, - POSITIVE, - NEGATIVE, - REDEEMED, - OUTDATED, - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt index 9d4030826fe6fd3671757b5e1b1ecda8b23e4b40..ed6b4d56af89d29120f12ed7c1cf1f007be6d500 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt @@ -1,20 +1,60 @@ package de.rki.coronawarnapp.coronatest.type.pcr -import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.coronatest.type.TestIdentifier +import org.joda.time.Instant data class PCRCoronaTest( - override val testGUID: CoronaTestGUID, + @SerializedName("identifier") + override val identifier: TestIdentifier, + + @SerializedName("registeredAt") + override val registeredAt: Instant, + + @SerializedName("registrationToken") override val registrationToken: RegistrationToken, - override val isRefreshing: Boolean, - override val isSubmissionAllowed: Boolean, - override val isSubmitted: Boolean, - val state: State, + + @SerializedName("isSubmitted") + override val isSubmitted: Boolean = false, + + @SerializedName("isViewed") + override val isViewed: Boolean = false, + + @SerializedName("isAdvancedConsentGiven") + override val isAdvancedConsentGiven: Boolean = false, + + @SerializedName("isJournalEntryCreated") + override val isJournalEntryCreated: Boolean = false, + + @SerializedName("isResultAvailableNotificationSent") + override val isResultAvailableNotificationSent: Boolean = false, + + @SerializedName("testResultReceivedAt") + override val testResultReceivedAt: Instant? = null, + + @SerializedName("testResult") + override val testResult: CoronaTestResult, + + @Transient override val isProcessing: Boolean = false, + @Transient override val lastError: Throwable? = null, ) : CoronaTest { override val type: CoronaTest.Type = CoronaTest.Type.PCR + override val isSubmissionAllowed: Boolean = testResult == CoronaTestResult.PCR_POSITIVE + + val state: State = when (testResult) { + CoronaTestResult.PCR_OR_RAT_PENDING -> State.PENDING + CoronaTestResult.PCR_NEGATIVE -> State.NEGATIVE + CoronaTestResult.PCR_POSITIVE -> State.POSITIVE + CoronaTestResult.PCR_INVALID -> State.INVALID + CoronaTestResult.PCR_REDEEMED -> State.REDEEMED + else -> throw IllegalArgumentException("Invalid PCR test state $testResult") + } + enum class State { PENDING, INVALID, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ce1c65829e0f85eff22df70bb980e40dd264d1f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensions.kt @@ -0,0 +1,121 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import de.rki.coronawarnapp.exception.http.CwaServerError +import de.rki.coronawarnapp.util.CWADebug +import de.rki.coronawarnapp.util.DeviceUIState +import de.rki.coronawarnapp.util.NetworkRequestWrapper +import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess +import timber.log.Timber + +fun PCRCoronaTest?.toSubmissionState(): SubmissionStatePCR { + if (this == null) return SubmissionStatePCR.NoTest + + val uiState: DeviceUIState = when (state) { + PCRCoronaTest.State.PENDING -> DeviceUIState.PAIRED_NO_RESULT + PCRCoronaTest.State.INVALID -> DeviceUIState.PAIRED_ERROR + PCRCoronaTest.State.POSITIVE -> DeviceUIState.PAIRED_POSITIVE + PCRCoronaTest.State.NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE + PCRCoronaTest.State.REDEEMED -> DeviceUIState.PAIRED_REDEEMED + } + + val networkWrapper = when { + isProcessing -> NetworkRequestWrapper.RequestStarted + lastError != null -> NetworkRequestWrapper.RequestFailed(lastError) + else -> NetworkRequestWrapper.RequestSuccessful(uiState) + } + + val eval = Evaluation( + deviceUiState = networkWrapper, + isDeviceRegistered = registrationToken != null, + hasTestResultBeenSeen = this.isViewed + ) + Timber.d("eval: %s", eval) + return when { + eval.isUnregistered() -> SubmissionStatePCR.NoTest + eval.isFetching() -> SubmissionStatePCR.FetchingResult + eval.isTestResultReady() -> SubmissionStatePCR.TestResultReady + eval.isResultPositive() -> SubmissionStatePCR.TestPositive + eval.isInvalid() -> SubmissionStatePCR.TestInvalid + eval.isError() -> SubmissionStatePCR.TestError + eval.isResultNegative() -> SubmissionStatePCR.TestNegative + eval.isSubmissionDone() -> SubmissionStatePCR.SubmissionDone(testRegisteredAt = registeredAt) + eval.isPending() -> SubmissionStatePCR.TestPending + else -> { + if (CWADebug.isDeviceForTestersBuild) throw IllegalStateException(eval.toString()) + else SubmissionStatePCR.TestPending + } + } +} + +// TODO Refactor this to be easier to understand, probably remove the "withSuccess" logic. +private data class Evaluation( + val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>, + val isDeviceRegistered: Boolean, + val hasTestResultBeenSeen: Boolean +) { + + fun isUnregistered(): Boolean = !isDeviceRegistered + + fun isTestResultReady(): Boolean = deviceUiState.withSuccess(false) { + when (it) { + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> !hasTestResultBeenSeen + else -> false + } + } + + fun isFetching(): Boolean = + isDeviceRegistered && when (deviceUiState) { + is NetworkRequestWrapper.RequestFailed -> false + is NetworkRequestWrapper.RequestStarted -> true + is NetworkRequestWrapper.RequestIdle -> true + else -> false + } + + fun isResultPositive(): Boolean = + deviceUiState.withSuccess(false) { + when (it) { + DeviceUIState.PAIRED_POSITIVE, DeviceUIState.PAIRED_POSITIVE_TELETAN -> hasTestResultBeenSeen + else -> false + } + } + + fun isResultNegative(): Boolean = + deviceUiState.withSuccess(false) { + when (it) { + DeviceUIState.PAIRED_NEGATIVE -> true + else -> false + } + } + + fun isSubmissionDone(): Boolean = + when (deviceUiState) { + is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == DeviceUIState.SUBMITTED_FINAL + else -> false + } + + fun isInvalid(): Boolean = + isDeviceRegistered && when (deviceUiState) { + is NetworkRequestWrapper.RequestFailed -> deviceUiState.error !is CwaServerError + is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == DeviceUIState.PAIRED_REDEEMED + else -> false + } + + fun isError(): Boolean = + deviceUiState.withSuccess(false) { + when (it) { + DeviceUIState.PAIRED_ERROR -> true + else -> false + } + } + + fun isPending(): Boolean = + when (deviceUiState) { + is NetworkRequestWrapper.RequestFailed -> true + is NetworkRequestWrapper.RequestSuccessful -> { + deviceUiState.data == DeviceUIState.PAIRED_ERROR || + deviceUiState.data == DeviceUIState.PAIRED_NO_RESULT + } + else -> false + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..90eee46fc2409ca41f3290abe4ff144c455e91b9 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt @@ -0,0 +1,185 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import dagger.Reusable +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor +import de.rki.coronawarnapp.coronatest.type.CoronaTestService +import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector +import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.util.TimeStamper +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class PCRProcessor @Inject constructor( + private val timeStamper: TimeStamper, + private val submissionService: CoronaTestService, + private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, + private val testResultDataCollector: TestResultDataCollector, + private val deadmanNotificationScheduler: DeadmanNotificationScheduler, + private val testResultScheduler: TestResultScheduler, +) : CoronaTestProcessor { + + override val type: CoronaTest.Type = CoronaTest.Type.PCR + + override suspend fun create(request: CoronaTestQRCode): PCRCoronaTest { + Timber.tag(TAG).d("create(data=%s)", request) + request as CoronaTestQRCode.PCR + + val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.qrCodeGUID) + + testResultDataCollector.saveTestResultAnalyticsSettings(registrationData.testResult) // This saves received at + + return createCoronaTest(request, registrationData) + } + + override suspend fun create(request: CoronaTestTAN): CoronaTest { + Timber.tag(TAG).d("create(data=%s)", request) + request as CoronaTestTAN.PCR + + val registrationData = submissionService.asyncRegisterDeviceViaTAN(request.tan) + + analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() + + return createCoronaTest(request, registrationData) + } + + private suspend fun createCoronaTest( + request: TestRegistrationRequest, + response: CoronaTestService.RegistrationData + ): PCRCoronaTest { + analyticsKeySubmissionCollector.reset() + response.testResult.validOrThrow() + + testResultDataCollector.updatePendingTestResultReceivedTime(response.testResult) + + if (response.testResult == CoronaTestResult.PCR_POSITIVE) { + analyticsKeySubmissionCollector.reportPositiveTestResultReceived() + deadmanNotificationScheduler.cancelScheduledWork() + } + + analyticsKeySubmissionCollector.reportTestRegistered() + +// val currentTime = timeStamper.nowUTC +// submissionSettings.initialTestResultReceivedAt = currentTime +// testResultReceivedDateFlowInternal.value = currentTime.toDate() + if (response.testResult == CoronaTestResult.PCR_OR_RAT_PENDING) { +// riskWorkScheduler.setPeriodicRiskCalculation(enabled = true) + + testResultScheduler.setPeriodicTestPolling(enabled = true) + } + + return PCRCoronaTest( + identifier = request.identifier, + registeredAt = timeStamper.nowUTC, + registrationToken = response.registrationToken, + testResult = response.testResult, + ) + } + + override suspend fun pollServer(test: CoronaTest): CoronaTest { + return try { + Timber.tag(TAG).v("pollServer(test=%s)", test) + test as PCRCoronaTest + + if (test.isSubmitted || test.isSubmissionAllowed) { + Timber.tag(TAG).w("Not refreshing already final test.") + return test + } + + val testResult = submissionService.asyncRequestTestResult(test.registrationToken) + Timber.tag(TAG).d("Test result was %s", testResult) + + testResult.validOrThrow() + + testResultDataCollector.updatePendingTestResultReceivedTime(testResult) + + if (testResult == CoronaTestResult.PCR_POSITIVE) { + analyticsKeySubmissionCollector.reportPositiveTestResultReceived() + deadmanNotificationScheduler.cancelScheduledWork() + } + + test.copy( + testResult = testResult, + lastError = null + ) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to poll server for %s", test) + if (e !is CwaWebException) e.report(ExceptionCategory.INTERNAL) + + test as PCRCoronaTest + test.copy(lastError = e) + } + } + + override suspend fun onRemove(toBeRemoved: CoronaTest) { + Timber.tag(TAG).v("onRemove(toBeRemoved=%s)", toBeRemoved) + testResultDataCollector.clear() + } + + override suspend fun markSubmitted(test: CoronaTest): PCRCoronaTest { + Timber.tag(TAG).v("markSubmitted(test=%s)", test) + test as PCRCoronaTest + + return test.copy(isSubmitted = true) + } + + override suspend fun markProcessing(test: CoronaTest, isProcessing: Boolean): CoronaTest { + Timber.tag(TAG).v("markProcessing(test=%s, isProcessing=%b)", test, isProcessing) + test as PCRCoronaTest + + return test.copy(isProcessing = isProcessing) + } + + override suspend fun markViewed(test: CoronaTest): CoronaTest { + Timber.tag(TAG).v("markViewed(test=%s)", test) + test as PCRCoronaTest + + return test.copy(isViewed = true) + } + + override suspend fun updateConsent(test: CoronaTest, consented: Boolean): CoronaTest { + Timber.tag(TAG).v("updateConsent(test=%s, consented=%b)", test, consented) + test as PCRCoronaTest + + return test.copy(isAdvancedConsentGiven = consented) + } + + override suspend fun updateResultNotification(test: CoronaTest, sent: Boolean): CoronaTest { + Timber.tag(TAG).v("updateResultNotification(test=%s, sent=%b)", test, sent) + test as PCRCoronaTest + + return test.copy(isResultAvailableNotificationSent = sent) + } + + companion object { + private const val TAG = "PCRProcessor" + } +} + +private fun CoronaTestResult.validOrThrow() { + val isValid = when (this) { + CoronaTestResult.PCR_OR_RAT_PENDING, + CoronaTestResult.PCR_NEGATIVE, + CoronaTestResult.PCR_POSITIVE, + CoronaTestResult.PCR_INVALID, + CoronaTestResult.PCR_REDEEMED -> true + + CoronaTestResult.RAT_PENDING, + CoronaTestResult.RAT_NEGATIVE, + CoronaTestResult.RAT_POSITIVE, + CoronaTestResult.RAT_INVALID, + CoronaTestResult.RAT_REDEEMED -> false + } + + if (!isValid) throw IllegalArgumentException("Invalid testResult $this") +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/SubmissionStatePCR.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/SubmissionStatePCR.kt new file mode 100644 index 0000000000000000000000000000000000000000..2a45ebb8a01fbd08b4ef2e0568163986539c28f0 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/SubmissionStatePCR.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import de.rki.coronawarnapp.coronatest.type.CommonSubmissionStates +import org.joda.time.Instant + +sealed class SubmissionStatePCR { + + object NoTest : SubmissionStatePCR(), CommonSubmissionStates.TestUnregistered + object FetchingResult : SubmissionStatePCR(), CommonSubmissionStates.TestFetching + object TestResultReady : SubmissionStatePCR() + object TestPositive : SubmissionStatePCR() + object TestNegative : SubmissionStatePCR() + object TestError : SubmissionStatePCR() + object TestInvalid : SubmissionStatePCR() + object TestPending : SubmissionStatePCR() + data class SubmissionDone( + override val testRegisteredAt: Instant + ) : SubmissionStatePCR(), CommonSubmissionStates.SubmissionDone +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..7cdf2cf5bcb094d3c11235626e7b2288a1737d3b --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.RegistrationToken +import de.rki.coronawarnapp.coronatest.type.TestIdentifier +import org.joda.time.Instant +import org.joda.time.LocalDate + +data class RACoronaTest( + @SerializedName("identifier") + override val identifier: TestIdentifier, + + @SerializedName("registeredAt") + override val registeredAt: Instant, + + @SerializedName("registrationToken") + override val registrationToken: RegistrationToken, + + @SerializedName("isSubmitted") + override val isSubmitted: Boolean = false, + + @SerializedName("isViewed") + override val isViewed: Boolean = false, + + @SerializedName("isAdvancedConsentGiven") + override val isAdvancedConsentGiven: Boolean = false, + + @SerializedName("isJournalEntryCreated") + override val isJournalEntryCreated: Boolean = false, + + @SerializedName("isResultAvailableNotificationSent") + override val isResultAvailableNotificationSent: Boolean = false, + + @SerializedName("testResultReceivedAt") + override val testResultReceivedAt: Instant? = null, + + @SerializedName("testResult") + override val testResult: CoronaTestResult, + + @SerializedName("testedAt") + val testedAt: Instant, + + @SerializedName("firstName") + val firstName: String?, + + @SerializedName("lastName") + val lastName: String?, + + @SerializedName("dateOfBirth") + val dateOfBirth: LocalDate?, + + @Transient override val isProcessing: Boolean = false, + @Transient override val lastError: Throwable? = null, +) : CoronaTest { + + override val type: CoronaTest.Type = CoronaTest.Type.RAPID_ANTIGEN + + fun getState(nowUTC: Instant): State { + // TODO + return State.PENDING + } + + override val isSubmissionAllowed: Boolean = testResult == CoronaTestResult.RAT_POSITIVE + + enum class State { + PENDING, + INVALID, + POSITIVE, + NEGATIVE, + REDEEMED, + OUTDATED, + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..fa8ad42e982d75d0e766afd6ac74c2c7fe7adca5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensions.kt @@ -0,0 +1,7 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +fun RACoronaTest?.toSubmissionState(): SubmissionStateRAT { + if (this == null) return SubmissionStateRAT.NoTest + + return SubmissionStateRAT.FetchingResult +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt new file mode 100644 index 0000000000000000000000000000000000000000..6052fa0a06cc18df4e07161d807c579f3247792e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessor.kt @@ -0,0 +1,138 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import dagger.Reusable +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor +import de.rki.coronawarnapp.coronatest.type.CoronaTestService +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.http.CwaWebException +import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.util.TimeStamper +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class RapidAntigenProcessor @Inject constructor( + private val timeStamper: TimeStamper, + private val submissionService: CoronaTestService, +) : CoronaTestProcessor { + + override val type: CoronaTest.Type = CoronaTest.Type.RAPID_ANTIGEN + + override suspend fun create(request: CoronaTestQRCode): RACoronaTest { + Timber.tag(TAG).d("create(data=%s)", request) + request as CoronaTestQRCode.RapidAntigen + + val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.registrationIdentifier) + + registrationData.testResult.validOrThrow() + + return RACoronaTest( + identifier = request.identifier, + registeredAt = timeStamper.nowUTC, + registrationToken = registrationData.registrationToken, + testResult = registrationData.testResult, + testedAt = request.createdAt, + firstName = request.firstName, + lastName = request.lastName, + dateOfBirth = request.dateOfBirth, + ) + } + + override suspend fun create(request: CoronaTestTAN): CoronaTest { + Timber.tag(TAG).d("create(data=%s)", request) + request as CoronaTestTAN.RapidAntigen + throw UnsupportedOperationException("There are no TAN based RATs") + } + + override suspend fun pollServer(test: CoronaTest): CoronaTest { + return try { + Timber.tag(TAG).v("pollServer(test=%s)", test) + test as RACoronaTest + + if (test.isSubmitted || test.isSubmissionAllowed) { + Timber.tag(TAG).w("Not refreshing already final test.") + return test + } + + val testResult = submissionService.asyncRequestTestResult(test.registrationToken) + Timber.tag(TAG).d("Test result was %s", testResult) + + test.copy( + testResult = testResult, + lastError = null + ) + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Failed to poll server for %s", test) + if (e !is CwaWebException) e.report(ExceptionCategory.INTERNAL) + + test as RACoronaTest + test.copy(lastError = e) + } + } + + override suspend fun onRemove(toBeRemoved: CoronaTest) { + Timber.tag(TAG).v("onRemove(toBeRemoved=%s)", toBeRemoved) + // Currently nothing to do + } + + override suspend fun markSubmitted(test: CoronaTest): RACoronaTest { + Timber.tag(TAG).d("markSubmitted(test=%s)", test) + test as RACoronaTest + + return test.copy(isSubmitted = true) + } + + override suspend fun markProcessing(test: CoronaTest, isProcessing: Boolean): CoronaTest { + Timber.tag(TAG).v("markProcessing(test=%s, isProcessing=%b)", test, isProcessing) + test as RACoronaTest + + return test.copy(isProcessing = isProcessing) + } + + override suspend fun markViewed(test: CoronaTest): CoronaTest { + Timber.tag(TAG).v("markViewed(test=%s)", test) + test as RACoronaTest + + return test.copy(isViewed = true) + } + + override suspend fun updateConsent(test: CoronaTest, consented: Boolean): CoronaTest { + Timber.tag(TAG).v("updateConsent(test=%s, consented=%b)", test, consented) + test as RACoronaTest + + return test.copy(isAdvancedConsentGiven = consented) + } + + override suspend fun updateResultNotification(test: CoronaTest, sent: Boolean): CoronaTest { + Timber.tag(TAG).v("updateResultNotification(test=%s, sent=%b)", test, sent) + test as RACoronaTest + + return test.copy(isResultAvailableNotificationSent = sent) + } + + companion object { + private const val TAG = "RapidAntigenProcessor" + } +} + +private fun CoronaTestResult.validOrThrow() { + val isValid = when (this) { + CoronaTestResult.PCR_OR_RAT_PENDING, + CoronaTestResult.RAT_PENDING, + CoronaTestResult.RAT_NEGATIVE, + CoronaTestResult.RAT_POSITIVE, + CoronaTestResult.RAT_INVALID, + CoronaTestResult.RAT_REDEEMED -> true + + CoronaTestResult.PCR_NEGATIVE, + CoronaTestResult.PCR_POSITIVE, + CoronaTestResult.PCR_INVALID, + CoronaTestResult.PCR_REDEEMED -> false + } + + if (!isValid) throw IllegalArgumentException("Invalid testResult $this") +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/SubmissionStateRAT.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/SubmissionStateRAT.kt new file mode 100644 index 0000000000000000000000000000000000000000..bbb6cd6474fb89385c762be591e47a486df038e3 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/SubmissionStateRAT.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import de.rki.coronawarnapp.coronatest.type.CommonSubmissionStates +import org.joda.time.Instant + +sealed class SubmissionStateRAT { + + object NoTest : SubmissionStateRAT(), CommonSubmissionStates.TestUnregistered + object FetchingResult : SubmissionStateRAT(), CommonSubmissionStates.TestFetching + object TestResultReady : SubmissionStateRAT() + object TestPositive : SubmissionStateRAT() + object TestNegative : SubmissionStateRAT() + object TestError : SubmissionStateRAT() + object TestInvalid : SubmissionStateRAT() + object TestPending : SubmissionStateRAT() + data class SubmissionDone( + override val testRegisteredAt: Instant + ) : SubmissionStateRAT(), CommonSubmissionStates.SubmissionDone +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollector.kt index 5335b72d1ad71c4e88d5806cc9ae4411331ea630..1bc2cb42d31db03d73f5cf36784b0f41bf5f093f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollector.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.risk.tryLatestEwResultsWithDefaults import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.formatter.TestResult import kotlinx.coroutines.flow.first import timber.log.Timber import javax.inject.Inject @@ -21,11 +21,11 @@ class TestResultDataCollector @Inject constructor( * Collect Test result registration only after user has given a consent. * exclude any registered test result before giving a consent */ - suspend fun saveTestResultAnalyticsSettings(testResult: TestResult) { + suspend fun saveTestResultAnalyticsSettings(testResult: CoronaTestResult) { val validTestResults = listOf( - TestResult.POSITIVE, - TestResult.PENDING, - TestResult.NEGATIVE + CoronaTestResult.PCR_POSITIVE, + CoronaTestResult.PCR_OR_RAT_PENDING, + CoronaTestResult.PCR_NEGATIVE ) if (testResult !in validTestResults) return // Not interested in other values @@ -42,16 +42,16 @@ class TestResultDataCollector @Inject constructor( } } - fun updatePendingTestResultReceivedTime(newTestResult: TestResult) { + fun updatePendingTestResultReceivedTime(newTestResult: CoronaTestResult) { // Analytics is enabled val shouldUpdate = analyticsSettings.analyticsEnabled.value && // Test was scanned after giving consent and this a QR-Code Test registration // For TAN test registration this flag is not set testResultDonorSettings.testScannedAfterConsent.value && // Result was Pending - testResultDonorSettings.testResultAtRegistration.value == TestResult.PENDING && + testResultDonorSettings.testResultAtRegistration.value == CoronaTestResult.PCR_OR_RAT_PENDING && // Final Test result received - newTestResult in listOf(TestResult.POSITIVE, TestResult.NEGATIVE) + newTestResult in listOf(CoronaTestResult.PCR_POSITIVE, CoronaTestResult.PCR_NEGATIVE) if (shouldUpdate) { val receivedAt = timeStamper.nowUTC Timber.d("updatePendingTestResultReceivedTime($newTestResult, $receivedAt") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt index 4a1344a4d0994d274aed67f6422909850eee0586..23e8c593739c9f92b7219153aa4a54aabb69469b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonor.kt @@ -1,12 +1,14 @@ package de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.latestPCRT +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.datadonation.analytics.common.calculateDaysSinceMostRecentDateAtRiskLevelAtTestRegistration import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.formatter.TestResult +import kotlinx.coroutines.flow.first import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber @@ -17,7 +19,7 @@ import javax.inject.Singleton class TestResultDonor @Inject constructor( private val testResultDonorSettings: TestResultDonorSettings, private val timeStamper: TimeStamper, - private val submissionSettings: SubmissionSettings + private val coronaTestRepository: CoronaTestRepository, ) : DonorModule { override suspend fun beginDonation(request: DonorModule.Request): DonorModule.Contribution { @@ -27,7 +29,7 @@ class TestResultDonor @Inject constructor( return TestResultMetadataNoContribution } - val timestampAtRegistration = submissionSettings.initialTestResultReceivedAt + val timestampAtRegistration = coronaTestRepository.latestPCRT.first()?.registeredAt if (timestampAtRegistration == null) { Timber.d("Skipping TestResultMetadata donation (timestampAtRegistration is missing)") return TestResultMetadataNoContribution @@ -132,7 +134,7 @@ class TestResultDonor @Inject constructor( private fun pendingTestMetadataDonation( hoursSinceTestRegistrationTime: Int, - testResult: TestResult, + testResult: CoronaTestResult, daysSinceMostRecentDateAtRiskLevelAtTestRegistration: Int, hoursSinceHighRiskWarningAtTestRegistration: Int ): DonorModule.Contribution { @@ -152,7 +154,7 @@ class TestResultDonor @Inject constructor( private fun finalTestMetadataDonation( registrationTime: Instant, - testResult: TestResult, + testResult: CoronaTestResult, daysSinceMostRecentDateAtRiskLevelAtTestRegistration: Int, hoursSinceHighRiskWarningAtTestRegistration: Int ): DonorModule.Contribution { @@ -192,14 +194,18 @@ class TestResultDonor @Inject constructor( ).standardHours.toInt() } - private inline val TestResult.isFinal: Boolean get() = this in listOf(TestResult.POSITIVE, TestResult.NEGATIVE) - private inline val TestResult.isPending get() = this == TestResult.PENDING + private inline val CoronaTestResult.isFinal: Boolean + get() = this in listOf( + CoronaTestResult.PCR_POSITIVE, + CoronaTestResult.PCR_NEGATIVE + ) + private inline val CoronaTestResult.isPending get() = this == CoronaTestResult.PCR_OR_RAT_PENDING - private fun TestResult.toPPATestResult(): PpaData.PPATestResult { + private fun CoronaTestResult.toPPATestResult(): PpaData.PPATestResult { return when (this) { - TestResult.PENDING -> PpaData.PPATestResult.TEST_RESULT_PENDING - TestResult.POSITIVE -> PpaData.PPATestResult.TEST_RESULT_POSITIVE - TestResult.NEGATIVE -> PpaData.PPATestResult.TEST_RESULT_NEGATIVE + CoronaTestResult.PCR_OR_RAT_PENDING -> PpaData.PPATestResult.TEST_RESULT_PENDING + CoronaTestResult.PCR_POSITIVE -> PpaData.PPATestResult.TEST_RESULT_POSITIVE + CoronaTestResult.PCR_NEGATIVE -> PpaData.PPATestResult.TEST_RESULT_NEGATIVE else -> PpaData.PPATestResult.TEST_RESULT_UNKNOWN } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt index 3cec71475ff6dd8801f73babdd62b8a340f5c32d..ebe80397f94a7efc88d8460bcf59f85c4028e104 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/datadonation/analytics/storage/TestResultDonorSettings.kt @@ -1,12 +1,12 @@ package de.rki.coronawarnapp.datadonation.analytics.storage import android.content.Context +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.datadonation.analytics.common.toMetadataRiskLevel import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.preferences.createFlowPreference import org.joda.time.Instant @@ -59,7 +59,7 @@ class TestResultDonorSettings @Inject constructor( if (value == -1) { null } else { - TestResult.fromInt(value) + CoronaTestResult.fromInt(value) } }, writer = { key, result -> @@ -95,10 +95,10 @@ class TestResultDonorSettings @Inject constructor( } ) - fun saveTestResultDonorDataAtRegistration(testResult: TestResult, lastEwRiskResult: EwRiskLevelResult) { + fun saveTestResultDonorDataAtRegistration(testResult: CoronaTestResult, lastEwRiskResult: EwRiskLevelResult) { testScannedAfterConsent.update { true } testResultAtRegistration.update { testResult } - if (testResult in listOf(TestResult.POSITIVE, TestResult.NEGATIVE)) { + if (testResult in listOf(CoronaTestResult.PCR_POSITIVE, CoronaTestResult.PCR_NEGATIVE)) { finalTestResultReceivedAt.update { timeStamper.nowUTC } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoiseOneTimeWorker.kt similarity index 93% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoiseOneTimeWorker.kt index ad5a57f87918c3dfdf5764b7f67206309c31afc7..91186227358718960d1b495094d8292348076999 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoiseOneTimeWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoiseOneTimeWorker.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.worker +package de.rki.coronawarnapp.deniability import android.content.Context import androidx.work.CoroutineWorker @@ -8,12 +8,11 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory +import de.rki.coronawarnapp.worker.BackgroundConstants import timber.log.Timber /** * One time background noise worker - * - * @see BackgroundWorkScheduler */ class BackgroundNoiseOneTimeWorker @AssistedInject constructor( @Assisted val context: Context, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt similarity index 69% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt index 2168b02875a0b162f33c781faa418839b1dd000f..fc0852a0d8c8979e1aba380d233f9a549628f560 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundNoisePeriodicWorker.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/BackgroundNoisePeriodicWorker.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.worker +package de.rki.coronawarnapp.deniability import android.content.Context import androidx.work.CoroutineWorker @@ -6,9 +6,12 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory +import de.rki.coronawarnapp.worker.BackgroundConstants +import de.rki.coronawarnapp.worker.BackgroundWorkScheduler +import kotlinx.coroutines.flow.first import org.joda.time.Duration import org.joda.time.Instant import timber.log.Timber @@ -21,9 +24,9 @@ import timber.log.Timber class BackgroundNoisePeriodicWorker @AssistedInject constructor( @Assisted val context: Context, @Assisted workerParams: WorkerParameters, - private val submissionSettings: SubmissionSettings, private val timeStamper: TimeStamper, - private val backgroundWorkScheduler: BackgroundWorkScheduler, + private val coronaTestRepository: CoronaTestRepository, + private val noiseScheduler: NoiseScheduler, ) : CoroutineWorker(context, workerParams) { /** @@ -34,18 +37,20 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( var result = Result.success() try { - val initialPairingDate = submissionSettings.devicePairingSuccessfulAt ?: Instant.ofEpochMilli(0) + val initialPairingDate = coronaTestRepository.coronaTests.first().maxByOrNull { + it.registeredAt + }?.registeredAt ?: Instant.ofEpochMilli(0) // Check if the numberOfDaysToRunPlaybook are over - if (initialPairingDate - .plus(Duration.standardDays(NUMBER_OF_DAYS_TO_RUN_PLAYBOOK)) - .isBefore(timeStamper.nowUTC) + if ( + initialPairingDate.plus(Duration.standardDays(NUMBER_OF_DAYS_TO_RUN_PLAYBOOK)) + .isBefore(timeStamper.nowUTC) ) { stopWorker() return result } - backgroundWorkScheduler.scheduleBackgroundNoiseOneTimeWork() + noiseScheduler.scheduleBackgroundNoiseOneTimeWork() } catch (e: Exception) { result = if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Result.failure() @@ -58,7 +63,7 @@ class BackgroundNoisePeriodicWorker @AssistedInject constructor( } private fun stopWorker() { - backgroundWorkScheduler.stopBackgroundNoisePeriodicWork() + noiseScheduler.setPeriodicNoise(enabled = false) Timber.tag(TAG).d("$id: worker stopped") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt new file mode 100644 index 0000000000000000000000000000000000000000..c7d8db4da0a6b70aae134a341db3b3d2cd8fdfba --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/deniability/NoiseScheduler.kt @@ -0,0 +1,105 @@ +package de.rki.coronawarnapp.deniability + +import androidx.work.BackoffPolicy +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import dagger.Reusable +import de.rki.coronawarnapp.worker.BackgroundConstants +import de.rki.coronawarnapp.worker.BackgroundWorkHelper +import timber.log.Timber +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +@Reusable +class NoiseScheduler @Inject constructor( + private val workManager: WorkManager, +) { + + fun setPeriodicNoise(enabled: Boolean) { + Timber.tag(TAG).d("setPeriodicNoise(enabled=$enabled)") + if (enabled) { + enqueueBackgroundNoisePeriodicWork() + } else { + workManager.cancelUniqueWork(BACKGROUND_NOISE_PERIODIC_WORK_NAME) + } + } + + fun scheduleBackgroundNoiseOneTimeWork() { + workManager.enqueueUniqueWork( + BACKGROUND_NOISE_ONE_TIME_WORK_NAME, + ExistingWorkPolicy.REPLACE, + buildBackgroundNoiseOneTimeWork() + ) + } + + private fun enqueueBackgroundNoisePeriodicWork() = workManager.enqueueUniquePeriodicWork( + BACKGROUND_NOISE_PERIODIC_WORK_NAME, + ExistingPeriodicWorkPolicy.REPLACE, + buildBackgroundNoisePeriodicWork() + ) + + private fun buildBackgroundNoiseOneTimeWork() = + OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>() + .addTag(BACKGROUND_NOISE_ONE_TIME_WORKER_TAG) + .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) + .setInitialDelay( + BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(), + TimeUnit.HOURS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() + + /** + * Build background noise periodic work request + * Set "kind delay" for accessibility reason. + * + * @return PeriodicWorkRequest + * + * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION + */ + private fun buildBackgroundNoisePeriodicWork() = + PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>( + BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION, + TimeUnit.HOURS + ) + .addTag(BACKGROUND_NOISE_PERIODIC_WORKER_TAG) + .setInitialDelay( + BackgroundConstants.KIND_DELAY, + TimeUnit.SECONDS + ).setBackoffCriteria( + BackoffPolicy.LINEAR, + BackgroundConstants.KIND_DELAY, + TimeUnit.MINUTES + ) + .build() + + companion object { + /** + * Tag for background noise playbook periodic work + */ + const val BACKGROUND_NOISE_PERIODIC_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER" + + /** + * Tag for background noise playbook one time work + */ + const val BACKGROUND_NOISE_ONE_TIME_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER" + + /** + * Unique name for background noise playbook periodic work + */ + const val BACKGROUND_NOISE_PERIODIC_WORK_NAME = "BackgroundNoisePeriodicWork" + + /** + * Unique name for background noise playbook one time work + */ + const val BACKGROUND_NOISE_ONE_TIME_WORK_NAME = "BackgroundNoiseOneTimeWork" + + private const val TAG = "NoiseScheduler" + } +} 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 66ecae8a8c6a1d467ce314b3c71db8ab90ef3893..9c415cdc5e307527452650674c60ee45f184a5e8 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 @@ -3,12 +3,12 @@ package de.rki.coronawarnapp.diagnosiskeys.download import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.ExposureDetectionConfig +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.diagnosiskeys.server.LocationCode import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection import de.rki.coronawarnapp.risk.RollbackItem -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -34,7 +34,7 @@ class DownloadDiagnosisKeysTask @Inject constructor( private val keyPackageSyncTool: KeyPackageSyncTool, private val timeStamper: TimeStamper, private val settings: DownloadDiagnosisKeysSettings, - private val submissionSettings: SubmissionSettings + private val coronaTestRepository: CoronaTestRepository, ) : Task<DownloadDiagnosisKeysTask.Progress, Task.Result> { private val internalProgress = ConflatedBroadcastChannel<Progress>() @@ -114,7 +114,8 @@ class DownloadDiagnosisKeysTask @Inject constructor( // remember version code of this execution for next time settings.updateLastVersionCodeToCurrent() - if (submissionSettings.isAllowedToSubmitKeys) { + val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } + if (isAllowedToSubmitKeys) { Timber.tag(TAG).i("task aborted, positive test result") return object : Task.Result {} } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt index a6d942933d7aa3a595e3e5efa3a69d00337a53db..c0b7a2943500af8bcc434d11c042a15bfb5f7de5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationService.kt @@ -3,11 +3,11 @@ package de.rki.coronawarnapp.notification import android.content.Context import androidx.navigation.NavDeepLinkBuilder import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.ui.main.MainActivity import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppContext -import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.notifications.setContentTextExpandable import kotlinx.coroutines.flow.first import timber.log.Timber @@ -22,7 +22,7 @@ class TestResultAvailableNotificationService @Inject constructor( private val cwaSettings: CWASettings ) { - suspend fun showTestResultAvailableNotification(testResult: TestResult) { + suspend fun showTestResultAvailableNotification(testResult: CoronaTestResult) { Timber.d("showTestResultAvailableNotification(testResult=%s)", testResult) if (foregroundState.isInForeground.first()) { @@ -67,5 +67,5 @@ class TestResultAvailableNotificationService @Inject constructor( * By letting the forwarding happen via the PendingResultFragment, * we have a common location to retrieve the test result. */ - fun getNotificationDestination(testResult: TestResult): Int = R.id.submissionTestResultPendingFragment + fun getNotificationDestination(testResult: CoronaTestResult): Int = R.id.submissionTestResultPendingFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt index 57791940bf1a983cda707f6804b8c0d6aee22db4..4a0918b645e62d98680c0bf8a0a83b98bbc5ac25 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/BackgroundNoise.kt @@ -1,25 +1,20 @@ package de.rki.coronawarnapp.playbook -import de.rki.coronawarnapp.submission.SubmissionSettings -import de.rki.coronawarnapp.worker.BackgroundConstants -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Singleton import kotlin.random.Random @Singleton class BackgroundNoise @Inject constructor( - private val submissionSettings: SubmissionSettings, + private val coronaTestRepository: CoronaTestRepository, private val playbook: Playbook, - private val backgroundWorkScheduler: BackgroundWorkScheduler ) { - fun scheduleDummyPattern() { - if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0) - backgroundWorkScheduler.scheduleBackgroundNoisePeriodicWork() - } suspend fun foregroundScheduleCheck() { - if (submissionSettings.isAllowedToSubmitKeys) { + val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } + if (isAllowedToSubmitKeys) { val chance = Random.nextFloat() * 100 if (chance < DefaultPlaybook.PROBABILITY_TO_EXECUTE_PLAYBOOK_ON_APP_OPEN) { playbook.dummy() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt index 3c432bc42d25dbde66ce663dc127508c11be47fc..a678e0686faa1ad1c067a43d7b140350094eb199 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt @@ -1,11 +1,11 @@ package de.rki.coronawarnapp.playbook +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.coronatest.server.VerificationServer import de.rki.coronawarnapp.exception.TanPairingException import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.submission.server.SubmissionServer -import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.verification.server.VerificationKeyType -import de.rki.coronawarnapp.verification.server.VerificationServer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -28,7 +28,7 @@ class DefaultPlaybook @Inject constructor( override suspend fun initialRegistration( key: String, keyType: VerificationKeyType - ): Pair<String, TestResult> { + ): Pair<String, CoronaTestResult> { Timber.i("[$uid] New Initial Registration Playbook") // real registration @@ -43,7 +43,7 @@ class DefaultPlaybook @Inject constructor( // if the registration succeeded continue with the real test result retrieval // if it failed, execute a dummy request to satisfy the required playbook pattern val (testResult, testResultException) = if (registrationToken != null) { - executeCapturingExceptions { verificationServer.retrieveTestResults(registrationToken) } + executeCapturingExceptions { verificationServer.pollTestResult(registrationToken) } } else { ignoreExceptions { verificationServer.retrieveTanFake() } null to null @@ -56,18 +56,18 @@ class DefaultPlaybook @Inject constructor( // if registration and test result retrieval succeeded, return the result if (registrationToken != null && testResult != null) - return registrationToken to TestResult.fromInt(testResult) + return registrationToken to testResult // else propagate the exception of either the first or the second step propagateException(registrationException, testResultException) } - override suspend fun testResult(registrationToken: String): TestResult { + override suspend fun testResult(registrationToken: String): CoronaTestResult { Timber.i("[$uid] New Test Result Playbook") // real test result val (testResult, exception) = - executeCapturingExceptions { verificationServer.retrieveTestResults(registrationToken) } + executeCapturingExceptions { verificationServer.pollTestResult(registrationToken) } // fake verification ignoreExceptions { verificationServer.retrieveTanFake() } @@ -77,7 +77,7 @@ class DefaultPlaybook @Inject constructor( coroutineScope.launch { followUpPlaybooks() } - return testResult?.let { TestResult.fromInt(it) } ?: propagateException(exception) + return testResult ?: propagateException(exception) } override suspend fun submit( 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 3da7b683d0611d94e64d51ecc0f4af5f75f80f4b..b46a6dda6ad0c9bd3cf8d502a40df4ae2a6e81c7 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 @@ -1,9 +1,9 @@ package de.rki.coronawarnapp.playbook +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass -import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.verification.server.VerificationKeyType /** * The concept of Plausible Deniability aims to hide the existence of a positive test result by always using a defined @@ -20,14 +20,14 @@ import de.rki.coronawarnapp.verification.server.VerificationKeyType interface Playbook { /** - * @return pair of Registration token [String] & [TestResult] + * @return pair of Registration token [String] & [CoronaTestResult] */ suspend fun initialRegistration( key: String, keyType: VerificationKeyType - ): Pair<String, TestResult> + ): Pair<String, CoronaTestResult> - suspend fun testResult(registrationToken: String): TestResult + suspend fun testResult(registrationToken: String): CoronaTestResult suspend fun submit(data: SubmissionData) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckIn.kt index 6330d8dd0d464fb13c05cd81ba5bb7fdc7c8fb52..04485e66cd82f0b0b628779ced082b203365b01b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckIn.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckIn.kt @@ -24,6 +24,7 @@ data class CheckIn( val completed: Boolean, val createJournalEntry: Boolean, val isSubmitted: Boolean = false, + val hasSubmissionConsent: Boolean = false, ) { /** * Returns SHA-256 hash of [traceLocationId] which itself may also be SHA-256 hash. @@ -51,4 +52,5 @@ fun CheckIn.toEntity() = TraceLocationCheckInEntity( completed = completed, createJournalEntry = createJournalEntry, isSubmitted = isSubmitted, + hasSubmissionConsent = hasSubmissionConsent, ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepository.kt index 4528fc1138c0c111aae3e822d569e730f472e046..6985100faabe2d8fce22e1551c7f5adbc8d96e57 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepository.kt @@ -65,10 +65,14 @@ class CheckInRepository @Inject constructor( checkInDao.updateEntityById(checkInId, update) } - suspend fun markCheckInAsSubmitted(checkInId: Long) { + suspend fun updatePostSubmissionFlags(checkInId: Long) { Timber.d("markCheckInAsSubmitted(checkInId=$checkInId)") checkInDao.updateEntity( - TraceLocationCheckInEntity.SubmissionUpdate(checkInId = checkInId, isSubmitted = true) + TraceLocationCheckInEntity.SubmissionUpdate( + checkInId = checkInId, + isSubmitted = true, + hasSubmissionConsent = false, + ) ) } @@ -88,4 +92,15 @@ class CheckInRepository @Inject constructor( return checkIn.toCheckIn() } + + suspend fun updateSubmissionConsents(checkInIds: Collection<Long>, consent: Boolean) { + Timber.d("updateSubmissionConsents(checkInIds=%s, consent=%b)", checkInIds, consent) + val consentUpdates = checkInIds.map { + TraceLocationCheckInEntity.SubmissionConsentUpdate( + checkInId = it, + hasSubmissionConsent = consent + ) + } + checkInDao.updateSubmissionConsents(consentUpdates) + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..d146560bef7b273da74e3f020f838ca100cc63d2 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt @@ -0,0 +1,13 @@ +package de.rki.coronawarnapp.presencetracing.checkins.common + +import de.rki.coronawarnapp.presencetracing.checkins.CheckIn +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import kotlinx.coroutines.flow.map + +/** + * Returns completed [CheckIn]s only + */ +val CheckInRepository.completedCheckIns + get() = checkInsWithinRetention.map { checkIns -> + checkIns.filter { checkIn -> checkIn.completed } + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt index 06b72ed4620c465999247db75079fae56aef49ee..f906c47bbd65bf57708ba0578b4105bcca44614f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapper.kt @@ -6,22 +6,35 @@ import de.rki.coronawarnapp.risk.DefaultRiskLevels.Companion.inRange import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.mapToRiskState import kotlinx.coroutines.flow.first +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import timber.log.Timber import javax.inject.Inject +import javax.inject.Singleton +@Singleton class PresenceTracingRiskMapper @Inject constructor( private val configProvider: AppConfigProvider ) { private var presenceTracingRiskCalculationParamContainer: PresenceTracingRiskCalculationParamContainer? = null + private val mutex = Mutex() + + suspend fun clearConfig() { + mutex.withLock { + Timber.tag(TAG).i("Clearing config params.") + presenceTracingRiskCalculationParamContainer = null + } + } + suspend fun lookupTransmissionRiskValue(transmissionRiskLevel: Int): Double { - return getTransmissionRiskValueMapping()?.find { + return getTransmissionRiskValueMapping().find { (it.transmissionRiskLevel == transmissionRiskLevel) }?.transmissionRiskValue ?: 0.0 } suspend fun lookupRiskStatePerDay(normalizedTime: Double): RiskState { - return getNormalizedTimePerDayToRiskLevelMapping()?.find { + return getNormalizedTimePerDayToRiskLevelMapping().find { it.normalizedTimeRange.inRange(normalizedTime) } ?.riskLevel @@ -29,7 +42,7 @@ class PresenceTracingRiskMapper @Inject constructor( } suspend fun lookupRiskStatePerCheckIn(normalizedTime: Double): RiskState { - return getNormalizedTimePerCheckInToRiskLevelMapping()?.find { + return getNormalizedTimePerCheckInToRiskLevelMapping().find { it.normalizedTimeRange.inRange(normalizedTime) } ?.riskLevel @@ -37,20 +50,26 @@ class PresenceTracingRiskMapper @Inject constructor( } private suspend fun getTransmissionRiskValueMapping() = - getRiskCalculationParameters()?.transmissionRiskValueMapping + getRiskCalculationParameters().transmissionRiskValueMapping private suspend fun getNormalizedTimePerDayToRiskLevelMapping() = - getRiskCalculationParameters()?.normalizedTimePerDayToRiskLevelMapping + getRiskCalculationParameters().normalizedTimePerDayToRiskLevelMapping private suspend fun getNormalizedTimePerCheckInToRiskLevelMapping() = - getRiskCalculationParameters()?.normalizedTimePerCheckInToRiskLevelMapping + getRiskCalculationParameters().normalizedTimePerCheckInToRiskLevelMapping - private suspend fun getRiskCalculationParameters(): PresenceTracingRiskCalculationParamContainer? { - if (presenceTracingRiskCalculationParamContainer == null) { - presenceTracingRiskCalculationParamContainer = - configProvider.currentConfig.first().presenceTracing.riskCalculationParameters - Timber.d(presenceTracingRiskCalculationParamContainer.toString()) + private suspend fun getRiskCalculationParameters(): PresenceTracingRiskCalculationParamContainer = mutex.withLock { + presenceTracingRiskCalculationParamContainer.let { + if (it == null) { + val newParams = configProvider.currentConfig.first().presenceTracing.riskCalculationParameters + Timber.d("New params %s", newParams) + presenceTracingRiskCalculationParamContainer = newParams + newParams + } else { + it + } } - return presenceTracingRiskCalculationParamContainer } } + +private const val TAG = "PtRiskMapper" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt index 3cf08b5ca2ff59ea8492fe04e1d1dfbcc77ec718..b5c6b195dc3b54b186f8f5d71edd1c8a7a39fb64 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTask.kt @@ -2,10 +2,11 @@ package de.rki.coronawarnapp.presencetracing.risk.execution import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.reportProblem -import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskMapper import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository import de.rki.coronawarnapp.presencetracing.warning.download.TraceWarningPackageSyncTool import de.rki.coronawarnapp.presencetracing.warning.storage.TraceWarningRepository @@ -30,6 +31,7 @@ class PresenceTracingWarningTask @Inject constructor( private val presenceTracingRiskRepository: PresenceTracingRiskRepository, private val traceWarningRepository: TraceWarningRepository, private val checkInsRepository: CheckInRepository, + private val presenceTracingRiskMapper: PresenceTracingRiskMapper ) : Task<PresenceTracingWarningTaskProgress, PresenceTracingWarningTask.Result> { private val internalProgress = ConflatedBroadcastChannel<PresenceTracingWarningTaskProgress>() @@ -60,6 +62,9 @@ class PresenceTracingWarningTask @Inject constructor( val nowUTC = timeStamper.nowUTC checkCancel() + Timber.tag(TAG).d("Resetting config to make sure latest changes are considered.") + presenceTracingRiskMapper.clearConfig() + Timber.tag(TAG).d("Syncing packages.") internalProgress.send(PresenceTracingWarningTaskProgress.Downloading()) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt index 98fcb83d2fbdc5c01473f1fff3250c5cb28d9341..5d0186469dea987b9fbb9d43b5bbc7d10a6ba045 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/risk/storage/PresenceTracingRiskRepository.kt @@ -9,12 +9,12 @@ import androidx.room.OnConflictStrategy.REPLACE import androidx.room.PrimaryKey import androidx.room.Query import androidx.room.TypeConverter -import de.rki.coronawarnapp.presencetracing.storage.entity.TraceLocationCheckInEntity import de.rki.coronawarnapp.presencetracing.risk.PtRiskLevelResult import de.rki.coronawarnapp.presencetracing.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningOverlap import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingDayRisk import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskCalculator +import de.rki.coronawarnapp.presencetracing.storage.entity.TraceLocationCheckInEntity import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import de.rki.coronawarnapp.util.TimeStamper @@ -31,7 +31,7 @@ import javax.inject.Singleton class PresenceTracingRiskRepository @Inject constructor( private val presenceTracingRiskCalculator: PresenceTracingRiskCalculator, private val databaseFactory: PresenceTracingRiskDatabase.Factory, - private val timeStamper: TimeStamper, + private val timeStamper: TimeStamper ) { private val database by lazy { @@ -144,9 +144,15 @@ class PresenceTracingRiskRepository @Inject constructor( get() = timeStamper.nowUTC.minus(Days.days(15).toStandardDuration()) suspend fun clearAllTables() { + Timber.i("Deleting all matches and results.") traceTimeIntervalMatchDao.deleteAll() riskLevelResultDao.deleteAll() } + + suspend fun clearResults() { + Timber.i("Deleting all results.") + riskLevelResultDao.deleteAll() + } } /* diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/TraceLocationDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/TraceLocationDatabase.kt index 7e6facc03e154d5eeb5505ec3743cc8f6b51c61c..d17f1ccc6a1d5d0c687ad0e623d2384c4f85cb8b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/TraceLocationDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/TraceLocationDatabase.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.presencetracing.storage.dao.TraceLocationDao import de.rki.coronawarnapp.presencetracing.storage.entity.TraceLocationCheckInEntity import de.rki.coronawarnapp.presencetracing.storage.entity.TraceLocationConverters import de.rki.coronawarnapp.presencetracing.storage.entity.TraceLocationEntity +import de.rki.coronawarnapp.presencetracing.storage.migration.PresenceTracingDatabaseMigration1To2 import de.rki.coronawarnapp.util.database.CommonConverters import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Inject @@ -19,7 +20,7 @@ import javax.inject.Inject TraceLocationCheckInEntity::class, TraceLocationEntity::class ], - version = 1, + version = 2, exportSchema = true ) @TypeConverters(CommonConverters::class, TraceLocationConverters::class) @@ -29,8 +30,9 @@ abstract class TraceLocationDatabase : RoomDatabase() { abstract fun traceLocationDao(): TraceLocationDao class Factory @Inject constructor(@AppContext private val context: Context) { - fun create() = Room - .databaseBuilder(context, TraceLocationDatabase::class.java, TRACE_LOCATIONS_DATABASE_NAME) + fun create(databaseName: String = TRACE_LOCATIONS_DATABASE_NAME): TraceLocationDatabase = Room + .databaseBuilder(context, TraceLocationDatabase::class.java, databaseName) + .addMigrations(PresenceTracingDatabaseMigration1To2) .build() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/dao/CheckInDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/dao/CheckInDao.kt index 1d56e9455d19f8e7c9336692f0b6b588dea121f3..01261763e56279392b7b2ce8973ecfd69235ecc9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/dao/CheckInDao.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/dao/CheckInDao.kt @@ -40,6 +40,9 @@ interface CheckInDao { @Update(entity = TraceLocationCheckInEntity::class) suspend fun updateEntity(update: TraceLocationCheckInEntity.SubmissionUpdate) + @Update(entity = TraceLocationCheckInEntity::class) + suspend fun updateSubmissionConsents(update: Collection<TraceLocationCheckInEntity.SubmissionConsentUpdate>) + @Query("DELETE FROM checkin") suspend fun deleteAll() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/entity/TraceLocationCheckInEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/entity/TraceLocationCheckInEntity.kt index 0d9fda64de57a889e6ca43212f32df7194a2a5ab..582d5f7ac85614fb82033861c65cea384c06a07c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/entity/TraceLocationCheckInEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/entity/TraceLocationCheckInEntity.kt @@ -25,12 +25,20 @@ data class TraceLocationCheckInEntity( @ColumnInfo(name = "completed") val completed: Boolean, @ColumnInfo(name = "createJournalEntry") val createJournalEntry: Boolean, @ColumnInfo(name = "submitted") val isSubmitted: Boolean, + @ColumnInfo(name = "submissionConsent") val hasSubmissionConsent: Boolean, ) { @Entity data class SubmissionUpdate( @PrimaryKey @ColumnInfo(name = "id") val checkInId: Long, @ColumnInfo(name = "submitted") val isSubmitted: Boolean, + @ColumnInfo(name = "submissionConsent") val hasSubmissionConsent: Boolean, + ) + + @Entity + data class SubmissionConsentUpdate( + @PrimaryKey @ColumnInfo(name = "id") val checkInId: Long, + @ColumnInfo(name = "submissionConsent") val hasSubmissionConsent: Boolean, ) } @@ -50,5 +58,6 @@ fun TraceLocationCheckInEntity.toCheckIn() = CheckIn( checkInEnd = checkInEnd, completed = completed, createJournalEntry = createJournalEntry, - isSubmitted = isSubmitted + isSubmitted = isSubmitted, + hasSubmissionConsent = hasSubmissionConsent ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/migration/PresenceTracingDatabaseMigration1To2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/migration/PresenceTracingDatabaseMigration1To2.kt new file mode 100644 index 0000000000000000000000000000000000000000..97d046ec0f870729b54b13715a9bda1716c9e022 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/storage/migration/PresenceTracingDatabaseMigration1To2.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.presencetracing.storage.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import de.rki.coronawarnapp.exception.ExceptionCategory +import de.rki.coronawarnapp.exception.reporting.report +import timber.log.Timber + +/** + * Migrates the presence tracing database from version 1 to 2. + * The additional attribute: + * PresenceTracingCheckInEntity.submissionConsent was added + */ +object PresenceTracingDatabaseMigration1To2 : Migration(1, 2) { + + override fun migrate(database: SupportSQLiteDatabase) { + try { + Timber.i("Attempting migration 1->2...") + performMigration(database) + Timber.i("Migration 1->2 successful.") + } catch (e: Exception) { + Timber.e(e, "Migration 1->2 failed") + e.report(ExceptionCategory.INTERNAL, "PresenceTracing database migration failed.") + throw e + } + } + + private fun performMigration(database: SupportSQLiteDatabase) = with(database) { + Timber.d("Running MIGRATION_1_2") + + migrateTraceLocationCheckInEntity() + } + + private val migrateTraceLocationCheckInEntity: SupportSQLiteDatabase.() -> Unit = { + Timber.d("Table 'checkin': Add column 'submissionConsent'") + execSQL("ALTER TABLE `checkin` ADD COLUMN `submissionConsent` INTEGER NOT NULL DEFAULT '0'") + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt index 123f45edceef714e0fefff56dff06bb8dc7f6f73..799cf04a61366a289895429d79a1fb9adfe50345 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetector.kt @@ -4,13 +4,13 @@ import android.content.Context import androidx.annotation.VisibleForTesting import androidx.core.app.NotificationManagerCompat import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.di.AppContext @@ -35,7 +35,7 @@ class RiskLevelChangeDetector @Inject constructor( private val foregroundState: ForegroundState, private val notificationHelper: GeneralNotifications, private val surveys: Surveys, - private val submissionSettings: SubmissionSettings, + private val coronaTestRepository: CoronaTestRepository, private val tracingSettings: TracingSettings, private val testResultDonorSettings: TestResultDonorSettings ) { @@ -146,7 +146,8 @@ class RiskLevelChangeDetector @Inject constructor( oldRiskState: RiskState, newRiskState: RiskState ) { - if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !submissionSettings.isSubmissionSuccessful) { + val isSubmissionSuccessful = coronaTestRepository.coronaTests.first().any { it.isSubmitted } + if (hasHighLowLevelChanged(oldRiskState, newRiskState) && !isSubmissionSuccessful) { Timber.d("Notification Permission = ${notificationManagerCompat.areNotificationsEnabled()}") if (!foregroundState.isInForeground.first()) { 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 c79f789b7d07d76b8132f1adea7cb3fd3c114bb6..d81f7eff26394e0c622943efb89dde6e84aab444 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 @@ -5,6 +5,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureWindow import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData import de.rki.coronawarnapp.appconfig.ExposureWindowRiskCalculationConfig +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.datadonation.analytics.modules.exposurewindows.AnalyticsExposureWindowCollector import de.rki.coronawarnapp.diagnosiskeys.storage.KeyCacheRepository import de.rki.coronawarnapp.exception.ExceptionCategory @@ -16,7 +17,6 @@ import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut import de.rki.coronawarnapp.risk.EwRiskLevelResult.FailureReason import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.task.TaskFactory @@ -46,7 +46,7 @@ class RiskLevelTask @Inject constructor( private val appConfigProvider: AppConfigProvider, private val riskLevelStorage: RiskLevelStorage, private val keyCacheRepository: KeyCacheRepository, - private val submissionSettings: SubmissionSettings, + private val coronaTestRepository: CoronaTestRepository, private val analyticsExposureWindowCollector: AnalyticsExposureWindowCollector, private val autoCheckOut: AutoCheckOut, ) : Task<DefaultProgress, EwRiskLevelTaskResult> { @@ -91,7 +91,9 @@ class RiskLevelTask @Inject constructor( Timber.d("The current time is %s", it) } - if (submissionSettings.isAllowedToSubmitKeys && submissionSettings.hasViewedTestResult.value) { + val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } + val hasViewedTestResult = coronaTestRepository.coronaTests.first().any { it.isViewed } + if (isAllowedToSubmitKeys && hasViewedTestResult) { Timber.i("Positive test result and user has seen it, skip risk calculation") return EwRiskLevelTaskResult( calculatedAt = nowUTC, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt index 0582cc3c9579abb25f40c3ecb13a79bbd617f2c9..800757c75b5c3e32104b6d769e6d1901d8ec6dbe 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/BaseRiskLevelStorage.kt @@ -224,9 +224,17 @@ abstract class BaseRiskLevelStorage constructor( override suspend fun clear() { Timber.w("clear() - Clearing stored risklevel/exposure-detection results.") database.clearAllTables() + Timber.w("clear() - Clearing stored presence tracing matches and results.") presenceTracingRiskRepository.clearAllTables() } + override suspend fun clearResults() { + Timber.w("clearResults() - Clearing stored risklevel/exposure-detection results.") + database.clearAllTables() + Timber.w("clearResults() - Clearing stored presence tracing results.") + presenceTracingRiskRepository.clearResults() + } + companion object { private const val TAG = "RiskLevelStorage" } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt index a72b458ba00fde938c47d01db704dc33001864d5..e0d42b72dbb3806ec60fbeca0e4aa060a336e957 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/risk/storage/RiskLevelStorage.kt @@ -79,4 +79,6 @@ interface RiskLevelStorage { suspend fun storeResult(resultEw: EwRiskLevelResult) suspend fun clear() + + suspend fun clearResults() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt deleted file mode 100644 index 2a054d0ebf1f23453ef26ca5d942235368fa8e63..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/service/submission/QRScanResult.kt +++ /dev/null @@ -1,28 +0,0 @@ -package de.rki.coronawarnapp.service.submission - -import java.util.regex.Pattern - -data class QRScanResult(val rawResult: String) { - - val isValid: Boolean - get() = guid != null - val guid: String? by lazy { extractGUID(rawResult) } - - private fun extractGUID(rawResult: String): String? { - if (!QR_CODE_REGEX.toRegex().matches(rawResult)) return null - - val matcher = QR_CODE_REGEX.matcher(rawResult) - return if (matcher.matches()) matcher.group(1) else null - } - - companion object { - // regex pattern for scanned QR code URL - val QR_CODE_REGEX: Pattern = ( - "^" + // Match start of string - "(?:https:\\/{2}localhost)" + // Match `https://localhost` - "(?:\\/{1}\\?)" + // Match the query param `/?` - "([a-f\\d]{6}[-][a-f\\d]{8}[-](?:[a-f\\d]{4}[-]){3}[a-f\\d]{12})" + // Match the UUID - "\$" - ).toPattern(Pattern.CASE_INSENSITIVE) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt index 63f4739e96fc9ebde92732f85661316e1d373638..4b632162771c9924ad35838a8b3aeb79e291230f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/storage/TracingSettings.kt @@ -5,6 +5,7 @@ import androidx.core.content.edit import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.preferences.clearAndNotify import de.rki.coronawarnapp.util.preferences.createFlowPreference +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -21,13 +22,15 @@ class TracingSettings @Inject constructor(@AppContext private val context: Conte putBoolean(TRACING_ACTIVATION_TIMESTAMP, value) } - var initialPollingForTestResultTimeStamp: Long + @Deprecated("Use CoronaTestRepository") + var initialPollingForTestResultTimeStampMigration: Long get() = prefs.getLong(TRACING_POOLING_TIMESTAMP, 0L) set(value) = prefs.edit(true) { putLong(TRACING_POOLING_TIMESTAMP, value) } - var isTestResultAvailableNotificationSent: Boolean + @Deprecated("Use CoronaTestRepository") + var isTestResultAvailableNotificationSentMigration: Boolean get() = prefs.getBoolean(TEST_RESULT_NOTIFICATION_SENT, false) set(value) = prefs.edit(true) { putBoolean(TEST_RESULT_NOTIFICATION_SENT, value) @@ -38,6 +41,31 @@ class TracingSettings @Inject constructor(@AppContext private val context: Conte defaultValue = false ) + fun deleteLegacyTestData() { +// Sourced from the behavior of SubmissionRepository.removeTestFromDevice() +// fun removeTestFromDevice() { +// submissionSettings.hasViewedTestResult.update { false } +// submissionSettings.hasGivenConsent.update { false } +// revokeConsentToSubmission() +// submissionSettings.registrationToken.update { null } +// submissionSettings.devicePairingSuccessfulAt = null +// tracingSettings.initialPollingForTestResultTimeStamp = 0L +// submissionSettings.initialTestResultReceivedAt = null +// submissionSettings.isAllowedToSubmitKeys = false +// tracingSettings.isTestResultAvailableNotificationSent = false +// submissionSettings.isSubmissionSuccessful = false +// testResultDataCollector.clear() +// } + Timber.d("deleteLegacyTestData()") + prefs.edit { + remove(TEST_RESULT_NOTIFICATION_SENT) + remove(TRACING_POOLING_TIMESTAMP) + } + + // TODO No longer needed, was for worker control? + // tracingSettings.initialPollingForTestResultTimeStamp = 0L + } + fun clear() = prefs.clearAndNotify() companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt index 29a64f31616a0e3b6f3e0ee5ca98b6d8b5007a8f..155f7c318ef9c79cc23ebee375b77792b0bfa56a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/SubmissionRepository.kt @@ -1,237 +1,128 @@ package de.rki.coronawarnapp.submission -import androidx.annotation.VisibleForTesting -import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector -import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler -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.storage.TracingSettings +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.TestRegistrationRequest +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage 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.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.flow.first +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import timber.log.Timber -import java.util.Date import javax.inject.Inject import javax.inject.Singleton @Suppress("LongParameterList") @Singleton class SubmissionRepository @Inject constructor( - private val submissionSettings: SubmissionSettings, - private val submissionService: SubmissionService, @AppScope private val scope: CoroutineScope, - private val timeStamper: TimeStamper, + private val submissionSettings: SubmissionSettings, private val tekHistoryStorage: TEKHistoryStorage, - private val deadmanNotificationScheduler: DeadmanNotificationScheduler, - private val backgroundNoise: BackgroundNoise, - private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, - private val tracingSettings: TracingSettings, - private val testResultDataCollector: TestResultDataCollector, - private val backgroundWorkScheduler: BackgroundWorkScheduler, + private val coronaTestRepository: CoronaTestRepository, ) { - private val testResultReceivedDateFlowInternal = - MutableStateFlow((submissionSettings.initialTestResultReceivedAt ?: timeStamper.nowUTC).toDate()) - val testResultReceivedDateFlow: Flow<Date> = testResultReceivedDateFlowInternal - - 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 - val hasViewedTestResult = submissionSettings.hasViewedTestResult.flow - val currentSymptoms = submissionSettings.symptoms + val pcrTest: Flow<PCRCoronaTest?> = coronaTestRepository.coronaTests.map { tests -> + tests.singleOrNull { it.type == CoronaTest.Type.PCR } as? PCRCoronaTest + } - private val testResultFlow = MutableStateFlow<TestResult?>(null) + val raTest: Flow<RACoronaTest?> = coronaTestRepository.coronaTests.map { tests -> + tests.singleOrNull { it.type == CoronaTest.Type.RAPID_ANTIGEN } as? RACoronaTest + } - // to be used by new submission flow screens - fun giveConsentToSubmission() { - Timber.d("giveConsentToSubmission()") - submissionSettings.hasGivenConsent.update { - true - } + fun testForType(type: CoronaTest.Type) = when (type) { + CoronaTest.Type.PCR -> pcrTest + CoronaTest.Type.RAPID_ANTIGEN -> raTest } + val currentSymptoms = submissionSettings.symptoms + // to be used by new submission flow screens - fun revokeConsentToSubmission() { - Timber.d("revokeConsentToSubmission()") - submissionSettings.hasGivenConsent.update { - false - } - } + fun giveConsentToSubmission(type: CoronaTest.Type) { + Timber.tag(TAG).v("giveConsentToSubmission(type=%s)", type) + scope.launch { + val test = coronaTestRepository.coronaTests.first().singleOrNull { it.type == type } + ?: throw IllegalStateException("No test of type $type available") - // to be set to true once the user has opened and viewed their test result - fun setViewedTestResult() { - Timber.d("setViewedTestResult()") - submissionSettings.hasViewedTestResult.update { - true + coronaTestRepository.updateConsent(identifier = test.identifier, consented = true) } } - fun refreshDeviceUIState(refreshTestResult: Boolean = true) { - if (submissionSettings.isSubmissionSuccessful) { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) - return - } - - val registrationToken = submissionSettings.registrationToken.value - if (registrationToken == null) { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) - return - } + // to be used by new submission flow screens + fun revokeConsentToSubmission(type: CoronaTest.Type) { + Timber.tag(TAG).v("revokeConsentToSubmission(type=%s)", type) + scope.launch { + val test = coronaTestRepository.coronaTests.first().singleOrNull { it.type == type } + ?: throw IllegalStateException("No test of type $type available") - if (submissionSettings.isAllowedToSubmitKeys) { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE) - return + coronaTestRepository.updateConsent(identifier = test.identifier, consented = false) } + } - var refresh = refreshTestResult - 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") - } - } + // to be set to true once the user has opened and viewed their test result + fun setViewedTestResult(type: CoronaTest.Type) { + Timber.tag(TAG).v("setViewedTestResult(type=%s)", type) + scope.launch { + val test = coronaTestRepository.coronaTests.first().singleOrNull { it.type == type } + ?: throw IllegalStateException("No test of type $type available") - if (refresh) { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestStarted - - scope.launch { - try { - deviceUIStateFlowInternal.value = - NetworkRequestWrapper.RequestSuccessful(fetchTestResult(registrationToken)) - } catch (err: CwaWebException) { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestFailed(err) - } catch (err: Exception) { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestFailed(err) - err.report(ExceptionCategory.INTERNAL) - } - } - } else { - deviceUIStateFlowInternal.value = - NetworkRequestWrapper.RequestSuccessful(deriveUiState(testResultFlow.value)) + coronaTestRepository.markAsViewed(identifier = test.identifier) } } - suspend fun asyncRegisterDeviceViaTAN(tan: String) { - analyticsKeySubmissionCollector.reset() - val registrationData = submissionService.asyncRegisterDeviceViaTAN(tan) - // START - Fix for EXPOSUREAPP-4484 relies on this call order - submissionSettings.registrationToken.update { - registrationData.registrationToken + fun refreshTest(type: CoronaTest.Type) { + Timber.tag(TAG).v("refreshTest(type=%s)", type) + + scope.launch { + coronaTestRepository.refresh(type = type) } - updateTestResult(registrationData.testResult) - submissionSettings.devicePairingSuccessfulAt = timeStamper.nowUTC - // END - backgroundNoise.scheduleDummyPattern() - analyticsKeySubmissionCollector.reportTestRegistered() - analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() } - suspend fun asyncRegisterDeviceViaGUID(guid: String): TestResult { - analyticsKeySubmissionCollector.reset() - val registrationData = submissionService.asyncRegisterDeviceViaGUID(guid) - // START - Fix for EXPOSUREAPP-4484 relies on this call order - submissionSettings.registrationToken.update { - registrationData.registrationToken - } - updateTestResult(registrationData.testResult) // This saves initial time - submissionSettings.devicePairingSuccessfulAt = timeStamper.nowUTC - // END - backgroundNoise.scheduleDummyPattern() - analyticsKeySubmissionCollector.reportTestRegistered() - testResultDataCollector.saveTestResultAnalyticsSettings(registrationData.testResult) // This saves received at - return registrationData.testResult + suspend fun registerTest(request: TestRegistrationRequest): CoronaTest { + Timber.tag(TAG).v("registerTest(request=%s)", request) + val coronaTest = coronaTestRepository.registerTest(request) + Timber.d("Registered test %s -> %s", request, coronaTest) + return coronaTest } suspend fun reset() { - deviceUIStateFlowInternal.value = NetworkRequestWrapper.RequestIdle + Timber.tag(TAG).v("reset()") tekHistoryStorage.clear() submissionSettings.clear() } - @VisibleForTesting - fun updateTestResult(testResult: TestResult) { - testResultFlow.value = testResult + fun removeTestFromDevice(type: CoronaTest.Type) { + Timber.tag(TAG).v("removeTestFromDevice(type=%s)", type) - testResultDataCollector.updatePendingTestResultReceivedTime(testResult) - - if (testResult == TestResult.POSITIVE) { - submissionSettings.isAllowedToSubmitKeys = true - analyticsKeySubmissionCollector.reportPositiveTestResultReceived() - deadmanNotificationScheduler.cancelScheduledWork() - } - - // https://jira-ibs.wbs.net.sap/browse/EXPOSUREAPP-4484 - // User removed a test before 1.11 where due to a bug the timestamp was not removed. - if (submissionSettings.initialTestResultReceivedAt != null && - submissionSettings.registrationToken.value != null && - submissionSettings.devicePairingSuccessfulAt == null - ) { - Timber.tag(TAG).w("User has stale initialTestResultReceivedAt, fixing EXPOSUREAPP-4484.") - submissionSettings.initialTestResultReceivedAt = null - } + scope.launch { + val test = coronaTestRepository.coronaTests.first().singleOrNull { it.type == type } + ?: throw IllegalStateException("No test of type $type available") - val initialTestResultReceivedTimestamp = submissionSettings.initialTestResultReceivedAt - - if (initialTestResultReceivedTimestamp == null) { - val currentTime = timeStamper.nowUTC - submissionSettings.initialTestResultReceivedAt = currentTime - testResultReceivedDateFlowInternal.value = currentTime.toDate() - if (testResult == TestResult.PENDING) { - backgroundWorkScheduler.startWorkScheduler() - } - } else { - testResultReceivedDateFlowInternal.value = initialTestResultReceivedTimestamp.toDate() + coronaTestRepository.removeTest(identifier = test.identifier) } } - private suspend fun fetchTestResult(registrationToken: String): DeviceUIState = try { - val testResult = submissionService.asyncRequestTestResult(registrationToken) - updateTestResult(testResult) - deriveUiState(testResult) - } catch (err: NoRegistrationTokenSetException) { - DeviceUIState.UNPAIRED - } - - fun removeTestFromDevice() { - submissionSettings.hasViewedTestResult.update { false } - submissionSettings.hasGivenConsent.update { false } - revokeConsentToSubmission() - submissionSettings.registrationToken.update { null } - submissionSettings.devicePairingSuccessfulAt = null - tracingSettings.initialPollingForTestResultTimeStamp = 0L - submissionSettings.initialTestResultReceivedAt = null - submissionSettings.isAllowedToSubmitKeys = false - tracingSettings.isTestResultAvailableNotificationSent = false - submissionSettings.isSubmissionSuccessful = false - testResultDataCollector.clear() - } - - 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 - } - companion object { private const val TAG = "SubmissionRepository" } } + +// TODO Temporary, mapping should be replaced with **[CoronaTest]** +fun CoronaTestResult?.toDeviceUIState(): DeviceUIState = when (this) { + CoronaTestResult.PCR_NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE + CoronaTestResult.PCR_POSITIVE -> DeviceUIState.PAIRED_POSITIVE + CoronaTestResult.PCR_OR_RAT_PENDING -> DeviceUIState.PAIRED_NO_RESULT + CoronaTestResult.PCR_REDEEMED -> DeviceUIState.PAIRED_REDEEMED + CoronaTestResult.PCR_INVALID -> DeviceUIState.PAIRED_ERROR + CoronaTestResult.RAT_PENDING -> DeviceUIState.PAIRED_NO_RESULT + CoronaTestResult.RAT_NEGATIVE -> DeviceUIState.PAIRED_NEGATIVE + CoronaTestResult.RAT_POSITIVE -> DeviceUIState.PAIRED_POSITIVE + CoronaTestResult.RAT_REDEEMED -> DeviceUIState.PAIRED_REDEEMED + CoronaTestResult.RAT_INVALID -> DeviceUIState.PAIRED_ERROR + null -> DeviceUIState.UNPAIRED +} 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 index 1b362ef1a3eb843e762cba992a45ea33a5557c7f..4a508248a1fb715acca97519726e447db723ad6f 100644 --- 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 @@ -11,6 +11,7 @@ import de.rki.coronawarnapp.util.preferences.createFlowPreference import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.adapter.RuntimeTypeAdapterFactory import org.joda.time.Instant +import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -37,36 +38,38 @@ class SubmissionSettings @Inject constructor( context.getSharedPreferences("submission_localdata", Context.MODE_PRIVATE) } - val registrationToken = prefs.createFlowPreference<String?>( - key = TEST_REGISTRATION_TOKEN, - defaultValue = null - ) + @Deprecated("Only available for migration, use CoronaTestRepository!") + var registrationTokenMigration: String? + get() = prefs.getString(TEST_REGISTRATION_TOKEN, null) + set(value) = prefs.edit { putString(TEST_REGISTRATION_TOKEN, value) } - var initialTestResultReceivedAt: Instant? + @Deprecated("Only available for migration, use CoronaTestRepository!") + var initialTestResultReceivedAtMigration: Instant? get() = prefs.getLong(TEST_RESULT_RECEIVED_AT, 0L).toInstantOrNull() set(value) = prefs.edit { putLong(TEST_RESULT_RECEIVED_AT, value?.millis ?: 0L) } - var devicePairingSuccessfulAt: Instant? + @Deprecated("Only available for migration, use CoronaTestRepository!") + var devicePairingSuccessfulAtMigration: Instant? get() = prefs.getLong(TEST_PARING_SUCCESSFUL_AT, 0L).toInstantOrNull() set(value) = prefs.edit { putLong(TEST_PARING_SUCCESSFUL_AT, value?.millis ?: 0L) } - var isSubmissionSuccessful: Boolean + @Deprecated("Only available for migration, use CoronaTestRepository!") + var isSubmissionSuccessfulMigration: Boolean get() = prefs.getBoolean(IS_KEY_SUBMISSION_SUCCESSFUL, false) set(value) = prefs.edit { putBoolean(IS_KEY_SUBMISSION_SUCCESSFUL, value) } - var isAllowedToSubmitKeys: Boolean + @Deprecated("Only available for migration, use CoronaTestRepository!") + var isAllowedToSubmitKeysMigration: Boolean get() = prefs.getBoolean(IS_KEY_SUBMISSION_ALLOWED, false) set(value) = prefs.edit { putBoolean(IS_KEY_SUBMISSION_ALLOWED, value) } - val hasGivenConsent = prefs.createFlowPreference( - key = SUBMISSION_CONSENT_GIVEN, - defaultValue = false - ) + @Deprecated("Only available for migration, use CoronaTestRepository!") + val hasGivenConsentMigration: Boolean + get() = prefs.getBoolean(SUBMISSION_CONSENT_GIVEN, false) - val hasViewedTestResult = prefs.createFlowPreference( - key = SUBMISSION_RESULT_VIEWED, - defaultValue = false - ) + @Deprecated("Only available for migration, use CoronaTestRepository!") + val hasViewedTestResultMigration: Boolean + get() = prefs.getBoolean(SUBMISSION_RESULT_VIEWED, false) val symptoms: FlowPreference<Symptoms?> = FlowPreference( prefs, @@ -105,6 +108,34 @@ class SubmissionSettings @Inject constructor( } ) + fun deleteLegacyTestData() { + Timber.d("deleteLegacyTestData()") +// Sourced from the behavior of SubmissionRepository.removeTestFromDevice() +// fun removeTestFromDevice() { +// submissionSettings.hasViewedTestResult.update { false } +// submissionSettings.hasGivenConsent.update { false } +// revokeConsentToSubmission() +// submissionSettings.registrationToken.update { null } +// submissionSettings.devicePairingSuccessfulAt = null +// tracingSettings.initialPollingForTestResultTimeStamp = 0L +// submissionSettings.initialTestResultReceivedAt = null +// submissionSettings.isAllowedToSubmitKeys = false +// tracingSettings.isTestResultAvailableNotificationSent = false +// submissionSettings.isSubmissionSuccessful = false +// testResultDataCollector.clear() +// } + + prefs.edit { + remove(SUBMISSION_RESULT_VIEWED) + remove(TEST_REGISTRATION_TOKEN) + remove(TEST_PARING_SUCCESSFUL_AT) + remove(TEST_RESULT_RECEIVED_AT) + remove(IS_KEY_SUBMISSION_ALLOWED) + remove(IS_KEY_SUBMISSION_SUCCESSFUL) + remove(SUBMISSION_CONSENT_GIVEN) + } + } + fun clear() = prefs.clearAndNotify() companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index db59a8c793d0cfcc7fa320e2f7ba56387628d8fd..49c162c39401af971dd5e0135121b419c30f55f2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt @@ -3,13 +3,15 @@ package de.rki.coronawarnapp.submission.task import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector -import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository -import de.rki.coronawarnapp.presencetracing.checkins.CheckInsTransformer -import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.checkins.CheckInsTransformer +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.submission.auto.AutoSubmission @@ -44,6 +46,7 @@ class SubmissionTask @Inject constructor( private val checkInsTransformer: CheckInsTransformer, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, private val backgroundWorkScheduler: BackgroundWorkScheduler, + private val coronaTestRepository: CoronaTestRepository, ) : Task<DefaultProgress, SubmissionTask.Result> { private val internalProgress = ConflatedBroadcastChannel<DefaultProgress>() @@ -66,8 +69,8 @@ class SubmissionTask @Inject constructor( inBackground = true } } - - if (!submissionSettings.hasGivenConsent.value) { + val hasGivenConsent = coronaTestRepository.coronaTests.first().any { it.isAdvancedConsentGiven } + if (!hasGivenConsent) { Timber.tag(TAG).w("Consent unavailable. Skipping execution, disabling auto submission.") autoSubmission.updateMode(AutoSubmission.Mode.DISABLED) return Result(state = Result.State.SKIPPED) @@ -124,8 +127,12 @@ class SubmissionTask @Inject constructor( } private suspend fun performSubmission(): Result { - val registrationToken = submissionSettings.registrationToken.value ?: throw NoRegistrationTokenSetException() - Timber.tag(TAG).d("Using registrationToken=$registrationToken") + val availableTests = coronaTestRepository.coronaTests.first() + Timber.tag(TAG).v("Available tests: %s", availableTests) + val coronaTest = availableTests.firstOrNull { it.isSubmissionAllowed && !it.isSubmitted } + ?: throw IllegalStateException("No valid test available to authorize submission") + + Timber.tag(TAG).d("Submission is authorized by coronaTest=%s", coronaTest) val keys: List<TemporaryExposureKey> = try { tekHistoryStorage.tekData.first().flatMap { it.keys } @@ -143,13 +150,15 @@ class SubmissionTask @Inject constructor( ) Timber.tag(TAG).d("Transformed keys with symptoms %s from %s to %s", symptoms, keys, transformedKeys) - val checkIns = checkInsRepository.checkInsWithinRetention.first() + val checkIns = checkInsRepository.completedCheckIns.first().filter { + it.hasSubmissionConsent && !it.isSubmitted + } val transformedCheckIns = checkInsTransformer.transform(checkIns, symptoms) Timber.tag(TAG).d("Transformed CheckIns from: %s to: %s", checkIns, transformedCheckIns) val submissionData = Playbook.SubmissionData( - registrationToken = registrationToken, + registrationToken = coronaTest.registrationToken, temporaryExposureKeys = transformedKeys, consentToFederation = true, visitedCountries = getSupportedCountries(), @@ -171,7 +180,7 @@ class SubmissionTask @Inject constructor( Timber.tag(TAG).d("Marking %d submitted CheckIns.", checkIns.size) checkIns.forEach { checkIn -> try { - checkInsRepository.markCheckInAsSubmitted(checkIn.id) + checkInsRepository.updatePostSubmissionFlags(checkIn.id) } catch (e: Exception) { e.reportProblem(TAG, "CheckIn $checkIn could not be marked as submitted") } @@ -179,15 +188,15 @@ class SubmissionTask @Inject constructor( autoSubmission.updateMode(AutoSubmission.Mode.DISABLED) - setSubmissionFinished() + setSubmissionFinished(coronaTest) return Result(state = Result.State.SUCCESSFUL) } - private fun setSubmissionFinished() { + private suspend fun setSubmissionFinished(coronaTest: CoronaTest) { Timber.tag(TAG).d("setSubmissionFinished()") backgroundWorkScheduler.stopWorkScheduler() - submissionSettings.isSubmissionSuccessful = true + coronaTestRepository.markAsSubmitted(coronaTest.identifier) backgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestErrorCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestErrorCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..7e652c7c9bf62d2da01e6ce02fb5efa50dcf4c13 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestErrorCard.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardErrorBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestErrorCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class PcrTestErrorCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardErrorBinding>(R.layout.home_card_container_layout, parent) { + + override val viewBinding = lazy { + HomeSubmissionPcrStatusCardErrorBinding.inflate( + layoutInflater, + itemView.findViewById(R.id.card_container), + true + ) + } + + override val onBindData: HomeSubmissionPcrStatusCardErrorBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + itemView.setOnClickListener { curItem.onDeleteTest(item) } + showTestAction.setOnClickListener { itemView.performClick() } + } + + data class Item( + val state: SubmissionStatePCR.TestError, + val onDeleteTest: (Item) -> Unit + ) : TestResultItem.PCR, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestInvalidCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestInvalidCard.kt similarity index 51% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestInvalidCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestInvalidCard.kt index 94d997b6a837972679991a405a0ca7ca7c29f5a2..28518361317f21ebf2e8781ae64a21389459bcba 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestInvalidCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestInvalidCard.kt @@ -2,20 +2,25 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardInvalidBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestInvalidCard.Item +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardInvalidBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestInvalidCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -class TestInvalidCard( +class PcrTestInvalidCard( parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardInvalidBinding>(R.layout.home_card_container_layout, parent) { +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardInvalidBinding>( + R.layout.home_card_container_layout, + parent +) { override val viewBinding = lazy { - HomeSubmissionStatusCardInvalidBinding.inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + HomeSubmissionPcrStatusCardInvalidBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) } - override val onBindData: HomeSubmissionStatusCardInvalidBinding.( + override val onBindData: HomeSubmissionPcrStatusCardInvalidBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, payloads -> @@ -24,9 +29,9 @@ class TestInvalidCard( } data class Item( - val state: TestInvalid, + val state: SubmissionStatePCR.TestInvalid, val onDeleteTest: (Item) -> Unit - ) : TestResultItem, HasPayloadDiffer { + ) : TestResultItem.PCR, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestNegativeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestNegativeCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..b9ae26372a44fea01df80384b7debbe5777396ab --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestNegativeCard.kt @@ -0,0 +1,36 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardNegativeBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestNegativeCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class PcrTestNegativeCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardNegativeBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionPcrStatusCardNegativeBinding.inflate( + layoutInflater, + itemView.findViewById(R.id.card_container), + true + ) + } + + override val onBindData: HomeSubmissionPcrStatusCardNegativeBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { _, _ -> } + + data class Item( + val state: SubmissionStatePCR.TestNegative + ) : TestResultItem.PCR, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestPendingCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestPendingCard.kt similarity index 53% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestPendingCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestPendingCard.kt index ae2591be8e6752ab862dcef6588c2ab3a0efdb5f..67ae2a7acb3040e2b93c1dbf92cb0b750e90f2e9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestPendingCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestPendingCard.kt @@ -2,20 +2,25 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardPendingBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestPendingCard.Item +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardPendingBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPendingCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -class TestPendingCard( +class PcrTestPendingCard( parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardPendingBinding>(R.layout.home_card_container_layout, parent) { +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardPendingBinding>( + R.layout.home_card_container_layout, + parent +) { override val viewBinding = lazy { - HomeSubmissionStatusCardPendingBinding.inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + HomeSubmissionPcrStatusCardPendingBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) } - override val onBindData: HomeSubmissionStatusCardPendingBinding.( + override val onBindData: HomeSubmissionPcrStatusCardPendingBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, payloads -> @@ -25,9 +30,9 @@ class TestPendingCard( } data class Item( - val state: TestPending, + val state: SubmissionStatePCR.TestPending, val onClickAction: (Item) -> Unit - ) : TestResultItem, HasPayloadDiffer { + ) : TestResultItem.PCR, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestPositiveCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestPositiveCard.kt similarity index 58% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestPositiveCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestPositiveCard.kt index c0bcd6cb232c6db6dcc8909c242504c7b9ecef77..327ea83f7ae04c1f08fd1b950e8950beca79e9d7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestPositiveCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestPositiveCard.kt @@ -2,24 +2,28 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardPositiveBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestPositiveCard.Item +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardPositiveNotSharedBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPositiveCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -class TestPositiveCard( +class PcrTestPositiveCard( parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardPositiveBinding>(R.layout.home_card_container_layout, parent) { +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardPositiveNotSharedBinding>( + R.layout.home_card_container_layout, + parent +) { override val viewBinding = lazy { - HomeSubmissionStatusCardPositiveBinding.inflate( + HomeSubmissionPcrStatusCardPositiveNotSharedBinding.inflate( layoutInflater, itemView.findViewById(R.id.card_container), true ) } - override val onBindData: HomeSubmissionStatusCardPositiveBinding.( + override val onBindData: HomeSubmissionPcrStatusCardPositiveNotSharedBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, payloads -> @@ -29,9 +33,9 @@ class TestPositiveCard( } data class Item( - val state: TestPositive, + val state: SubmissionStatePCR.TestPositive, val onClickAction: (Item) -> Unit - ) : TestResultItem, HasPayloadDiffer { + ) : TestResultItem.PCR, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestNegativeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestReadyCard.kt similarity index 54% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestNegativeCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestReadyCard.kt index 0762cf9c77bc48d90604e9f9ff680fffa34bcfce..4793dc7cebb11539c9fabcd56ee7dfe9a519cda9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestNegativeCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestReadyCard.kt @@ -2,24 +2,22 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardNegativeBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestNegativeCard.Item +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardReadyBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestReadyCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -class TestNegativeCard( +class PcrTestReadyCard( parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardNegativeBinding>(R.layout.home_card_container_layout, parent) { +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardReadyBinding>(R.layout.home_card_container_layout, parent) { override val viewBinding = lazy { - HomeSubmissionStatusCardNegativeBinding.inflate( - layoutInflater, - itemView.findViewById(R.id.card_container), - true - ) + HomeSubmissionPcrStatusCardReadyBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) } - override val onBindData: HomeSubmissionStatusCardNegativeBinding.( + override val onBindData: HomeSubmissionPcrStatusCardReadyBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, payloads -> @@ -29,9 +27,9 @@ class TestNegativeCard( } data class Item( - val state: TestNegative, + val state: SubmissionStatePCR.TestResultReady, val onClickAction: (Item) -> Unit - ) : TestResultItem, HasPayloadDiffer { + ) : TestResultItem.PCR, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..34ecfef89a27bc2c69769620493dc956f9eb07d6 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/PcrTestSubmissionDoneCard.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.databinding.HomeSubmissionPcrStatusCardPositiveSharedBinding +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestSubmissionDoneCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter + +class PcrTestSubmissionDoneCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionPcrStatusCardPositiveSharedBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionPcrStatusCardPositiveSharedBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + } + + override val onBindData: HomeSubmissionPcrStatusCardPositiveSharedBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { _, _ -> } + + data class Item( + val state: SubmissionStatePCR.SubmissionDone + ) : TestResultItem.PCR +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestErrorCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestErrorCard.kt similarity index 61% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestErrorCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestErrorCard.kt index a662b5e8e6bd6ac43a3efa10f61483ae2be65a02..2f0020df56626536d035874016adb097e1ef0ba4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestErrorCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestErrorCard.kt @@ -2,24 +2,27 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardErrorBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestErrorCard.Item +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardErrorBinding import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -class TestErrorCard( +class RapidTestErrorCard( parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardErrorBinding>(R.layout.home_card_container_layout, parent) { +) : HomeAdapter.HomeItemVH<RapidTestErrorCard.Item, HomeSubmissionRapidStatusCardErrorBinding>( + R.layout.home_card_container_layout, + parent +) { override val viewBinding = lazy { - HomeSubmissionStatusCardErrorBinding.inflate( + HomeSubmissionRapidStatusCardErrorBinding.inflate( layoutInflater, itemView.findViewById(R.id.card_container), true ) } - override val onBindData: HomeSubmissionStatusCardErrorBinding.( + override val onBindData: HomeSubmissionRapidStatusCardErrorBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, payloads -> @@ -29,9 +32,9 @@ class TestErrorCard( } data class Item( - val state: TestError, + val state: SubmissionStateRAT.TestError, val onDeleteTest: (Item) -> Unit - ) : TestResultItem, HasPayloadDiffer { + ) : TestResultItem.RA, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestInvalidCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestInvalidCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..729b65d5234877f1fb9e4feefde3f9fe680e6e34 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestInvalidCard.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardInvalidBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestInvalidCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class RapidTestInvalidCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardInvalidBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionRapidStatusCardInvalidBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + } + + override val onBindData: HomeSubmissionRapidStatusCardInvalidBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + deleteTestAction.setOnClickListener { curItem.onDeleteTest(item) } + } + + data class Item( + val state: SubmissionStateRAT.TestInvalid, + val onDeleteTest: (Item) -> Unit + ) : TestResultItem.RA, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestNegativeCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestNegativeCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..f8ae43ad4edb42a2d070f5cb21f3e250c36c8490 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestNegativeCard.kt @@ -0,0 +1,36 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardNegativeBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestNegativeCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class RapidTestNegativeCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardNegativeBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionRapidStatusCardNegativeBinding.inflate( + layoutInflater, + itemView.findViewById(R.id.card_container), + true + ) + } + + override val onBindData: HomeSubmissionRapidStatusCardNegativeBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { _, _ -> } + + data class Item( + val state: SubmissionStateRAT.TestNegative + ) : TestResultItem.RA, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestOutdatedCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestOutdatedCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..a35e11d70cfcd86a2560bf5b5cb0b996179f3279 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestOutdatedCard.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardOutdatedBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestOutdatedCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class RapidTestOutdatedCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardOutdatedBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionRapidStatusCardOutdatedBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + } + + override val onBindData: HomeSubmissionRapidStatusCardOutdatedBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + dontShowAnymoreButton.setOnClickListener { curItem.hideTest(item) } + } + + data class Item( + val state: SubmissionStateRAT.TestInvalid, + val hideTest: (Item) -> Unit + ) : TestResultItem.RA, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestPendingCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestPendingCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..85617c340f55642744bfca594b66fe7ea035c79c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestPendingCard.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardPendingBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPendingCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class RapidTestPendingCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardPendingBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionRapidStatusCardPendingBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + } + + override val onBindData: HomeSubmissionRapidStatusCardPendingBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + itemView.setOnClickListener { curItem.onClickAction(item) } + showTestAction.setOnClickListener { itemView.performClick() } + } + + data class Item( + val state: SubmissionStateRAT.TestPending, + val onClickAction: (Item) -> Unit + ) : TestResultItem.RA, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestPositiveCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestPositiveCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..e9c021499f4ee97962892bbbd762a18e92d7d77e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestPositiveCard.kt @@ -0,0 +1,41 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardPositiveNotSharedBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPositiveCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class RapidTestPositiveCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardPositiveNotSharedBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionRapidStatusCardPositiveNotSharedBinding.inflate( + layoutInflater, + itemView.findViewById(R.id.card_container), + true + ) + } + + override val onBindData: HomeSubmissionRapidStatusCardPositiveNotSharedBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + itemView.setOnClickListener { curItem.onClickAction(item) } + submissionStatusCardPositiveButton.setOnClickListener { itemView.performClick() } + } + + data class Item( + val state: SubmissionStateRAT.TestPositive, + val onClickAction: (Item) -> Unit + ) : TestResultItem.RA, HasPayloadDiffer { + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestReadyCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestReadyCard.kt similarity index 52% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestReadyCard.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestReadyCard.kt index 5dddc8c04dd3a617501d0f2e273d5fb1d9110edc..7509225d48d65e7cfdc5aad2a0bfa7ba7e344b34 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestReadyCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestReadyCard.kt @@ -2,20 +2,25 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardReadyBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestReadyCard.Item +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardReadyBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestReadyCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -class TestReadyCard( +class RapidTestReadyCard( parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardReadyBinding>(R.layout.home_card_container_layout, parent) { +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardReadyBinding>( + R.layout.home_card_container_layout, + parent +) { override val viewBinding = lazy { - HomeSubmissionStatusCardReadyBinding.inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + HomeSubmissionRapidStatusCardReadyBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) } - override val onBindData: HomeSubmissionStatusCardReadyBinding.( + override val onBindData: HomeSubmissionRapidStatusCardReadyBinding.( item: Item, payloads: List<Any> ) -> Unit = { item, payloads -> @@ -25,9 +30,9 @@ class TestReadyCard( } data class Item( - val state: TestResultReady, + val state: SubmissionStateRAT.TestResultReady, val onClickAction: (Item) -> Unit - ) : TestResultItem, HasPayloadDiffer { + ) : TestResultItem.RA, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt new file mode 100644 index 0000000000000000000000000000000000000000..a96d2a531c24f998838019bf19bfa1cbfe1f5f09 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/RapidTestSubmissionDoneCard.kt @@ -0,0 +1,30 @@ +package de.rki.coronawarnapp.submission.ui.homecards + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.databinding.HomeSubmissionRapidStatusCardPositiveSharedBinding +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestSubmissionDoneCard.Item +import de.rki.coronawarnapp.ui.main.home.HomeAdapter + +class RapidTestSubmissionDoneCard( + parent: ViewGroup +) : HomeAdapter.HomeItemVH<Item, HomeSubmissionRapidStatusCardPositiveSharedBinding>( + R.layout.home_card_container_layout, + parent +) { + + override val viewBinding = lazy { + HomeSubmissionRapidStatusCardPositiveSharedBinding + .inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) + } + + override val onBindData: HomeSubmissionRapidStatusCardPositiveSharedBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { _, _ -> } + + data class Item( + val state: SubmissionStateRAT.SubmissionDone + ) : TestResultItem.RA +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionState.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionState.kt deleted file mode 100644 index 4daf23636f12def024f22f9b4d6721213836ccbf..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package de.rki.coronawarnapp.submission.ui.homecards - -import android.content.Context -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUIFormat -import java.util.Date - -sealed class SubmissionState - -object NoTest : SubmissionState() -object FetchingResult : SubmissionState() -object TestResultReady : SubmissionState() -object TestPositive : SubmissionState() -object TestNegative : SubmissionState() -object TestError : SubmissionState() -object TestInvalid : SubmissionState() -object TestPending : SubmissionState() -data class SubmissionDone(val testRegisteredOn: Date) : SubmissionState() { - - fun formatTestRegistrationText(context: Context): String = - context.getString(R.string.reenable_risk_card_test_registration_string) - .format(testRegisteredOn.toUIFormat(context)) -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt deleted file mode 100644 index 54f584682f70f72917c1c9805010ea5b3774e093..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/SubmissionStateProvider.kt +++ /dev/null @@ -1,132 +0,0 @@ -package de.rki.coronawarnapp.submission.ui.homecards - -import dagger.Reusable -import de.rki.coronawarnapp.exception.http.CwaServerError -import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.submission.SubmissionSettings -import de.rki.coronawarnapp.util.CWADebug -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper -import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart -import timber.log.Timber -import javax.inject.Inject - -@Reusable -class SubmissionStateProvider @Inject constructor( - submissionRepository: SubmissionRepository, - submissionSettings: SubmissionSettings -) { - - val state: Flow<SubmissionState> = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.hasViewedTestResult, - submissionRepository.testResultReceivedDateFlow, - submissionSettings.registrationToken.flow - ) { uiState, hasTestBeenSeen, testRegistrationDate, registrationToken -> - - val eval = Evaluation( - deviceUiState = uiState, - isDeviceRegistered = registrationToken != null, - hasTestResultBeenSeen = hasTestBeenSeen - ) - Timber.d("eval: %s", eval) - when { - eval.isUnregistered() -> NoTest - eval.isFetching() -> FetchingResult - eval.isTestResultReady() -> TestResultReady - eval.isResultPositive() -> TestPositive - eval.isInvalid() -> TestInvalid - eval.isError() -> TestError - eval.isResultNegative() -> TestNegative - eval.isSubmissionDone() -> SubmissionDone(testRegisteredOn = testRegistrationDate) - eval.isPending() -> TestPending - else -> if (CWADebug.isDeviceForTestersBuild) throw IllegalStateException(eval.toString()) else TestPending - } - } - .onStart { Timber.v("SubmissionStateProvider FLOW start") } - .onEach { Timber.w("SubmissionStateProvider FLOW emission: %s", it) } - .onCompletion { Timber.v("SubmissionStateProvider FLOW completed.") } - - // TODO Refactor this to be easier to understand, probably remove the "withSuccess" logic. - private data class Evaluation( - val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>, - val isDeviceRegistered: Boolean, - val hasTestResultBeenSeen: Boolean - ) { - - fun isUnregistered(): Boolean = !isDeviceRegistered - - fun isTestResultReady(): Boolean = deviceUiState.withSuccess(false) { - when (it) { - DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN -> !hasTestResultBeenSeen - else -> false - } - } - - fun isFetching(): Boolean = - isDeviceRegistered && when (deviceUiState) { - is NetworkRequestWrapper.RequestFailed -> false - is NetworkRequestWrapper.RequestStarted -> true - is NetworkRequestWrapper.RequestIdle -> true - else -> false - } - - fun isResultPositive(): Boolean = - deviceUiState.withSuccess(false) { - when (it) { - DeviceUIState.PAIRED_POSITIVE, DeviceUIState.PAIRED_POSITIVE_TELETAN -> hasTestResultBeenSeen - else -> false - } - } - - fun isResultNegative(): Boolean = - deviceUiState.withSuccess(false) { - when (it) { - DeviceUIState.PAIRED_NEGATIVE -> true - else -> false - } - } - - fun isSubmissionDone(): Boolean = - when (deviceUiState) { - is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == DeviceUIState.SUBMITTED_FINAL - else -> false - } - - fun isInvalid(): Boolean = - isDeviceRegistered && when (deviceUiState) { - is NetworkRequestWrapper.RequestFailed -> deviceUiState.error !is CwaServerError - is NetworkRequestWrapper.RequestSuccessful -> deviceUiState.data == DeviceUIState.PAIRED_REDEEMED - else -> false - } - - fun isError(): Boolean = - deviceUiState.withSuccess(false) { - when (it) { - DeviceUIState.PAIRED_ERROR -> true - else -> false - } - } - - fun isPending(): Boolean = - when (deviceUiState) { - is NetworkRequestWrapper.RequestFailed -> true - is NetworkRequestWrapper.RequestSuccessful -> { - deviceUiState.data == DeviceUIState.PAIRED_ERROR || - deviceUiState.data == DeviceUIState.PAIRED_NO_RESULT - } - else -> false - } - - override fun toString() = - "Evaluation(deviceUiState=$deviceUiState, " + - "isDeviceRegistered=$isDeviceRegistered, " + - "hasTestResultBeenSeen=$hasTestResultBeenSeen)" - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestFetchingCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestFetchingCard.kt index 42f3b7c828d4fbd41234d272e10f4f4b8c6fc61c..e367416f8bc80ae7c44e108d57908b51d8b4ac8c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestFetchingCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestFetchingCard.kt @@ -2,6 +2,9 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CommonSubmissionStates +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardFetchingBinding import de.rki.coronawarnapp.submission.ui.homecards.TestFetchingCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter @@ -24,6 +27,13 @@ class TestFetchingCard( ) -> Unit = { _, _ -> } data class Item( - val state: FetchingResult - ) : TestResultItem + val state: CommonSubmissionStates.TestFetching + ) : TestResultItem { + override val stableId: Long + get() = when (state) { + is SubmissionStatePCR -> TestResultItem.PCR.LIST_ID + is SubmissionStateRAT -> TestResultItem.RA.LIST_ID + else -> throw IllegalArgumentException("Invalid card argument $state") + } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestResultItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestResultItem.kt index 14136421fc8ac8d26e6e25a2cc2050bcc587f311..2271042d492467547db54f49f2825fb3e875f328 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestResultItem.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestResultItem.kt @@ -5,4 +5,22 @@ import de.rki.coronawarnapp.ui.main.home.items.HomeItem interface TestResultItem : HomeItem { override val stableId: Long get() = TestResultItem::class.java.name.hashCode().toLong() + + interface RA : TestResultItem { + override val stableId: Long + get() = LIST_ID + + companion object { + val LIST_ID = RA::class.java.name.hashCode().toLong() + } + } + + interface PCR : TestResultItem { + override val stableId: Long + get() = LIST_ID + + companion object { + val LIST_ID = PCR::class.java.name.hashCode().toLong() + } + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestSubmissionDoneCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestSubmissionDoneCard.kt deleted file mode 100644 index 4203d6fd8fe1afbcf98594786bae2d13268041ec..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestSubmissionDoneCard.kt +++ /dev/null @@ -1,25 +0,0 @@ -package de.rki.coronawarnapp.submission.ui.homecards - -import android.view.ViewGroup -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardDoneBinding -import de.rki.coronawarnapp.submission.ui.homecards.TestSubmissionDoneCard.Item -import de.rki.coronawarnapp.ui.main.home.HomeAdapter - -class TestSubmissionDoneCard( - parent: ViewGroup -) : HomeAdapter.HomeItemVH<Item, HomeSubmissionStatusCardDoneBinding>(R.layout.home_card_container_layout, parent) { - - override val viewBinding = lazy { - HomeSubmissionStatusCardDoneBinding.inflate(layoutInflater, itemView.findViewById(R.id.card_container), true) - } - - override val onBindData: HomeSubmissionStatusCardDoneBinding.( - item: Item, - payloads: List<Any> - ) -> Unit = { _, _ -> } - - data class Item( - val state: SubmissionDone - ) : TestResultItem -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestUnregisteredCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestUnregisteredCard.kt index bbecf61dc124982d730d30d1edb3fff31dab8416..e6d2e9749377654d991d5d27aca45b7c87acf1b4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestUnregisteredCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/ui/homecards/TestUnregisteredCard.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.submission.ui.homecards import android.view.ViewGroup import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CommonSubmissionStates import de.rki.coronawarnapp.databinding.HomeSubmissionStatusCardUnregisteredBinding import de.rki.coronawarnapp.submission.ui.homecards.TestUnregisteredCard.Item import de.rki.coronawarnapp.ui.main.home.HomeAdapter @@ -33,7 +34,7 @@ class TestUnregisteredCard( } data class Item( - val state: NoTest, + val state: CommonSubmissionStates.TestUnregistered, val onClickAction: (Item) -> Unit ) : TestResultItem, HasPayloadDiffer { override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt index e1037bf8281f2ca5309167b5eae7629a0b9f752e..34ca2c6cf8bb9e469804f2fd13cb75209129f210 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/information/InformationContactFragment.kt @@ -38,5 +38,9 @@ class InformationContactFragment : Fragment(R.layout.fragment_information_contac val number = getString(R.string.information_contact_phone_call_number) ExternalActionHelper.call(this, number) } + binding.informationContactNavigationRowInternationalPhone.navigationRow.setOnClickListener { + val number = getString(R.string.information_contact_button_international_phone) + ExternalActionHelper.call(this, number) + } } } 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 177f1ce6f84d42e7f51bd02b98b229ec13cabe05..7d6fd992124a05732356a8b1bd4d5e4b9f977678 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 @@ -21,14 +21,13 @@ import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryWorkScheduler import de.rki.coronawarnapp.contactdiary.ui.overview.ContactDiaryOverviewFragmentDirections import de.rki.coronawarnapp.databinding.ActivityMainBinding import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.ui.base.startActivitySafely import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsFragment import de.rki.coronawarnapp.ui.setupWithNavController2 import de.rki.coronawarnapp.util.AppShortcuts import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.ConnectivityHelper +import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.DialogHelper import de.rki.coronawarnapp.util.device.PowerManagement import de.rki.coronawarnapp.util.di.AppInjector @@ -76,10 +75,8 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { private val navController by lazy { supportFragmentManager.findNavController(R.id.nav_host_fragment) } @Inject lateinit var powerManagement: PowerManagement - @Inject lateinit var deadmanScheduler: DeadmanNotificationScheduler @Inject lateinit var contactDiaryWorkScheduler: ContactDiaryWorkScheduler @Inject lateinit var dataDonationAnalyticsScheduler: DataDonationAnalyticsScheduler - @Inject lateinit var submissionSettings: SubmissionSettings @Inject lateinit var backgroundWorkScheduler: BackgroundWorkScheduler override fun onCreate(savedInstanceState: Bundle?) { @@ -118,7 +115,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { if (count > 0) { val badge = getOrCreateBadge(targetId) badge.number = count - badge.badgeTextColor = getColor(android.R.color.white) + badge.badgeTextColor = getColorCompat(android.R.color.white) } else { removeBadge(targetId) } @@ -195,9 +192,7 @@ class MainActivity : AppCompatActivity(), HasAndroidInjector { vm.doBackgroundNoiseCheck() contactDiaryWorkScheduler.schedulePeriodic() dataDonationAnalyticsScheduler.schedulePeriodic() - if (!submissionSettings.isAllowedToSubmitKeys) { - deadmanScheduler.schedulePeriodic() - } + vm.checkDeadMan() } private fun showEnergyOptimizedEnabledForBackground() { 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 08d5f165d9a5e5dd6ec550b1b49f10f84b04818f..1949db08144a868cd025f9a46b64e90fbab4a9d4 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 @@ -6,10 +6,12 @@ import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.presencetracing.TraceLocationSettings import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository -import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -19,7 +21,9 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map +import timber.log.Timber +@Suppress("LongParameterList") class MainActivityViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val environmentSetup: EnvironmentSetup, @@ -29,6 +33,8 @@ class MainActivityViewModel @AssistedInject constructor( private val onboardingSettings: OnboardingSettings, private val traceLocationSettings: TraceLocationSettings, private val checkInRepository: CheckInRepository, + private val deadmanScheduler: DeadmanNotificationScheduler, + private val coronaTestRepository: CoronaTestRepository, ) : CWAViewModel( dispatcherProvider = dispatcherProvider ) { @@ -91,6 +97,16 @@ class MainActivityViewModel @AssistedInject constructor( } } + fun checkDeadMan() { + launch { + val isAllowedToSubmitKeys = coronaTestRepository.coronaTests.first().any { it.isSubmissionAllowed } + if (!isAllowedToSubmitKeys) { + Timber.v("We are not allowed to submit keys, scheduling deadman.") + deadmanScheduler.schedulePeriodic() + } + } + } + @AssistedFactory interface Factory : SimpleCWAViewModelFactory<MainActivityViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt index 8b2e07957abd3b10bfc31393d0522f646b329ac5..be2720d7a0caf7f755e0370eec37e59e439ab549 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/HomeAdapter.kt @@ -4,14 +4,22 @@ import android.view.ViewGroup import androidx.annotation.LayoutRes import androidx.viewbinding.ViewBinding import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard -import de.rki.coronawarnapp.submission.ui.homecards.TestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestReadyCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestSubmissionDoneCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestOutdatedCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestReadyCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestFetchingCard -import de.rki.coronawarnapp.submission.ui.homecards.TestInvalidCard -import de.rki.coronawarnapp.submission.ui.homecards.TestNegativeCard -import de.rki.coronawarnapp.submission.ui.homecards.TestPendingCard -import de.rki.coronawarnapp.submission.ui.homecards.TestPositiveCard -import de.rki.coronawarnapp.submission.ui.homecards.TestReadyCard -import de.rki.coronawarnapp.submission.ui.homecards.TestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestUnregisteredCard import de.rki.coronawarnapp.tracing.ui.homecards.IncreasedRiskCard import de.rki.coronawarnapp.tracing.ui.homecards.LowRiskCard @@ -50,14 +58,22 @@ class HomeAdapter : TypedVHCreatorMod({ data[it] is TracingFailedCard.Item }) { TracingFailedCard(it) }, TypedVHCreatorMod({ data[it] is TracingDisabledCard.Item }) { TracingDisabledCard(it) }, TypedVHCreatorMod({ data[it] is TracingProgressCard.Item }) { TracingProgressCard(it) }, - TypedVHCreatorMod({ data[it] is TestSubmissionDoneCard.Item }) { TestSubmissionDoneCard(it) }, - TypedVHCreatorMod({ data[it] is TestInvalidCard.Item }) { TestInvalidCard(it) }, - TypedVHCreatorMod({ data[it] is TestErrorCard.Item }) { TestErrorCard(it) }, TypedVHCreatorMod({ data[it] is TestFetchingCard.Item }) { TestFetchingCard(it) }, - TypedVHCreatorMod({ data[it] is TestPositiveCard.Item }) { TestPositiveCard(it) }, - TypedVHCreatorMod({ data[it] is TestNegativeCard.Item }) { TestNegativeCard(it) }, - TypedVHCreatorMod({ data[it] is TestReadyCard.Item }) { TestReadyCard(it) }, - TypedVHCreatorMod({ data[it] is TestPendingCard.Item }) { TestPendingCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestSubmissionDoneCard.Item }) { PcrTestSubmissionDoneCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestInvalidCard.Item }) { PcrTestInvalidCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestErrorCard.Item }) { PcrTestErrorCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestPositiveCard.Item }) { PcrTestPositiveCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestNegativeCard.Item }) { PcrTestNegativeCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestReadyCard.Item }) { PcrTestReadyCard(it) }, + TypedVHCreatorMod({ data[it] is PcrTestPendingCard.Item }) { PcrTestPendingCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestSubmissionDoneCard.Item }) { RapidTestSubmissionDoneCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestInvalidCard.Item }) { RapidTestInvalidCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestErrorCard.Item }) { RapidTestErrorCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestPositiveCard.Item }) { RapidTestPositiveCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestNegativeCard.Item }) { RapidTestNegativeCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestReadyCard.Item }) { RapidTestReadyCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestPendingCard.Item }) { RapidTestPendingCard(it) }, + TypedVHCreatorMod({ data[it] is RapidTestOutdatedCard.Item }) { RapidTestOutdatedCard(it) }, TypedVHCreatorMod({ data[it] is TestUnregisteredCard.Item }) { TestUnregisteredCard(it) }, TypedVHCreatorMod({ data[it] is StatisticsHomeCard.Item }) { StatisticsHomeCard(it) }, SavedStateMod<HomeItemVH<HomeItem, ViewBinding>>() // For statistics card scroll position 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 f171d8a2fc0703a8eb0662ad300b8af3fb48f01e..6a761f77735f46cb759bc32d52cf1881e27c17da 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 @@ -6,6 +6,17 @@ import androidx.navigation.NavDirections import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.latestPCRT +import de.rki.coronawarnapp.coronatest.latestRAT +import de.rki.coronawarnapp.coronatest.type.CommonSubmissionStates +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.SubmissionStatePCR +import de.rki.coronawarnapp.coronatest.type.pcr.toSubmissionState +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest +import de.rki.coronawarnapp.coronatest.type.rapidantigen.SubmissionStateRAT +import de.rki.coronawarnapp.coronatest.type.rapidantigen.toSubmissionState import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.notification.ShareTestResultNotificationService @@ -14,24 +25,22 @@ import de.rki.coronawarnapp.statistics.ui.homecards.StatisticsHomeCard import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.submission.ui.homecards.FetchingResult -import de.rki.coronawarnapp.submission.ui.homecards.NoTest -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionDone -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider -import de.rki.coronawarnapp.submission.ui.homecards.TestError -import de.rki.coronawarnapp.submission.ui.homecards.TestErrorCard +import de.rki.coronawarnapp.submission.toDeviceUIState +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestReadyCard +import de.rki.coronawarnapp.submission.ui.homecards.PcrTestSubmissionDoneCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestErrorCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestInvalidCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestNegativeCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPendingCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestPositiveCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestReadyCard +import de.rki.coronawarnapp.submission.ui.homecards.RapidTestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestFetchingCard -import de.rki.coronawarnapp.submission.ui.homecards.TestInvalid -import de.rki.coronawarnapp.submission.ui.homecards.TestInvalidCard -import de.rki.coronawarnapp.submission.ui.homecards.TestNegative -import de.rki.coronawarnapp.submission.ui.homecards.TestNegativeCard -import de.rki.coronawarnapp.submission.ui.homecards.TestPending -import de.rki.coronawarnapp.submission.ui.homecards.TestPendingCard -import de.rki.coronawarnapp.submission.ui.homecards.TestPositive -import de.rki.coronawarnapp.submission.ui.homecards.TestPositiveCard -import de.rki.coronawarnapp.submission.ui.homecards.TestReadyCard -import de.rki.coronawarnapp.submission.ui.homecards.TestResultReady -import de.rki.coronawarnapp.submission.ui.homecards.TestSubmissionDoneCard import de.rki.coronawarnapp.submission.ui.homecards.TestUnregisteredCard import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.states.IncreasedRisk @@ -47,15 +56,14 @@ import de.rki.coronawarnapp.tracing.ui.homecards.TracingFailedCard import de.rki.coronawarnapp.tracing.ui.homecards.TracingProgressCard import de.rki.coronawarnapp.tracing.ui.statusbar.TracingHeaderState import de.rki.coronawarnapp.tracing.ui.statusbar.toHeaderState -import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowErrorResetDialog import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents.ShowTracingExplanation import de.rki.coronawarnapp.ui.main.home.items.CreateTraceLocationCard import de.rki.coronawarnapp.ui.main.home.items.FAQCard import de.rki.coronawarnapp.ui.main.home.items.HomeItem import de.rki.coronawarnapp.ui.main.home.items.ReenableRiskCard +import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper.Companion.withSuccess import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper @@ -74,7 +82,7 @@ class HomeFragmentViewModel @AssistedInject constructor( private val errorResetTool: EncryptionErrorResetTool, tracingStatus: GeneralTracingStatus, tracingStateProviderFactory: TracingStateProvider.Factory, - submissionStateProvider: SubmissionStateProvider, + private val coronaTestRepository: CoronaTestRepository, private val tracingRepository: TracingRepository, private val shareTestResultNotificationService: ShareTestResultNotificationService, private val submissionRepository: SubmissionRepository, @@ -124,6 +132,7 @@ class HomeFragmentViewModel @AssistedInject constructor( }.launchInViewModel() } } + private val tracingCardItems = tracingStateProvider.state.map { tracingState -> when (tracingState) { is TracingInProgress -> TracingProgressCard.Item( @@ -165,68 +174,113 @@ class HomeFragmentViewModel @AssistedInject constructor( } }.distinctUntilChanged() - private val submissionCardItems = submissionStateProvider.state.map { state -> - when (state) { - is NoTest -> TestUnregisteredCard.Item(state) { - routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToSubmissionDispatcher()) - } - is FetchingResult -> TestFetchingCard.Item(state) - is TestResultReady -> TestReadyCard.Item(state) { - routeToScreen.postValue( - HomeFragmentDirections.actionMainFragmentToSubmissionTestResultAvailableFragment() - ) - } - is TestPositive -> TestPositiveCard.Item(state) { - routeToScreen.postValue( - HomeFragmentDirections - .actionMainFragmentToSubmissionResultPositiveOtherWarningNoConsentFragment() - ) - } - is TestNegative -> TestNegativeCard.Item(state) { - routeToScreen.postValue( - HomeFragmentDirections - .actionMainFragmentToSubmissionTestResultNegativeFragment() - ) - } - is TestInvalid -> TestInvalidCard.Item(state) { - popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog) - } - is TestError -> TestErrorCard.Item(state) { - routeToScreen.postValue( - HomeFragmentDirections - .actionMainFragmentToSubmissionTestResultPendingFragment() - ) - } - is TestPending -> TestPendingCard.Item(state) { - routeToScreen.postValue( - HomeFragmentDirections - .actionMainFragmentToSubmissionTestResultPendingFragment() - ) - } - is SubmissionDone -> TestSubmissionDoneCard.Item(state) + private fun PCRCoronaTest?.toTestCardItem() = when (val state = this.toSubmissionState()) { + is SubmissionStatePCR.NoTest -> TestUnregisteredCard.Item(state) { + routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToSubmissionDispatcher()) } - }.distinctUntilChanged() + is SubmissionStatePCR.FetchingResult -> TestFetchingCard.Item(state) + is SubmissionStatePCR.TestResultReady -> PcrTestReadyCard.Item(state) { + routeToScreen.postValue( + HomeFragmentDirections.actionMainFragmentToSubmissionTestResultAvailableFragment() + ) + } + is SubmissionStatePCR.TestPositive -> PcrTestPositiveCard.Item(state) { + routeToScreen.postValue( + HomeFragmentDirections + .actionMainFragmentToSubmissionResultPositiveOtherWarningNoConsentFragment() + ) + } + is SubmissionStatePCR.TestNegative -> PcrTestNegativeCard.Item(state) + is SubmissionStatePCR.TestInvalid -> PcrTestInvalidCard.Item(state) { + popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog) + } + is SubmissionStatePCR.TestError -> PcrTestErrorCard.Item(state) { + routeToScreen.postValue( + HomeFragmentDirections + .actionMainFragmentToSubmissionTestResultPendingFragment() + ) + } + is SubmissionStatePCR.TestPending -> PcrTestPendingCard.Item(state) { + routeToScreen.postValue( + HomeFragmentDirections + .actionMainFragmentToSubmissionTestResultPendingFragment() + ) + } + is SubmissionStatePCR.SubmissionDone -> PcrTestSubmissionDoneCard.Item(state) + } + + private fun RACoronaTest?.toTestCardItem() = when (val state = this.toSubmissionState()) { + is SubmissionStateRAT.NoTest -> TestUnregisteredCard.Item(state) { + // TODO +// routeToScreen.postValue(HomeFragmentDirections.actionMainFragmentToSubmissionDispatcher()) + } + is SubmissionStateRAT.FetchingResult -> TestFetchingCard.Item(state) + is SubmissionStateRAT.TestResultReady -> RapidTestReadyCard.Item(state) { + // TODO +// routeToScreen.postValue( +// HomeFragmentDirections.actionMainFragmentToSubmissionTestResultAvailableFragment() +// ) + } + is SubmissionStateRAT.TestPositive -> RapidTestPositiveCard.Item(state) { + // TODO +// routeToScreen.postValue( +// HomeFragmentDirections +// .actionMainFragmentToSubmissionResultPositiveOtherWarningNoConsentFragment() +// ) + } + is SubmissionStateRAT.TestNegative -> RapidTestNegativeCard.Item(state) + is SubmissionStateRAT.TestInvalid -> RapidTestInvalidCard.Item(state) { + // TODO +// popupEvents.postValue(HomeFragmentEvents.ShowDeleteTestDialog) + } + is SubmissionStateRAT.TestError -> RapidTestErrorCard.Item(state) { + // TODO +// routeToScreen.postValue( +// HomeFragmentDirections +// .actionMainFragmentToSubmissionTestResultPendingFragment() +// ) + } + is SubmissionStateRAT.TestPending -> RapidTestPendingCard.Item(state) { + // TODO +// routeToScreen.postValue( +// HomeFragmentDirections +// .actionMainFragmentToSubmissionTestResultPendingFragment() +// ) + } + is SubmissionStateRAT.SubmissionDone -> RapidTestSubmissionDoneCard.Item(state) + } val homeItems: LiveData<List<HomeItem>> = combine( tracingCardItems, - submissionCardItems, - submissionStateProvider.state.distinctUntilChanged(), + coronaTestRepository.latestPCRT, + coronaTestRepository.latestRAT, statisticsProvider.current.distinctUntilChanged() - ) { tracingItem, submissionItem, submissionState, statsData -> + ) { tracingItem, testPCR, testRAT, statsData -> + val statePCR = testPCR.toSubmissionState() + val stateRAT = testRAT.toSubmissionState() + val bothTestStates = setOf(statePCR, stateRAT) mutableListOf<HomeItem>().apply { - when (submissionState) { - TestPositive, is SubmissionDone -> { + when { + statePCR is SubmissionStatePCR.TestPositive || statePCR is SubmissionStatePCR.SubmissionDone -> { + // Don't show risk card + } + stateRAT is SubmissionStateRAT.TestPositive || stateRAT is SubmissionStateRAT.SubmissionDone -> { // Don't show risk card } else -> add(tracingItem) } - add(submissionItem) + add(testPCR.toTestCardItem()) + + if (stateRAT != SubmissionStateRAT.NoTest || statePCR != SubmissionStatePCR.NoTest) { + add(testRAT.toTestCardItem()) + } - if (submissionState is SubmissionDone) { + bothTestStates.firstOrNull { it is CommonSubmissionStates.SubmissionDone }?.let { + it as CommonSubmissionStates.SubmissionDone add( ReenableRiskCard.Item( - state = submissionState, + data = it, onClickAction = { popupEvents.postValue(HomeFragmentEvents.ShowReactivateRiskCheckDialog) } ) ) @@ -253,13 +307,14 @@ class HomeFragmentViewModel @AssistedInject constructor( private var isLoweredRiskLevelDialogBeingShown = false fun observeTestResultToSchedulePositiveTestResultReminder() = launch { - submissionRepository.deviceUIStateFlow - .first { state -> - state.withSuccess(false) { - when (it) { - DeviceUIState.PAIRED_POSITIVE, DeviceUIState.PAIRED_POSITIVE_TELETAN -> true - else -> false - } + submissionRepository.pcrTest + .first { test -> + when { + test == null -> false + test.lastError != null -> false + test.testResult.toDeviceUIState() == DeviceUIState.PAIRED_POSITIVE -> true + test.testResult.toDeviceUIState() == DeviceUIState.PAIRED_POSITIVE_TELETAN -> true + else -> false } } .also { shareTestResultNotificationService.scheduleSharePositiveTestResultReminder() } @@ -292,7 +347,7 @@ class HomeFragmentViewModel @AssistedInject constructor( fun refreshRequiredData() { launch { - submissionRepository.refreshDeviceUIState() + submissionRepository.refreshTest(type = CoronaTest.Type.PCR) tracingRepository.refreshRiskLevel() } } @@ -308,8 +363,8 @@ class HomeFragmentViewModel @AssistedInject constructor( } fun deregisterWarningAccepted() { - submissionRepository.removeTestFromDevice() - submissionRepository.refreshDeviceUIState() + submissionRepository.removeTestFromDevice(type = CoronaTest.Type.PCR) + submissionRepository.refreshTest(type = CoronaTest.Type.PCR) } fun userHasAcknowledgedTheLoweredRiskLevel() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/ReenableRiskCard.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/ReenableRiskCard.kt index 2c181b509efa5f043ccfe2a7f2b4add8338fdba9..5c9df231a7c6a9106169323c436d11b1766f96b2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/ReenableRiskCard.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/main/home/items/ReenableRiskCard.kt @@ -3,10 +3,11 @@ package de.rki.coronawarnapp.ui.main.home.items import android.view.ViewGroup import android.widget.Button import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CommonSubmissionStates import de.rki.coronawarnapp.databinding.HomeReenableRiskCardLayoutBinding -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionDone import de.rki.coronawarnapp.ui.main.home.HomeAdapter import de.rki.coronawarnapp.ui.main.home.items.ReenableRiskCard.Item +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUIFormat import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer class ReenableRiskCard(parent: ViewGroup) : HomeAdapter.HomeItemVH<Item, HomeReenableRiskCardLayoutBinding>( @@ -19,15 +20,21 @@ class ReenableRiskCard(parent: ViewGroup) : HomeAdapter.HomeItemVH<Item, HomeRee } override val onBindData: HomeReenableRiskCardLayoutBinding.(Item, List<Any>) -> Unit = { item, payloads -> + reenableRiskCardTestRegistrationDate.text = + context.getString(R.string.reenable_risk_card_test_registration_string) + .format(item.data.testRegisteredAt.toDate().toUIFormat(context)) - state = item.state itemView.findViewById<Button>(R.id.reenable_risk_card_button).setOnClickListener { val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item curItem.onClickAction(item) } } - data class Item(val state: SubmissionDone, val onClickAction: (Item) -> Unit) : HomeItem, HasPayloadDiffer { + data class Item( + val data: CommonSubmissionStates.SubmissionDone, + val onClickAction: (Item) -> Unit + ) : HomeItem, + HasPayloadDiffer { override val stableId: Long = Item::class.java.name.hashCode().toLong() override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/PresenceTracingUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/PresenceTracingUIModule.kt index b741e3d8820158a311a449f7a889fe0a0e7a35d7..092117efc9fb48cd7e7873c57ca54577a29a94df 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/PresenceTracingUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/PresenceTracingUIModule.kt @@ -4,6 +4,8 @@ import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsFragment import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.CheckInsModule +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragmentModule import de.rki.coronawarnapp.ui.presencetracing.attendee.confirm.ConfirmCheckInFragment import de.rki.coronawarnapp.ui.presencetracing.attendee.confirm.ConfirmCheckInModule import de.rki.coronawarnapp.ui.presencetracing.attendee.edit.EditCheckInFragment @@ -60,4 +62,7 @@ internal abstract class PresenceTracingUIModule { @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class]) abstract fun qrCodeDetailFragment(): QrCodeDetailFragment + + @ContributesAndroidInjector(modules = [CheckInsConsentFragmentModule::class]) + abstract fun checkInsConsentFragment(): CheckInsConsentFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/TraceLocationPreferences.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/TraceLocationPreferences.kt index d6451170caa4bf68d3085e92620787b042c21903..2297ac704807433eb989e4c36bfb0ff5d27dd84f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/TraceLocationPreferences.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/TraceLocationPreferences.kt @@ -20,6 +20,11 @@ class TraceLocationPreferences @Inject constructor( defaultValue = false ) + val createJournalEntryCheckedState = prefs.createFlowPreference( + key = "trace_location_create_journal_entry_checked_state", + defaultValue = true + ) + fun clear() { prefs.clearAndNotify() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/TraceLocationAttendeeSettings.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/TraceLocationAttendeeSettings.kt new file mode 100644 index 0000000000000000000000000000000000000000..1603fe47c8de43a6eb4e2ca5fc0c1fad4421abf1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/TraceLocationAttendeeSettings.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee + +import dagger.Reusable +import de.rki.coronawarnapp.ui.presencetracing.TraceLocationPreferences +import javax.inject.Inject + +@Reusable +class TraceLocationAttendeeSettings @Inject constructor(private val preferences: TraceLocationPreferences) { + + val createJournalEntryCheckedState = preferences.createJournalEntryCheckedState.flow + + fun setCreateJournalEntryCheckedState(isChecked: Boolean) = + preferences.createJournalEntryCheckedState.update { isChecked } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt new file mode 100644 index 0000000000000000000000000000000000000000..e5679346f5f9a930e8c768e298f24a25ec716514 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt @@ -0,0 +1,16 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.common + +import de.rki.coronawarnapp.presencetracing.checkins.CheckIn +import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone +import org.joda.time.format.DateTimeFormat + +inline val CheckIn.checkoutInfo: String + get() { + val checkInStartUserTZ = checkInStart.toUserTimeZone() + val checkInEndUserTZ = checkInEnd.toUserTimeZone() + + val dayFormatted = checkInStartUserTZ.toLocalDate().toString(DateTimeFormat.mediumDate()) + val startTimeFormatted = checkInStartUserTZ.toLocalTime().toString(DateTimeFormat.shortTime()) + val endTimeFormatted = checkInEndUserTZ.toLocalTime().toString(DateTimeFormat.shortTime()) + return "$dayFormatted, $startTimeFormatted - $endTimeFormatted" + } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..fbd39ba15f070ad8f04c65d2de269c5c027e399d --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt @@ -0,0 +1,37 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.viewbinding.ViewBinding +import de.rki.coronawarnapp.util.lists.BindableVH +import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter +import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffer +import de.rki.coronawarnapp.util.lists.modular.ModularAdapter +import de.rki.coronawarnapp.util.lists.modular.mods.DataBinderMod +import de.rki.coronawarnapp.util.lists.modular.mods.StableIdMod +import de.rki.coronawarnapp.util.lists.modular.mods.TypedVHCreatorMod + +class CheckInsConsentAdapter : + ModularAdapter<CheckInsConsentAdapter.ItemVH<CheckInsConsentItem, ViewBinding>>(), + AsyncDiffUtilAdapter<CheckInsConsentItem> { + + override val asyncDiffer: AsyncDiffer<CheckInsConsentItem> = AsyncDiffer(adapter = this) + + init { + modules.addAll( + listOf( + StableIdMod(data), + DataBinderMod<CheckInsConsentItem, ItemVH<CheckInsConsentItem, ViewBinding>>(data), + TypedVHCreatorMod({ data[it] is HeaderCheckInsVH.Item }) { HeaderCheckInsVH(it) }, + TypedVHCreatorMod({ data[it] is SelectableCheckInVH.Item }) { SelectableCheckInVH(it) }, + ) + ) + } + + override fun getItemCount(): Int = data.size + + abstract class ItemVH<Item : CheckInsConsentItem, VB : ViewBinding>( + @LayoutRes layoutRes: Int, + parent: ViewGroup + ) : ModularAdapter.VH(layoutRes, parent), BindableVH<Item, VB> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..33dfa460c14a79465c8fd39988dc15e0701b8d90 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt @@ -0,0 +1,106 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import android.os.Bundle +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.CheckInsConsentFragmentBinding +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.lists.diffutil.update +import de.rki.coronawarnapp.util.ui.doNavigate +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 + +class CheckInsConsentFragment : Fragment(R.layout.check_ins_consent_fragment), AutoInject { + + private val binding: CheckInsConsentFragmentBinding by viewBindingLazy() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: CheckInsConsentViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, savedState -> + factory as CheckInsConsentViewModel.Factory + factory.create( + savedState = savedState + ) + } + ) + + private val adapter = CheckInsConsentAdapter() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() = viewModel.onCloseClick() + } + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) + + with(binding) { + checkInsRecycler.adapter = adapter + toolbar.setNavigationOnClickListener { + viewModel.onCloseClick() + } + skipButton.setOnClickListener { viewModel.onSkipClick() } + continueButton.setOnClickListener { viewModel.shareSelectedCheckIns() } + } + + viewModel.checkIns.observe(viewLifecycleOwner) { + adapter.update(it) + binding.continueButton.isEnabled = it.any { item -> + item is SelectableCheckInVH.Item && item.checkIn.hasSubmissionConsent + } + } + + viewModel.events.observe(viewLifecycleOwner) { + when (it) { + CheckInsConsentNavigation.OpenCloseDialog -> showCloseDialog() + CheckInsConsentNavigation.OpenSkipDialog -> showSkipDialog() + CheckInsConsentNavigation.ToHomeFragment -> doNavigate( + CheckInsConsentFragmentDirections.actionCheckInsConsentFragmentToMainFragment() + ) + CheckInsConsentNavigation.ToSubmissionResultReadyFragment -> doNavigate( + CheckInsConsentFragmentDirections.actionCheckInsConsentFragmentToSubmissionResultReadyFragment() + ) + CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment -> doNavigate( + CheckInsConsentFragmentDirections + .actionCheckInsConsentFragmentToSubmissionTestResultConsentGivenFragment() + ) + } + } + } + + private fun showSkipDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.trace_location_attendee_consent_dialog_title) + .setMessage(R.string.trace_location_attendee_consent_dialog_message) + .setPositiveButton(R.string.trace_location_attendee_consent_dialog_positive_button) { _, _ -> + Timber.d("showSkipDialog:Stay on CheckInsConsentFragment") + } + .setNegativeButton(R.string.trace_location_attendee_consent_dialog_negative_button) { _, _ -> + viewModel.doNotShareCheckIns() + } + .show() + } + + private fun showCloseDialog() { + val closeDialogInstance = DialogHelper.DialogInstance( + context = requireActivity(), + title = R.string.submission_test_result_available_close_dialog_title_consent_given, + message = R.string.submission_test_result_available_close_dialog_body_consent_given, + positiveButton = R.string.submission_test_result_available_close_dialog_continue_button, + negativeButton = R.string.submission_test_result_available_close_dialog_cancel_button, + cancelable = true, + positiveButtonFunction = { + Timber.d("showCloseDialog:Stay on CheckInsConsentFragment") + }, + negativeButtonFunction = { viewModel.onCancelConfirmed() } + ) + DialogHelper.showDialog(closeDialogInstance) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..d7fd545fd48907ffc4084221dcdb3a68ec5b579c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt @@ -0,0 +1,19 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.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 CheckInsConsentFragmentModule { + + @Binds + @IntoMap + @CWAViewModelKey(CheckInsConsentViewModel::class) + abstract fun checkInsConsentFragment( + factory: CheckInsConsentViewModel.Factory + ): CWAViewModelFactory<out CWAViewModel> +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..53f9a7859bcc43c86587bef3beca68bb67965f2e --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import de.rki.coronawarnapp.util.lists.HasStableId + +interface CheckInsConsentItem : HasStableId diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff3bbef59c0b267c038ab61d9c8be54afce1ac7f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt @@ -0,0 +1,9 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +sealed class CheckInsConsentNavigation { + object OpenCloseDialog : CheckInsConsentNavigation() + object OpenSkipDialog : CheckInsConsentNavigation() + object ToHomeFragment : CheckInsConsentNavigation() + object ToSubmissionTestResultConsentGivenFragment : CheckInsConsentNavigation() + object ToSubmissionResultReadyFragment : CheckInsConsentNavigation() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..698969365ff832c50599f13f811632e2ca08142f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt @@ -0,0 +1,171 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import androidx.lifecycle.LiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.asLiveData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.presencetracing.checkins.CheckIn +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.auto.AutoSubmission +import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.ui.SingleLiveEvent +import de.rki.coronawarnapp.util.viewmodel.CWAViewModel +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import timber.log.Timber + +class CheckInsConsentViewModel @AssistedInject constructor( + @Assisted private val savedState: SavedStateHandle, + dispatcherProvider: DispatcherProvider, + private val checkInRepository: CheckInRepository, + private val submissionRepository: SubmissionRepository, + private val autoSubmission: AutoSubmission +) : CWAViewModel(dispatcherProvider) { + + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + private val selectedSetFlow = MutableStateFlow(initialSet()) + private val coronaTest = submissionRepository.testForType(coronaTestType).filterNotNull() + + val checkIns: LiveData<List<CheckInsConsentItem>> = combine( + checkInRepository.completedCheckIns, + selectedSetFlow + ) { checkIns, ids -> + mutableListOf<CheckInsConsentItem>().apply { + add(headerItem(checkIns)) + addAll(mapCheckIns(checkIns, ids)) + } + }.asLiveData(context = dispatcherProvider.Default) + + val events = SingleLiveEvent<CheckInsConsentNavigation>() + + fun shareSelectedCheckIns() = launch { + // Reset selected check-ins from previous selection + resetPreviousSubmissionConsents() + + Timber.d("Navigate to shareSelectedCheckIns") + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + + // Update CheckIns for new submission + val idsWithConsent = selectedSetFlow.value + checkInRepository.updateSubmissionConsents( + checkInIds = idsWithConsent, + consent = true, + ) + + val event = if (coronaTest.first().isViewed) { + Timber.d("Navigate to SubmissionResultReadyFragment") + CheckInsConsentNavigation.ToSubmissionResultReadyFragment + } else { + Timber.d("Navigate to SubmissionTestResultConsentGivenFragment") + CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment + } + events.postValue(event) + } + + fun doNotShareCheckIns() = launch { + // Reset selected check-ins from previous selection + resetPreviousSubmissionConsents() + + Timber.d("Navigate to doNotShareCheckIns") + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + val event = if (coronaTest.first().isViewed) { + Timber.d("Navigate to SubmissionResultReadyFragment") + CheckInsConsentNavigation.ToSubmissionResultReadyFragment + } else { + Timber.d("Navigate to SubmissionTestResultConsentGivenFragment") + CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment + } + events.postValue(event) + } + + fun onCloseClick() = launch { + val event = if (coronaTest.first().isViewed) { + Timber.d("openSkipDialog") + CheckInsConsentNavigation.OpenSkipDialog + } else { + Timber.d("openCloseDialog") + CheckInsConsentNavigation.OpenCloseDialog + } + events.postValue(event) + } + + fun onCancelConfirmed() { + Timber.d("onCancelConfirmed") + events.postValue(CheckInsConsentNavigation.ToHomeFragment) + } + + fun onSkipClick() { + Timber.d("onSkipClick") + events.postValue(CheckInsConsentNavigation.OpenSkipDialog) + } + + private fun headerItem(checkIns: List<CheckIn>) = HeaderCheckInsVH.Item( + selectAll = { + val ids = checkIns.map { it.id } + if (!selectedSetFlow.value.containsAll(ids)) { + selectedSetFlow.value = updateSet(ids) + } + } + ) + + private fun mapCheckIns(checkIns: List<CheckIn>, ids: Set<Long>): List<CheckInsConsentItem> = + checkIns.sortedByDescending { it.checkInEnd } + .map { checkIn -> + SelectableCheckInVH.Item( + checkIn = checkIn.copy(hasSubmissionConsent = ids.contains(checkIn.id)), + onItemSelected = { selectedSetFlow.value = updateSet(listOf(it.id)) } + ) + } + + private fun updateSet(ids: List<Long>) = + mutableSetOf<Long>().apply { + if (!selectedSetFlow.value.containsAll(ids)) { + addAll(ids) // New Ids + addAll(selectedSetFlow.value) // Existing Ids + } else { + addAll( + selectedSetFlow.value.toMutableSet().apply { removeAll(ids) } + ) + } + }.also { + savedState.set(SET_KEY, it) + Timber.d("SelectedCheckIns=$it") + } + + private fun initialSet(): Set<Long> = savedState.get(SET_KEY) ?: emptySet() + + private fun resetPreviousSubmissionConsents() = launch { + try { + Timber.d("Trying to reset submission consents") + checkInRepository.apply { + val ids = completedCheckIns.first().filter { it.hasSubmissionConsent }.map { it.id } + updateSubmissionConsents(ids, consent = false) + } + + Timber.d("Resetting submission consents was successful") + } catch (error: Exception) { + Timber.e(error, "Failed to reset SubmissionConsents") + } + } + + @AssistedFactory + interface Factory : CWAViewModelFactory<CheckInsConsentViewModel> { + fun create( + savedState: SavedStateHandle, + ): CheckInsConsentViewModel + } + + companion object { + private const val SET_KEY = "selected_checkIn_set" + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt new file mode 100644 index 0000000000000000000000000000000000000000..397b621ac0264b1f6695b4e711ad6990e1b697a7 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.TraceLocationAttendeeConsentHeaderBinding +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class HeaderCheckInsVH(parent: ViewGroup) : + CheckInsConsentAdapter.ItemVH<HeaderCheckInsVH.Item, TraceLocationAttendeeConsentHeaderBinding>( + layoutRes = R.layout.trace_location_attendee_consent_header, + parent = parent + ) { + + override val viewBinding: Lazy<TraceLocationAttendeeConsentHeaderBinding> = lazy { + TraceLocationAttendeeConsentHeaderBinding.bind(itemView) + } + + override val onBindData: TraceLocationAttendeeConsentHeaderBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, _ -> + selectAllButton.setOnClickListener { item.selectAll() } + } + + data class Item( + val selectAll: () -> Unit + ) : CheckInsConsentItem, HasPayloadDiffer { + override val stableId: Long = Item::class.simpleName.hashCode().toLong() + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt new file mode 100644 index 0000000000000000000000000000000000000000..98178f9378b24aa8da9509c45a2c4f90ec9f7850 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import android.view.ViewGroup +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.TraceLocationAttendeeConsentSelectableCheckInBinding +import de.rki.coronawarnapp.presencetracing.checkins.CheckIn +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.common.checkoutInfo +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer + +class SelectableCheckInVH(parent: ViewGroup) : + CheckInsConsentAdapter.ItemVH<SelectableCheckInVH.Item, TraceLocationAttendeeConsentSelectableCheckInBinding>( + layoutRes = R.layout.trace_location_attendee_consent_selectable_check_in, + parent = parent + ) { + + override val viewBinding: Lazy<TraceLocationAttendeeConsentSelectableCheckInBinding> = lazy { + TraceLocationAttendeeConsentSelectableCheckInBinding.bind(itemView) + } + + override val onBindData: TraceLocationAttendeeConsentSelectableCheckInBinding.( + item: Item, + payloads: List<Any> + ) -> Unit = { item, payloads -> + val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item + val checkIn = curItem.checkIn + val imageResource = if (checkIn.hasSubmissionConsent) R.drawable.ic_selected else R.drawable.ic_unselected + + checkbox.setImageResource(imageResource) + title.text = checkIn.description + subtitle.text = checkIn.address + checkoutInfo.text = checkIn.checkoutInfo + + checkbox.setOnClickListener { item.onItemSelected(checkIn) } + itemView.setOnClickListener { item.onItemSelected(checkIn) } + } + + data class Item( + val checkIn: CheckIn, + val onItemSelected: (CheckIn) -> Unit + ) : CheckInsConsentItem, HasPayloadDiffer { + override val stableId: Long = checkIn.id + override fun diffPayload(old: Any, new: Any): Any? = if (old::class == new::class) new else null + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/PastCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/PastCheckInVH.kt index 392de2548ba9c038e928fa23ce336ca1c5a138b1..106b09f13341fddceb6265d968b8ab136b5ef922 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/PastCheckInVH.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/items/PastCheckInVH.kt @@ -4,10 +4,9 @@ import android.view.ViewGroup import de.rki.coronawarnapp.R import de.rki.coronawarnapp.databinding.TraceLocationAttendeeCheckinsItemPastBinding import de.rki.coronawarnapp.presencetracing.checkins.CheckIn -import de.rki.coronawarnapp.util.TimeAndDateExtensions.toUserTimeZone +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.common.checkoutInfo import de.rki.coronawarnapp.util.list.SwipeConsumer import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer -import org.joda.time.format.DateTimeFormat class PastCheckInVH(parent: ViewGroup) : BaseCheckInVH<PastCheckInVH.Item, TraceLocationAttendeeCheckinsItemPastBinding>( @@ -24,20 +23,10 @@ class PastCheckInVH(parent: ViewGroup) : payloads: List<Any> ) -> Unit = { item, payloads -> val curItem = payloads.filterIsInstance<Item>().singleOrNull() ?: item - - val checkInStartUserTZ = curItem.checkin.checkInStart.toUserTimeZone() - val checkInEndUserTZ = curItem.checkin.checkInEnd.toUserTimeZone() - description.text = curItem.checkin.description address.text = curItem.checkin.address - checkoutInfo.text = run { - val dayFormatted = checkInStartUserTZ.toLocalDate().toString(DateTimeFormat.mediumDate()) - val startTimeFormatted = checkInStartUserTZ.toLocalTime().toString(DateTimeFormat.shortTime()) - val endTimeFormatted = checkInEndUserTZ.toLocalTime().toString(DateTimeFormat.shortTime()) - - "$dayFormatted, $startTimeFormatted - $endTimeFormatted" - } + checkoutInfo.text = curItem.checkin.checkoutInfo menuAction.setupMenu(R.menu.menu_trace_location_attendee_checkin_item) { when (it.itemId) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/confirm/ConfirmCheckInViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/confirm/ConfirmCheckInViewModel.kt index ae6338f35bae7c8dea56f31b8466f1e2a54381aa..4acce9986eb10ac33a056e211927cf0b1c068b87 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/confirm/ConfirmCheckInViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/confirm/ConfirmCheckInViewModel.kt @@ -11,6 +11,7 @@ import de.rki.coronawarnapp.presencetracing.checkins.qrcode.VerifiedTraceLocatio import de.rki.coronawarnapp.presencetracing.checkins.qrcode.getDefaultAutoCheckoutLengthInMinutes import de.rki.coronawarnapp.ui.durationpicker.toContactDiaryFormat import de.rki.coronawarnapp.ui.durationpicker.toReadableDuration +import de.rki.coronawarnapp.ui.presencetracing.attendee.TraceLocationAttendeeSettings import de.rki.coronawarnapp.ui.presencetracing.organizer.category.adapter.category.mapTraceLocationToTitleRes import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.ui.SingleLiveEvent @@ -18,6 +19,7 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import org.joda.time.Duration import org.joda.time.Instant import org.joda.time.format.DateTimeFormat @@ -25,10 +27,11 @@ import org.joda.time.format.DateTimeFormat class ConfirmCheckInViewModel @AssistedInject constructor( @Assisted private val verifiedTraceLocation: VerifiedTraceLocation, private val checkInRepository: CheckInRepository, - private val timeStamper: TimeStamper + private val timeStamper: TimeStamper, + private val traceLocationAttendeeSettings: TraceLocationAttendeeSettings ) : CWAViewModel() { private val traceLocation = MutableStateFlow(verifiedTraceLocation.traceLocation) - private val createJournalEntry = MutableStateFlow(true) + private val createJournalEntry = traceLocationAttendeeSettings.createJournalEntryCheckedState private val autoCheckOutLength = MutableStateFlow( Duration.standardMinutes( @@ -64,7 +67,7 @@ class ConfirmCheckInViewModel @AssistedInject constructor( checkInRepository.addCheckIn( verifiedTraceLocation.toCheckIn( checkInStart = now, - createJournalEntry = createJournalEntry.value, + createJournalEntry = createJournalEntry.first(), checkInEnd = now + autoCheckOutLength.value ) ) @@ -73,7 +76,7 @@ class ConfirmCheckInViewModel @AssistedInject constructor( } fun createJournalEntryToggled(state: Boolean) { - createJournalEntry.value = state + traceLocationAttendeeSettings.setCreateJournalEntryCheckedState(state) } fun dateSelectorClicked() { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/details/QrCodeDetailFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/details/QrCodeDetailFragment.kt index b85dea9f8730792b52d0ad473b44e0a6e14d0203..2607499099a7fe0790bb63e57acfb8b70dd3a318 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/details/QrCodeDetailFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/organizer/details/QrCodeDetailFragment.kt @@ -138,7 +138,7 @@ class QrCodeDetailFragment : Fragment(R.layout.trace_location_organizer_qr_code_ binding.progressBar.hide() binding.qrCodeImage.apply { val resourceId = RoundedBitmapDrawableFactory.create(resources, it) - resourceId.cornerRadius = it.width * 0.1f + resourceId.cornerRadius = 15f setImageDrawable(resourceId) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt index 5bd94db299550124fb07ce5bec5682034b0a36cb..1cfcb3edcb2063e3b657184d3ad01764fa00d885 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/fragment/SubmissionContactFragment.kt @@ -45,10 +45,6 @@ class SubmissionContactFragment : Fragment(R.layout.fragment_submission_contact) ) } } - - viewModel.dial.observe2(this) { - dial() - } } override fun onResume() { @@ -60,19 +56,18 @@ class SubmissionContactFragment : Fragment(R.layout.fragment_submission_contact) binding.submissionContactHeader.headerButtonBack.buttonIcon.setOnClickListener { viewModel.onBackPressed() } - binding.submissionContactButtonCall.setOnClickListener { - viewModel.onDialPressed() - } - binding.includeSubmissionContact.submissionContactStep1Number.setOnClickListener { - viewModel.onDialPressed() - } + binding.includeSubmissionContact.submissionContactNavigationRowPhone.navigationRow + .setOnClickListener { + val number = getString(R.string.submission_contact_number_display) + ExternalActionHelper.call(this, number) + } + binding.includeSubmissionContact.submissionContactNavigationRowInternationalPhone.navigationRow + .setOnClickListener { + val number = getString(R.string.submission_contact_button_international_phone) + ExternalActionHelper.call(this, number) + } binding.submissionContactButtonEnter.setOnClickListener { viewModel.onEnterTanPressed() } } - - private fun dial() = context?.let { - val number = getString(R.string.submission_contact_number_dial) - ExternalActionHelper.call(this, number) - } } 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 index 1428e0f4fc1d1a3862db37a91653c4a934ae0704..459ea67f482258a9e58aa664e74dec82f37587d0 100644 --- 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 @@ -29,7 +29,8 @@ class SubmissionConsentViewModel @AssistedInject constructor( .asLiveData(context = dispatcherProvider.Default) fun onConsentButtonClick() { - submissionRepository.giveConsentToSubmission() + // TODO Do we have a Test registered at this time? We need to forward the decission with navargs? +// submissionRepository.giveConsentToSubmission(type = CoronaTest.Type.PCR) analyticsKeySubmissionCollector.reportAdvancedConsentGiven() launch { try { 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 855e5810489aef55471e6980f336ea3aa506ad83..f19296ecba2b0a05766d8ddd40b6ae8d4546ee30 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 @@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment import com.google.zxing.BarcodeFormat import com.journeyapps.barcodescanner.DefaultDecoderFactory import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.databinding.FragmentSubmissionQrCodeScanBinding import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.exception.http.CwaClientError @@ -20,7 +21,6 @@ import de.rki.coronawarnapp.ui.submission.ScanStatus 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.TestResult import de.rki.coronawarnapp.util.permission.CameraPermissionHelper import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 @@ -86,7 +86,7 @@ class SubmissionQRCodeScanFragment : else -> View.GONE } if (ApiRequestState.SUCCESS == state.apiRequestState) { - if (state.testResult == TestResult.POSITIVE) { + if (state.testResult == CoronaTestResult.PCR_POSITIVE) { doNavigate( SubmissionQRCodeScanFragmentDirections .actionSubmissionQRCodeScanFragmentToSubmissionTestResultAvailableFragment() 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 8ebceb7827da23d3fd9d2adab78675332da5991b..ff300dd094c8610d70ccd2ec42bc7231d4114c37 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 @@ -5,16 +5,19 @@ import androidx.lifecycle.MutableLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator +import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest 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.QRScanResult import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ApiRequestState import de.rki.coronawarnapp.ui.submission.ScanStatus import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents -import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.permission.CameraSettings import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -23,21 +26,21 @@ import timber.log.Timber class SubmissionQRCodeScanViewModel @AssistedInject constructor( private val submissionRepository: SubmissionRepository, - private val cameraSettings: CameraSettings + private val cameraSettings: CameraSettings, + private val qrCodeValidator: CoronaTestQrCodeValidator ) : CWAViewModel() { val routeToScreen = SingleLiveEvent<SubmissionNavigationEvents>() val showRedeemedTokenWarning = SingleLiveEvent<Unit>() val scanStatusValue = SingleLiveEvent<ScanStatus>() - open class InvalidQRCodeException : Exception("error in qr code") - fun validateTestGUID(rawResult: String) { - val scanResult = QRScanResult(rawResult) - if (scanResult.isValid) { - QRCodeCensor.lastGUID = scanResult.guid + try { + val coronaTestQRCode = qrCodeValidator.validate(rawResult) + // TODO this needs to be adapted to work for different types + QRCodeCensor.lastGUID = coronaTestQRCode.registrationIdentifier scanStatusValue.postValue(ScanStatus.SUCCESS) - doDeviceRegistration(scanResult) - } else { + doDeviceRegistration(coronaTestQRCode) + } catch (err: InvalidQRCodeException) { scanStatusValue.postValue(ScanStatus.INVALID) } } @@ -47,16 +50,17 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( data class RegistrationState( val apiRequestState: ApiRequestState, - val testResult: TestResult? = null + val testResult: CoronaTestResult? = null ) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun doDeviceRegistration(scanResult: QRScanResult) = launch { + internal fun doDeviceRegistration(coronaTestQRCode: CoronaTestQRCode) = launch { try { registrationState.postValue(RegistrationState(ApiRequestState.STARTED)) - val testResult = submissionRepository.asyncRegisterDeviceViaGUID(scanResult.guid!!) - checkTestResult(testResult) - registrationState.postValue(RegistrationState(ApiRequestState.SUCCESS, testResult)) + val coronaTest = submissionRepository.registerTest(coronaTestQRCode) + submissionRepository.giveConsentToSubmission(type = coronaTestQRCode.type) + checkTestResult(coronaTest.testResult) + registrationState.postValue(RegistrationState(ApiRequestState.SUCCESS, coronaTest.testResult)) } catch (err: CwaWebException) { registrationState.postValue(RegistrationState(ApiRequestState.FAILED)) registrationError.postValue(err) @@ -77,8 +81,8 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( } } - private fun checkTestResult(testResult: TestResult) { - if (testResult == TestResult.REDEEMED) { + private fun checkTestResult(testResult: CoronaTestResult) { + if (testResult == CoronaTestResult.PCR_REDEEMED) { throw InvalidQRCodeException() } } @@ -87,7 +91,7 @@ class SubmissionQRCodeScanViewModel @AssistedInject constructor( launch { Timber.d("deregisterTestFromDevice()") - submissionRepository.removeTestFromDevice() + submissionRepository.removeTestFromDevice(type = CoronaTest.Type.PCR) routeToScreen.postValue(SubmissionNavigationEvents.NavigateToMainActivity) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt index 3e5d0a7cbaf823d3ec6cdf162fa0a64992ffbc39..069dbdbe54906de32218bb61202cd0d263a996ef 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt @@ -7,9 +7,12 @@ import androidx.navigation.NavDirections import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater @@ -17,20 +20,28 @@ import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map import timber.log.Timber class SubmissionTestResultAvailableViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory, submissionRepository: SubmissionRepository, + private val checkInRepository: CheckInRepository, private val autoSubmission: AutoSubmission, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this? + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + val routeToScreen = SingleLiveEvent<NavDirections>() - val consentFlow = submissionRepository.hasGivenConsentToSubmission + private val consentFlow = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { it.isAdvancedConsentGiven } val consent = consentFlow.asLiveData(dispatcherProvider.Default) val showPermissionRequest = SingleLiveEvent<(Activity) -> Unit>() val showCloseDialog = SingleLiveEvent<Unit>() @@ -39,14 +50,21 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor( private val tekHistoryUpdater = tekHistoryUpdaterFactory.create( object : TEKHistoryUpdater.Callback { - override fun onTEKAvailable(teks: List<TemporaryExposureKey>) { - Timber.d("onTEKAvailable(teks.size=%d)", teks.size) - autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + override fun onTEKAvailable(teks: List<TemporaryExposureKey>) = launch { + Timber.tag(TAG).d("onTEKAvailable(teks.size=%d)", teks.size) showKeysRetrievalProgress.postValue(false) - routeToScreen.postValue( + val completedCheckInsExist = checkInRepository.completedCheckIns.first().isNotEmpty() + val navDirections = if (completedCheckInsExist) { + Timber.tag(TAG).d("Navigate to CheckInsConsentFragment") + SubmissionTestResultAvailableFragmentDirections + .actionSubmissionTestResultAvailableFragmentToCheckInsConsentFragment() + } else { + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + Timber.tag(TAG).d("Navigate to SubmissionTestResultConsentGivenFragment") SubmissionTestResultAvailableFragmentDirections .actionSubmissionTestResultAvailableFragmentToSubmissionTestResultConsentGivenFragment() - ) + } + routeToScreen.postValue(navDirections) } override fun onTEKPermissionDeclined() { @@ -59,13 +77,13 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor( } override fun onTracingConsentRequired(onConsentResult: (given: Boolean) -> Unit) { - Timber.d("onTracingConsentRequired") + Timber.tag(TAG).d("onTracingConsentRequired") showKeysRetrievalProgress.postValue(false) showTracingConsentDialog.postValue(onConsentResult) } override fun onPermissionRequired(permissionRequest: (Activity) -> Unit) { - Timber.d("onPermissionRequired") + Timber.tag(TAG).d("onPermissionRequired") showKeysRetrievalProgress.postValue(false) showPermissionRequest.postValue(permissionRequest) } @@ -82,7 +100,7 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor( ) init { - submissionRepository.refreshDeviceUIState(refreshTestResult = false) + submissionRepository.refreshTest(type = CoronaTest.Type.PCR) } fun goBack() { @@ -109,10 +127,10 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor( showKeysRetrievalProgress.value = true launch { if (consentFlow.first()) { - Timber.d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission") + Timber.tag(TAG).d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission") tekHistoryUpdater.updateTEKHistoryOrRequestPermission() } else { - Timber.d("routeToScreen:SubmissionTestResultNoConsentFragment") + Timber.tag(TAG).d("routeToScreen:SubmissionTestResultNoConsentFragment") analyticsKeySubmissionCollector.reportConsentWithdrawn() showKeysRetrievalProgress.postValue(false) routeToScreen.postValue( @@ -130,4 +148,8 @@ class SubmissionTestResultAvailableViewModel @AssistedInject constructor( @AssistedFactory interface Factory : SimpleCWAViewModelFactory<SubmissionTestResultAvailableViewModel> + + companion object { + private const val TAG = "TestAvailableViewModel" + } } 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 84b91e5b5d1600042d7f7fbfa33b28ce305570e6..ed42c6abe15548d9258ed11a022cfc54c8030bd0 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 @@ -4,6 +4,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.TransactionException import de.rki.coronawarnapp.exception.http.CwaWebException @@ -51,7 +53,8 @@ class SubmissionTanViewModel @AssistedInject constructor( launch { try { registrationState.postValue(ApiRequestState.STARTED) - submissionRepository.asyncRegisterDeviceViaTAN(teletan.value) + val request = CoronaTestTAN.PCR(tan = teletan.value) + submissionRepository.registerTest(request) registrationState.postValue(ApiRequestState.SUCCESS) } catch (err: CwaWebException) { registrationState.postValue(ApiRequestState.FAILED) @@ -67,7 +70,8 @@ class SubmissionTanViewModel @AssistedInject constructor( registrationState.postValue(ApiRequestState.FAILED) err.report(ExceptionCategory.INTERNAL) } finally { - submissionRepository.refreshDeviceUIState(refreshTestResult = false) + // TODO Should not be necessary? What new data would we + submissionRepository.refreshTest(type = CoronaTest.Type.PCR) } } } 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 a34a02cad13d8a16e1c550ef47aa4c2ce27dfd2d..d9a2808b6eeaeb5c83781d89b52d4b8b44af5c24 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,10 +1,7 @@ package de.rki.coronawarnapp.ui.submission.testresult -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper -import java.util.Date +import de.rki.coronawarnapp.coronatest.type.CoronaTest data class TestResultUIState( - val deviceUiState: NetworkRequestWrapper<DeviceUIState, Throwable>, - val testResultReceivedDate: Date? + val coronaTest: CoronaTest ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidFragment.kt index e4fcde50e12fc1d1c91af144a7f58ba3306fbe09..8ee096b5f6da43022e8b30083fbd8775f0c97924 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidFragment.kt @@ -34,7 +34,7 @@ class SubmissionTestResultInvalidFragment : Fragment(R.layout.fragment_submissio } viewModel.testResult.observe2(this) { - binding.submissionTestResultSection.setTestResultSection(it.deviceUiState, it.testResultReceivedDate) + binding.submissionTestResultSection.setTestResultSection(it.coronaTest) } viewModel.routeToScreen.observe2(this) { navDirections -> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt index 870df2853cfdc629cf13f2873ec5cd39a39519ad..81a4ebbc057e1effeed28b8b0f1505ef35b8cbfc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/invalid/SubmissionTestResultInvalidViewModel.kt @@ -5,45 +5,48 @@ import androidx.lifecycle.asLiveData import androidx.navigation.NavDirections import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import timber.log.Timber class SubmissionTestResultInvalidViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val submissionRepository: SubmissionRepository, - private val testResultAvailableNotificationService: TestResultAvailableNotificationService + private val testResultAvailableNotificationService: TestResultAvailableNotificationService, ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } val routeToScreen = SingleLiveEvent<NavDirections?>() - val testResult: LiveData<TestResultUIState> = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.testResultReceivedDateFlow - ) { deviceUiState, resultDate -> - TestResultUIState( - deviceUiState = deviceUiState, - testResultReceivedDate = resultDate - ) - }.asLiveData(context = dispatcherProvider.Default) - - fun deregisterTestFromDevice() { + val testResult: LiveData<TestResultUIState> = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { test -> + TestResultUIState(coronaTest = test) + }.asLiveData(context = dispatcherProvider.Default) + + fun deregisterTestFromDevice() = launch { Timber.d("deregisterTestFromDevice()") - launch { - submissionRepository.removeTestFromDevice() - routeToScreen.postValue(null) - } + submissionRepository.removeTestFromDevice(type = coronaTestType) + routeToScreen.postValue(null) } - fun onTestOpened() { - submissionRepository.setViewedTestResult() + fun onTestOpened() = launch { + Timber.d("onTestOpened()") + submissionRepository.setViewedTestResult(type = coronaTestType) testResultAvailableNotificationService.cancelTestResultAvailableNotification() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeFragment.kt index 609cec522f708fd4171355299dcb72fce81670b3..3372b725f72f8731e29955ac4a37d8e49b3af704 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeFragment.kt @@ -34,7 +34,7 @@ class SubmissionTestResultNegativeFragment : Fragment(R.layout.fragment_submissi } viewModel.testResult.observe2(this) { - binding.submissionTestResultSection.setTestResultSection(it.deviceUiState, it.testResultReceivedDate) + binding.submissionTestResultSection.setTestResultSection(it.coronaTest) } viewModel.routeToScreen.observe2(this) { navDirections -> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt index b1d734482f18730dee076c93430059264ab5f0d9..e7f6ebec332e3ad125f57f9dc7164cebff9ae569 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/negative/SubmissionTestResultNegativeViewModel.kt @@ -5,14 +5,16 @@ import androidx.lifecycle.asLiveData import androidx.navigation.NavDirections import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import timber.log.Timber class SubmissionTestResultNegativeViewModel @AssistedInject constructor( @@ -20,29 +22,30 @@ class SubmissionTestResultNegativeViewModel @AssistedInject constructor( private val submissionRepository: SubmissionRepository, private val testResultAvailableNotificationService: TestResultAvailableNotificationService ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } val routeToScreen = SingleLiveEvent<NavDirections?>() - val testResult: LiveData<TestResultUIState> = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.testResultReceivedDateFlow - ) { deviceUiState, resultDate -> - TestResultUIState( - deviceUiState = deviceUiState, - testResultReceivedDate = resultDate - ) - }.asLiveData(context = dispatcherProvider.Default) - - fun deregisterTestFromDevice() { - launch { - Timber.tag(TAG).d("deregisterTestFromDevice()") - submissionRepository.removeTestFromDevice() - - routeToScreen.postValue(null) - } + val testResult: LiveData<TestResultUIState> = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { test -> + TestResultUIState(coronaTest = test) + }.asLiveData(context = dispatcherProvider.Default) + + fun deregisterTestFromDevice() = launch { + Timber.tag(TAG).d("deregisterTestFromDevice()") + submissionRepository.removeTestFromDevice(type = coronaTestType) + + routeToScreen.postValue(null) } - fun onTestOpened() { - submissionRepository.setViewedTestResult() + fun onTestOpened() = launch { + Timber.tag(TAG).d("onTestOpened()") + submissionRepository.setViewedTestResult(type = coronaTestType) testResultAvailableNotificationService.cancelTestResultAvailableNotification() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt index 864629916a35ab0a185955bbbe351502c80e48c3..2c2ed4a3ca78d308b9dfefdea4cd66dcc85e4fec 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingFragment.kt @@ -11,7 +11,6 @@ import de.rki.coronawarnapp.exception.http.CwaClientError import de.rki.coronawarnapp.exception.http.CwaServerError import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.DialogHelper -import de.rki.coronawarnapp.util.NetworkRequestWrapper import de.rki.coronawarnapp.util.di.AutoInject import de.rki.coronawarnapp.util.ui.doNavigate import de.rki.coronawarnapp.util.ui.observe2 @@ -42,9 +41,9 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio } pendingViewModel.testState.observe2(this) { result -> - val hasResult = result.deviceUiState is NetworkRequestWrapper.RequestSuccessful + val hasResult = !result.coronaTest.isProcessing binding.apply { - submissionTestResultSection.setTestResultSection(result.deviceUiState, result.testResultReceivedDate) + submissionTestResultSection.setTestResultSection(result.coronaTest) submissionTestResultSpinner.setInvisible(hasResult) submissionTestResultContent.setInvisible(!hasResult) buttonContainer.setInvisible(!hasResult) @@ -89,7 +88,7 @@ class SubmissionTestResultPendingFragment : Fragment(R.layout.fragment_submissio override fun onResume() { super.onResume() binding.submissionTestResultContainer.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT) - pendingViewModel.refreshDeviceUIState(refreshTestResult = !skipInitialTestResultRefresh) + pendingViewModel.refreshDeviceUIState() skipInitialTestResultRefresh = false pendingViewModel.cwaWebExceptionLiveData.observeOnce(this.viewLifecycleOwner) { exception -> handleError(exception) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt index 9c82958cca133c04f554e1affcfb2362f19de084..b01b89606ed2a5b22e16efeacf5164f9b077abf5 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/pending/SubmissionTestResultPendingViewModel.kt @@ -5,19 +5,18 @@ import androidx.lifecycle.asLiveData import androidx.navigation.NavDirections import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.toDeviceUIState import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState 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.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -30,60 +29,58 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor( private val shareTestResultNotificationService: ShareTestResultNotificationService, private val submissionRepository: SubmissionRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } val routeToScreen = SingleLiveEvent<NavDirections?>() val showRedeemedTokenWarning = SingleLiveEvent<Unit>() - val consentGiven = submissionRepository.hasGivenConsentToSubmission.asLiveData() + val consentGiven = submissionRepository.testForType(type = coronaTestType).map { + it?.isAdvancedConsentGiven ?: false + }.asLiveData() private var wasRedeemedTokenErrorShown = false private val tokenErrorMutex = Mutex() - private val testResultFlow = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.testResultReceivedDateFlow - ) { deviceUiState, resultDate -> - - tokenErrorMutex.withLock { - if (!wasRedeemedTokenErrorShown) { - deviceUiState.withSuccess { - if (it == DeviceUIState.PAIRED_REDEEMED) { + private val testResultFlow = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { test -> + tokenErrorMutex.withLock { + if (!wasRedeemedTokenErrorShown) { + if (test.testResult.toDeviceUIState() == DeviceUIState.PAIRED_REDEEMED) { wasRedeemedTokenErrorShown = true showRedeemedTokenWarning.postValue(Unit) } } } + TestResultUIState(coronaTest = test) } - TestResultUIState( - deviceUiState = deviceUiState, - testResultReceivedDate = resultDate - ) - } val testState: LiveData<TestResultUIState> = testResultFlow .onEach { testResultUIState -> - testResultUIState.deviceUiState.withSuccess { deviceState -> - when (deviceState) { - DeviceUIState.PAIRED_POSITIVE -> - SubmissionTestResultPendingFragmentDirections - .actionSubmissionTestResultPendingFragmentToSubmissionTestResultAvailableFragment() - DeviceUIState.PAIRED_NEGATIVE -> - SubmissionTestResultPendingFragmentDirections - .actionSubmissionTestResultPendingFragmentToSubmissionTestResultNegativeFragment() - DeviceUIState.PAIRED_REDEEMED, - DeviceUIState.PAIRED_ERROR -> - SubmissionTestResultPendingFragmentDirections - .actionSubmissionTestResultPendingFragmentToSubmissionTestResultInvalidFragment() - else -> { - Timber.w("Unknown success state: %s", deviceState) - null - } - }?.let { routeToScreen.postValue(it) } - } + when (val deviceState = testResultUIState.coronaTest.testResult.toDeviceUIState()) { + DeviceUIState.PAIRED_POSITIVE -> + SubmissionTestResultPendingFragmentDirections + .actionSubmissionTestResultPendingFragmentToSubmissionTestResultAvailableFragment() + DeviceUIState.PAIRED_NEGATIVE -> + SubmissionTestResultPendingFragmentDirections + .actionSubmissionTestResultPendingFragmentToSubmissionTestResultNegativeFragment() + DeviceUIState.PAIRED_REDEEMED, + DeviceUIState.PAIRED_ERROR -> + SubmissionTestResultPendingFragmentDirections + .actionSubmissionTestResultPendingFragmentToSubmissionTestResultInvalidFragment() + else -> { + Timber.w("Unknown success state: %s", deviceState) + null + } + }?.let { routeToScreen.postValue(it) } } - .filter { - val isPositiveTest = it.deviceUiState is NetworkRequestWrapper.RequestSuccessful && - it.deviceUiState.data == DeviceUIState.PAIRED_POSITIVE + .filter { testResultUIState -> + val isPositiveTest = testResultUIState.coronaTest.isSubmissionAllowed if (isPositiveTest) { Timber.w("Filtering out positive test emission as we don't display this here.") } @@ -91,31 +88,27 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor( } .asLiveData(context = dispatcherProvider.Default) - val cwaWebExceptionLiveData = submissionRepository.deviceUIStateFlow - .filterIsInstance<NetworkRequestWrapper.RequestFailed<DeviceUIState, Throwable>>() - .map { it.error } + val cwaWebExceptionLiveData = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .filter { it.lastError != null } + .map { it.lastError!! } .asLiveData() fun observeTestResultToSchedulePositiveTestResultReminder() = launch { - submissionRepository.deviceUIStateFlow - .first { request -> - request.withSuccess(false) { - it == DeviceUIState.PAIRED_POSITIVE || it == DeviceUIState.PAIRED_POSITIVE_TELETAN - } - } + submissionRepository.testForType(type = coronaTestType) + .first { request -> request?.isSubmissionAllowed ?: false } .also { shareTestResultNotificationService.scheduleSharePositiveTestResultReminder() } } - fun deregisterTestFromDevice() { + fun deregisterTestFromDevice() = launch { Timber.d("deregisterTestFromDevice()") - launch { - submissionRepository.removeTestFromDevice() - routeToScreen.postValue(null) - } + submissionRepository.removeTestFromDevice(type = coronaTestType) + routeToScreen.postValue(null) } - fun refreshDeviceUIState(refreshTestResult: Boolean = true) { - submissionRepository.refreshDeviceUIState(refreshTestResult) + fun refreshDeviceUIState() = launch { + Timber.v("refreshDeviceUIState()") + submissionRepository.refreshTest(type = coronaTestType) } fun onConsentClicked() { @@ -127,8 +120,4 @@ class SubmissionTestResultPendingViewModel @AssistedInject constructor( @AssistedFactory interface Factory : SimpleCWAViewModelFactory<SubmissionTestResultPendingViewModel> - - companion object { - private const val TAG = "SubmissionTestResult:VM" - } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenFragment.kt index 4b61145a714f16401e56cc941d5e6ad282729d82..411733b90aebaa7f501eeba3f8f40036222d0690 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenFragment.kt @@ -47,7 +47,7 @@ class SubmissionTestResultConsentGivenFragment : viewModel.uiState.observe2(this) { binding.apply { uiState = it - submissionTestResultSection.setTestResultSection(it.deviceUiState, it.testResultReceivedDate) + submissionTestResultSection.setTestResultSection(it.coronaTest) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt index a1277d4c81ad3c6ffd3e2a4df94e520c82891496..053baa300837faac283f575edda2cd5633f30e78 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultConsentGivenViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService @@ -12,11 +13,12 @@ import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState import de.rki.coronawarnapp.ui.submission.viewmodel.SubmissionNavigationEvents import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import timber.log.Timber class SubmissionTestResultConsentGivenViewModel @AssistedInject constructor( @@ -26,26 +28,29 @@ class SubmissionTestResultConsentGivenViewModel @AssistedInject constructor( private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector, dispatcherProvider: DispatcherProvider ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } val showUploadDialog = autoSubmission.isSubmissionRunning .asLiveData(context = dispatcherProvider.Default) - val uiState: LiveData<TestResultUIState> = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.testResultReceivedDateFlow - ) { deviceUiState, resultDate -> - TestResultUIState( - deviceUiState = deviceUiState, - testResultReceivedDate = resultDate - ) - }.asLiveData(context = Dispatchers.Default) + val uiState: LiveData<TestResultUIState> = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { test -> + TestResultUIState(coronaTest = test) + }.asLiveData(context = Dispatchers.Default) val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() val showCancelDialog = SingleLiveEvent<Unit>() - fun onTestOpened() { - submissionRepository.setViewedTestResult() + fun onTestOpened() = launch { + Timber.d("onTestOpened()") + submissionRepository.setViewedTestResult(type = coronaTestType) testResultAvailableNotificationService.cancelTestResultAvailableNotification() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentFragment.kt index 2b1fa0d5a3a26617ed28f3031f18c4e5ef0fb68a..04374d1b4d688bfe8e66003f303e30dd16312d45 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentFragment.kt @@ -41,8 +41,7 @@ class SubmissionTestResultNoConsentFragment : requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) viewModel.uiState.observe2(this) { - binding.submissionTestResultSection - .setTestResultSection(it.deviceUiState, it.testResultReceivedDate) + binding.submissionTestResultSection.setTestResultSection(it.coronaTest) } binding.apply { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt index 49b07dec8537509cf4040c211f5203004102b380..8161c5e817c77b091843d54556f7425ec76fa7c8 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/testresult/positive/SubmissionTestResultNoConsentViewModel.kt @@ -4,36 +4,41 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.TestResultUIState -import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import timber.log.Timber class SubmissionTestResultNoConsentViewModel @AssistedInject constructor( private val submissionRepository: SubmissionRepository, private val testResultAvailableNotificationService: TestResultAvailableNotificationService, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector ) : CWAViewModel() { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR - val uiState: LiveData<TestResultUIState> = combine( - submissionRepository.deviceUIStateFlow, - submissionRepository.testResultReceivedDateFlow - ) { deviceUiState, resultDate -> + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } - TestResultUIState( - deviceUiState = deviceUiState, - testResultReceivedDate = resultDate - ) - }.asLiveData(context = Dispatchers.Default) + val uiState: LiveData<TestResultUIState> = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { test -> + TestResultUIState(coronaTest = test) + }.asLiveData(context = Dispatchers.Default) - fun onTestOpened() { + fun onTestOpened() = launch { + Timber.v("onTestOpened()") analyticsKeySubmissionCollector.reportLastSubmissionFlowScreen(Screen.TEST_RESULT) - submissionRepository.setViewedTestResult() + submissionRepository.setViewedTestResult(type = coronaTestType) testResultAvailableNotificationService.cancelTestResultAvailableNotification() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionContactViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionContactViewModel.kt index 37840183038353ac054037063d756fed0ee4e855..dc290b19fe035ca2cd2dd8caf049a6a957012ea0 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionContactViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/viewmodel/SubmissionContactViewModel.kt @@ -9,16 +9,11 @@ import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory class SubmissionContactViewModel @AssistedInject constructor() : CWAViewModel() { val routeToScreen: SingleLiveEvent<SubmissionNavigationEvents> = SingleLiveEvent() - val dial = SingleLiveEvent<Unit>() fun onBackPressed() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToDispatcher) } - fun onDialPressed() { - dial.postValue(Unit) - } - fun onEnterTanPressed() { routeToScreen.postValue(SubmissionNavigationEvents.NavigateToTAN) } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt index da6c4e5148d24e6535c49def2d4de866d86047c9..8dad6e8631dcf73bd095391cfe2b28d55b537bb4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt @@ -7,11 +7,14 @@ import androidx.navigation.NavDirections import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission @@ -30,8 +33,15 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory, interoperabilityRepository: InteroperabilityRepository, private val submissionRepository: SubmissionRepository, + private val checkInRepository: CheckInRepository, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } val routeToScreen = SingleLiveEvent<NavDirections>() @@ -48,36 +58,44 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con private val tekHistoryUpdater = tekHistoryUpdaterFactory.create( object : TEKHistoryUpdater.Callback { - override fun onTEKAvailable(teks: List<TemporaryExposureKey>) { - Timber.d("onTEKAvailable(tek.size=%d)", teks.size) - autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + override fun onTEKAvailable(teks: List<TemporaryExposureKey>) = launch { + Timber.tag(TAG).d("onTEKAvailable(tek.size=%d)", teks.size) showKeysRetrievalProgress.postValue(false) - routeToScreen.postValue( + + val completedCheckInsExist = checkInRepository.completedCheckIns.first().isNotEmpty() + val navDirections = if (completedCheckInsExist) { + Timber.tag(TAG).d("Navigate to CheckInsConsentFragment") + SubmissionResultPositiveOtherWarningNoConsentFragmentDirections + .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToCheckInsConsentFragment() + } else { + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + Timber.tag(TAG).d("Navigate to SubmissionResultReadyFragment") SubmissionResultPositiveOtherWarningNoConsentFragmentDirections .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToSubmissionResultReadyFragment() - ) + } + routeToScreen.postValue(navDirections) } override fun onTEKPermissionDeclined() { - Timber.d("onTEKPermissionDeclined") + Timber.tag(TAG).d("onTEKPermissionDeclined") showKeysRetrievalProgress.postValue(false) // stay on screen } override fun onTracingConsentRequired(onConsentResult: (given: Boolean) -> Unit) { - Timber.d("onTracingConsentRequired") + Timber.tag(TAG).d("onTracingConsentRequired") showKeysRetrievalProgress.postValue(false) showTracingConsentDialog.postValue(onConsentResult) } override fun onPermissionRequired(permissionRequest: (Activity) -> Unit) { - Timber.d("onPermissionRequired") + Timber.tag(TAG).d("onPermissionRequired") showKeysRetrievalProgress.postValue(false) showPermissionRequest.postValue(permissionRequest) } override fun onError(error: Throwable) { - Timber.e(error, "Couldn't access temporary exposure key history.") + Timber.tag(TAG).e(error, "Couldn't access temporary exposure key history.") showKeysRetrievalProgress.postValue(false) error.report(ExceptionCategory.EXPOSURENOTIFICATION, "Failed to obtain TEKs.") } @@ -91,15 +109,15 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con ) } - fun onConsentButtonClicked() { - showKeysRetrievalProgress.value = true - submissionRepository.giveConsentToSubmission() + fun onConsentButtonClicked() = launch { + showKeysRetrievalProgress.postValue(true) + submissionRepository.giveConsentToSubmission(type = coronaTestType) launch { if (enfClient.isTracingEnabled.first()) { - Timber.d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission()") + Timber.tag(TAG).d("tekHistoryUpdater.updateTEKHistoryOrRequestPermission()") tekHistoryUpdater.updateTEKHistoryOrRequestPermission() } else { - Timber.d("showEnableTracingEvent:Unit") + Timber.tag(TAG).d("showEnableTracingEvent:Unit") showKeysRetrievalProgress.postValue(false) showEnableTracingEvent.postValue(Unit) } @@ -107,6 +125,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con } fun onDataPrivacyClick() { + Timber.tag(TAG).d("onDataPrivacyClick") routeToScreen.postValue( SubmissionResultPositiveOtherWarningNoConsentFragmentDirections .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToInformationPrivacyFragment() @@ -114,6 +133,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con } fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + Timber.tag(TAG).d("handleActivityResult($resultCode)") showKeysRetrievalProgress.value = true tekHistoryUpdater.handleActivityResult(requestCode, resultCode, data) } @@ -126,4 +146,8 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con interface Factory : CWAViewModelFactory<SubmissionResultPositiveOtherWarningNoConsentViewModel> { fun create(): SubmissionResultPositiveOtherWarningNoConsentViewModel } + + companion object { + private const val TAG = "WarnNoConsentViewModel" + } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt index 8e76e60416cfd52d94eb293d41942a579f7c85d1..10e46bb07917a4188943464085ef60fd9d28257c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModel.kt @@ -3,22 +3,36 @@ package de.rki.coronawarnapp.ui.submission.yourconsent import androidx.lifecycle.asLiveData import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import timber.log.Timber class SubmissionYourConsentViewModel @AssistedInject constructor( val dispatcherProvider: DispatcherProvider, interoperabilityRepository: InteroperabilityRepository, val submissionRepository: SubmissionRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { + // TODO Use navargs to supply this + private val coronaTestType: CoronaTest.Type = CoronaTest.Type.PCR + + init { + Timber.v("init() coronaTestType=%s", coronaTestType) + } val clickEvent: SingleLiveEvent<SubmissionYourConsentEvents> = SingleLiveEvent() - val consent = submissionRepository.hasGivenConsentToSubmission.asLiveData() + private val consentFlow = submissionRepository.testForType(type = coronaTestType) + .filterNotNull() + .map { it.isAdvancedConsentGiven } + val consent = consentFlow.asLiveData(context = dispatcherProvider.Default) + val countryList = interoperabilityRepository.countryList .asLiveData(context = dispatcherProvider.Default) @@ -26,13 +40,13 @@ class SubmissionYourConsentViewModel @AssistedInject constructor( clickEvent.postValue(SubmissionYourConsentEvents.GoBack) } - fun switchConsent() { - launch { - if (submissionRepository.hasGivenConsentToSubmission.first()) { - submissionRepository.revokeConsentToSubmission() - } else { - submissionRepository.giveConsentToSubmission() - } + fun switchConsent() = launch { + if (consentFlow.first()) { + Timber.v("revokeConsentToSubmission()") + submissionRepository.revokeConsentToSubmission(type = coronaTestType) + } else { + Timber.v("giveConsentToSubmission()") + submissionRepository.giveConsentToSubmission(type = coronaTestType) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/TestResultSectionView.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/TestResultSectionView.kt index 2170fe780f40ff8847bb557fc3d63aaad4e3904f..11c140a9ff906b736dbafec4afda1f2c1aeddbc3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/TestResultSectionView.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/view/TestResultSectionView.kt @@ -9,14 +9,14 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.withStyledAttributes import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.databinding.ViewTestResultSectionBinding +import de.rki.coronawarnapp.submission.toDeviceUIState import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat 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 de.rki.coronawarnapp.util.formatter.formatTestResult -import java.util.Date +import org.joda.time.Instant /** * The [TestResultSectionView] Displays the appropriate test result. @@ -47,49 +47,46 @@ constructor( } } - fun setTestResultSection(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?, registeredAt: Date?) { + fun setTestResultSection(coronaTest: CoronaTest?) { binding.apply { testResultSectionHeadline.text = context.getString(R.string.test_result_card_headline) - testResultSectionRegisteredAtText.text = formatTestResultRegisteredAtText(registeredAt) - val testResultIcon = formatTestStatusIcon(uiState) + testResultSectionRegisteredAtText.text = formatTestResultRegisteredAtText(coronaTest?.registeredAt) + val testResultIcon = formatTestStatusIcon(coronaTest) testResultSectionStatusIcon.setImageDrawable(testResultIcon) - testResultSectionContent.text = formatTestResultSectionContent(uiState) + testResultSectionContent.text = formatTestResultSectionContent(coronaTest) } } - private 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 { context.getDrawableCompat(it) } + private fun formatTestStatusIcon(coronaTest: CoronaTest?): Drawable? { + val drawable = when (coronaTest?.testResult.toDeviceUIState()) { + 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 + } + return context.getDrawableCompat(drawable) } - private fun formatTestResultRegisteredAtText(registeredAt: Date?): String { + private fun formatTestResultRegisteredAtText(registeredAt: Instant?): String { return context.getString(R.string.test_result_card_registered_at_text) - .format(registeredAt?.toUIFormat(context)) + .format(registeredAt?.toDate()?.toUIFormat(context)) } - private fun formatTestResultSectionContent(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Spannable { - return uiState.withSuccess(SpannableString("")) { - when (it) { - DeviceUIState.PAIRED_NO_RESULT -> - SpannableString(context.getString(R.string.test_result_card_status_pending)) - DeviceUIState.PAIRED_ERROR, - DeviceUIState.PAIRED_REDEEMED -> - SpannableString(context.getString(R.string.test_result_card_status_invalid)) + private fun formatTestResultSectionContent(coronaTest: CoronaTest?): Spannable { + return when (val uiState = coronaTest?.testResult.toDeviceUIState()) { + DeviceUIState.PAIRED_NO_RESULT -> + SpannableString(context.getString(R.string.test_result_card_status_pending)) + DeviceUIState.PAIRED_ERROR, + DeviceUIState.PAIRED_REDEEMED -> + SpannableString(context.getString(R.string.test_result_card_status_invalid)) - DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN, - DeviceUIState.PAIRED_NEGATIVE -> SpannableString(formatTestResult(context, uiState)) - else -> SpannableString("") - } + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_NEGATIVE -> SpannableString(formatTestResult(context, uiState)) + else -> SpannableString("") } } } 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 9f43056902f79eba4ee9652a49d09fcf2a7fc151..6bcdf97ff9844aa616c2b6b62c890a3447cdd9bd 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 @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.contactdiary.storage.ContactDiaryPreferences import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.datadonation.analytics.Analytics import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.datadonation.survey.SurveySettings @@ -60,6 +61,7 @@ class DataReset @Inject constructor( private val checkInRepository: CheckInRepository, private val traceLocationSettings: TraceLocationSettings, private val traceWarningRepository: TraceWarningRepository, + private val coronaTestRepository: CoronaTestRepository, ) { private val mutex = Mutex() @@ -101,6 +103,7 @@ class DataReset @Inject constructor( traceWarningRepository.clear() traceLocationRepository.deleteAllTraceLocations() checkInRepository.clear() + coronaTestRepository.clear() Timber.w("CWA LOCAL DATA DELETION COMPLETED.") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt index f7f9f38cfecdbeb0fe260e791236ceb6d3cb8e9b..64dcf305d5544431a1ffd6e5e5a118a5a8d303a2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/TimeAndDateExtensions.kt @@ -93,18 +93,6 @@ object TimeAndDateExtensions { TimeUnit.MILLISECONDS.toSeconds(this) % TimeUnit.MINUTES.toSeconds(1) ) - /** - * Calculates the difference between two timestamps in Days Units - * - * @return Long - * - * @see TimeUnit - */ - fun calculateDays(firstDate: Long, secondDate: Long): Long { - val millionSeconds = secondDate - firstDate - return TimeUnit.MILLISECONDS.toDays(millionSeconds) - } - fun LocalDate.ageInDays(now: LocalDate) = Days.daysBetween(this, now).days fun Instant.toLocalDateUtc(): LocalDate = this.toDateTime(DateTimeZone.UTC).toLocalDate() 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 9ddda9b3ec68c4068a7577798d6b3dfeb7d6377d..fafb7e29de202995224714daf4bbcfe5178112b8 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 @@ -12,6 +12,7 @@ import de.rki.coronawarnapp.bugreporting.BugReportingModule import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule import de.rki.coronawarnapp.bugreporting.debuglog.DebugLogger import de.rki.coronawarnapp.coronatest.CoronaTestModule +import de.rki.coronawarnapp.coronatest.server.VerificationModule import de.rki.coronawarnapp.datadonation.DataDonationModule import de.rki.coronawarnapp.diagnosiskeys.DiagnosisKeysModule import de.rki.coronawarnapp.diagnosiskeys.DownloadDiagnosisKeysTaskModule @@ -43,7 +44,6 @@ import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.security.SecurityModule import de.rki.coronawarnapp.util.serialization.SerializationModule import de.rki.coronawarnapp.util.worker.WorkerBinder -import de.rki.coronawarnapp.verification.VerificationModule import de.rki.coronawarnapp.worker.BackgroundWorkScheduler import javax.inject.Singleton diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt index 64929458b059e6c40a2ea3e1705d438707916f80..b2eea5c60ce439ad2bb43140aabacaf9e1a4a151 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigration.kt @@ -64,22 +64,21 @@ class EncryptedPreferencesMigration @Inject constructor( } onboardingSettings.isBackgroundCheckDone = isBackgroundCheckDone() } - + @Suppress("DEPRECATION") TracingLocalData(encryptedSharedPreferences).apply { - tracingSettings.initialPollingForTestResultTimeStamp = initialPollingForTestResultTimeStamp() - tracingSettings.isTestResultAvailableNotificationSent = isTestResultAvailableNotificationSent() + tracingSettings.initialPollingForTestResultTimeStampMigration = initialPollingForTestResultTimeStamp() + tracingSettings.isTestResultAvailableNotificationSentMigration = isTestResultAvailableNotificationSent() tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel.update { isUserToBeNotifiedOfLoweredRiskLevel() } tracingSettings.isConsentGiven = initialTracingActivationTimestamp() != 0L } - + @Suppress("DEPRECATION") SubmissionLocalData(encryptedSharedPreferences).apply { - submissionSettings.registrationToken.update { - registrationToken() - } - submissionSettings.initialTestResultReceivedAt = initialTestResultReceivedTimestamp().toInstantOrNull() - submissionSettings.devicePairingSuccessfulAt = devicePairingSuccessfulTimestamp().toInstantOrNull() - submissionSettings.isSubmissionSuccessful = numberOfSuccessfulSubmissions() >= 1 - submissionSettings.isAllowedToSubmitKeys = isAllowedToSubmitDiagnosisKeys() + submissionSettings.registrationTokenMigration = registrationToken() + submissionSettings.initialTestResultReceivedAtMigration = + initialTestResultReceivedTimestamp().toInstantOrNull() + submissionSettings.devicePairingSuccessfulAtMigration = devicePairingSuccessfulTimestamp().toInstantOrNull() + submissionSettings.isSubmissionSuccessfulMigration = numberOfSuccessfulSubmissions() >= 1 + submissionSettings.isAllowedToSubmitKeysMigration = isAllowedToSubmitDiagnosisKeys() } Timber.i("copyData(): EncryptedPreferences have been copied.") } 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 9957c98680e872732a2d0bdde917a15bb0a8c5f6..4983861085b09dbad197b772a0847b2a331a4faf 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,14 +8,11 @@ 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.R import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat 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 @@ -68,29 +65,23 @@ fun formatSymptomBackgroundButtonStyleByState( R.color.colorCalendarBackgroundUnselected ) -fun formatTestResultStatusText(context: Context, 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 { context.getString(it) } - -fun formatTestResultStatusColor(context: Context, 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 { context.getColorCompat(it) } +fun formatTestResultStatusText(context: Context, uiState: DeviceUIState): String = when (uiState) { + 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 { context.getString(it) } + +fun formatTestResultStatusColor(context: Context, uiState: DeviceUIState): Int = when (uiState) { + DeviceUIState.PAIRED_NEGATIVE -> R.color.colorTextSemanticGreen + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN -> R.color.colorTextSemanticRed + else -> R.color.colorTextSemanticRed +}.let { context.getColorCompat(it) } fun formatTestResult( context: Context, - uiState: NetworkRequestWrapper<DeviceUIState, Throwable>? + uiState: DeviceUIState ): Spannable { return SpannableStringBuilder() .append(context.getString(R.string.test_result_card_virus_name_text)) @@ -104,51 +95,37 @@ fun formatTestResult( fun formatTestResultCardContent( context: Context, - uiState: NetworkRequestWrapper<DeviceUIState, Throwable>? + uiState: DeviceUIState ): Spannable { - return uiState.withSuccess(SpannableString("")) { - when (it) { - DeviceUIState.PAIRED_NO_RESULT -> - SpannableString(context.getString(R.string.test_result_card_status_pending)) - DeviceUIState.PAIRED_ERROR, - DeviceUIState.PAIRED_REDEEMED -> - SpannableString(context.getString(R.string.test_result_card_status_invalid)) - - DeviceUIState.PAIRED_POSITIVE, - DeviceUIState.PAIRED_POSITIVE_TELETAN, - DeviceUIState.PAIRED_NEGATIVE -> SpannableString(formatTestResult(context, uiState)) - else -> SpannableString("") - } + return when (uiState) { + DeviceUIState.PAIRED_NO_RESULT -> + SpannableString(context.getString(R.string.test_result_card_status_pending)) + DeviceUIState.PAIRED_ERROR, + DeviceUIState.PAIRED_REDEEMED -> + SpannableString(context.getString(R.string.test_result_card_status_invalid)) + + DeviceUIState.PAIRED_POSITIVE, + DeviceUIState.PAIRED_POSITIVE_TELETAN, + DeviceUIState.PAIRED_NEGATIVE -> SpannableString(formatTestResult(context, uiState)) + else -> SpannableString("") } } -fun formatTestStatusIcon(context: Context, 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 { context.getDrawableCompat(it) } -} +fun formatTestStatusIcon(context: Context, uiState: DeviceUIState): Drawable? = when (uiState) { + 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 { context.getDrawableCompat(it) } fun formatTestResultRegisteredAtText(context: Context, registeredAt: Date?): String { return context.getString(R.string.test_result_card_registered_at_text) .format(registeredAt?.toUIFormat(context)) } -fun formatTestResultPendingStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = - uiState.withSuccess(View.GONE) { formatVisibility(it == DeviceUIState.PAIRED_NO_RESULT) } - -fun formatTestResultInvalidStepsVisible(uiState: NetworkRequestWrapper<DeviceUIState, Throwable>?): Int = - uiState.withSuccess(View.GONE) { - formatVisibility(it == DeviceUIState.PAIRED_ERROR || it == DeviceUIState.PAIRED_REDEEMED) - } - fun formatCountryIsoTagToLocalizedName(isoTag: String?): String { val country = if (isoTag != null) Locale("", isoTag).displayCountry else "" return country diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/TestResult.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/TestResult.kt deleted file mode 100644 index 912799429291f7b7e5c99cc9d3f0629b3acbad94..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/formatter/TestResult.kt +++ /dev/null @@ -1,13 +0,0 @@ -package de.rki.coronawarnapp.util.formatter - -enum class TestResult(val value: Int) { - PENDING(0), - NEGATIVE(1), - POSITIVE(2), - INVALID(3), - REDEEMED(4); - - companion object { - fun fromInt(value: Int) = values().first { it.value == value } - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt index 400286b19a31baefe0423f273f64fc5d4b8bd839..90dc9a5a50241e1c52314f239eaece15e33a28cd 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/worker/WorkerBinder.kt @@ -8,14 +8,14 @@ import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryRetentionWorker import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsPeriodicWorker import de.rki.coronawarnapp.deadman.DeadmanNotificationOneTimeWorker import de.rki.coronawarnapp.deadman.DeadmanNotificationPeriodicWorker +import de.rki.coronawarnapp.deniability.BackgroundNoiseOneTimeWorker +import de.rki.coronawarnapp.deniability.BackgroundNoisePeriodicWorker import de.rki.coronawarnapp.diagnosiskeys.execution.DiagnosisKeyRetrievalWorker import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpPeriodicWorker import de.rki.coronawarnapp.nearby.ExposureStateUpdateWorker import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOutWorker import de.rki.coronawarnapp.presencetracing.risk.execution.PresenceTracingWarningWorker import de.rki.coronawarnapp.submission.auto.SubmissionWorker -import de.rki.coronawarnapp.worker.BackgroundNoiseOneTimeWorker -import de.rki.coronawarnapp.worker.BackgroundNoisePeriodicWorker import de.rki.coronawarnapp.worker.DiagnosisTestResultRetrievalPeriodicWorker @Module diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt index 7e7340ebb38c7a76df1ccdbfdc4059cadf3b6057..b250c13b7b41292d63406c65e60ded636630426d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundConstants.kt @@ -9,36 +9,6 @@ import java.util.concurrent.TimeUnit */ object BackgroundConstants { - /** - * Tag for background polling tp check test result periodic work - */ - const val DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG = "DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER" - - /** - * Tag for background noise playbook periodic work - */ - const val BACKGROUND_NOISE_PERIODIC_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER" - - /** - * Tag for background noise playbook one time work - */ - const val BACKGROUND_NOISE_ONE_TIME_WORKER_TAG = "BACKGROUND_NOISE_PERIODIC_WORKER" - - /** - * Unique name for diagnosis test result retrieval periodic work - */ - const val DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME = "DiagnosisTestResultBackgroundPeriodicWork" - - /** - * Unique name for background noise playbook periodic work - */ - const val BACKGROUND_NOISE_PERIODIC_WORK_NAME = "BackgroundNoisePeriodicWork" - - /** - * Unique name for background noise playbook one time work - */ - const val BACKGROUND_NOISE_ONE_TIME_WORK_NAME = "BackgroundNoiseOneTimeWork" - /** * Total minutes in one day */ @@ -57,13 +27,6 @@ object BackgroundConstants { */ const val KIND_DELAY = 1L - /** - * Kind initial delay in minutes for periodic work for accessibility reason - * - * @see TimeUnit.SECONDS - */ - const val DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY = 10L - /** * Retries before work would set as FAILED */ diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt deleted file mode 100644 index 2c03e9544d7eb89824da44430afdf47ad39f0eb8..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/worker/BackgroundWorkBuilder.kt +++ /dev/null @@ -1,88 +0,0 @@ -package de.rki.coronawarnapp.worker - -import androidx.work.BackoffPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.PeriodicWorkRequestBuilder -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler.WorkTag -import java.util.concurrent.TimeUnit -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BackgroundWorkBuilder @Inject constructor() { - - /** - * Build diagnosis Test Result periodic work request - * Set "kind delay" for accessibility reason. - * - * @return PeriodicWorkRequest - * - * @see WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - */ - fun buildDiagnosisTestResultRetrievalPeriodicWork() = - PeriodicWorkRequestBuilder<DiagnosisTestResultRetrievalPeriodicWorker>( - BackgroundWorkHelper.getDiagnosisTestResultRetrievalPeriodicWorkTimeInterval(), - TimeUnit.MINUTES - ) - .addTag(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, - TimeUnit.SECONDS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() - - /** - * Build background noise one time work request - * Set BackgroundNoiseOneTimeWorkDelay for timing randomness. - * - * @return PeriodicWorkRequest - * - * @see WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER - * @see BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay - */ - fun buildBackgroundNoiseOneTimeWork() = - OneTimeWorkRequestBuilder<BackgroundNoiseOneTimeWorker>() - .addTag(WorkTag.BACKGROUND_NOISE_ONE_TIME_WORKER.tag) - .setConstraints(BackgroundWorkHelper.getConstraintsForDiagnosisKeyOneTimeBackgroundWork()) - .setInitialDelay( - BackgroundWorkHelper.getBackgroundNoiseOneTimeWorkDelay(), - TimeUnit.HOURS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() - - /** - * Build background noise periodic work request - * Set "kind delay" for accessibility reason. - * - * @return PeriodicWorkRequest - * - * @see BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION - * @see WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER - * @see BackgroundConstants.KIND_DELAY - */ - fun buildBackgroundNoisePeriodicWork() = - PeriodicWorkRequestBuilder<BackgroundNoisePeriodicWorker>( - BackgroundConstants.MIN_HOURS_TO_NEXT_BACKGROUND_NOISE_EXECUTION, - TimeUnit.HOURS - ) - .addTag(WorkTag.BACKGROUND_NOISE_PERIODIC_WORKER.tag) - .setInitialDelay( - BackgroundConstants.KIND_DELAY, - TimeUnit.SECONDS - ).setBackoffCriteria( - BackoffPolicy.LINEAR, - BackgroundConstants.KIND_DELAY, - TimeUnit.MINUTES - ) - .build() -} 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 4f83979c39c974861e80c04df100c1439be3b50e..706db5566173d3b84763594291e1e025f2fe07f6 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 @@ -1,16 +1,12 @@ package de.rki.coronawarnapp.worker -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.ExistingWorkPolicy -import androidx.work.Operation -import androidx.work.WorkInfo -import androidx.work.WorkManager -import de.rki.coronawarnapp.CoronaWarnApplication +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler +import de.rki.coronawarnapp.deniability.NoiseScheduler import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler -import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import timber.log.Timber -import java.util.concurrent.ExecutionException import javax.inject.Inject import javax.inject.Singleton @@ -23,228 +19,31 @@ import javax.inject.Singleton */ @Singleton class BackgroundWorkScheduler @Inject constructor( - private val backgroundWorkBuilder: BackgroundWorkBuilder, - private val submissionSettings: SubmissionSettings, - private val tracingSettings: TracingSettings, - private val riskWorkScheduler: RiskWorkScheduler + private val riskWorkScheduler: RiskWorkScheduler, + private val coronaTestRepository: CoronaTestRepository, + private val testResultScheduler: TestResultScheduler, + private val noiseScheduler: NoiseScheduler, ) { - /** - * Enum class for work tags - * - * @param tag the tag of the worker - * - * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG - * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG - * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG - */ - enum class WorkTag(val tag: String) { - DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER_TAG), - BACKGROUND_NOISE_ONE_TIME_WORKER(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORKER_TAG), - BACKGROUND_NOISE_PERIODIC_WORKER(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORKER_TAG) - } - - /** - * Enum class for work type - * - * @param uniqueName the unique name of specified work - * - * @see BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME - * @see BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME - * @see BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME - */ - enum class WorkType(val uniqueName: String) { - DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_WORK_NAME), - BACKGROUND_NOISE_PERIODIC_WORK(BackgroundConstants.BACKGROUND_NOISE_PERIODIC_WORK_NAME), - BACKGROUND_NOISE_ONE_TIME_WORK(BackgroundConstants.BACKGROUND_NOISE_ONE_TIME_WORK_NAME) - } - - /** - * Work manager instance - */ - private val workManager by lazy { WorkManager.getInstance(CoronaWarnApplication.getAppContext()) } - - /** - * Start work scheduler - * Checks if periodic worker was already scheduled. If not - reschedule it again. - * For [WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER] also checks if User is Registered - * - * @see de.rki.coronawarnapp.submission.SubmissionSettings.registrationToken - * @see isWorkActive - */ fun startWorkScheduler() { Timber.d("startWorkScheduler()") riskWorkScheduler.setPeriodicRiskCalculation(enabled = true) - if (!submissionSettings.isSubmissionSuccessful) { - if (!isWorkActive(WorkTag.DIAGNOSIS_TEST_RESULT_RETRIEVAL_PERIODIC_WORKER.tag) && - submissionSettings.registrationToken.value != null && - !tracingSettings.isTestResultAvailableNotificationSent - ) { - WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() - tracingSettings.initialPollingForTestResultTimeStamp = System.currentTimeMillis() - Timber.d("Starting DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER") - } + // TODO Blocking isn't very nice here... + val coronatests = runBlocking { coronaTestRepository.coronaTests.first() } + + val isSubmissionSuccessful = coronatests.any { it.isSubmitted } + val hasPendingTests = coronatests.any { !it.isResultAvailableNotificationSent } + + if (!isSubmissionSuccessful && hasPendingTests) { + testResultScheduler.setPeriodicTestPolling(enabled = true) } } - /** - * Stop work scheduler - * Stops all background work by tag. - */ fun stopWorkScheduler() { - WorkTag.values().map { workTag: WorkTag -> - workManager.cancelAllWorkByTag(workTag.tag) - .also { it.logOperationCancelByTag(workTag) } - } + noiseScheduler.setPeriodicNoise(enabled = false) riskWorkScheduler.setPeriodicRiskCalculation(enabled = false) + testResultScheduler.setPeriodicTestPolling(enabled = false) Timber.d("All Background Jobs Stopped") } - - /** - * Stop work by unique name - * - * @return Operation - * - * @see WorkType - */ - fun WorkType.stop(): Operation = - workManager.cancelUniqueWork(this.uniqueName) - - /** - * Checks if defined work is active - * Non-active means worker was Cancelled, Failed or have not been enqueued at all - * - * @param tag String tag of the worker - * - * @return Boolean - * - * @see WorkInfo.State.CANCELLED - * @see WorkInfo.State.FAILED - */ - private fun isWorkActive(tag: String): Boolean { - val workStatus = workManager.getWorkInfosByTag(tag) - var result = true - try { - val workInfoList = workStatus.get() - if (workInfoList.size == 0) result = false - for (info in workInfoList) { - if (info.state == WorkInfo.State.CANCELLED || info.state == WorkInfo.State.FAILED) { - result = false - } - } - } catch (e: ExecutionException) { - result = false - } catch (e: InterruptedException) { - result = false - } - return result - } - - fun scheduleDiagnosisTestResultPeriodicWork() { - WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.start() - } - - fun stopDiagnosisTestResultPeriodicWork() { - WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.stop() - } - - /** - * Schedule background noise periodic work - * - * @see WorkType.BACKGROUND_NOISE_PERIODIC_WORK - */ - fun scheduleBackgroundNoisePeriodicWork() { - WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start() - } - - fun stopBackgroundNoisePeriodicWork() { - WorkType.BACKGROUND_NOISE_PERIODIC_WORK.start() - } - - /** - * Schedule background noise one time work - * - * @see WorkType.BACKGROUND_NOISE_ONE_TIME_WORK - */ - fun scheduleBackgroundNoiseOneTimeWork() { - WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.start() - } - - /** - * Enqueue operation for work type defined in WorkType enum class - * - * @return Operation - * - * @see WorkType - */ - private fun WorkType.start(): Operation = when (this) { - WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER -> enqueueDiagnosisTestResultBackgroundPeriodicWork() - WorkType.BACKGROUND_NOISE_PERIODIC_WORK -> enqueueBackgroundNoisePeriodicWork() - WorkType.BACKGROUND_NOISE_ONE_TIME_WORK -> enqueueBackgroundNoiseOneTimeWork() - } - - /** - * Enqueue diagnosis Test Result periodic - * Show a Notification when new Test Results are in. - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER - */ - private fun enqueueDiagnosisTestResultBackgroundPeriodicWork() = - workManager.enqueueUniquePeriodicWork( - WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER.uniqueName, - ExistingPeriodicWorkPolicy.REPLACE, - backgroundWorkBuilder.buildDiagnosisTestResultRetrievalPeriodicWork() - ).also { it.logOperationSchedule(WorkType.DIAGNOSIS_TEST_RESULT_PERIODIC_WORKER) } - - /** - * Enqueue background noise periodic - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.BACKGROUND_NOISE_PERIODIC_WORK - */ - private fun enqueueBackgroundNoisePeriodicWork() = - workManager.enqueueUniquePeriodicWork( - WorkType.BACKGROUND_NOISE_PERIODIC_WORK.uniqueName, - ExistingPeriodicWorkPolicy.REPLACE, - backgroundWorkBuilder.buildBackgroundNoisePeriodicWork() - ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_PERIODIC_WORK) } - - /** - * Enqueue background noise one time - * Replace with new if older work exists. - * - * @return Operation - * - * @see WorkType.BACKGROUND_NOISE_ONE_TIME_WORK - */ - private fun enqueueBackgroundNoiseOneTimeWork() = - workManager.enqueueUniqueWork( - WorkType.BACKGROUND_NOISE_ONE_TIME_WORK.uniqueName, - ExistingWorkPolicy.REPLACE, - backgroundWorkBuilder.buildBackgroundNoiseOneTimeWork() - ).also { it.logOperationSchedule(WorkType.BACKGROUND_NOISE_ONE_TIME_WORK) } - - /** - * Log operation schedule - */ - private fun Operation.logOperationSchedule(workType: WorkType) = - 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.") }, - { it.run() } - ).also { Timber.d("Canceling all work with tag ${workTag.tag}") } } 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 0b719ddd8021c35549a621f4c051c423cdf07c26..0d2b9c43a0cfa7841f4cfed0583b117d72075c71 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 @@ -6,17 +6,20 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler +import de.rki.coronawarnapp.coronatest.latestPCRT +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.NotificationConstants import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService -import de.rki.coronawarnapp.service.submission.SubmissionService -import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings -import de.rki.coronawarnapp.util.TimeAndDateExtensions import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.worker.InjectedWorkerFactory +import kotlinx.coroutines.flow.first +import org.joda.time.Duration +import org.joda.time.Instant import timber.log.Timber /** @@ -29,11 +32,9 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( @Assisted workerParams: WorkerParameters, private val testResultAvailableNotificationService: TestResultAvailableNotificationService, private val notificationHelper: GeneralNotifications, - private val submissionSettings: SubmissionSettings, - private val submissionService: SubmissionService, + private val coronaTestRepository: CoronaTestRepository, private val timeStamper: TimeStamper, - private val tracingSettings: TracingSettings, - private val backgroundWorkScheduler: BackgroundWorkScheduler, + private val testResultScheduler: TestResultScheduler, ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { @@ -42,37 +43,39 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( if (runAttemptCount > BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD) { Timber.tag(TAG).d("$id doWork() failed after $runAttemptCount attempts. Rescheduling") - backgroundWorkScheduler.scheduleDiagnosisTestResultPeriodicWork() + testResultScheduler.setPeriodicTestPolling(enabled = true) Timber.tag(TAG).d("$id Rescheduled background worker") return Result.failure() } var result = Result.success() try { - - if (abortConditionsMet(timeStamper.nowUTC.millis)) { + if (abortConditionsMet(timeStamper.nowUTC)) { Timber.tag(TAG).d(" $id Stopping worker.") stopWorker() } else { Timber.tag(TAG).d(" $id Running worker.") - val registrationToken = - submissionSettings.registrationToken.value ?: throw NoRegistrationTokenSetException() - val testResult = submissionService.asyncRequestTestResult(registrationToken) + val coronaTest = coronaTestRepository.refresh( + type = CoronaTest.Type.PCR + ).single() as PCRCoronaTest + val testResult = coronaTest.testResult + Timber.tag(TAG).d("$id: Test Result retrieved is $testResult") - if (testResult == TestResult.NEGATIVE || - testResult == TestResult.POSITIVE || - testResult == TestResult.INVALID + if (testResult == CoronaTestResult.PCR_NEGATIVE || + testResult == CoronaTestResult.PCR_POSITIVE || + testResult == CoronaTestResult.PCR_INVALID ) { - sendTestResultAvailableNotification(testResult) + sendTestResultAvailableNotification(coronaTest) cancelRiskLevelScoreNotification() Timber.tag(TAG) .d("$id: Test Result available - notification sent & risk level notification canceled") stopWorker() } } - } catch (ex: Exception) { + } catch (e: Exception) { + Timber.tag(TAG).e(e, "Test result retrieval worker failed.") result = Result.retry() } @@ -81,34 +84,37 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( return result } - private fun abortConditionsMet(currentMillis: Long): Boolean { - if (tracingSettings.isTestResultAvailableNotificationSent) { + private suspend fun abortConditionsMet(nowUTC: Instant): Boolean { + val pcrTest = coronaTestRepository.latestPCRT.first() + if (pcrTest == null) { + Timber.tag(TAG).w("There is no PCR test available!?") + return true + } + + if (pcrTest.isResultAvailableNotificationSent) { Timber.tag(TAG).d("$id: Notification already sent.") return true } - if (submissionSettings.hasViewedTestResult.value) { + + if (pcrTest.isViewed) { Timber.tag(TAG).d("$id: Test result has already been viewed.") return true } - val calculateDays = TimeAndDateExtensions.calculateDays( - tracingSettings.initialPollingForTestResultTimeStamp, - currentMillis - ) + val calculateDays = Duration(pcrTest.registeredAt, nowUTC).standardDays Timber.tag(TAG).d("Calculated days: %d", calculateDays) if (calculateDays >= BackgroundConstants.POLLING_VALIDITY_MAX_DAYS) { - Timber.tag(TAG) - .d(" $id Maximum of ${BackgroundConstants.POLLING_VALIDITY_MAX_DAYS} days for polling exceeded.") + Timber.tag(TAG).d("$id $calculateDays is exceeding the maximum polling duration") return true } return false } - private suspend fun sendTestResultAvailableNotification(testResult: TestResult) { - testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) - tracingSettings.isTestResultAvailableNotificationSent = true + private suspend fun sendTestResultAvailableNotification(coronaTest: CoronaTest) { + testResultAvailableNotificationService.showTestResultAvailableNotification(coronaTest.testResult) + coronaTestRepository.updateResultNotification(identifier = coronaTest.identifier, sent = true) } private fun cancelRiskLevelScoreNotification() { @@ -118,8 +124,7 @@ class DiagnosisTestResultRetrievalPeriodicWorker @AssistedInject constructor( } private fun stopWorker() { - tracingSettings.initialPollingForTestResultTimeStamp = 0L - backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() + testResultScheduler.setPeriodicTestPolling(enabled = false) Timber.tag(TAG).d("$id: Background worker stopped") } diff --git a/Corona-Warn-App/src/main/res/drawable-hdpi/ic_splash_logo_round.png b/Corona-Warn-App/src/main/res/drawable-hdpi/ic_splash_logo_round.png new file mode 100644 index 0000000000000000000000000000000000000000..98fb80f06506fc7aad0a1191ef4b22f6fef1e40d Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-hdpi/ic_splash_logo_round.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-ldpi/ic_splash_logo_round.png b/Corona-Warn-App/src/main/res/drawable-ldpi/ic_splash_logo_round.png new file mode 100644 index 0000000000000000000000000000000000000000..1a6ce4d2a116010ba5dcf91bc01dee2be58d7abc Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-ldpi/ic_splash_logo_round.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-mdpi/ic_splash_logo_round.png b/Corona-Warn-App/src/main/res/drawable-mdpi/ic_splash_logo_round.png new file mode 100644 index 0000000000000000000000000000000000000000..3afd257ea151f25b3f6c7aa20e105ff502787042 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-mdpi/ic_splash_logo_round.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-xhdpi/ic_splash_logo_round.png b/Corona-Warn-App/src/main/res/drawable-xhdpi/ic_splash_logo_round.png new file mode 100644 index 0000000000000000000000000000000000000000..29cd5074fc4ef500f69160a91d4d0dace6fa9db5 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-xhdpi/ic_splash_logo_round.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-xxhdpi/ic_splash_logo_round.png b/Corona-Warn-App/src/main/res/drawable-xxhdpi/ic_splash_logo_round.png new file mode 100644 index 0000000000000000000000000000000000000000..0d7cde1f43ec085d9b295b405b9f6f06a431cd61 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-xxhdpi/ic_splash_logo_round.png differ diff --git a/Corona-Warn-App/src/main/res/drawable-xxxhdpi/ic_splash_logo_round.png b/Corona-Warn-App/src/main/res/drawable-xxxhdpi/ic_splash_logo_round.png new file mode 100644 index 0000000000000000000000000000000000000000..17eb8e49327a109b903a0f4f5061fdf3c0b403e1 Binary files /dev/null and b/Corona-Warn-App/src/main/res/drawable-xxxhdpi/ic_splash_logo_round.png differ diff --git a/Corona-Warn-App/src/main/res/drawable/ic_international_phone.xml b/Corona-Warn-App/src/main/res/drawable/ic_international_phone.xml new file mode 100644 index 0000000000000000000000000000000000000000..622119eaa5e1d0e09d2d0179eac8de65266e02fc --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_international_phone.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="23dp" + android:viewportWidth="24" + android:viewportHeight="23"> + <path + android:pathData="M12.1484,22.4805C18.3037,22.4805 23.374,17.3994 23.374,11.2441C23.374,5.0889 18.293,0.0186 12.1484,0.0186C5.9932,0.0186 0.9229,5.0889 0.9229,11.2441C0.9229,17.3994 5.9932,22.4805 12.1484,22.4805ZM5.6494,4.6807C6.7021,3.6387 8.0127,2.833 9.4629,2.3926C8.6895,3.1553 8.0342,4.2402 7.54,5.5723C6.7988,5.3359 6.165,5.0352 5.6494,4.6807ZM14.8447,2.3926C16.2949,2.8438 17.5947,3.6387 18.6475,4.6914C18.1426,5.0352 17.498,5.3359 16.7676,5.5723C16.2734,4.251 15.6182,3.1553 14.8447,2.3926ZM9.1191,5.9482C9.6992,4.4229 10.4941,3.2842 11.3643,2.8545V6.1846C10.5693,6.1523 9.8174,6.0771 9.1191,5.9482ZM12.9434,2.8545C13.8135,3.2842 14.6084,4.4229 15.1777,5.9482C14.4902,6.0771 13.7383,6.1523 12.9434,6.1846V2.8545ZM2.9209,10.4492C3.0713,8.752 3.6836,7.1836 4.6396,5.8623C5.2734,6.3457 6.1113,6.7432 7.0889,7.0547C6.831,8.0967 6.6699,9.2353 6.6162,10.4492H2.9209ZM17.6807,10.4492C17.6377,9.2353 17.4658,8.0967 17.2188,7.0547C18.1963,6.7432 19.0342,6.3457 19.668,5.8731C20.6133,7.1836 21.2256,8.7627 21.376,10.4492H17.6807ZM12.9434,10.4492V7.7637C13.8887,7.7207 14.8018,7.624 15.6396,7.4521C15.8652,8.3867 16.0156,9.3965 16.0693,10.4492H12.9434ZM8.2275,10.4492C8.2813,9.3965 8.4316,8.3867 8.6572,7.4521C9.5059,7.624 10.4189,7.7207 11.3643,7.7637V10.4492H8.2275ZM2.9209,12.0283H6.6162C6.6699,13.2637 6.831,14.4238 7.0889,15.4766C6.1221,15.7881 5.2949,16.1855 4.6611,16.6582C3.6943,15.3262 3.0713,13.7471 2.9209,12.0283ZM8.2275,12.0283H11.3643V14.7783C10.4189,14.8105 9.5059,14.918 8.668,15.0791C8.4316,14.1338 8.2813,13.1025 8.2275,12.0283ZM12.9434,14.7783V12.0283H16.0693C16.0264,13.1025 15.8652,14.1338 15.6289,15.0791C14.791,14.918 13.8887,14.8105 12.9434,14.7783ZM17.208,15.4766C17.4766,14.4238 17.6377,13.2637 17.6807,12.0283H21.376C21.2363,13.7471 20.6025,15.3262 19.6465,16.6582C19.0127,16.1855 18.1855,15.7881 17.208,15.4766ZM9.1406,16.583C9.8281,16.4648 10.5693,16.3896 11.3643,16.3574V19.6553C10.4941,19.2148 9.71,18.0869 9.1406,16.583ZM12.9434,16.3574C13.7275,16.3896 14.4688,16.4648 15.167,16.583C14.5977,18.0869 13.8027,19.2148 12.9434,19.6553V16.3574ZM5.6709,17.8398C6.1865,17.4854 6.8203,17.1953 7.5508,16.9697C8.0342,18.2695 8.6895,19.3438 9.4522,20.0957C8.0127,19.6553 6.7236,18.8711 5.6709,17.8398ZM16.7568,16.9697C17.4873,17.1953 18.1211,17.4854 18.626,17.8398C17.5732,18.8711 16.2842,19.6553 14.8555,20.0957C15.6182,19.3438 16.2627,18.2695 16.7568,16.9697Z" + android:fillColor="#17191A"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_phone.xml b/Corona-Warn-App/src/main/res/drawable/ic_phone.xml new file mode 100644 index 0000000000000000000000000000000000000000..80f7fca27287063af17dc13fd122a852f46197b6 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="23dp" + android:height="23dp" + android:viewportWidth="23" + android:viewportHeight="23"> + <path + android:pathData="M16.6592,22.6309C17.5615,22.6309 18.3708,22.459 19.0869,22.1152C19.8102,21.7786 20.512,21.2738 21.1924,20.6006C21.2282,20.5648 21.2604,20.529 21.2891,20.4932C21.3249,20.4645 21.3571,20.4287 21.3857,20.3857C21.6436,20.0778 21.8548,19.7269 22.0195,19.333C22.1842,18.9391 22.2666,18.5667 22.2666,18.2158C22.2666,17.7718 22.1735,17.3672 21.9873,17.002C21.8011,16.6296 21.4788,16.2822 21.0205,15.96L17.6475,13.6182C17.0674,13.2171 16.4873,13.0238 15.9072,13.0381C15.3343,13.0452 14.8294,13.2744 14.3926,13.7256L13.7803,14.3594C13.5654,14.5742 13.3327,14.6852 13.082,14.6924C12.8385,14.6924 12.5521,14.5527 12.2227,14.2734C12.0365,14.1159 11.8359,13.944 11.6211,13.7578C11.4134,13.5716 11.2057,13.3818 10.998,13.1885C10.7904,12.9951 10.597,12.8125 10.418,12.6406C10.2819,12.4974 10.1315,12.3398 9.9668,12.168C9.8092,11.9889 9.6481,11.8099 9.4834,11.6309C9.3258,11.4447 9.179,11.2656 9.043,11.0938C8.7852,10.7643 8.6527,10.4779 8.6455,10.2344C8.6455,9.9909 8.7529,9.7617 8.9678,9.5469L9.6016,8.9238C10.0456,8.487 10.2747,7.9821 10.2891,7.4092C10.3034,6.8291 10.1064,6.2526 9.6982,5.6797L7.0986,2.0381C6.8408,1.68 6.5042,1.4043 6.0889,1.2109C5.6735,1.0176 5.2366,0.9209 4.7783,0.9209C4.0049,0.9209 3.3066,1.1716 2.6836,1.6729C2.6478,1.6943 2.612,1.7194 2.5762,1.748C2.5475,1.7767 2.526,1.7982 2.5117,1.8125C1.7813,2.5501 1.237,3.2878 0.8789,4.0254C0.5208,4.763 0.3418,5.6869 0.3418,6.7969C0.3418,7.9499 0.5173,9.1136 0.8682,10.2881C1.2191,11.4554 1.7848,12.6406 2.5654,13.8438C3.346,15.0469 4.3809,16.2715 5.6699,17.5176C6.9518,18.7637 8.1872,19.7591 9.376,20.5039C10.5648,21.2559 11.7536,21.7965 12.9424,22.126C14.1312,22.4626 15.3701,22.6309 16.6592,22.6309ZM16.5732,20.2783C15.7139,20.2783 14.8223,20.1315 13.8984,19.8379C12.9746,19.5514 11.9863,19.0645 10.9336,18.377C9.888,17.6823 8.7493,16.737 7.5176,15.541C6.2786,14.3379 5.3154,13.2135 4.6279,12.168C3.9404,11.1152 3.4606,10.1198 3.1885,9.1816C2.9163,8.2435 2.7803,7.3447 2.7803,6.4854C2.7803,5.9984 2.8698,5.54 3.0488,5.1104C3.2279,4.6807 3.4785,4.3262 3.8008,4.0469C3.8509,4.0039 3.9046,3.9609 3.9619,3.918C4.0192,3.8678 4.0729,3.8249 4.1231,3.7891C4.2305,3.7103 4.3379,3.653 4.4453,3.6172C4.5527,3.5742 4.6602,3.5527 4.7676,3.5527C5.0755,3.5527 5.3369,3.7067 5.5518,4.0146L7.1738,6.3779C7.3529,6.6429 7.4281,6.9186 7.3994,7.2051C7.3708,7.4915 7.2311,7.7529 6.9805,7.9893L6.5508,8.376C6.0065,8.8701 5.7236,9.3499 5.7021,9.8154C5.6878,10.2809 5.7845,10.6963 5.9922,11.0615C6.1856,11.4124 6.5365,11.8851 7.0449,12.4795C7.5606,13.0667 8.137,13.6719 8.7744,14.2949C9.3044,14.8105 9.8021,15.2796 10.2676,15.7021C10.7331,16.1247 11.1377,16.4792 11.4814,16.7656C11.8324,17.0521 12.0902,17.2383 12.2549,17.3242C12.6273,17.5319 13.0462,17.6286 13.5117,17.6143C13.9772,17.5928 14.457,17.3135 14.9512,16.7764L15.3379,16.3467C15.5742,16.0889 15.832,15.9492 16.1113,15.9277C16.3978,15.9062 16.6735,15.9779 16.9385,16.1426L19.0439,17.5068C19.2158,17.6214 19.334,17.7432 19.3984,17.8721C19.4701,18.001 19.5059,18.1335 19.5059,18.2695C19.5059,18.513 19.4271,18.735 19.2695,18.9355C19.2337,18.9857 19.1908,19.0394 19.1406,19.0967C19.0977,19.154 19.0547,19.2077 19.0117,19.2578C18.7324,19.5801 18.3779,19.8307 17.9482,20.0098C17.5186,20.1888 17.0602,20.2783 16.5732,20.2783Z" + android:fillColor="#17191A"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_risk_details_pcr.xml b/Corona-Warn-App/src/main/res/drawable/ic_risk_details_pcr.xml new file mode 100644 index 0000000000000000000000000000000000000000..7c2fcb55f53139b5a296e2dc47dfe1c404527d86 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_risk_details_pcr.xml @@ -0,0 +1,17 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="22dp" + android:viewportWidth="20" + android:viewportHeight="22"> + <path + android:fillColor="#ffffff" + android:pathData="M6,0L14,0A1,1 0,0 1,15 1L15,1A1,1 0,0 1,14 2L6,2A1,1 0,0 1,5 1L5,1A1,1 0,0 1,6 0z" /> + <path + android:fillColor="#00000000" + android:pathData="M7.2449,6.1667V3.1579H12.7551V6.1667V6.3816L12.8323,6.5822L15.8949,14.5486C17.0611,17.5821 14.8218,20.8421 11.5717,20.8421H8.4283C5.1782,20.8421 2.9389,17.5821 4.1051,14.5486L7.1677,6.5822L7.2449,6.3816V6.1667Z" + android:strokeWidth="2.31579" + android:strokeColor="#ffffff" /> + <path + android:fillColor="#ffffff" + android:pathData="M3.5,16.5295L4.5,19.5295L5.5,20.5304L10,21.0304L15.5,20.0304L16.5,16.5295C16.8333,15.5295 16.4,11.4011 14,13.0011C11.6,14.6011 8,17.9875 4,16L3.5,16.5295Z" /> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_submission_share_cards.xml b/Corona-Warn-App/src/main/res/drawable/ic_submission_share_cards.xml new file mode 100644 index 0000000000000000000000000000000000000000..78495b12be958b064888f28b3a3584477621c027 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_submission_share_cards.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> + <path + android:pathData="M12,4.5C15.64,4.5 18.67,7.09 19.35,10.54C21.95,10.72 24,12.86 24,15.5C24,18.26 21.76,20.5 19,20.5H6C2.69,20.5 0,17.81 0,14.5C0,11.41 2.34,8.86 5.35,8.54C6.6,6.14 9.11,4.5 12,4.5ZM14,17.5V13.5H17L12.36,8.85C12.16,8.65 11.85,8.65 11.65,8.85L7,13.5H10V17.5H14Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative_card.xml b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..fa73b136ca20edcec54e80ee3bb8f6cec9a516e9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_negative_card.xml @@ -0,0 +1,49 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="87dp" + android:height="110dp" + android:viewportWidth="87" + android:viewportHeight="110"> + <path + android:pathData="M15.211,15.749V109.852H86.414V33.676L69.317,15.749H15.211Z" + android:fillColor="#657888" + android:fillAlpha="0.1" + android:fillType="evenOdd"/> + <path + android:pathData="M69.361,33.676H86.357L69.361,15.763V33.676Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M42.763,96.966H36.275C35.168,96.966 34.272,96.075 34.272,94.974V88.519C34.272,87.417 35.168,86.527 36.275,86.527H42.763C43.868,86.527 44.764,87.417 44.764,88.519V94.974C44.764,96.075 43.868,96.966 42.763,96.966Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.099,96.966H61.608C60.503,96.966 59.607,96.075 59.607,94.975V88.517C59.607,87.417 60.503,86.527 61.608,86.527H68.099C69.204,86.527 70.1,87.417 70.1,88.517V94.975C70.1,96.075 69.204,96.966 68.099,96.966Z" + android:fillColor="#2E854B" + android:fillType="evenOdd"/> + <path + android:pathData="M32.397,56.782H71.431C72.197,56.842 72.677,57.376 72.677,58.109C72.677,58.752 72.197,59.469 71.431,59.435H32.397C31.392,59.469 30.912,58.841 30.912,58.109C30.912,57.376 31.392,56.842 32.397,56.782Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M32.397,42.454H71.431C72.197,42.513 72.677,43.048 72.677,43.78C72.677,44.423 72.197,45.14 71.431,45.105H32.397C31.392,45.14 30.912,44.511 30.912,43.78C30.912,43.048 31.392,42.513 32.397,42.454Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M32.397,70.874H71.431C72.197,70.933 72.677,71.468 72.677,72.2C72.677,72.843 72.197,73.56 71.431,73.525H32.397C31.392,73.56 30.912,72.931 30.912,72.2C30.912,71.468 31.392,70.933 32.397,70.874Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.062,89.099C67.942,89.091 67.825,89.142 67.751,89.237L63.815,94.055L61.813,91.936C61.729,91.828 61.59,91.777 61.456,91.805C61.322,91.831 61.214,91.93 61.177,92.062C61.141,92.193 61.181,92.333 61.283,92.426L63.572,94.852C63.644,94.926 63.745,94.967 63.849,94.963C63.954,94.959 64.051,94.91 64.116,94.829L68.319,89.694C68.409,89.588 68.43,89.443 68.375,89.316C68.321,89.191 68.2,89.107 68.062,89.099Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:strokeWidth="1" + android:pathData="M68.062,89.099C67.942,89.091 67.825,89.142 67.751,89.237L63.815,94.055L61.813,91.936C61.729,91.828 61.59,91.777 61.456,91.805C61.322,91.831 61.214,91.93 61.177,92.062C61.141,92.193 61.181,92.333 61.283,92.426L63.572,94.852C63.644,94.926 63.745,94.967 63.849,94.963C63.954,94.959 64.051,94.91 64.116,94.829L68.319,89.694C68.409,89.588 68.43,89.443 68.375,89.316C68.321,89.191 68.2,89.107 68.062,89.099Z" + android:fillColor="#00000000" + android:fillType="evenOdd" + android:strokeColor="#ffffff"/> + <path + android:pathData="M22.538,7.105L22.596,7.136C24.156,7.956 25.455,9.076 26.461,10.388C26.725,10.428 26.988,10.438 27.221,10.395C27.695,10.308 29.009,9.734 29.292,9.101C29.575,8.468 30.139,7.25 31.479,9.23L31.493,9.251C31.789,9.687 32.712,11.05 30.849,10.972C29.085,10.897 28.177,11.979 27.801,12.606C28.519,14.158 28.897,15.858 28.895,17.581C29.267,17.841 29.802,18.032 30.565,18.02C31.842,17.999 32.603,18.021 33.023,18.044C33.295,18.059 34.301,18.052 34.525,17.898C35.077,17.519 36.143,17.031 35.986,19.06C35.854,20.76 35.453,21.406 34.648,20.759C34.237,20.429 33.035,20.113 32.524,19.986C31.452,19.721 29.717,19.447 28.622,20.08C28.4,21.086 28.042,22.08 27.54,23.036C27.467,23.174 27.392,23.31 27.315,23.444L11.007,7.492C11.63,7.115 12.284,6.797 12.962,6.542C13.104,6.288 13.228,5.912 13.139,5.44C13.127,5.378 13.116,5.305 13.103,5.224C13.037,4.797 12.937,4.154 12.574,3.887C12.176,3.596 11.349,2.621 13.682,2.288C13.74,2.28 13.797,2.271 13.854,2.262C14.442,2.169 15.011,2.078 14.733,3.093C14.626,3.48 14.608,3.854 14.639,4.198C14.71,4.961 14.939,5.431 15.435,5.904C17.067,5.667 18.758,5.77 20.399,6.248C20.773,5.977 21.137,5.544 21.416,4.863C21.735,4.085 22.005,3.224 22.189,2.639L22.19,2.639L22.19,2.639L22.19,2.638C22.264,2.403 22.323,2.212 22.367,2.089C22.482,1.762 22.53,1.415 22.502,1.069C22.452,0.446 22.618,-0.588 24.559,0.427C26.809,1.605 26.099,2.169 25.351,2.466C25.049,2.586 24.783,2.784 24.565,3.026C23.937,3.729 22.716,5.583 22.538,7.105ZM6.887,11.731L23.18,27.654C22.579,28.013 21.949,28.318 21.298,28.565C21.273,28.874 21.281,29.242 21.341,29.68C21.473,30.632 21.991,31.966 22.292,32.74C22.391,32.995 22.466,33.19 22.497,33.29C22.564,33.514 22.691,33.602 22.867,33.723C22.895,33.743 22.924,33.763 22.954,33.784C23.499,34.171 24.324,35.005 22.365,35.554C20.723,36.013 19.979,35.858 20.309,34.881C20.479,34.382 20.558,34.011 20.502,33.488C20.394,32.492 19.768,30.265 18.899,29.197C17.302,29.442 15.644,29.361 14.031,28.921C13.594,29.075 13.121,29.298 12.827,29.602C12.491,29.948 11.755,31.179 11.893,31.858C12.03,32.538 12.278,33.857 10.037,33.023L10.013,33.014C9.52,32.831 7.976,32.259 9.539,31.24C11.612,29.888 10.996,27.663 10.996,27.663L10.972,27.6C9.584,26.753 8.429,25.656 7.533,24.399C7.224,24.505 6.894,24.677 6.548,24.94C4.578,26.436 4.191,26.853 4.191,26.853C4.191,26.853 2.583,30.117 0.895,27.529C-0.793,24.941 0.595,24.328 2.441,24.757C2.441,24.757 5.2,23.982 6.422,22.479C5.765,21.049 5.397,19.495 5.348,17.913C5.193,17.805 5.035,17.708 4.876,17.632C4.378,17.396 2.848,17.183 2.104,17.487C1.3,17.815 -0.181,18.285 0.018,16.452C0.178,14.985 0.795,14.281 2.104,15.191C3.31,16.029 4.605,15.767 5.552,15.338C5.766,14.229 6.144,13.131 6.696,12.08C6.758,11.962 6.822,11.846 6.887,11.731ZM2.79,5.225L29.162,31.022C29.765,31.611 30.618,31.717 31.065,31.258C31.513,30.8 31.386,29.95 30.784,29.361L4.411,3.565C3.808,2.976 2.956,2.871 2.508,3.329C2.061,3.787 2.187,4.637 2.79,5.225Z" + android:fillColor="#2E854B" + android:fillType="evenOdd"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_outdated_card.xml b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_outdated_card.xml new file mode 100644 index 0000000000000000000000000000000000000000..6136cb130644d155c77c5f673a523b19ced9e3b7 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/ic_test_result_illustration_outdated_card.xml @@ -0,0 +1,49 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="87dp" + android:height="110dp" + android:viewportWidth="87" + android:viewportHeight="110"> + <path + android:pathData="M15.211,15.749V109.852H86.414V33.676L69.317,15.749H15.211Z" + android:fillColor="#657888" + android:fillAlpha="0.1" + android:fillType="evenOdd"/> + <path + android:pathData="M69.361,33.676H86.357L69.361,15.763V33.676Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M42.763,96.966H36.275C35.168,96.966 34.272,96.075 34.272,94.974V88.519C34.272,87.417 35.168,86.527 36.275,86.527H42.763C43.868,86.527 44.764,87.417 44.764,88.519V94.974C44.764,96.075 43.868,96.966 42.763,96.966Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.099,96.966H61.608C60.503,96.966 59.607,96.075 59.607,94.975V88.517C59.607,87.417 60.503,86.527 61.608,86.527H68.099C69.204,86.527 70.1,87.417 70.1,88.517V94.975C70.1,96.075 69.204,96.966 68.099,96.966Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M32.397,56.782H71.431C72.197,56.842 72.677,57.376 72.677,58.109C72.677,58.752 72.197,59.469 71.431,59.435H32.397C31.392,59.469 30.912,58.841 30.912,58.109C30.912,57.376 31.392,56.842 32.397,56.782Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M32.397,42.454H71.431C72.197,42.513 72.677,43.048 72.677,43.78C72.677,44.423 72.197,45.14 71.431,45.105H32.397C31.392,45.14 30.912,44.511 30.912,43.78C30.912,43.048 31.392,42.513 32.397,42.454Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M32.397,70.874H71.431C72.197,70.933 72.677,71.468 72.677,72.2C72.677,72.843 72.197,73.56 71.431,73.525H32.397C31.392,73.56 30.912,72.931 30.912,72.2C30.912,71.468 31.392,70.933 32.397,70.874Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> + <path + android:pathData="M68.062,89.099C67.942,89.091 67.825,89.142 67.751,89.237L63.815,94.055L61.813,91.936C61.729,91.828 61.59,91.777 61.456,91.805C61.322,91.831 61.214,91.93 61.177,92.062C61.141,92.193 61.181,92.333 61.283,92.426L63.572,94.852C63.644,94.926 63.745,94.967 63.849,94.963C63.954,94.959 64.051,94.91 64.116,94.829L68.319,89.694C68.409,89.588 68.43,89.443 68.375,89.316C68.321,89.191 68.2,89.107 68.062,89.099Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> + <path + android:strokeWidth="1" + android:pathData="M68.062,89.099C67.942,89.091 67.825,89.142 67.751,89.237L63.815,94.055L61.813,91.936C61.729,91.828 61.59,91.777 61.456,91.805C61.322,91.831 61.214,91.93 61.177,92.062C61.141,92.193 61.181,92.333 61.283,92.426L63.572,94.852C63.644,94.926 63.745,94.967 63.849,94.963C63.954,94.959 64.051,94.91 64.116,94.829L68.319,89.694C68.409,89.588 68.43,89.443 68.375,89.316C68.321,89.191 68.2,89.107 68.062,89.099Z" + android:fillColor="#00000000" + android:fillType="evenOdd" + android:strokeColor="#ffffff"/> + <path + android:pathData="M22.538,7.105L22.596,7.136C24.156,7.956 25.455,9.076 26.461,10.388C26.725,10.428 26.988,10.438 27.221,10.395C27.695,10.308 29.009,9.734 29.292,9.101C29.575,8.468 30.139,7.25 31.479,9.23L31.493,9.251C31.789,9.687 32.712,11.05 30.849,10.972C29.085,10.897 28.177,11.979 27.801,12.606C28.519,14.158 28.897,15.858 28.895,17.581C29.267,17.841 29.802,18.032 30.565,18.02C31.842,17.999 32.603,18.021 33.023,18.044C33.295,18.059 34.301,18.052 34.525,17.898C35.077,17.519 36.143,17.031 35.986,19.06C35.854,20.76 35.453,21.406 34.648,20.759C34.237,20.429 33.035,20.113 32.524,19.986C31.452,19.721 29.717,19.447 28.622,20.08C28.4,21.086 28.042,22.08 27.54,23.036C27.467,23.174 27.392,23.31 27.315,23.444L11.007,7.492C11.63,7.115 12.284,6.797 12.962,6.542C13.104,6.288 13.228,5.912 13.139,5.44C13.127,5.378 13.116,5.305 13.103,5.224C13.037,4.797 12.937,4.154 12.574,3.887C12.176,3.596 11.349,2.621 13.682,2.288C13.74,2.28 13.797,2.271 13.854,2.262C14.442,2.169 15.011,2.078 14.733,3.093C14.626,3.48 14.608,3.854 14.639,4.198C14.71,4.961 14.939,5.431 15.435,5.904C17.067,5.667 18.758,5.77 20.399,6.248C20.773,5.977 21.137,5.544 21.416,4.863C21.735,4.085 22.005,3.224 22.189,2.639L22.19,2.639L22.19,2.639L22.19,2.638C22.264,2.403 22.323,2.212 22.367,2.089C22.482,1.762 22.53,1.415 22.502,1.069C22.452,0.446 22.618,-0.588 24.559,0.427C26.809,1.605 26.099,2.169 25.351,2.466C25.049,2.586 24.783,2.784 24.565,3.026C23.937,3.729 22.716,5.583 22.538,7.105ZM6.887,11.731L23.18,27.654C22.579,28.013 21.949,28.318 21.298,28.565C21.273,28.874 21.281,29.242 21.341,29.68C21.473,30.632 21.991,31.966 22.292,32.74C22.391,32.995 22.466,33.19 22.497,33.29C22.564,33.514 22.691,33.602 22.867,33.723C22.895,33.743 22.924,33.763 22.954,33.784C23.499,34.171 24.324,35.005 22.365,35.554C20.723,36.013 19.979,35.858 20.309,34.881C20.479,34.382 20.558,34.011 20.502,33.488C20.394,32.492 19.768,30.265 18.899,29.197C17.302,29.442 15.644,29.361 14.031,28.921C13.594,29.075 13.121,29.298 12.827,29.602C12.491,29.948 11.755,31.179 11.893,31.858C12.03,32.538 12.278,33.857 10.037,33.023L10.013,33.014C9.52,32.831 7.976,32.259 9.539,31.24C11.612,29.888 10.996,27.663 10.996,27.663L10.972,27.6C9.584,26.753 8.429,25.656 7.533,24.399C7.224,24.505 6.894,24.677 6.548,24.94C4.578,26.436 4.191,26.853 4.191,26.853C4.191,26.853 2.583,30.117 0.895,27.529C-0.793,24.941 0.595,24.328 2.441,24.757C2.441,24.757 5.2,23.982 6.422,22.479C5.765,21.049 5.397,19.495 5.348,17.913C5.193,17.805 5.035,17.708 4.876,17.632C4.378,17.396 2.848,17.183 2.104,17.487C1.3,17.815 -0.181,18.285 0.018,16.452C0.178,14.985 0.795,14.281 2.104,15.191C3.31,16.029 4.605,15.767 5.552,15.338C5.766,14.229 6.144,13.131 6.696,12.08C6.758,11.962 6.822,11.846 6.887,11.731ZM2.79,5.225L29.162,31.022C29.765,31.611 30.618,31.717 31.065,31.258C31.513,30.8 31.386,29.95 30.784,29.361L4.411,3.565C3.808,2.976 2.956,2.871 2.508,3.329C2.061,3.787 2.187,4.637 2.79,5.225Z" + android:fillColor="#657888" + android:fillType="evenOdd"/> +</vector> diff --git a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml index 6cddd5383ec531388ccad2d578fd3428c1cafeae..2277fe672d8fe4b98a599c31289f0bed4ad0c11e 100644 --- a/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/bugreporting_debuglog_fragment.xml @@ -127,7 +127,7 @@ android:layout_marginVertical="8dp" android:layout_marginStart="16dp" android:importantForAccessibility="no" - android:src="@drawable/ic_debug_log_indicator_deactivated" + app:srcCompat="@drawable/ic_debug_log_indicator_deactivated" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml b/Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..37cd803b92a2764b00f7c1e51b2b5626fd3d35e9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:context=".ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar.Close" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:title="@string/trace_location_attendee_consent_title" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/checkInsRecycler" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginBottom="16dp" + android:orientation="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintBottom_toTopOf="@id/continue_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar" + tools:listitem="@layout/trace_location_attendee_consent_selectable_check_in" /> + + <Button + android:id="@+id/continue_button" + style="@style/buttonPrimary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginBottom="16dp" + android:text="@string/trace_location_attendee_consent_continue" + app:layout_constraintBottom_toTopOf="@id/skip_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + + <Button + android:id="@+id/skip_button" + style="@style/buttonPrimary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="24dp" + android:layout_marginBottom="24dp" + android:text="@string/trace_location_attendee_consent_skip" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml index 36df6ecdea2e1afe02d3c5a25cf6558a85fd2bef..efca1f88d350e7d38ed0d7e08f356eee58941548 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml @@ -20,7 +20,7 @@ android:layout_marginTop="@dimen/spacing_tiny" android:contentDescription="@string/accessibility_close" android:padding="@dimen/spacing_mega_tiny" - android:src="@drawable/ic_close" + app:srcCompat="@drawable/ic_close" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -45,7 +45,7 @@ android:layout_marginEnd="@dimen/spacing_tiny" android:contentDescription="@string/contact_diary_delete_icon_content_description" android:padding="@dimen/button_icon_padding" - android:src="@drawable/ic_baseline_delete" + app:srcCompat="@drawable/ic_baseline_delete" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml index 254bed1eeb31ff2c3bbbb31ab9f8b7a69b60ef4f..c77fce4cadefa04a6f52993edc5a02541b5ee0e0 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml @@ -20,7 +20,7 @@ android:layout_marginTop="@dimen/spacing_tiny" android:contentDescription="@string/accessibility_close" android:padding="@dimen/spacing_mega_tiny" - android:src="@drawable/ic_close" + app:srcCompat="@drawable/ic_close" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -44,7 +44,7 @@ android:layout_marginEnd="@dimen/spacing_tiny" android:contentDescription="@string/contact_diary_delete_icon_content_description" android:padding="@dimen/button_icon_padding" - android:src="@drawable/ic_baseline_delete" + app:srcCompat="@drawable/ic_baseline_delete" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_edit_list_item.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_edit_list_item.xml index 172335e1c41f698ca0d0ef66628dfec44aecc814..e14e44081fe32f09bf569aa9a878bea1abe06f1c 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_edit_list_item.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_edit_list_item.xml @@ -29,7 +29,7 @@ android:layout_height="wrap_content" android:contentDescription="@string/contact_diary_edit_icon_content_description" android:padding="@dimen/spacing_small" - android:src="@drawable/ic_baseline_edit_24" + app:srcCompat="@drawable/ic_baseline_edit_24" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_onboarding_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_onboarding_fragment.xml index 18a6ca07ae46636da2f3faf085f0cdfe88394ae6..e726b8d0cc6036164b243272ae9d2242f4d81892 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_onboarding_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_onboarding_fragment.xml @@ -40,7 +40,7 @@ android:layout_height="wrap_content" android:contentDescription="@string/contact_diary_onboarding_image_content_description" android:focusable="true" - android:src="@drawable/ic_contact_diary_illustration_onboarding" + app:srcCompat="@drawable/ic_contact_diary_illustration_onboarding" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml index 3378d49878f8e3dcd0f2bd47405f85c0d3635192..ae153228eedec1a0163d72c7bc918aa09314f894 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml @@ -19,7 +19,7 @@ android:layout_marginStart="@dimen/spacing_small" android:importantForAccessibility="no" android:scaleType="centerInside" - android:src="@drawable/ic_contact_diary_person_item" + app:srcCompat="@drawable/ic_contact_diary_person_item" app:layout_constraintBaseline_toBaselineOf="@id/contact_diary_overview_element_name" app:layout_constraintEnd_toStartOf="@id/contact_diary_overview_element_name" app:layout_constraintStart_toStartOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml index f0cb4058e4d53fad5afb91f9fe538ca989cfe5c0..75163b421f29afa948d220349347a3c74e1ab7d4 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_information_contact.xml @@ -6,8 +6,8 @@ android:id="@+id/information_contact_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:focusable="true" - android:contentDescription="@string/information_contact_title"> + android:contentDescription="@string/information_contact_title" + android:focusable="true"> <include android:id="@+id/information_contact_header" @@ -52,43 +52,62 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_medium" - app:layout_constraintEnd_toEndOf="@+id/guideline_end" + app:layout_constraintEnd_toEndOf="@id/guideline_end" app:layout_constraintStart_toStartOf="@id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/information_contact_header_details" /> + app:layout_constraintTop_toBottomOf="@id/information_contact_header_details" /> <TextView android:id="@+id/information_contact_subtitle_phone" style="@style/headline6" - android:accessibilityHeading="true" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" - android:text="@string/information_contact_subtitle_phone" + android:accessibilityHeading="true" android:focusable="true" - app:layout_constraintEnd_toStartOf="@+id/guideline_end" - app:layout_constraintStart_toEndOf="@+id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/divider" /> + android:text="@string/information_contact_subtitle_phone" + app:layout_constraintEnd_toStartOf="@id/guideline_end" + app:layout_constraintStart_toEndOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/divider" /> <include android:id="@+id/information_contact_navigation_row_phone" layout="@layout/include_navigation_row" android:layout_width="0dp" android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginEnd="@dimen/spacing_normal" + app:icon="@{@drawable/ic_phone}" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/information_contact_subtitle_phone" - app:subtitle="@{@string/information_contact_button_phone}" /> + app:layout_constraintStart_toStartOf="@id/information_contact_subtitle_phone" + app:layout_constraintTop_toBottomOf="@id/information_contact_subtitle_phone" + app:subtitle="@{@string/information_contact_button_phone_description}" + app:title="@{@string/information_contact_button_phone}" /> + + <include + android:id="@+id/information_contact_navigation_row_international_phone" + layout="@layout/include_navigation_row" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:layout_marginEnd="@dimen/spacing_normal" + app:icon="@{@drawable/ic_international_phone}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/information_contact_subtitle_phone" + app:layout_constraintTop_toBottomOf="@id/information_contact_navigation_row_phone" + app:subtitle="@{@string/information_contact_button_international_phone_description}" + app:title="@{@string/information_contact_button_international_phone}" /> <TextView android:id="@+id/information_contact_body_phone" style="@style/body2" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@string/information_contact_body_phone" android:focusable="true" - app:layout_constraintEnd_toEndOf="@+id/guideline_end" - app:layout_constraintStart_toStartOf="@+id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/information_contact_navigation_row_phone" /> + android:layout_marginTop="@dimen/spacing_normal" + android:text="@string/information_contact_body_phone" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/information_contact_navigation_row_international_phone" /> <TextView android:id="@+id/information_contact_body_open" @@ -96,11 +115,11 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="@string/information_contact_body_open" android:focusable="true" - app:layout_constraintEnd_toEndOf="@+id/guideline_end" - app:layout_constraintStart_toStartOf="@+id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/information_contact_body_phone" /> + android:text="@string/information_contact_body_open" + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/information_contact_body_phone" /> <TextView android:id="@+id/information_contact_body_other" @@ -108,12 +127,12 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:text="@string/information_contact_body_other" android:focusable="true" + android:text="@string/information_contact_body_other" android:textColorLink="@color/colorTextTint" - app:layout_constraintEnd_toEndOf="@+id/guideline_end" - app:layout_constraintStart_toStartOf="@+id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/information_contact_body_open" /> + app:layout_constraintEnd_toEndOf="@id/guideline_end" + app:layout_constraintStart_toStartOf="@id/guideline_start" + app:layout_constraintTop_toBottomOf="@id/information_contact_body_open" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline_start" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_interoperability_configuration.xml b/Corona-Warn-App/src/main/res/layout/fragment_interoperability_configuration.xml index 1aeb9f6e56af982cfbb97f9baea1e5918d6bcef2..be2c804d65a220a405505ab73be2466327cf7dc4 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_interoperability_configuration.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_interoperability_configuration.xml @@ -48,7 +48,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:contentDescription="@{@string/interoperability_eu_illustration_description}" - android:src="@drawable/ic_illustration_interoperability" + app:srcCompat="@drawable/ic_illustration_interoperability" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml b/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml index b9efe5256f307b04c36963dbd0dcab79d36059e5..a34da0c9424fbd812f17a364ab83371d645a6a1e 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_onboarding.xml @@ -47,25 +47,25 @@ android:text="@string/onboarding_headline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/onboarding_illustration" + app:layout_constraintTop_toBottomOf="@id/onboarding_illustration" tools:text="@string/onboarding_headline" /> <TextView - android:id="@+id/onboarding_subtitle" + android:id="@+id/onboarding_body_1" style="@style/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/spacing_normal" android:layout_marginTop="@dimen/spacing_normal" android:focusable="true" - android:text="@string/onboarding_subtitle" + android:text="@string/onboarding_body_1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/onboarding_headline" - tools:text="@string/onboarding_subtitle" /> + app:layout_constraintTop_toBottomOf="@id/onboarding_headline" + tools:text="@string/onboarding_body_1" /> <TextView - android:id="@+id/onboarding_body" + android:id="@+id/onboarding_body_2" style="@style/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" @@ -73,26 +73,58 @@ android:layout_marginTop="@dimen/spacing_normal" android:autoLink="web|email" android:focusable="true" - android:text="@string/onboarding_body" + android:text="@string/onboarding_body_2" android:textColorLink="@color/colorTextTint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/onboarding_subtitle" - tools:text="@string/onboarding_body" /> + app:layout_constraintTop_toBottomOf="@id/onboarding_body_1" + tools:text="@string/onboarding_body_2" /> <TextView - android:id="@+id/onboarding_body_emphasized" + android:id="@+id/onboarding_body_3" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_normal" + android:autoLink="web|email" + android:focusable="true" + android:text="@string/onboarding_body_3" + android:textColorLink="@color/colorTextTint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/onboarding_body_2" + tools:text="@string/onboarding_body_3" /> + + <TextView + android:id="@+id/onboarding_body_4" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_normal" + android:autoLink="web|email" + android:focusable="true" + android:text="@string/onboarding_body_4" + android:textColorLink="@color/colorTextTint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/onboarding_body_3" + tools:text="@string/onboarding_body_4" /> + + <TextView + android:id="@+id/onboarding_body_5" style="@style/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/spacing_normal" android:layout_marginTop="@dimen/spacing_normal" android:focusable="true" - android:text="@string/onboarding_body_emphasized" + android:text="@string/onboarding_body_5" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/onboarding_body" - tools:text="@string/onboarding_body_emphasized" /> + app:layout_constraintTop_toBottomOf="@id/onboarding_body_4" + tools:text="@string/onboarding_body_5" /> <TextView android:id="@+id/onboarding_easy_language" @@ -108,7 +140,7 @@ android:textColorLink="@color/colorTextTint" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/onboarding_body_emphasized" + app:layout_constraintTop_toBottomOf="@id/onboarding_body_5" tools:text="@string/onboarding_tracing_easy_language_explanation" /> </androidx.constraintlayout.widget.ConstraintLayout> 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 index 5ea4a2968ef5df16c6c964c020a64a3e34bc3b7e..e176c094ac61800d6f5df5ae9bd68ea5f377a666 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_consent.xml @@ -46,7 +46,7 @@ android:id="@+id/submission_consent_illustration" android:layout_width="0dp" android:layout_height="wrap_content" - android:src="@drawable/ic_submission_consent" + app:srcCompat="@drawable/ic_submission_consent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_contact.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_contact.xml index a8b7ca3fd3c2c12eca992a6f6bef5377bb092e55..a4f2fb2e3e7a40bc9fdb90c2fd4ac8a4c613b2a3 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_contact.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_contact.xml @@ -32,21 +32,9 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/submission_contact_header" /> - <Button - android:id="@+id/submission_contact_button_call" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:text="@string/submission_contact_button_call" - android:textAllCaps="true" - app:layout_constraintBottom_toTopOf="@+id/submission_contact_button_enter" - app:layout_constraintEnd_toStartOf="@+id/guideline_end" - app:layout_constraintStart_toStartOf="@id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/guideline_bottom" /> - <Button android:id="@+id/submission_contact_button_enter" - style="@style/buttonLight" + style="@style/buttonPrimary" android:layout_width="0dp" android:layout_height="wrap_content" android:text="@string/submission_contact_button_enter" @@ -54,14 +42,14 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@id/guideline_start" - app:layout_constraintTop_toBottomOf="@+id/submission_contact_button_call" /> + app:layout_constraintTop_toBottomOf="@+id/include_submission_contact" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline_bottom" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_end="@dimen/guideline_action_large" /> + app:layout_constraintGuide_end="@dimen/guideline_action" /> <include layout="@layout/merge_guidelines_side" /> 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 f769e6fe74599e776179e946c7da286a1299f129..c7f1068de61ae287278824a91ba383edf3604e82 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 @@ -43,7 +43,7 @@ android:contentDescription="@string/submission_intro_illustration_description" android:focusable="true" android:layout_height="wrap_content" - android:src="@drawable/ic_illustration_test" + app:srcCompat="@drawable/ic_illustration_test" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_no_consent_positive_other_warning.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_no_consent_positive_other_warning.xml index e011e4b3ed20d4560b530fe257b61a1142707ea0..080cab6b67d87c6305648406bb8aaad6cffd7a75 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_no_consent_positive_other_warning.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_no_consent_positive_other_warning.xml @@ -46,7 +46,7 @@ android:id="@+id/submission_positive_other_warning_hero_illustration" android:layout_width="0dp" android:layout_height="wrap_content" - android:src="@drawable/ic_submission_illustration_other_warning" + app:srcCompat="@drawable/ic_submission_illustration_other_warning" android:contentDescription="@string/submission_positive_other_illustration_description" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_result_ready.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_result_ready.xml index 7ef34d33e1ad6d7a77fe5d928d417c12675d3828..19af93c4224f7a2ae6ab6c335c12a2fb7921bede 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_result_ready.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_result_ready.xml @@ -45,7 +45,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:focusable="true" - android:src="@drawable/ic_illustration_together" + app:srcCompat="@drawable/ic_illustration_together" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_available.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_available.xml index 494201687e24ae17343f7fbfe52b09a59c38dedd..f99c53fd0e19a574434d0d59aaaff60ff5e476b4 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_available.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_available.xml @@ -43,7 +43,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:focusable="true" - android:src="@drawable/ic_test_result_illustration_result_available" + app:srcCompat="@drawable/ic_test_result_illustration_result_available" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_no_consent.xml b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_no_consent.xml index d320ca471a02a0d43ce56a4e6af72d956acdfcef..22c8a25e0c3c164f43102b69b0f7481dc9ba3dcf 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_no_consent.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_submission_test_result_positive_no_consent.xml @@ -71,7 +71,7 @@ android:backgroundTint="@color/button_red" android:importantForAccessibility="no" android:padding="@dimen/circle_icon_padding" - android:src="@drawable/ic_submission_share" + app:srcCompat="@drawable/ic_submission_share" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/submission_test_result_positive_no_consent_subtitle" app:tint="@color/colorStableLight" /> @@ -96,7 +96,7 @@ android:layout_marginTop="@dimen/spacing_normal" android:background="@drawable/circle" android:backgroundTint="@color/button_red" - android:src="@drawable/ic_lock" + app:srcCompat="@drawable/ic_lock" android:importantForAccessibility="no" android:padding="@dimen/circle_icon_big_padding" app:layout_constraintTop_toBottomOf="@+id/submission_test_result_positive_no_consent_text_1" @@ -121,7 +121,7 @@ android:layout_marginTop="@dimen/spacing_normal" android:background="@drawable/circle" android:backgroundTint="@color/button_red" - android:src="@drawable/ic_risk_details_home" + app:srcCompat="@drawable/ic_risk_details_home" android:importantForAccessibility="no" android:padding="@dimen/circle_icon_big_padding" app:layout_constraintTop_toBottomOf="@+id/submission_test_result_positive_no_consent_text_2" diff --git a/Corona-Warn-App/src/main/res/layout/fragment_test_home_test_cards_layout.xml b/Corona-Warn-App/src/main/res/layout/fragment_test_home_test_cards_layout.xml new file mode 100644 index 0000000000000000000000000000000000000000..03465872a364efac9d328977fc27308d20df4190 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/fragment_test_home_test_cards_layout.xml @@ -0,0 +1,96 @@ +<?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:bind="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + <data> + <variable + name="tracingHeader" + type="de.rki.coronawarnapp.tracing.ui.statusbar.TracingHeaderState" /> + </data> + + <LinearLayout + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/main_title" + android:orientation="vertical" + tools:context=".ui.main.MainActivity"> + + <Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/CWAToolbar" + app:popupTheme="@style/CWAToolbar.Menu"> + <ImageView + android:id="@+id/main_header_logo" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="start|center_vertical" + android:focusable="true" + android:scaleType="fitStart" + android:src="@drawable/ic_main_header" + bind:cwaContentDescription="@{@string/accessibility_logo}" + tools:ignore="ContentDescription" /> + </Toolbar> + + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.appbar.AppBarLayout + android:layout_width="match_parent" + android:theme="@style/CWAToolbar" + android:layout_height="wrap_content"> + + <Toolbar + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="8dp" + style="@style/CWAToolbar" + android:id="@+id/main_tracing" + android:clickable="true" + android:focusable="true" + android:foreground="?selectableItemBackground" + app:layout_scrollFlags="scroll|snap|enterAlways"> + + <TextView + android:id="@+id/main_tracing_headline" + style="@style/bodyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="72dp" + android:contentDescription="@{tracingHeader.getTracingContentDescription(context)}" + android:focusable="false" + android:gravity="start|center_vertical" + android:text="@{tracingHeader.getTracingDescription(context)}" + tools:text="@string/settings_tracing_body_inactive_location" /> + + <com.airbnb.lottie.LottieAnimationView + android:id="@+id/main_tracing_icon" + android:layout_width="@dimen/icon_size_main_card" + android:layout_height="@dimen/icon_size_main_card" + android:layout_gravity="end|center_vertical" + android:layout_marginEnd="@dimen/spacing_small" + android:importantForAccessibility="no" + app:animation="@{tracingHeader.getTracingAnimation(context)}" + app:animation_tint="@{tracingHeader.getTracingTint(context)}" /> + + </Toolbar> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:itemCount="3" + tools:listitem="@layout/home_submission_status_card_unregistered" /> + + </androidx.coordinatorlayout.widget.CoordinatorLayout> + + </LinearLayout> +</layout> diff --git a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml index 3d8399ed798a5a067a043be3e65539c5df86eccb..fd7e77dd2b97a4de774bf6831549a61990609b76 100644 --- a/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml +++ b/Corona-Warn-App/src/main/res/layout/fragment_trace_location_onboarding.xml @@ -46,7 +46,7 @@ android:layout_marginHorizontal="24dp" android:layout_marginTop="4dp" android:contentDescription="@string/trace_location_onboarding_content_description" - android:src="@drawable/trace_location_onboarding" + app:srcCompat="@drawable/trace_location_onboarding" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -83,7 +83,7 @@ android:layout_marginStart="@dimen/spacing_normal" android:layout_marginTop="@dimen/spacing_large" android:importantForAccessibility="no" - android:src="@drawable/ic_qr_tracing_static" + app:srcCompat="@drawable/ic_qr_tracing_static" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_subtitle" /> @@ -105,7 +105,7 @@ android:layout_marginStart="@dimen/spacing_normal" android:layout_marginTop="@dimen/spacing_medium" android:importantForAccessibility="no" - android:src="@drawable/ic_qr_time" + app:srcCompat="@drawable/ic_qr_time" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_warning" /> @@ -158,7 +158,7 @@ android:layout_width="8dp" android:layout_height="8dp" android:layout_marginTop="24dp" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_card_subtitle" /> @@ -194,7 +194,7 @@ android:layout_width="8dp" android:layout_height="8dp" android:layout_marginTop="22dp" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_body3" /> @@ -217,7 +217,7 @@ android:layout_width="8dp" android:layout_height="8dp" android:layout_marginTop="22dp" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/check_in_onboarding_body4" /> @@ -261,4 +261,4 @@ </androidx.constraintlayout.widget.ConstraintLayout> </androidx.core.widget.NestedScrollView> -</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_create_trace_location_card.xml b/Corona-Warn-App/src/main/res/layout/home_create_trace_location_card.xml index bd13f263097bac37c72691edcc6641fd6e7054f5..e63bb3580bb19b981148fbe005306bdb3dc81cde 100644 --- a/Corona-Warn-App/src/main/res/layout/home_create_trace_location_card.xml +++ b/Corona-Warn-App/src/main/res/layout/home_create_trace_location_card.xml @@ -36,7 +36,7 @@ android:id="@+id/create_trace_location_card_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:src="@drawable/ic_event_organizer" + app:srcCompat="@drawable/ic_event_organizer" android:importantForAccessibility="no" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/create_trace_location_card_subtitle" /> diff --git a/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml b/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml index 55e41cb3cfde7f09bd5169f55931a5be4028b1d2..aeb33f5ad66e44f8153b2b01d6859aca5c03bd68 100644 --- a/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_faq_card_layout.xml @@ -15,7 +15,7 @@ android:layout_width="@dimen/icon_size_button" android:layout_height="@dimen/icon_size_button" android:importantForAccessibility="no" - android:src="@drawable/ic_main_about" + app:srcCompat="@drawable/ic_main_about" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/main_card_header_headline" app:layout_constraintStart_toStartOf="parent" @@ -42,7 +42,7 @@ android:layout_width="@dimen/icon_size_external_link" android:layout_height="@dimen/icon_size_external_link" android:importantForAccessibility="no" - android:src="@drawable/ic_link" + app:srcCompat="@drawable/ic_link" app:layout_constraintBottom_toBottomOf="@+id/main_card_header_headline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/main_card_header_headline" /> diff --git a/Corona-Warn-App/src/main/res/layout/home_fragment_layout.xml b/Corona-Warn-App/src/main/res/layout/home_fragment_layout.xml index 03465872a364efac9d328977fc27308d20df4190..683de0e8d2f457d3cfe850a568d1d6439032a072 100644 --- a/Corona-Warn-App/src/main/res/layout/home_fragment_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_fragment_layout.xml @@ -31,7 +31,7 @@ android:layout_gravity="start|center_vertical" android:focusable="true" android:scaleType="fitStart" - android:src="@drawable/ic_main_header" + app:srcCompat="@drawable/ic_main_header" bind:cwaContentDescription="@{@string/accessibility_logo}" tools:ignore="ContentDescription" /> </Toolbar> diff --git a/Corona-Warn-App/src/main/res/layout/home_reenable_risk_card_layout.xml b/Corona-Warn-App/src/main/res/layout/home_reenable_risk_card_layout.xml index 9ea36f1e033de8ad1f81be69ffed907d820e7a80..34fcc456137c95fbce1b2a6d5a5b3a242f5fb89f 100644 --- a/Corona-Warn-App/src/main/res/layout/home_reenable_risk_card_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_reenable_risk_card_layout.xml @@ -2,12 +2,6 @@ <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="state" - type="de.rki.coronawarnapp.submission.ui.homecards.SubmissionDone" /> - </data> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" @@ -66,7 +60,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/spacing_small" - android:text="@{state.formatTestRegistrationText(context)}" tools:text="@string/reenable_risk_card_test_registration_string" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/reenable_risk_card_positive_body" /> diff --git a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_incidence_layout.xml b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_incidence_layout.xml index a2bd07c8f2264973bfb2a1e84db6985393cc5e08..7eee9560c166dc4b7e507df6d0fd25f7b1a25c92 100644 --- a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_incidence_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_incidence_layout.xml @@ -18,7 +18,7 @@ android:layout_height="wrap_content" android:importantForAccessibility="no" android:contentDescription="@null" - android:src="@drawable/ic_statistics_incidence" + app:srcCompat="@drawable/ic_statistics_incidence" android:paddingStart="0dp" android:paddingEnd="@dimen/spacing_small" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_infections_layout.xml b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_infections_layout.xml index f44904922ab4c86fe30b4e7141d40e5deb3897c9..1d54ca078e19c2008ac9b13851aaa11bb7d8fe20 100644 --- a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_infections_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_infections_layout.xml @@ -18,7 +18,7 @@ android:layout_height="wrap_content" android:importantForAccessibility="no" android:contentDescription="@null" - android:src="@drawable/ic_main_illustration_infection" + app:srcCompat="@drawable/ic_main_illustration_infection" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/flow_layout" /> diff --git a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_keysubmissions_layout.xml b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_keysubmissions_layout.xml index 781e99cdf4a48cc807509c0b6f227212d8d5a91d..1d486842a6fee8dbac82c1637f95608e5b6e3114 100644 --- a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_keysubmissions_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_keysubmissions_layout.xml @@ -18,7 +18,7 @@ android:layout_height="wrap_content" android:importantForAccessibility="no" android:contentDescription="@null" - android:src="@drawable/ic_main_illustration_warnende_personen" + app:srcCompat="@drawable/ic_main_illustration_warnende_personen" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@id/flow_layout" /> diff --git a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_sevendayrvalue_layout.xml b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_sevendayrvalue_layout.xml index feca8a674d5c9f9583b3c1bd2a67794507d57128..57f7e82475116fe8e63ca7d7a8eab9beb0c464b0 100644 --- a/Corona-Warn-App/src/main/res/layout/home_statistics_cards_sevendayrvalue_layout.xml +++ b/Corona-Warn-App/src/main/res/layout/home_statistics_cards_sevendayrvalue_layout.xml @@ -17,7 +17,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:importantForAccessibility="no" - android:src="@drawable/ic_7_day_r_value" + app:srcCompat="@drawable/ic_7_day_r_value" android:paddingStart="@dimen/spacing_normal" android:paddingEnd="@dimen/spacing_normal" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_error.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_error.xml new file mode 100644 index 0000000000000000000000000000000000000000..c49ecb6fb79e3c97427dfdd60238de2e7cf77e75 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_error.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_error" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_error" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_invalid" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/show_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_show_results" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_invalid.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_invalid.xml new file mode 100644 index 0000000000000000000000000000000000000000..6c03a73581718fbcb1cc1ae1141b536c37c3d49f --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_invalid.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_invalid" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_not_valid_test" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_invalid" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/delete_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_failed" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_negative.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_negative.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b23023977d63eda004770d1cf3ba16fa93d96d8 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_negative.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/findings" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_findings" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/corona_name" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_name_of_the_cause_of_this_app" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/findings" /> + + <TextView + android:id="@+id/status" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_negative" + android:textColor="@color/colorTextSemanticGreen" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/corona_name" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_negative" + app:layout_constraintBottom_toTopOf="@id/date" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status" /> + + <TextView + android:id="@+id/date" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginBottom="32dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_body_result_date" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/body" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="24dp" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_negative_card" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_pending.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_pending.xml new file mode 100644 index 0000000000000000000000000000000000000000..da6cb6b82918c50ac40cc55550cf4f550ff4f7f9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_pending.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="20dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_no_result" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_body_no_result" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_pending" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/show_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_show_results" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_positive_not_shared.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_positive_not_shared.xml new file mode 100644 index 0000000000000000000000000000000000000000..65812e9d4cd43430cba8cc75fd50b5139125c4f6 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_positive_not_shared.xml @@ -0,0 +1,213 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/card_padding"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/findings" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_findings" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/corona_name" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_name_of_the_cause_of_this_app" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/findings" /> + + <TextView + android:id="@+id/status" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_positiv" + android:textColor="@color/colorTextSemanticRed" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/corona_name" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_positive" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status" /> + + <TextView + android:id="@+id/date" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginBottom="32dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_body_result_date" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/body" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_positive_card" /> + + <include + android:id="@+id/divider" + layout="@layout/include_divider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/date" /> + + <TextView + android:id="@+id/result_subtitle" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:text="@string/submission_status_card_positive_result_subtitle" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contact_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_contact" + app:layout_constraintEnd_toStartOf="@id/contact_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/result_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contact_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_contact" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contact_icon" + app:layout_constraintTop_toTopOf="@+id/contact_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contagious_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_home" + app:layout_constraintEnd_toStartOf="@id/contagious_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contagious_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_contagious" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contagious_icon" + app:layout_constraintTop_toTopOf="@+id/contagious_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/share_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_submission_share_cards" + app:layout_constraintEnd_toStartOf="@id/share_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contagious_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/share_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_share" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/share_icon" + app:layout_constraintTop_toTopOf="@+id/share_icon" /> + + <Button + android:id="@+id/submission_status_card_positive_button" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:text="@string/submission_test_result_positive_no_consent_button_warn_others" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_subtitle" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_positive_shared.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_positive_shared.xml new file mode 100644 index 0000000000000000000000000000000000000000..bc0dd85a63b74a3bc6535adf01720ef3b162ce55 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_positive_shared.xml @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/card_padding"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/findings" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_findings" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/corona_name" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_name_of_the_cause_of_this_app" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/findings" /> + + <TextView + android:id="@+id/status" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_positiv" + android:textColor="@color/colorTextSemanticRed" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/corona_name" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_positive" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status" /> + + <TextView + android:id="@+id/date" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginBottom="32dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_body_result_date" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/body" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_positive_card" /> + + <include + android:id="@+id/divider" + layout="@layout/include_divider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/date" /> + + <TextView + android:id="@+id/result_subtitle" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:text="@string/submission_status_card_positive_result_subtitle" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contact_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_contact" + app:layout_constraintEnd_toStartOf="@id/contact_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/result_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contact_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_contact" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contact_icon" + app:layout_constraintTop_toTopOf="@+id/contact_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contagious_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_home" + app:layout_constraintEnd_toStartOf="@id/contagious_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contagious_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginBottom="12dp" + android:text="@string/submission_status_card_positive_result_contagious" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contagious_icon" + app:layout_constraintTop_toTopOf="@+id/contagious_icon" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_ready.xml b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_ready.xml new file mode 100644 index 0000000000000000000000000000000000000000..b4d9ecbbc923c8b46b325a88aee703e881bc1484 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_pcr_status_card_ready.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_pcr_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_result_available" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_available" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_invalid" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/show_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_show_results" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_error.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_error.xml new file mode 100644 index 0000000000000000000000000000000000000000..eec3d36c7890f062905f402dea4b655fb337c9cd --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_error.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_error" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_error" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_invalid" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/show_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_show_results" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_invalid.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_invalid.xml new file mode 100644 index 0000000000000000000000000000000000000000..5ee0fdbae1dff0e9e2f1e858e0c6c246cc3d1c0e --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_invalid.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_invalid" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_not_valid_test" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_invalid" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/delete_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_failed" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_negative.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_negative.xml new file mode 100644 index 0000000000000000000000000000000000000000..c08d9fef1b9255c8f777133500ac06f80c87b5cc --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_negative.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/findings" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_findings" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/corona_name" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_name_of_the_cause_of_this_app" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/findings" /> + + <TextView + android:id="@+id/status" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_negative" + android:textColor="@color/colorTextSemanticGreen" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/corona_name" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_negative" + app:layout_constraintBottom_toTopOf="@id/date" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status" /> + + <TextView + android:id="@+id/date" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginBottom="32dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapid_body_result_date" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/body" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="24dp" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_negative_card" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_outdated.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_outdated.xml new file mode 100644 index 0000000000000000000000000000000000000000..8ba510968c65f48c5a71a07cf4fd0832fe514ff7 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_outdated.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="2dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_status_outdated_test" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_body_outdated_test" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="24dp" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_outdated_card" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/dont_show_anymore_button" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/ag_homescreen_card_rapidtest_dont_show_anymore_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_pending.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_pending.xml new file mode 100644 index 0000000000000000000000000000000000000000..5c9eb6e5541b817f82f7dfaa98704166d77bf1bc --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_pending.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_no_result" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_body_no_result" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_pending" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/show_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_show_results" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_positive_not_shared.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_positive_not_shared.xml new file mode 100644 index 0000000000000000000000000000000000000000..33bf2d3e74a207b9d98019a3d909ac701ceb5355 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_positive_not_shared.xml @@ -0,0 +1,242 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/card_padding"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/findings" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_findings" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/corona_name" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_name_of_the_cause_of_this_app" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/findings" /> + + <TextView + android:id="@+id/status" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_positiv" + android:textColor="@color/colorTextSemanticRed" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/corona_name" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_positive" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status" /> + + <TextView + android:id="@+id/date" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginBottom="32dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapid_body_result_date" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/body" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_positive_card" /> + + <include + android:id="@+id/divider" + layout="@layout/include_divider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/date" /> + + <TextView + android:id="@+id/result_subtitle" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:text="@string/submission_status_card_positive_result_subtitle" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/prc_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:paddingHorizontal="12dp" + android:paddingVertical="11dp" + android:src="@drawable/ic_risk_details_pcr" + app:layout_constraintEnd_toStartOf="@id/pcr_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/result_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/pcr_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/ag_homescreen_card_rapidtest_body_result_positive_pcr_check" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/prc_icon" + app:layout_constraintTop_toTopOf="@+id/prc_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contact_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_contact" + app:layout_constraintEnd_toStartOf="@id/contact_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/pcr_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contact_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_contact" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contact_icon" + app:layout_constraintTop_toTopOf="@+id/contact_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contagious_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_home" + app:layout_constraintEnd_toStartOf="@id/contagious_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contagious_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_contagious" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contagious_icon" + app:layout_constraintTop_toTopOf="@+id/contagious_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/share_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_submission_share_cards" + app:layout_constraintEnd_toStartOf="@id/share_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contagious_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/share_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_share" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/share_icon" + app:layout_constraintTop_toTopOf="@+id/share_icon" /> + + <Button + android:id="@+id/submission_status_card_positive_button" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:text="@string/submission_test_result_positive_no_consent_button_warn_others" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_subtitle" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_positive_shared.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_positive_shared.xml new file mode 100644 index 0000000000000000000000000000000000000000..9ecf98e1234564db8b62a7b6c85f92dd1929c866 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_positive_shared.xml @@ -0,0 +1,205 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="@dimen/card_padding"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/findings" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_findings" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/corona_name" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_name_of_the_cause_of_this_app" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/findings" /> + + <TextView + android:id="@+id/status" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_positiv" + android:textColor="@color/colorTextSemanticRed" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/corona_name" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_positive" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status" /> + + <TextView + android:id="@+id/date" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/spacing_small" + android:layout_marginBottom="32dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapid_body_result_date" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/body" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_test_result_illustration_positive_card" /> + + <include + android:id="@+id/divider" + layout="@layout/include_divider" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/date" /> + + <TextView + android:id="@+id/result_subtitle" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + android:accessibilityHeading="true" + android:text="@string/submission_status_card_positive_result_subtitle" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/prc_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:paddingHorizontal="12dp" + android:paddingVertical="11dp" + android:src="@drawable/ic_risk_details_pcr" + app:layout_constraintEnd_toStartOf="@id/pcr_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/result_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/pcr_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/ag_homescreen_card_rapidtest_body_result_positive_pcr_check" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/prc_icon" + app:layout_constraintTop_toTopOf="@+id/prc_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contact_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_contact" + app:layout_constraintEnd_toStartOf="@id/contact_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/pcr_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contact_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:text="@string/submission_status_card_positive_result_contact" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contact_icon" + app:layout_constraintTop_toTopOf="@+id/contact_icon" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/contagious_icon" + style="@style/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="28dp" + android:background="@drawable/circle" + android:backgroundTint="@color/colorSemanticHighRisk" + android:focusable="false" + android:importantForAccessibility="no" + android:padding="10dp" + android:src="@drawable/ic_risk_details_home" + app:layout_constraintEnd_toStartOf="@id/contagious_subtitle" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_subtitle" + app:tint="@color/colorStableLight" /> + + <TextView + android:id="@+id/contagious_subtitle" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginBottom="12dp" + android:text="@string/submission_status_card_positive_result_contagious" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/contagious_icon" + app:layout_constraintTop_toTopOf="@+id/contagious_icon" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_ready.xml b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_ready.xml new file mode 100644 index 0000000000000000000000000000000000000000..26747ccb991403571436853831e9c862d6b917f0 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/home_submission_rapid_status_card_ready.xml @@ -0,0 +1,83 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline5" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="32dp" + android:layout_marginEnd="@dimen/spacing_small" + android:accessibilityHeading="true" + android:focusable="false" + android:text="@string/ag_homescreen_card_rapidtest_title" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/subtitle" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="8dp" + android:focusable="false" + android:text="@string/ag_homescreen_card_status_result_available" + android:textColor="@color/colorTextSemanticNeutral" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/title" /> + + <TextView + android:id="@+id/body" + style="@style/subtitleMedium" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="8dp" + android:layout_marginEnd="@dimen/spacing_small" + android:focusable="false" + android:text="@string/ag_homescreen_card_body_result_available" + app:layout_constraintEnd_toStartOf="@+id/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/subtitle" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/title" + app:srcCompat="@drawable/ic_main_illustration_invalid" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="icon,body" /> + + <Button + android:id="@+id/show_test_action" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/card_padding" + android:layout_marginTop="24dp" + android:layout_marginEnd="@dimen/card_padding" + android:layout_marginBottom="@dimen/card_padding" + android:text="@string/submission_status_card_button_show_results" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/button_barrier" /> + + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_done.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_done.xml index 7d43888b64c917b0c02fa946b95a41ec52ba7034..0e7f27076e6d6dd165c763adeba1d2769e9ab5e0 100644 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_done.xml +++ b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_done.xml @@ -29,7 +29,7 @@ android:layout_marginTop="@dimen/spacing_tiny" android:contentDescription="@string/submission_done_illustration_description" android:importantForAccessibility="no" - android:src="@drawable/ic_illustration_together" + app:srcCompat="@drawable/ic_illustration_together" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/submission_done_card_title" /> diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_error.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_error.xml deleted file mode 100644 index 63aa89dc006172f5cdb9f4abd4945d877388f53c..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_error.xml +++ /dev/null @@ -1,88 +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:layout_width="match_parent" - android:layout_height="wrap_content" - tools:showIn="@layout/home_card_container_layout"> - - <TextView - android:id="@+id/title" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/card_padding" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:focusable="false" - android:text="@string/submission_status_card_title_available" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/subtitle" - style="@style/headline6" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_subtitle_invalid" - android:textColor="@color/colorTextSemanticNeutral" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <TextView - android:id="@+id/body" - style="@style/subtitleMedium" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_tiny" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_body_invalid" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/subtitle" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:importantForAccessibility="no" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@id/title" - app:srcCompat="@drawable/ic_main_illustration_invalid" /> - - <androidx.constraintlayout.widget.Barrier - android:id="@+id/button_barrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="bottom" - app:constraint_referenced_ids="icon,body" /> - - <Button - android:id="@+id/show_test_action" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/card_padding" - android:layout_marginBottom="@dimen/card_padding" - android:text="@string/submission_status_card_button_show_results" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> - - </androidx.constraintlayout.widget.ConstraintLayout> - -</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_fetching.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_fetching.xml index bc6badd3cc05e4d9b4cec32cd376d78254756055..8cab6e02e053f0ff116905fecf3341fb4d8eacea 100644 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_fetching.xml +++ b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_fetching.xml @@ -18,7 +18,15 @@ android:text="@string/submission_status_card_title_fetching" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@+id/header_barrier"/> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/header_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="top" + app:constraint_referenced_ids="submission_status_card_fetching_spinner,submission_status_card_fetching_body" /> <ProgressBar android:id="@+id/submission_status_card_fetching_spinner" @@ -27,7 +35,7 @@ android:layout_height="wrap_content" android:layout_marginVertical="@dimen/spacing_normal" app:layout_constraintStart_toStartOf="@+id/submission_status_card_fetching_title" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_fetching_title" /> + app:layout_constraintTop_toBottomOf="@+id/header_barrier" /> <TextView android:id="@+id/submission_status_card_fetching_body" @@ -41,6 +49,13 @@ app:layout_constraintStart_toEndOf="@+id/submission_status_card_fetching_spinner" app:layout_constraintTop_toTopOf="@+id/submission_status_card_fetching_spinner" /> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/button_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="submission_status_card_fetching_spinner,submission_status_card_fetching_body" /> + <Button android:id="@+id/show_test_action" style="@style/buttonPrimary" @@ -51,7 +66,7 @@ android:text="@string/submission_status_card_button_show_results" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_fetching_spinner" /> + app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_invalid.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_invalid.xml deleted file mode 100644 index 2c16e8dfc436d69b8fee51894c38694a415da11b..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_invalid.xml +++ /dev/null @@ -1,73 +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:layout_width="match_parent" - android:layout_height="wrap_content" - tools:showIn="@layout/home_card_container_layout"> - - <TextView - android:id="@+id/title" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/card_padding" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:focusable="false" - android:text="@string/submission_status_card_title_failed" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/body" - style="@style/subtitleMedium" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_body_failed" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="@+id/title" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/body" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/body" - app:srcCompat="@drawable/ic_main_illustration_invalid" /> - - <androidx.constraintlayout.widget.Barrier - android:id="@+id/button_barrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="bottom" - app:constraint_referenced_ids="icon,body" /> - - <Button - android:id="@+id/delete_test_action" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/card_padding" - android:layout_marginBottom="@dimen/card_padding" - android:text="@string/submission_status_card_button_failed" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> - - </androidx.constraintlayout.widget.ConstraintLayout> - -</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_negative.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_negative.xml deleted file mode 100644 index 74b0a58c7f43aeb1b90d54c380820aa6f6f8ab65..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_negative.xml +++ /dev/null @@ -1,87 +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:layout_width="match_parent" - android:layout_height="wrap_content" - tools:showIn="@layout/home_card_container_layout"> - - <TextView - android:id="@+id/title" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/card_padding" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:focusable="false" - android:text="@string/submission_status_card_title_available" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/subtitle" - style="@style/headline6" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_subtitle_negative" - android:textColor="@color/colorTextSemanticGreen" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <TextView - android:id="@+id/body" - style="@style/subtitleMedium" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_tiny" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_body_negative" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/subtitle" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:srcCompat="@drawable/ic_main_illustration_negative" - android:importantForAccessibility="no" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@id/title" /> - - <androidx.constraintlayout.widget.Barrier - android:id="@+id/button_barrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="bottom" - app:constraint_referenced_ids="icon,body" /> - - <Button - android:id="@+id/show_test_action" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/card_padding" - android:layout_marginBottom="@dimen/card_padding" - android:text="@string/submission_status_card_button_show_results" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> - - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_pending.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_pending.xml deleted file mode 100644 index 9a2257a3cc6c8559d8fb024905bb8b786a8ab7b0..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_pending.xml +++ /dev/null @@ -1,71 +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:layout_width="match_parent" - android:layout_height="wrap_content" - tools:showIn="@layout/home_card_container_layout"> - - <TextView - android:id="@+id/title" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/card_padding" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:focusable="false" - android:text="@string/submission_status_card_title_pending" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/body" - style="@style/subtitleMedium" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_tiny" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_body_pending" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/title" /> - - <androidx.appcompat.widget.AppCompatImageView - android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:importantForAccessibility="no" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@id/title" - app:srcCompat="@drawable/ic_main_illustration_pending" /> - - <androidx.constraintlayout.widget.Barrier - android:id="@+id/button_barrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="bottom" - app:constraint_referenced_ids="icon,body" /> - - <Button - android:id="@+id/show_test_action" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_small" - android:layout_marginEnd="@dimen/card_padding" - android:layout_marginBottom="@dimen/card_padding" - android:text="@string/submission_status_card_button_show_results" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/button_barrier" /> - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_positive.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_positive.xml deleted file mode 100644 index dc381249a1cffde20e1a393f25ed9049ad63a06b..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_positive.xml +++ /dev/null @@ -1,116 +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:layout_width="match_parent" - android:layout_height="wrap_content" - android:padding="@dimen/card_padding" - tools:showIn="@layout/home_card_container_layout"> - - <TextView - android:id="@+id/submission_status_card_positive_title" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:text="@string/submission_status_card_title_positive" - app:layout_constraintEnd_toStartOf="@id/submission_status_card_positive_next" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <include - android:id="@+id/submission_status_card_positive_next" - layout="@layout/include_button_icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:icon="@{@drawable/ic_forward}" - app:iconDescription="@{@string/accessibility_next}" - app:layout_constraintBottom_toBottomOf="@+id/submission_status_card_positive_title" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/submission_status_card_positive_title" /> - - <include - android:id="@+id/submission_status_card_positive_result_card" - layout="@layout/include_test_result_card_positive" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_title" /> - - <include - android:id="@+id/divider" - layout="@layout/include_divider" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_card" /> - - <TextView - android:id="@+id/submission_status_card_positive_result_subtitle" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:accessibilityHeading="true" - android:text="@string/submission_status_card_positive_result_subtitle" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/divider" /> - - <include - android:id="@+id/submission_status_card_positive_result_contact" - layout="@layout/include_submission_behaviour_row" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - app:body="@{@string/submission_status_card_positive_result_contact}" - app:icon="@{@drawable/ic_risk_details_contact}" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_subtitle" /> - - <include - android:id="@+id/submission_status_card_positive_result_contagious" - layout="@layout/include_submission_behaviour_row" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - app:body="@{@string/submission_status_card_positive_result_contagious}" - app:icon="@{@drawable/ic_submission_home}" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_contact" /> - - <include - android:id="@+id/submission_status_card_positive_result_share" - layout="@layout/include_submission_behaviour_row" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - app:body="@{@string/submission_status_card_positive_result_share}" - app:icon="@{@drawable/ic_submission_share}" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_contagious" /> - - <Button - android:id="@+id/submission_status_card_positive_button" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:text="@string/submission_test_result_positive_no_consent_button_warn_others" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/submission_status_card_positive_result_share" /> - - - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_ready.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_ready.xml deleted file mode 100644 index 4898df7f38d0af23fa707f8ca168bd6bf189d1ad..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_ready.xml +++ /dev/null @@ -1,86 +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:layout_width="match_parent" - android:layout_height="wrap_content" - tools:showIn="@layout/home_card_container_layout"> - - <TextView - android:id="@+id/title" - style="@style/headline5" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/card_padding" - android:layout_marginEnd="@dimen/spacing_small" - android:accessibilityHeading="true" - android:focusable="false" - android:text="@string/submission_status_card_title_ready" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/subtitle" - style="@style/headline6Sixteen" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_subtitle_ready" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="@+id/title" - app:layout_constraintTop_toBottomOf="@+id/title" /> - - <TextView - android:id="@+id/body" - style="@style/subtitleMedium" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/spacing_small" - android:focusable="false" - android:text="@string/submission_status_card_body_ready" - app:layout_constraintEnd_toStartOf="@+id/icon" - app:layout_constraintStart_toStartOf="@+id/subtitle" - app:layout_constraintTop_toBottomOf="@+id/subtitle" /> - - <ImageView - android:id="@+id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:importantForAccessibility="no" - app:layout_constraintBottom_toBottomOf="@+id/body" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/subtitle" - app:srcCompat="@drawable/ic_main_illustration_invalid" /> - - <androidx.constraintlayout.widget.Barrier - android:id="@+id/button_barrier" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - app:barrierDirection="bottom" - app:constraint_referenced_ids="icon,body" /> - - <Button - android:id="@+id/show_test_action" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/card_padding" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginEnd="@dimen/card_padding" - android:layout_marginBottom="@dimen/card_padding" - android:text="@string/submission_status_card_retrieve_test_result" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/button_barrier" /> - - - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_unregistered.xml b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_unregistered.xml index e7f47cc6b0a30c5f92dda088514df2bcf89fc3d0..4004baa2d329e216b4ca2f88560529a381f8bfed 100644 --- a/Corona-Warn-App/src/main/res/layout/home_submission_status_card_unregistered.xml +++ b/Corona-Warn-App/src/main/res/layout/home_submission_status_card_unregistered.xml @@ -18,7 +18,7 @@ android:layout_marginEnd="@dimen/spacing_small" android:accessibilityHeading="true" android:focusable="false" - android:text="@string/submission_status_card_title_unregistered" + android:text="@string/ag_homescreen_card_test_register_title" app:layout_constraintEnd_toStartOf="@+id/icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -32,7 +32,7 @@ android:layout_marginTop="@dimen/spacing_normal" android:layout_marginEnd="@dimen/spacing_small" android:focusable="false" - android:text="@string/submission_status_card_body_unregistered" + android:text="@string/ag_homescreen_card_test_register_body" app:layout_constraintEnd_toStartOf="@+id/icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/title" /> 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 index 099185c7ef900b417cb1b5382f195da98da6e0dc..38b441f371c276918ee45353ed30c9538eae575d 100644 --- a/Corona-Warn-App/src/main/res/layout/include_bullet_point.xml +++ b/Corona-Warn-App/src/main/res/layout/include_bullet_point.xml @@ -7,7 +7,7 @@ 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" + app:srcCompat="@drawable/bullet_point" android:importantForAccessibility="no" app:layout_constraintStart_toStartOf="parent" tools:showIn="@layout/include_submission_consent_body" /> diff --git a/Corona-Warn-App/src/main/res/layout/include_dispatcher_card.xml b/Corona-Warn-App/src/main/res/layout/include_dispatcher_card.xml index 968a7b08957fc02c7274e65190138e541dcad47a..d2a3907c5506158b2d26d2d037fe2d87be67dbe1 100644 --- a/Corona-Warn-App/src/main/res/layout/include_dispatcher_card.xml +++ b/Corona-Warn-App/src/main/res/layout/include_dispatcher_card.xml @@ -50,7 +50,7 @@ android:layout_marginTop="@dimen/spacing_small" android:layout_marginEnd="@dimen/spacing_normal" android:importantForAccessibility="no" - android:src="@drawable/ic_forward" + app:srcCompat="@drawable/ic_forward" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:tint="@color/colorTextPrimary1" /> diff --git a/Corona-Warn-App/src/main/res/layout/include_interop_riskdetails_no_countries_infoview.xml b/Corona-Warn-App/src/main/res/layout/include_interop_riskdetails_no_countries_infoview.xml index 5038e7ba3ced6952a41d33f711c8e59c3992452c..1ade9dc9d3b8c8a400d2b80d7c1760582af92cd0 100644 --- a/Corona-Warn-App/src/main/res/layout/include_interop_riskdetails_no_countries_infoview.xml +++ b/Corona-Warn-App/src/main/res/layout/include_interop_riskdetails_no_countries_infoview.xml @@ -17,7 +17,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:importantForAccessibility="no" - android:src="@drawable/ic_interop_no_network" + app:srcCompat="@drawable/ic_interop_no_network" android:visibility="visible" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/include_interoperability.xml b/Corona-Warn-App/src/main/res/layout/include_interoperability.xml index 092efae0ac1754aeb5b7901f6372ee8fd26081a8..9a9dac88001cac1b8d79bc51eecd80df9bb26c3d 100644 --- a/Corona-Warn-App/src/main/res/layout/include_interoperability.xml +++ b/Corona-Warn-App/src/main/res/layout/include_interoperability.xml @@ -86,7 +86,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:contentDescription="@{@string/interoperability_eu_illustration_description}" - android:src="@drawable/ic_illustration_interoperability" + app:srcCompat="@drawable/ic_illustration_interoperability" android:visibility="@{FormatterHelper.formatVisibility(!isOnboarding)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/include_navigation_row.xml b/Corona-Warn-App/src/main/res/layout/include_navigation_row.xml index 1daa4173899acc7a5080e99384755e5e8b9c0462..192d8b7ed6df2f8d27f20aadfdf923240d7359e0 100644 --- a/Corona-Warn-App/src/main/res/layout/include_navigation_row.xml +++ b/Corona-Warn-App/src/main/res/layout/include_navigation_row.xml @@ -13,52 +13,56 @@ name="icon" type="android.graphics.drawable.Drawable" /> + <variable + name="title" + type="String" /> + <variable name="subtitle" type="String" /> </data> <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/navigation_row" + style="@style/phoneNumber" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:focusable="true"> - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/navigation_row" - style="@style/row" + <ImageView + android:id="@+id/navigation_row_icon" + android:layout_width="@dimen/icon_size" + android:layout_height="@dimen/icon_size" + android:layout_marginEnd="@dimen/spacing_small" + android:importantForAccessibility="no" + android:src="@{icon}" + android:visibility="@{FormatterHelper.formatVisibilityIcon(icon)}" + app:layout_constraintBottom_toBottomOf="@id/navigation_row_title" + app:layout_constraintEnd_toStartOf="@+id/navigation_row_title" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/navigation_row_title" /> + + <TextView + android:id="@+id/navigation_row_title" + style="@style/headline5Tint" android:layout_width="0dp" android:layout_height="wrap_content" - android:focusable="true" + android:text="@{title}" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintStart_toEndOf="@+id/navigation_row_icon" + app:layout_constraintTop_toTopOf="parent" + tools:text="@string/settings_notifications_button_open_settings" /> - <ImageView - android:id="@+id/navigation_row_icon" - style="@style/icon" - android:layout_width="@dimen/icon_size_settings" - android:layout_height="@dimen/icon_size_settings" - android:layout_marginEnd="@dimen/spacing_small" - android:importantForAccessibility="no" - android:src="@{icon}" - android:visibility="@{FormatterHelper.formatVisibilityIcon(icon)}" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/navigation_row_subtitle" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/navigation_row_subtitle" - style="@style/headline5Tint" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:text="@{subtitle}" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/navigation_row_icon" - app:layout_constraintTop_toTopOf="parent" - tools:text="@string/settings_notifications_button_open_settings" /> - - </androidx.constraintlayout.widget.ConstraintLayout> + <TextView + android:id="@+id/navigation_row_subtitle" + style="@style/body2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:text="@{subtitle}" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/navigation_row_title" + app:layout_constraintTop_toBottomOf="@id/navigation_row_title" /> </androidx.constraintlayout.widget.ConstraintLayout> </layout> \ No newline at end of file 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 index c9475f94df2f31f1f10e6fba2e5f28fec1b052cf..dcef17ee05da26127ee138b5e4eb435c384628d9 100644 --- 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 @@ -15,7 +15,7 @@ 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_your_consent_subsection_third_point" + app:layout_constraintBottom_toBottomOf="@id/submission_consent_your_consent_subsection_fourth_point" style="@style/GreyCard" /> <TextView @@ -40,32 +40,32 @@ 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="0dp" - 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" + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/submission_consent_your_consent_subsection_first_point" android:layout_width="0dp" 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" /> + 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="0dp" + 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> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/submission_consent_your_consent_subsection_second_point" @@ -94,8 +94,35 @@ </androidx.constraintlayout.widget.ConstraintLayout> + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/submission_consent_your_consent_subsection_third_point" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:paddingBottom="@dimen/spacing_normal" + app:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_second_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_third_point_text"/> + + <TextView + android:id="@+id/submission_consent_your_consent_subsection_third_point_text" + style="@style/subtitle" + android:layout_width="0dp" + 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_third_point" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + <TextView - android:id="@+id/submission_consent_your_consent_subsection_third_point" + android:id="@+id/submission_consent_your_consent_subsection_fourth_point" style="@style/subtitle" android:layout_width="0dp" android:layout_height="wrap_content" @@ -103,8 +130,8 @@ android:paddingBottom="@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" + app:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_third_point" + android:text="@string/submission_consent_your_consent_subsection_fourth_point" tools:ignore="RtlSymmetry" /> <include layout="@layout/view_bullet_point_text" @@ -114,7 +141,7 @@ 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:layout_constraintTop_toBottomOf="@id/submission_consent_your_consent_subsection_fourth_point" app:itemText="@{@string/submission_consent_main_first_point}" /> <include layout="@layout/view_bullet_point_text" 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 index 26d2702fea7740b3b8aa6a6a5554e93a9119984a..4bd8c6d0bb6afbbebfa7a735be2f716da5dd4def 100644 --- 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 @@ -33,7 +33,7 @@ android:layout_width="@dimen/circle_icon" android:layout_height="@dimen/circle_icon" android:layout_marginTop="@dimen/spacing_normal" - android:src="@drawable/ic_qr_code" + app:srcCompat="@drawable/ic_qr_code" android:background="@drawable/circle" android:backgroundTint="@color/card_dark" android:padding="@dimen/circle_icon_padding" @@ -60,7 +60,7 @@ android:layout_marginTop="@dimen/spacing_normal" android:background="@drawable/circle" android:backgroundTint="@color/card_dark" - android:src="@drawable/ic_qr_1x_test" + app:srcCompat="@drawable/ic_qr_1x_test" android:importantForAccessibility="no" android:padding="8dp" app:layout_constraintTop_toBottomOf="@+id/submission_consent_call_test_result_scan_your_test_only" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_contact.xml b/Corona-Warn-App/src/main/res/layout/include_submission_contact.xml index 803f64096c2751a5f945e042d9214780fd30ace5..fa7bc987503ed860b8f32ca10412e8ebce046b66 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_contact.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_contact.xml @@ -16,8 +16,8 @@ android:id="@+id/submission_contact_illustration" android:layout_width="0dp" android:layout_height="wrap_content" - android:src="@drawable/ic_submission_illustration_hotline" android:focusable="true" + app:srcCompat="@drawable/ic_submission_illustration_hotline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" @@ -30,8 +30,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_small" - android:text="@string/submission_contact_body" android:focusable="true" + android:text="@string/submission_contact_body" app:layout_constraintEnd_toStartOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" app:layout_constraintTop_toBottomOf="@id/submission_contact_illustration" /> @@ -39,12 +39,12 @@ <TextView android:id="@+id/submission_contact_headline" style="@style/headline5" - android:accessibilityHeading="true" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_medium" - android:text="@string/submission_contact_headline" + android:accessibilityHeading="true" android:focusable="true" + android:text="@string/submission_contact_headline" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@id/guideline_start" app:layout_constraintTop_toBottomOf="@+id/submission_contact_body" /> @@ -54,8 +54,8 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_normal" - android:focusable="true" android:contentDescription="@string/submission_contact_step_1_content" + android:focusable="true" app:layout_constraintEnd_toStartOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" app:layout_constraintTop_toBottomOf="@id/submission_contact_headline" @@ -77,30 +77,40 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - <TextView - android:id="@+id/submission_contact_step_1_number" - style="@style/headline5" - android:accessibilityHeading="true" + <include + android:id="@+id/submission_contact_navigation_row_phone" + layout="@layout/include_navigation_row" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" - android:text="@string/submission_contact_number_display" - android:textColor="@color/colorTextTint" - android:clickable="true" + android:layout_marginTop="@dimen/spacing_normal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/submission_contact_step_1_body" /> + app:layout_constraintTop_toBottomOf="@id/submission_contact_step_1_body" + app:subtitle="@{@string/information_contact_button_phone_description}" + app:title="@{@string/submission_contact_number_display}" /> + + <include + android:id="@+id/submission_contact_navigation_row_international_phone" + layout="@layout/include_navigation_row" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_normal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/submission_contact_navigation_row_phone" + app:subtitle="@{@string/information_contact_button_international_phone_description}" + app:title="@{@string/submission_contact_button_international_phone}" /> <TextView android:id="@+id/submission_contact_operating_hours_body" style="@style/body2" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/spacing_small" + android:layout_marginTop="@dimen/spacing_normal" android:text="@string/submission_contact_operating_hours_body" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/submission_contact_step_1_number" /> + app:layout_constraintTop_toBottomOf="@id/submission_contact_navigation_row_international_phone" /> <TextView android:id="@+id/submission_contact_body_other" @@ -121,8 +131,8 @@ android:id="@+id/submission_contact_step_2" android:layout_width="0dp" android:layout_height="wrap_content" - android:focusable="true" android:contentDescription="@string/submission_contact_step_2_content" + android:focusable="true" app:layout_constraintEnd_toStartOf="@+id/guideline_end" app:layout_constraintStart_toStartOf="@+id/guideline_start" app:layout_constraintTop_toBottomOf="@+id/submission_contact_step_1" diff --git a/Corona-Warn-App/src/main/res/layout/include_submission_done.xml b/Corona-Warn-App/src/main/res/layout/include_submission_done.xml index b199de4cec6a04d42d224a849c4b02a8664c3850..3e6226cf040a033607934d78954bfe81eae9b50f 100644 --- a/Corona-Warn-App/src/main/res/layout/include_submission_done.xml +++ b/Corona-Warn-App/src/main/res/layout/include_submission_done.xml @@ -18,7 +18,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:focusable="true" - android:src="@drawable/ic_illustration_together" + app:srcCompat="@drawable/ic_illustration_together" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" 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 688adae687beb54590ddaf5bf1186b08d013c7d2..f8b15560263ab87d295a4164ecc0ff76bfc2fb0a 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 @@ -25,7 +25,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" bind:cwaContentDescription="@{@string/submission_positive_other_illustration_description}" - android:src="@drawable/ic_submission_illustration_other_warning" + app:srcCompat="@drawable/ic_submission_illustration_other_warning" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" 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 54e8aaf5becc0597eb9667956d84bd4633243a29..e3c6daffd7549badb95fa1bcf755e7efe0c5e39d 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.NetworkRequestWrapper<de.rki.coronawarnapp.util.DeviceUIState,java.lang.Throwable>" /> + type="de.rki.coronawarnapp.util.DeviceUIState" /> </data> <androidx.constraintlayout.widget.ConstraintLayout diff --git a/Corona-Warn-App/src/main/res/layout/new_release_info_item.xml b/Corona-Warn-App/src/main/res/layout/new_release_info_item.xml index c5a0c49a6aac2f350c82a45119862e36e49c3e0a..a1b010aae990ccdc95a6fba9af35dc10f6f69244 100644 --- a/Corona-Warn-App/src/main/res/layout/new_release_info_item.xml +++ b/Corona-Warn-App/src/main/res/layout/new_release_info_item.xml @@ -25,7 +25,7 @@ android:layout_marginStart="@dimen/spacing_normal" android:layout_marginTop="@dimen/spacing_tiny" android:importantForAccessibility="no" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" /> diff --git a/Corona-Warn-App/src/main/res/layout/new_release_info_screen_fragment.xml b/Corona-Warn-App/src/main/res/layout/new_release_info_screen_fragment.xml index 78182d1514903c3832529ac05a92e3234d932629..9761fcdcf4c4c0ac5dc8596a25ea536ce56538fc 100644 --- a/Corona-Warn-App/src/main/res/layout/new_release_info_screen_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/new_release_info_screen_fragment.xml @@ -40,7 +40,7 @@ android:contentDescription="@string/release_info_header" android:focusable="true" android:layout_marginTop="@dimen/spacing_normal" - android:src="@drawable/ic_new_release_info" + app:srcCompat="@drawable/ic_new_release_info" android:importantForAccessibility="no" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/statistics_trend_view.xml b/Corona-Warn-App/src/main/res/layout/statistics_trend_view.xml index d9c7fca9a466a4cd532a5facac95eb95f768c1e2..28b4dfaa304ef0674d6a93f56d7db2397ab3d0ac 100644 --- a/Corona-Warn-App/src/main/res/layout/statistics_trend_view.xml +++ b/Corona-Warn-App/src/main/res/layout/statistics_trend_view.xml @@ -1,11 +1,12 @@ <?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/trend" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="1dp" android:background="@drawable/bg_statistics_trend_neutral" android:scaleType="center" - android:src="@drawable/ic_trend_stable" /> + app:srcCompat="@drawable/ic_trend_stable" /> diff --git a/Corona-Warn-App/src/main/res/layout/survey_consent_fragment.xml b/Corona-Warn-App/src/main/res/layout/survey_consent_fragment.xml index 114e09de6df2f0c9a191818f4a189186033db836..fe1c3c7ef61715c5c5e095444fe87ac18213bc71 100644 --- a/Corona-Warn-App/src/main/res/layout/survey_consent_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/survey_consent_fragment.xml @@ -39,7 +39,7 @@ android:layout_height="wrap_content" android:contentDescription="@string/contact_diary_onboarding_image_content_description" android:focusable="true" - android:src="@drawable/ic_illustration_datenspende" + app:srcCompat="@drawable/ic_illustration_datenspende" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml index 1e665b70bab77733c13522cf12757475f33c3306..b2934dbf99a1385ba0ce5a991093769fe4e9b70a 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_fragment.xml @@ -49,7 +49,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/trace_location_checkins_empty_illustration_accessibility" - android:src="@drawable/trace_location_my_check_ins_empty_illustration" /> + app:srcCompat="@drawable/trace_location_my_check_ins_empty_illustration" /> <TextView style="@style/subtitleMedium" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_item_past.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_item_past.xml index 8a08c8cae5c1d37737e064dc3d05774e3fca87bd..3ead1f5c9f3a44c01dab82dc8354845bf80d0520 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_item_past.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_checkins_item_past.xml @@ -17,7 +17,7 @@ android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:orientation="vertical" - android:src="@drawable/ic_old_checkin" + app:srcCompat="@drawable/ic_old_checkin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml new file mode 100644 index 0000000000000000000000000000000000000000..04f98963fdd96a10a3b35d32d63c2b6791bde0f0 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="24dp" + android:paddingTop="18dp" + android:paddingEnd="24dp" + android:paddingBottom="8dp"> + + <TextView + style="@style/subtitleMedium" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/trace_location_attendee_consent_header_description" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/select_all_button" + style="@style/materialTextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_marginTop="8dp" + android:text="@string/trace_location_attendee_consent_header_button" /> +</LinearLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml new file mode 100644 index 0000000000000000000000000000000000000000..7037299401d7b52e5a6c3c3ed248bbd79a9d7618 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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" + style="@style/contactDiaryCardRipple" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginHorizontal="16dp" + android:layout_marginVertical="8dp" + android:focusable="true"> + + <ImageView + android:id="@+id/checkbox" + android:layout_width="@dimen/spacing_medium" + android:layout_height="@dimen/spacing_medium" + android:layout_marginStart="@dimen/spacing_small" + android:layout_marginTop="13dp" + android:background="?selectableItemBackgroundBorderless" + android:clickable="false" + android:focusable="false" + android:importantForAccessibility="no" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_unselected" + tools:srcCompat="@drawable/ic_selected" /> + + <TextView + android:id="@+id/title" + style="@style/materialSubtitleSixteen" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="15dp" + android:layout_marginTop="13dp" + android:layout_marginEnd="10dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/checkbox" + app:layout_constraintTop_toTopOf="parent" + tools:text="Hairdresser" /> + + <TextView + android:id="@+id/subtitle" + style="@style/body2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginEnd="10dp" + android:textColor="@color/colorTextPrimary2" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/title" + app:layout_constraintTop_toBottomOf="@id/title" + tools:text="Berlin" /> + + <TextView + android:id="@+id/checkoutInfo" + style="@style/body2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginEnd="10dp" + android:layout_marginBottom="10dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/title" + app:layout_constraintTop_toBottomOf="@id/subtitle" + tools:text="21.01.21, 18:01 - 21:00 Uhr" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml index 9396dc1576dfc32de53f628d19d795c828a87d19..36589681fc501f9e38582a7790969b2071e1dab8 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_detail_fragment.xml @@ -36,7 +36,7 @@ android:id="@+id/expandedImage" android:layout_width="match_parent" android:layout_height="match_parent" - android:src="@drawable/trace_location_view_cardhighlight_gradient" + app:srcCompat="@drawable/trace_location_view_cardhighlight_gradient" app:layout_collapseMode="parallax" /> <LinearLayout @@ -95,7 +95,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginEnd="72dp" - android:src="@drawable/ic_cwa_logo_white" /> + app:srcCompat="@drawable/ic_cwa_logo_white" /> </LinearLayout> diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_info_fragment.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_info_fragment.xml index c106bb4ddcea1a87288b686c7b5d3415f985b7df..de5948e1fe8de67d48496593342c7b4bda2dde40 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_info_fragment.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_qr_code_info_fragment.xml @@ -39,7 +39,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_huge" android:contentDescription="@string/trace_location_qr_info_content_description" - android:src="@drawable/ic_qr_info" + app:srcCompat="@drawable/ic_qr_info" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -76,7 +76,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_large" android:importantForAccessibility="no" - android:src="@drawable/ic_qr_tracing_static" + app:srcCompat="@drawable/ic_qr_tracing_static" app:layout_constraintStart_toStartOf="@id/trace_location_qr_info_subtitle" app:layout_constraintTop_toBottomOf="@id/trace_location_qr_info_subtitle" /> @@ -98,7 +98,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_medium" android:importantForAccessibility="no" - android:src="@drawable/ic_qr_code_illustration" + app:srcCompat="@drawable/ic_qr_code_illustration" app:layout_constraintStart_toStartOf="@id/trace_location_tracing_icon" app:layout_constraintTop_toBottomOf="@id/trace_location_qr_info_tracing" /> @@ -120,7 +120,7 @@ android:layout_height="wrap_content" android:layout_marginTop="@dimen/spacing_medium" android:importantForAccessibility="no" - android:src="@drawable/ic_qr_time" + app:srcCompat="@drawable/ic_qr_time" app:layout_constraintTop_toBottomOf="@id/trace_location_qr_code_illustration_text" app:layout_constraintStart_toStartOf="@id/trace_location_qr_code_icon" /> @@ -166,7 +166,7 @@ </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> - <android.widget.Button + <Button android:id="@+id/trace_location_qr_info_understand_button" style="@style/buttonPrimary" android:layout_width="0dp" diff --git a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_item.xml b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_item.xml index 3b0d989a634d8d026167b26298a3e64c6c9bb7f7..89a547a8123b56d1b249532b67451b21a864dbdd 100644 --- a/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_item.xml +++ b/Corona-Warn-App/src/main/res/layout/trace_location_organizer_trace_locations_item.xml @@ -23,7 +23,7 @@ android:layout_width="42dp" android:layout_height="42dp" android:importantForAccessibility="no" - android:src="@drawable/ic_qr_code_list_item_icon" + app:srcCompat="@drawable/ic_qr_code_list_item_icon" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/tracing_content_disabled_view.xml b/Corona-Warn-App/src/main/res/layout/tracing_content_disabled_view.xml index 827d81c0156ad03c52f36737ba861c7949a6938c..b5ab40269e5bcc93d7b540ce596642d053fd03b1 100644 --- a/Corona-Warn-App/src/main/res/layout/tracing_content_disabled_view.xml +++ b/Corona-Warn-App/src/main/res/layout/tracing_content_disabled_view.xml @@ -34,7 +34,7 @@ android:layout_width="@dimen/icon_size_risk_card" android:layout_height="@dimen/icon_size_risk_card" android:importantForAccessibility="no" - android:src="@drawable/ic_forward" + app:srcCompat="@drawable/ic_forward" app:tint="@color/colorTextPrimary1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/tracing_content_failed_view.xml b/Corona-Warn-App/src/main/res/layout/tracing_content_failed_view.xml index 101480a1308bbae54cce8732059f937b7f994380..31da8067055d0218e12fe0675f01f10b53026c44 100644 --- a/Corona-Warn-App/src/main/res/layout/tracing_content_failed_view.xml +++ b/Corona-Warn-App/src/main/res/layout/tracing_content_failed_view.xml @@ -33,7 +33,7 @@ android:layout_width="@dimen/icon_size_risk_card" android:layout_height="@dimen/icon_size_risk_card" android:importantForAccessibility="no" - android:src="@drawable/ic_forward" + app:srcCompat="@drawable/ic_forward" app:tint="@color/colorTextPrimary1" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/tracing_content_increased_view.xml b/Corona-Warn-App/src/main/res/layout/tracing_content_increased_view.xml index 12af62996f2fc7360995f144a1f974461e5fe820..79688669a658d2ce286d953b6c04897b3dc5cc3c 100644 --- a/Corona-Warn-App/src/main/res/layout/tracing_content_increased_view.xml +++ b/Corona-Warn-App/src/main/res/layout/tracing_content_increased_view.xml @@ -35,7 +35,7 @@ android:layout_width="@dimen/icon_size_risk_card" android:layout_height="@dimen/icon_size_risk_card" android:importantForAccessibility="no" - android:src="@drawable/ic_forward" + app:srcCompat="@drawable/ic_forward" app:tint="@color/colorStableLight" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/tracing_content_low_view.xml b/Corona-Warn-App/src/main/res/layout/tracing_content_low_view.xml index 1fbb229efc0de19a2b00f8c9ebd378da8cffd902..0c569562d3d1fe3ad86431eb15aa91f29f59327d 100644 --- a/Corona-Warn-App/src/main/res/layout/tracing_content_low_view.xml +++ b/Corona-Warn-App/src/main/res/layout/tracing_content_low_view.xml @@ -37,7 +37,7 @@ android:layout_width="@dimen/icon_size_risk_card" android:layout_height="@dimen/icon_size_risk_card" android:importantForAccessibility="no" - android:src="@drawable/ic_forward" + app:srcCompat="@drawable/ic_forward" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:tint="@color/colorStableLight" diff --git a/Corona-Warn-App/src/main/res/layout/tracing_content_progress_view.xml b/Corona-Warn-App/src/main/res/layout/tracing_content_progress_view.xml index b425d258b53994166fe68cea2109ff11a51aedcf..10154cbf3cecfb6b580dac8e08dc49c4b288f87d 100644 --- a/Corona-Warn-App/src/main/res/layout/tracing_content_progress_view.xml +++ b/Corona-Warn-App/src/main/res/layout/tracing_content_progress_view.xml @@ -35,7 +35,7 @@ android:layout_width="@dimen/icon_size_risk_card" android:layout_height="@dimen/icon_size_risk_card" android:importantForAccessibility="no" - android:src="@drawable/ic_forward" + app:srcCompat="@drawable/ic_forward" app:tint="@{state.getStableIconColor(context)}" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/tracing_details_item_behavior_increased_view.xml b/Corona-Warn-App/src/main/res/layout/tracing_details_item_behavior_increased_view.xml index 62b3800457ba532756f6d8dc31ea351e0c0bc290..2d200a6d28b5737e5e0a71e59e46440234a5b20b 100644 --- a/Corona-Warn-App/src/main/res/layout/tracing_details_item_behavior_increased_view.xml +++ b/Corona-Warn-App/src/main/res/layout/tracing_details_item_behavior_increased_view.xml @@ -71,7 +71,7 @@ android:layout_marginStart="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_mega_tiny" android:importantForAccessibility="no" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:tint="@color/colorSemanticHighRisk" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/risk_details_behavior_bullet_point_1" /> @@ -97,7 +97,7 @@ android:layout_marginStart="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_mega_tiny" android:importantForAccessibility="no" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:tint="@color/colorSemanticHighRisk" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/risk_details_behavior_bullet_point_2" /> @@ -124,7 +124,7 @@ android:layout_marginStart="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_mega_tiny" android:importantForAccessibility="no" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" app:tint="@color/colorSemanticHighRisk" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/risk_details_behavior_bullet_point_3" /> diff --git a/Corona-Warn-App/src/main/res/layout/view_bullet_point_entry.xml b/Corona-Warn-App/src/main/res/layout/view_bullet_point_entry.xml index dd70a00c8d45f6a7ab3a04aa457f28a7949e7afc..8be17fc926adfb4d93b3a0e321929d6d4fa05a1d 100644 --- a/Corona-Warn-App/src/main/res/layout/view_bullet_point_entry.xml +++ b/Corona-Warn-App/src/main/res/layout/view_bullet_point_entry.xml @@ -12,7 +12,7 @@ android:layout_height="@dimen/bullet_point_size" android:layout_marginStart="@dimen/bullet_point_spacing_before" android:baseline="@dimen/bullet_point_baseline_offset" - android:src="@drawable/bullet_point" + app:srcCompat="@drawable/bullet_point" android:importantForAccessibility="no" app:layout_constraintBaseline_toBaselineOf="@+id/bullet_point_content" app:layout_constraintStart_toStartOf="parent" /> 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 index 776045ec7dc7532c1e64e97f1a32fff1581a66b0..65bf6f10070a81e8d9d15f30f9fa3026b28bf4cd 100644 --- 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 @@ -19,7 +19,7 @@ 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" + app:srcCompat="@drawable/bullet_point" android:importantForAccessibility="no" app:layout_constraintBaseline_toBaselineOf="@+id/bullet_point_content" app:layout_constraintStart_toStartOf="parent" /> diff --git a/Corona-Warn-App/src/main/res/layout/view_circle_progress.xml b/Corona-Warn-App/src/main/res/layout/view_circle_progress.xml index 209542e7621e609b83451ce49d1c0ab4b3d1f4cf..f5b023d5db5e8290d642dc22dd86a725d7f37484 100644 --- a/Corona-Warn-App/src/main/res/layout/view_circle_progress.xml +++ b/Corona-Warn-App/src/main/res/layout/view_circle_progress.xml @@ -22,7 +22,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:importantForAccessibility="no" - android:src="@drawable/ic_risk_card_saved_days" + app:srcCompat="@drawable/ic_risk_card_saved_days" app:tint="@color/colorAccentTintIcon" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/Corona-Warn-App/src/main/res/layout/view_consent_status.xml b/Corona-Warn-App/src/main/res/layout/view_consent_status.xml index 8f533776c8ebf58cdd2888e1df32d25814fa8671..4bbcf160f22e3e82960aae722126b940c4f915ac 100644 --- a/Corona-Warn-App/src/main/res/layout/view_consent_status.xml +++ b/Corona-Warn-App/src/main/res/layout/view_consent_status.xml @@ -27,7 +27,7 @@ android:focusable="false" android:importantForAccessibility="no" android:paddingTop="4dp" - android:src="@drawable/ic_consent_status_view_icon" + app:srcCompat="@drawable/ic_consent_status_view_icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.0" /> 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 f8b5895d66971e5a7352055e161eaad7677f69c1..5ff1d99cff64e4f55a40b3bcde41772a920a595b 100644 --- a/Corona-Warn-App/src/main/res/navigation/nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml @@ -278,6 +278,11 @@ app:destination="@id/submissionResultReadyFragment" app:popUpTo="@id/mainFragment" app:popUpToInclusive="false" /> + <action + android:id="@+id/action_submissionResultPositiveOtherWarningNoConsentFragment_to_checkInsConsentFragment" + app:destination="@id/checkInsConsentFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" /> </fragment> <fragment android:id="@+id/submissionTestResultPendingFragment" @@ -488,6 +493,11 @@ app:destination="@id/submissionTestResultConsentGivenFragment" app:popUpTo="@id/mainFragment" app:popUpToInclusive="false" /> + <action + android:id="@+id/action_submissionTestResultAvailableFragment_to_checkInsConsentFragment" + app:destination="@id/checkInsConsentFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" /> <action android:id="@+id/action_submissionTestResultAvailableFragment_to_submissionTestResultNoConsentFragment" app:destination="@id/submissionTestResultNoConsentFragment" @@ -609,4 +619,28 @@ android:name="de.rki.coronawarnapp.bugreporting.debuglog.ui.legal.DebugLogLegalFragment" android:label="DebugLogLegalFragment" tools:layout="@layout/bugreporting_legal_fragment" /> + + <fragment + android:id="@+id/checkInsConsentFragment" + android:name="de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment" + android:label="check_ins_consent_fragment" + tools:layout="@layout/check_ins_consent_fragment"> + <action + android:id="@+id/action_checkInsConsentFragment_to_mainFragment" + app:destination="@id/mainFragment" + app:popUpTo="@id/nav_graph" + app:popUpToInclusive="true" /> + + <action + android:id="@+id/action_checkInsConsentFragment_to_submissionResultReadyFragment" + app:destination="@id/submissionResultReadyFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" /> + + <action + android:id="@+id/action_checkInsConsentFragment_to_submissionTestResultConsentGivenFragment" + app:destination="@id/submissionTestResultConsentGivenFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="false" /> + </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 646797d6076689e0bb7ec0078da5c49ab7d18f6e..3b78ec9880e3753956cb89e11d5bc8bcd4960ee7 100644 --- a/Corona-Warn-App/src/main/res/values-bg/strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/strings.xml @@ -1973,4 +1973,22 @@ <string name="trace_location_organiser_poster_share">"СподелÑне"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"ВерÑÐ¸Ñ Ð·Ð° печат"</string> + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">"Ще Ñподелите ли Ñвоите региÑтрации от локациите, които Ñте поÑетили?"</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">"Предупредете оÑтаналите, които Ñъщо Ñа Ñе региÑтрирали в близоÑÑ‚ до ваÑ. Вашите лични данни нÑма да бъдат Ñподелени."</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">"Избери вÑички"</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">"Предаване"</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">"ПропуÑкане"</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">"ÐаиÑтина ли не желаете да Ñподелите Ñвоите региÑтрации?"</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">"Като пропуÑкате ÑподелÑнето, вие не изпращате предупреждение на други контактни лица, региÑтрирани в близоÑÑ‚ до ваÑ."</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">"СподелÑне"</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">"Да не Ñе ÑподелÑ"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml b/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..3f69d7869cbb2f30d4ed86edf20e0273a6fe2135 --- /dev/null +++ b/Corona-Warn-App/src/main/res/values-de/antigen_strings.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> + + <!-- #################################### + Homescreen cards - register test card + ###################################### --> + <!-- XHED: Register test homescreen card: title --> + <string name="ag_homescreen_card_test_register_title">"Test registrieren"</string> + <!-- XTXT: Register test homescreen card: body --> + <string name="ag_homescreen_card_test_register_body">"Nutzen Sie die App, um Ihre Testergebnisse abrufen und schneller warnen zu können."</string> + + <!-- #################################### + Homescreen cards - common buttons + ###################################### --> + <!-- XBUT: Homescreen card: show test button --> + <string name="ag_homescreen_card_show_button">"Test anzeigen"</string> + <!-- XBUT: Homescreen card: warn others button --> + <string name="ag_homescreen_card_warn_others_button">"Andere warnen"</string> + + <!-- #################################### + Homescreen cards - common status + ###################################### --> + <!-- XTXT: Homescreen card: status subtitle - no result --> + <string name="ag_homescreen_card_status_no_result">"Ergebnis liegt noch nicht vor"</string> + <!-- XTXT: Homescreen card: status subtitle - result available --> + <string name="ag_homescreen_card_status_result_available">"Testergebnis abrufen"</string> + <!-- XTXT: Homescreen card: status subtitle - error --> + <string name="ag_homescreen_card_status_error">"Fehlerhafter Test"</string> + <!-- XTXT: Homescreen card: status subtitle - invalid --> + <string name="ag_homescreen_card_status_invalid">"Nicht mehr gültig"</string> + <!-- XTXT: Homescreen card: status subtitle - findings --> + <string name="ag_homescreen_card_status_findings">"Befund"</string> + <!-- XTXT: Homescreen card: subtitle - corona official name --> + <string name="ag_homescreen_card_status_name_of_the_cause_of_this_app">"SARS-CoV-2"</string> + <!-- XTXT: Homescreen card: status - negative --> + <string name="ag_homescreen_card_status_negative">"Negativ"</string> + <!-- XTXT: Homescreen card: status - positiv --> + <string name="ag_homescreen_card_status_positiv">"Positiv"</string> + + <!-- #################################### + Homescreen cards - common body + ###################################### --> + <!-- XTXT: homescreen card: body - result available --> + <string name="ag_homescreen_card_body_result_available">"Warnen Sie Ihre Mitmenschen, wenn Sie positiv auf Corona getestet wurden."</string> + <!-- XTXT: homescreen card: body - error --> + <string name="ag_homescreen_card_body_error">"Ihr Test konnte nicht ausgewertet werden."</string> + <!-- XTXT: homescreen card: body - not valid test --> + <string name="ag_homescreen_card_body_not_valid_test">"Ihr Test liegt länger als 21 Tage zurück und ist daher nicht länger relevant. Bitte löschen Sie den Test. Danach können Sie einen neuen Test hinzufügen. "</string> + <!-- XTXT: homescreen card: body - negative --> + <string name="ag_homescreen_card_body_result_negative">"Das Virus SARS-CoV-2 wurde bei Ihnen nicht nachgewiesen."</string> + <!-- XTXT: homescreen card: body - positive --> + <string name="ag_homescreen_card_body_result_positive">"Das Virus SARS-CoV-2 wurde bei Ihnen nachgewiesen."</string> + + <!-- #################################### + Homescreen cards - rapid test + ###################################### --> + <!-- XHED: rapid test homescreen card: title --> + <string name="ag_homescreen_card_rapidtest_title">"Schnelltest"</string> + <!-- XBUT: rapid test homescreen card: dont show anymore button --> + <string name="ag_homescreen_card_rapidtest_dont_show_anymore_button">"Nicht mehr anzeigen"</string> + <!-- XTXT: rapid test homescreen card: status subtitle - outdated test --> + <string name="ag_homescreen_card_rapidtest_status_outdated_test">"Test nicht mehr aktuell"</string> + <!-- XTXT: rapid test homescreen card: result positive - check results with PCR test --> + <string name="ag_homescreen_card_rapidtest_body_result_positive_pcr_check">"Machen Sie einen PCR-Test, um dieses Test-Ergebnis zu verifizieren."</string> + + <!-- Body --> + <!-- XTXT: rapid test homescreen card: body - no result --> + <string name="ag_homescreen_card_rapidtest_body_no_result">"Die Auswertung Ihres Schnelltests ist noch nicht abgeschlossen."</string> + <!-- XTXT: rapid test homescreen card: body - outdated test --> + <string name="ag_homescreen_card_rapidtest_body_outdated_test">"Ihr Schnelltest ist älter als 48 Stunden und wird hier nicht mehr angezeigt."</string> + <!-- XTXT: homescreen card: body - negative --> + <string name="ag_homescreen_card_rapid_body_result_date">"Durchgeführt am %1$s"</string> + + <!-- #################################### + Homescreen cards - PCR + ###################################### --> + <!-- XHED: PCR homescreen card: title --> + <string name="ag_homescreen_card_pcr_title">"PCR-Test"</string> + <!-- XBUT: PCR homescreen card: clear test button --> + <string name="ag_homescreen_card_pcr_clear_test_button">"TEST LÖSCHEN"</string> + + <!-- Body --> + <!-- XTXT: PCR homescreen card: body - no result --> + <string name="ag_homescreen_card_pcr_body_no_result">"Die Auswertung Ihres PCR-Tests ist noch nicht abgeschlossen."</string> + <!-- XTXT: homescreen card: body - negative --> + <string name="ag_homescreen_card_pcr_body_result_date">"Test registriert am %1$s"</string> + +</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 1c1f0d484b10de9bb196b8474f3fad7e0a1e7a13..707f0e5913aa59fc7bc7a475794604b724890733 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 @@ -16,16 +16,20 @@ <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 in folgende Schritte 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 Corona-Apps der oben genannten Länder und Nutzer, die zeitgleich am selben Event oder Ort wie Sie eingecheckt waren.\n\nWenn Sie zusätzlich Angaben zum Beginn Ihrer Symptome machen, werden auch diese geteilt.</b>"</string> + <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 Corona-Apps der oben genannten Länder und Nutzer, die zeitgleich am selben Event oder Ort wie Sie eingecheckt waren. Im Falle von Schnelltests werden nur Nutzer der Corona-Warn-App gewarnt.</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."</string> + <string name="submission_consent_your_consent_subsection_third_point">"<b>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 fourth point --> + <string name="submission_consent_your_consent_subsection_fourth_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."</string> <!-- YTXT: Body for your consent screen agreement share test results --> - <string name="submission_your_consent_agreement_share_test_results">"<b>Wenn Sie positiv auf Corona getestet wurden, teilt die App Ihr Testergebnis, um Nutzer zu warnen, denen Sie begegnet sind.</b>\n\n<b>Die Warnung erreicht Nutzer von offiziellen Corona-Apps der folgenden teilnehmenden Länder:</b>"</string> + <string name="submission_your_consent_agreement_share_test_results">"<b>Wenn Sie positiv auf Corona getestet wurden, teilt die App Ihr Testergebnis, um Nutzer, denen Sie begegnet sind, zu warnen.</b>\n\n<b>Die Warnung betrifft Nutzer, die zeitgleich am selben Event oder Ort wie Sie eingecheckt waren und Nutzer der Corona-Apps der folgenden Länder:</b>"</string> <!-- YTXT: Body for your consent screen agreement share symptoms --> - <string name="submission_your_consent_agreement_share_symptoms">"<b>Wenn Sie zusätzlich Angaben zum Beginn Ihrer Symptome machen, werden auch diese geteilt.</b>\n\nSie können Ihr Einverständnis zurücknehmen, indem Sie oben „Andere warnen“ deaktivieren."</string> + <string name="submission_your_consent_agreement_share_symptoms">"<b>Im Falle von Schnelltests werden nur Nutzer der Corona-Warn-App gewarnt. Wenn Sie zusätzlich Angaben zum Beginn Ihrer Symptome machen, werden auch diese Daten geteilt.</b>\n\nSie können Ihr Einverständnis zurücknehmen, indem Sie oben „Andere warnen“ deaktivieren."</string> + <!-- YTXT: Body for your consent screen agreement share symptoms with additional hint for test result--> <string name="submission_your_consent_agreement_share_symptoms_2"><b>Wenn Sie zusätzlich Angaben zum Beginn Ihrer Symptome machen, werden auch diese geteilt.</b>\n\nSie können Ihr Einverständnis zurücknehmen, indem Sie oben „Andere warnen“ deaktivieren. Ihr Testergebnis wird Ihnen anschließend angezeigt.</string> <!-- YTXT: Body for keys submission no consent text first part--> diff --git a/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml b/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml index b1493e50e3f95cd0b0fa920142b2f9b4a9e49baa..9bdb4e9ea0f2db79e0535267cac45f49f94d5015 100644 --- a/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/release_info_strings.xml @@ -17,22 +17,34 @@ <!-- XHED: Titles for the release info screen bullet points --> <string-array name="new_release_title"> - <item>Check-in per QR-Code für effiziente Rückverfolgbarkeit von Infektionsketten</item> + <item>Registrierung von Antigen-Schnelltests</item> + <item>Anzeige von Nachweisen</item> + <item>Technische Hotline nun auch aus dem Ausland erreichbar</item> + <item>TAN-Hotline nun auch aus dem Ausland erreichbar</item> </string-array> <!-- XTXT: Text bodies for the release info screen bullet points --> <string-array name="new_release_body"> - <item>Die CWA ermöglicht nun einen Check-in für Events und Orte. Personen, die Events veranstalten oder ein Geschäft haben, können über die App einen QR-Code erstellen. Durch Scannen des QR-Codes können sich Gäste bei Ankunft einchecken, um so ihre Anwesenheit zu registrieren. Auf Wunsch legt die App außerdem einen entsprechenden Tagebuch-Eintrag an. Wird eine eingecheckte Person später positiv auf das Coronavirus getestet, können andere Personen gewarnt werden, die zur selben Zeit eingescheckt waren.</item> + <item>Sie können sich nun zusätzlich zu Ergebnissen von PCR-Tests auch Ergebnisse von Antigen-Schnelltests in der App anzeigen lassen und nutzen, um andere zu warnen.</item> + <item>Auf Wunsch können Sie über die App Ihren persönlichen Infektionsstatus nachweisen (z.B. negativer Schnelltest). Bitte beachten Sie, dass Sie grundsätzlich nicht zum Nachweis Ihres Infektionsstatus per App verpflichtet sind. Sie können Ihren Infektionsstatus im Rahmen der rechtlichen Bestimmungen an Ihrem Aufenthaltsort auch auf andere Weise nachweisen.</item> + <item>Sie können die technische Hotline nun auch aus dem Ausland unter der Nummer +49 30 498 75401 erreichen. Es fallen Gebühren des jeweiligen Telefonanbieters an.</item> + <item>Sie können bei einem positiven Befund nun auch aus dem Ausland eine TAN anfordern unter der Nummer +49 30 498 75402. Es fallen Gebühren des jeweiligen Telefonanbieters an.</item> </string-array> <!-- XTXT: Text labels that will be converted to Links --> <string-array name="new_release_linkified_labels"> <item></item> + <item></item> + <item></item> + <item></item> </string-array> <!-- XTXT: URL destinations for the lables in new_release_linkified_labels --> <string-array name="new_release_target_urls"> <item></item> + <item></item> + <item></item> + <item></item> </string-array> </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 74344fff12023323d98b03dfa9982c49bac570a5..c18d91172304704774f9953b876dd814312fb630 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -399,12 +399,16 @@ <string name="onboarding_onboarding_accessibility_title">"Einführung Seite 1 von 6: Gemeinsam Corona bekämpfen"</string> <!-- XHED: onboarding(together) - fight corona --> <string name="onboarding_headline">"Gemeinsam Corona bekämpfen"</string> - <!-- XHED: onboarding(together) - two/three line headline under an illustration --> - <string name="onboarding_subtitle">"Mehr Schutz für Sie und uns alle. Mit der Corona-Warn-App durchbrechen wir Infektionsketten schneller."</string> - <!-- YTXT: onboarding(together) - inform about the app --> - <string name="onboarding_body">"Machen Sie Ihr Smartphone zum Corona-Warn-System. Überblicken Sie Ihren Risikostatus und erfahren Sie, ob in den letzten 14 Tagen Corona-positiv getestete Personen in Ihrer Nähe waren."</string> - <!-- YTXT: onboarding(together) - explain application --> - <string name="onboarding_body_emphasized">"Die App merkt sich Begegnungen zwischen Menschen, indem ihre Smartphones verschlüsselte Zufalls-IDs austauschen. Persönliche Daten werden dabei nicht ausgetauscht."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_1">"Mehr Schutz für Sie und uns alle. Mit der Corona-Warn-App durchbrechen wir Infektionsketten schneller."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_2">"Machen Sie Ihr Smartphone zum Corona-Warn-System. Überblicken Sie Ihren Risikostatus und erfahren Sie, ob in den letzten 14 Tagen Corona-positiv getestete Personen in Ihrer Nähe waren."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_3">"Lassen Sie sich Ihre Testergebnisse (PCR-Test oder Antigen-Schnelltest) in der App anzeigen und warnen Sie andere, wenn Sie ein positives Testergebnis erhalten."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_4">"Auf Wunsch können Sie mit der App Ihren persönlichen Infektionsstatus nachweisen (z.B. negativer Schnelltest). Bitte beachten Sie, dass Sie grundsätzlich nicht zum Nachweis Ihres Infektionsstatus per App verpflichtet sind. Sie können Ihren Infektionsstatus im Rahmen der rechtlichen Bestimmungen an Ihrem Aufenthaltsort auch auf andere Weise nachweisen."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_5">"Die App merkt sich Begegnungen zwischen Menschen, indem ihre Smartphones verschlüsselte Zufalls-IDs austauschen. Persönliche Daten werden dabei nicht ausgetauscht."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"Eine vielfältige Gruppe in einer Stadt benutzt Smartphones."</string> <!-- XACT: Onboarding (privacy) page title --> @@ -784,12 +788,17 @@ <string name="information_contact_subtitle_phone">"Technische Hotline:"</string> <!-- XLNK: Button / hyperlink to phone call for technical contact and hotline information page --> <string name="information_contact_button_phone">"0800 7540001"</string> + <string name="information_contact_button_international_phone">"+49 30 498 75401"</string> + <!-- XLNK: Description for national technical contact and hotline information page --> + <string name="information_contact_button_phone_description">"Für Anrufe innerhalb Deutschlands. Der Anruf ist kostenfrei."</string> + <!-- XLNK: Description for international technical contact and hotline information page --> + <string name="information_contact_button_international_phone_description">"Für Anrufe aus dem Ausland. Es fallen die Gebühren des jeweiligen Telefonanbieters an."</string> <!-- XBUT: CAUTION - ONLY UPDATE THE NUMBER IF NEEDED, ONLY NUMBERS AND NO SPECIAL CHARACTERS EXCEPT "+" and "space" ALLOWED IN THIS FIELD; --> <string name="information_contact_phone_call_number">"0800 7540001"</string> <!-- XTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_phone">"Unser Kundenservice ist für Sie da."</string> + <string name="information_contact_body_phone">"Unser Kundenservice ist in den folgenden Sprachen für Sie da:"</string> <!-- YTXT: Body text for technical contact and hotline information page --> - <string name="information_contact_body_open">"Sprachen: Deutsch, Englisch, Türkisch \nErreichbarkeit:"<xliff:g id="line_break">"\n"</xliff:g>"Mo - Sa: 07:00 - 22:00 Uhr"<xliff:g id="line_break">"\n(außer an bundesweiten Feiertagen)"</xliff:g><xliff:g id="line_break">"\nDer Anruf ist kostenfrei."</xliff:g></string> + <string name="information_contact_body_open">"Deutsch, Englisch, Türkisch\n\nErreichbarkeit:\nMo – Sa: 07:00 – 22:00 Uhr\n(außer an bundesweiten Feiertagen)</string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="information_contact_body_other">"Für gesundheitliche Fragen wenden Sie sich bitte an Ihre Hausarztpraxis oder die Hotline des ärztlichen Bereitschaftsdienstes 116 117."</string> <!-- XACT: describes illustration --> @@ -1014,11 +1023,11 @@ <!-- YTXT: Body sub text 1 for Submission Consent call test result --> <string name="submission_consent_call_test_result_scan_your_test_only">"Scannen Sie nun per QR-Code Ihren eigenen Test und rufen Sie Ihr Testergebnis ab."</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> + <string name="submission_consent_call_test_result_scan_test_only_once">"Jeder Test kann nur einmal gescannt werden. Die App kann maximal einen Schnelltest und einen PCR-Test gleichzeitig 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> + <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. Bei einem Schnelltest funktioniert die Warnung nur in Deutschland, im Falle eines PCR-Tests funktioniert die Warnung in folgenden Ländern:"</string> <!-- YTXT: Body for consent help by warning others - info about events --> <string name="submission_consent_help_by_warning_others_body_event">"Es werden auch Ihre Mitmenschen gewarnt, die zeitgleich mit Ihnen an denselben Events oder Orten eingecheckt waren."</string> <!-- YTXT: Page bottom text for consent screen --> @@ -1284,7 +1293,7 @@ <!-- YTXT: Body text for step 2 of contact page--> <string name="submission_contact_step_2_body">"Geben Sie die TAN in der App ein, um Ihren Test zu registrieren."</string> <!-- YTXT: Body text for operating hours in contact page--> - <string name="submission_contact_operating_hours_body">"Sprachen:\nDeutsch, Englisch, Türkisch\n\nErreichbarkeit:\ntäglich 24 Stunden\n\nDer Anruf ist kostenfrei."</string> + <string name="submission_contact_operating_hours_body">"Unser Kundenservice ist in den folgenden Sprachen für Sie da: Deutsch, Englisch, Türkisch \n\nErreichbarkeit:\nMo-So: Täglich 24 Stunden"</string> <!-- YTXT: Body text for technical contact and hotline information page --> <string name="submission_contact_body_other">"Für gesundheitliche Fragen wenden Sie sich bitte an Ihre Hausarztpraxis oder die Hotline des ärztlichen Bereitschaftsdienstes 116 117."</string> @@ -1991,5 +2000,22 @@ <string name="trace_location_organiser_poster_share">"Teilen"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"Druckversion"</string> - + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">Check-ins für diese Orte teilen?</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">Teilen Sie Ihre Check-ins, um andere zu warnen, die in Ihrer Nähe waren. Ihre Identität bleibt geheim.</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">Alle auswählen</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">Weiter</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">Überspringen</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">Sind Sie sicher, dass Sie Ihre Check-Ins nicht teilen wollen?</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">Dadurch werden andere, die in Ihrer Nähe waren, nicht gewarnt.</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">Teilen</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">Nicht teilen</string> </resources> 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 15aa30d87239bd87eb73419ef462a17123181184..26cc64fa9ff54a2200edb956e0e9bc0becf8b91f 100644 --- a/Corona-Warn-App/src/main/res/values-en/strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/strings.xml @@ -964,7 +964,7 @@ <!-- XHED: Dialog title for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_title">"Do you want to cancel entering your symptoms?"</string> <!-- XMSG: Dialog body for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_body">"If you provide information about your symptoms, you can warn others more exactly."</string> + <string name="submission_error_dialog_confirm_cancellation_body">"If you provide information about your symptoms, you can warn others more accurately."</string> <!-- XBUT: Positive button for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_button_positive">"Yes"</string> <!-- XBUT: Negative button for keys submission process cancellation --> @@ -1973,4 +1973,22 @@ <string name="trace_location_organiser_poster_share">"Share"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"Print Version"</string> + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">"Share Your Location Check-Ins?"</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">"Warn others who checked in near you. Your personal data will not be shared."</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">"Select all"</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">"Submit"</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">"Skip"</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">"Are you sure you don’t want to share your check-ins?"</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">"Skipping will not warn any contacts who checked in near you."</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">"Share"</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">"Don’t Share"</string> </resources> \ No newline at end of file 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 c0c4d62a5e992784ad81c673262b3cad75116089..f5836ab498ff0a42066e643ae89b6b57a0347c1c 100644 --- a/Corona-Warn-App/src/main/res/values-pl/strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/strings.xml @@ -964,7 +964,7 @@ <!-- XHED: Dialog title for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_title">"Czy chcesz anulować wprowadzanie objawów?"</string> <!-- XMSG: Dialog body for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_body">"Przekazanie informacji o objawach pozwoli Ci na wysyÅ‚anie innym bardziej szczegółowych ostrzeżeÅ„."</string> + <string name="submission_error_dialog_confirm_cancellation_body">"Przekazanie informacji o objawach pozwoli Ci na wysyÅ‚anie innym dokÅ‚adniejszych ostrzeżeÅ„."</string> <!-- XBUT: Positive button for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_button_positive">"Tak"</string> <!-- XBUT: Negative button for keys submission process cancellation --> @@ -1973,4 +1973,22 @@ <string name="trace_location_organiser_poster_share">"UdostÄ™pnij"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"Wersja do druku"</string> + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">"UdostÄ™pnić Twoje zameldowania w lokalizacji?"</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">"Ostrzegaj inne osoby, które siÄ™ zameldowaÅ‚y w pobliżu. Twoje dane osobowe nie bÄ™dÄ… udostÄ™pniane."</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">"Wybierz wszystkie"</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">"PrzeÅ›lij"</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">"PomiÅ„"</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">"Czy na pewno nie chcesz udostÄ™pniać swoich zameldowaÅ„?"</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">"W przypadku pominiÄ™cia tej operacji żadne kontakty zameldowane w pobliżu nie otrzymajÄ… ostrzeżenia."</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">"UdostÄ™pnij"</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">"Nie udostÄ™pniaj"</string> </resources> \ No newline at end of file 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 50d09ff8ae9d5fb6ce85e1264004498cf2e98d83..26877ff0620859f82c66a73889f988fa8d9c8b2d 100644 --- a/Corona-Warn-App/src/main/res/values-ro/strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/strings.xml @@ -964,7 +964,7 @@ <!-- XHED: Dialog title for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_title">"DoriÈ›i să anulaÈ›i introducerea simptomelor dvs.?"</string> <!-- XMSG: Dialog body for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_body">"Dacă furnizaÈ›i informaÈ›ii despre simptomele dvs., îi puteÈ›i avertiza pe ceilalÈ›i într-un mod mai exact."</string> + <string name="submission_error_dialog_confirm_cancellation_body">"Dacă furnizaÈ›i informaÈ›ii despre simptomele dvs., îi puteÈ›i avertiza pe ceilalÈ›i într-un mod mai precis."</string> <!-- XBUT: Positive button for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_button_positive">"Da"</string> <!-- XBUT: Negative button for keys submission process cancellation --> @@ -1973,4 +1973,22 @@ <string name="trace_location_organiser_poster_share">"Partajare"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"Versiune de tipărire"</string> + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">"PartajaÈ›i check-inurile locaÈ›iei dvs.?"</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">"AvertizaÈ›i-i pe ceilalÈ›i care au făcut check-in în aproprierea dvs. Datele dvs. personale nu vor fi partajate."</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">"Selectare tot"</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">"Transmitere"</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">"Omitere"</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">"Sigur nu doriÈ›i să partajaÈ›i check-inurile dvs.?"</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">"Omiterea nu va avertiza contactele care au făcut check-in în apropierea dvs."</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">"Partajare"</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">"Nu partajez"</string> </resources> \ No newline at end of file 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 98fcec804ca472ed8f82a3c3cf4d08bdcaa46b65..47cf6a2bf11559775003354bd89b3e80499bf83f 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 @@ -19,13 +19,15 @@ <!-- YTXT: Body for consent sub section your consent subtext first point --> <string name="submission_consent_your_consent_subsection_first_point">"<b>Uygulama, test sonucunuzu çağırır.</b>Daha sonra fikrinizi deÄŸiÅŸtirirseniz, söz konusu testi Uygulamadan kaldırabilirsiniz."</string> <!-- YTXT: Body for consent sub section your consent subtext second point --> - <string name="submission_consent_your_consent_subsection_second_point">"<b>Korona testiniz pozitif çıkmışsa, Uygulama karşılaÅŸtığınız kullanıcıları uyarmak üzere test sonucunuzu paylaşır. Bu ise yukarıda adı geçen ülkelerdeki Korona uygulamalarının sizinle aynı olay veya konumda giriÅŸ denetimi yaptıran kullanıcılarını etkiler.\n\nAyrıca semptomlarınızın baÅŸlangıcı hakkında bilgi verirseniz, bu veri de paylaşılacaktır.</b>"</string> + <string name="submission_consent_your_consent_subsection_second_point">"<b>Korona testiniz pozitifse Uygulama test sonucunuzu karşılaÅŸtığınız kullanıcıları uyarmak için paylaşır. Bu kiÅŸiler yukarıda belirtilen ülkelerdeki Korona Uygulaması kullanıcılarla ve sizinle aynı zamanda aynı etkinlikte ya da yerde bulunan kullanıcılarla ilgilidir.</b>"</string> <!-- YTXT: Body for consent sub section your consent subtext third point --> - <string name="submission_consent_your_consent_subsection_third_point">"VerdiÄŸiniz rıza beyanını istediÄŸiniz zaman geri alabilirsiniz. Bunun ayarını, “Testi Göster†altında bulabilirsiniz. PaylaÅŸmadan önce, rıza beyanınıza iliÅŸkin yeniden bilgilendirileceksiniz."</string> + <string name="submission_consent_your_consent_subsection_third_point">"<b>Ayrıca semptomlarınızın baÅŸlangıcı hakkında bilgi verirseniz, bu veri de paylaşılacaktır.</b>"</string> + <!-- YTXT: Body for consent sub section your consent subtext fourth point --> + <string name="submission_consent_your_consent_subsection_fourth_point">"Onay beyanınızı istediÄŸiniz zaman geri çekebilirsiniz. İlgili ayar “Testleri göster†altında yer alır. Bilgiler paylaşılmadan önce tekrar onayınız alınır."</string> <!-- YTXT: Body for your consent screen agreement share test results --> - <string name="submission_your_consent_agreement_share_test_results">"<b>Korona testiniz pozitif çıkmışsa, Uygulama karşılaÅŸtığınız kullanıcıları uyarmak üzere test sonucunuzu paylaşır.</b>\n\n<b>Bu uyarı, yukarıda belirtilen ülkelerdeki resmi Korona uygulamalarının kullanıcılarına ulaÅŸabilir:</b>"</string> + <string name="submission_your_consent_agreement_share_test_results">"<b>Korona testiniz pozitif çıktıysa Uygulama test sonucunuzu, karşılaÅŸtığınız kullanıcıları yarmak için paylaşır.</b>\n\n<b>Uyarı sizinle aynı etkinlikteki ya da yerdeki kullanıcılara ve aÅŸağıdaki ülkelerdeki Korona Uygulaması kullanıcılarına gönderilir:</b>"</string> <!-- YTXT: Body for your consent screen agreement share symptoms --> - <string name="submission_your_consent_agreement_share_symptoms">"<b>Ayrıca semptomlarınızın baÅŸlangıcı hakkında bilgi verirseniz, bu veri de paylaşılacaktır.</b>\n\nYukarıdaki “DiÄŸer Kullanıcıları Uyarın†seçeneÄŸini devre dışı bırakarak, verdiÄŸiniz rıza beyanını geri alabilirsiniz."</string> + <string name="submission_your_consent_agreement_share_symptoms">"<b>Hızlı test durumunda sadece Korona Uyarı Uygulamasının kullanıcıları uyarılır. Semptomlarınızın baÅŸladığına iliÅŸkin ek bilgiler verirseniz bu veriler de paylaşılır.</b>\n\nYukarıdaki “DiÄŸer Kullanıcıları Uyarın†seçeneÄŸini devre dışı bırakarak, verdiÄŸiniz rıza beyanını geri alabilirsiniz."</string> <!-- YTXT: Body for your consent screen agreement share symptoms with additional hint for test result--> <string name="submission_your_consent_agreement_share_symptoms_2">"<b>Ayrıca semptomlarınızın baÅŸlangıcı hakkında bilgi verirseniz, bu veri de paylaşılacaktır.</b>\n\nYukarıdaki “DiÄŸer Kullanıcıları Uyarın†seçeneÄŸini devre dışı bırakarak, verdiÄŸiniz rıza beyanını geri alabilirsiniz. Bunun ardından test sonucunuz size görüntülenir.</string> <!-- YTXT: Body for keys submission no consent text first part--> 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 d080eb6d94cba83a4cab05fbffa1ce71adb7ee8b..523c06ce04769693a14319f31900212b0cb8b1c6 100644 --- a/Corona-Warn-App/src/main/res/values-tr/strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/strings.xml @@ -964,7 +964,7 @@ <!-- XHED: Dialog title for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_title">"Belirtilerinizi girmeyi iptal etmek istiyor musunuz?"</string> <!-- XMSG: Dialog body for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_body">"Belirtilerinizle ilgili olarak bilgi verirseniz diÄŸer kullanıcıları daha net bir biçimde uyarabilir."</string> + <string name="submission_error_dialog_confirm_cancellation_body">"Belirtilerinizle ilgili olarak bilgi verirseniz diÄŸer kullanıcıları daha doÄŸru bir ÅŸekilde uyarabilirsiniz."</string> <!-- XBUT: Positive button for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_button_positive">"Evet"</string> <!-- XBUT: Negative button for keys submission process cancellation --> @@ -1973,4 +1973,22 @@ <string name="trace_location_organiser_poster_share">"PaylaÅŸ"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"Yazdırma Sürümü"</string> + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">"Konum Check-In’leriniz Paylaşılsın Mı?"</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">"Sizin yakınınızda check in yapmış olan diÄŸer kullanıcıları uyarın. KiÅŸisel verileriniz paylaşılmayacaktır."</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">"Tümünü seç"</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">"Gönder"</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">"Atla"</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">"Check-in’lerinizi paylaÅŸmak istemediÄŸinizden emin misiniz?"</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">"Bu adımı atladığınızda sizin yakınınızda check in yapmış olan hiç kimseye uyarı gönderilmeyecektir."</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">"PaylaÅŸ"</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">"PaylaÅŸma"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values/antigen_strings.xml b/Corona-Warn-App/src/main/res/values/antigen_strings.xml new file mode 100644 index 0000000000000000000000000000000000000000..f31cf3f47e5e171b391924ccf15a33e4445f30c2 --- /dev/null +++ b/Corona-Warn-App/src/main/res/values/antigen_strings.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="UTF-8"?> +<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" tools:ignore="MissingTranslation"> + + <!-- #################################### + Homescreen cards - register test card + ###################################### --> + <!-- XHED: Register test homescreen card: title --> + <string name="ag_homescreen_card_test_register_title">"Test registrieren"</string> + <!-- XTXT: Register test homescreen card: body --> + <string name="ag_homescreen_card_test_register_body">"Nutzen Sie die App, um Ihre Testergebnisse abrufen und schneller warnen zu können."</string> + + <!-- #################################### + Homescreen cards - common buttons + ###################################### --> + <!-- XBUT: Homescreen card: show test button --> + <string name="ag_homescreen_card_show_button">"Test anzeigen"</string> + <!-- XBUT: Homescreen card: warn others button --> + <string name="ag_homescreen_card_warn_others_button">"Andere warnen"</string> + + <!-- #################################### + Homescreen cards - common status + ###################################### --> + <!-- XTXT: Homescreen card: status subtitle - no result --> + <string name="ag_homescreen_card_status_no_result">"Ergebnis liegt noch nicht vor"</string> + <!-- XTXT: Homescreen card: status subtitle - result available --> + <string name="ag_homescreen_card_status_result_available">"Testergebnis abrufen"</string> + <!-- XTXT: Homescreen card: status subtitle - error --> + <string name="ag_homescreen_card_status_error">"Fehlerhafter Test"</string> + <!-- XTXT: Homescreen card: status subtitle - invalid --> + <string name="ag_homescreen_card_status_invalid">"Nicht mehr gültig"</string> + <!-- XTXT: Homescreen card: status subtitle - findings --> + <string name="ag_homescreen_card_status_findings">"Befund"</string> + <!-- XTXT: Homescreen card: subtitle - corona official name --> + <string name="ag_homescreen_card_status_name_of_the_cause_of_this_app">"SARS-CoV-2"</string> + <!-- XTXT: Homescreen card: status - negative --> + <string name="ag_homescreen_card_status_negative">"Negativ"</string> + <!-- XTXT: Homescreen card: status - positiv --> + <string name="ag_homescreen_card_status_positiv">"Positiv"</string> + + <!-- #################################### + Homescreen cards - common body + ###################################### --> + <!-- XTXT: homescreen card: body - result available --> + <string name="ag_homescreen_card_body_result_available">"Warnen Sie Ihre Mitmenschen, wenn Sie positiv auf Corona getestet wurden."</string> + <!-- XTXT: homescreen card: body - error --> + <string name="ag_homescreen_card_body_error">"Ihr Test konnte nicht ausgewertet werden."</string> + <!-- XTXT: homescreen card: body - not valid test --> + <string name="ag_homescreen_card_body_not_valid_test">"Ihr Test liegt länger als 21 Tage zurück und ist daher nicht länger relevant. Bitte löschen Sie den Test. Danach können Sie einen neuen Test hinzufügen. "</string> + <!-- XTXT: homescreen card: body - negative --> + <string name="ag_homescreen_card_body_result_negative">"Das Virus SARS-CoV-2 wurde bei Ihnen nicht nachgewiesen."</string> + <!-- XTXT: homescreen card: body - positive --> + <string name="ag_homescreen_card_body_result_positive">"Das Virus SARS-CoV-2 wurde bei Ihnen nachgewiesen."</string> + + <!-- #################################### + Homescreen cards - rapid test + ###################################### --> + <!-- XHED: rapid test homescreen card: title --> + <string name="ag_homescreen_card_rapidtest_title">"Schnelltest"</string> + <!-- XBUT: rapid test homescreen card: dont show anymore button --> + <string name="ag_homescreen_card_rapidtest_dont_show_anymore_button">"Nicht mehr anzeigen"</string> + <!-- XTXT: rapid test homescreen card: status subtitle - outdated test --> + <string name="ag_homescreen_card_rapidtest_status_outdated_test">"Test nicht mehr aktuell"</string> + <!-- XTXT: rapid test homescreen card: result positive - check results with PCR test --> + <string name="ag_homescreen_card_rapidtest_body_result_positive_pcr_check">"Machen Sie einen PCR-Test um dieses Test-Ergebnis zu verifizieren."</string> + + <!-- Body --> + <!-- XTXT: rapid test homescreen card: body - no result --> + <string name="ag_homescreen_card_rapidtest_body_no_result">"Die Auswertung Ihres Schnelltests ist noch nicht abgeschlossen."</string> + <!-- XTXT: rapid test homescreen card: body - outdated test --> + <string name="ag_homescreen_card_rapidtest_body_outdated_test">"Ihr Schnelltest ist älter als 48 Stunden und wird hier nicht mehr angezeigt."</string> + <!-- XTXT: homescreen card: body - negative --> + <string name="ag_homescreen_card_rapid_body_result_date">"Durchgeführt am %1$s"</string> + + <!-- #################################### + Homescreen cards - PCR + ###################################### --> + <!-- XHED: PCR homescreen card: title --> + <string name="ag_homescreen_card_pcr_title">"PCR-Test"</string> + <!-- XBUT: PCR homescreen card: clear test button --> + <string name="ag_homescreen_card_pcr_clear_test_button">"TEST LÖSCHEN"</string> + + <!-- Body --> + <!-- XTXT: PCR homescreen card: body - no result --> + <string name="ag_homescreen_card_pcr_body_no_result">"Die Auswertung Ihres PCR-Tests ist noch nicht abgeschlossen."</string> + <!-- XTXT: homescreen card: body - negative --> + <string name="ag_homescreen_card_pcr_body_result_date">"Test registriert am %1$s"</string> + +</resources> \ No newline at end of file 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 39847db49f32b5c3264721eb114a01d541a5c63f..07623e1a72136ddc1b563ae55fde9aad8a4f9d2b 100644 --- a/Corona-Warn-App/src/main/res/values/legal_strings.xml +++ b/Corona-Warn-App/src/main/res/values/legal_strings.xml @@ -18,15 +18,17 @@ <!-- YTXT: Body for consent sub section your consent subtext --> <string name="submission_consent_your_consent_subsection_tapping_agree" translatable="false">"By tapping on “Acceptâ€, you consent to the following steps:"</string> <!-- YTXT: Body for consent sub section your consent subtext first point --> - <string name="submission_consent_your_consent_subsection_first_point" translatable="false">"<b>The app will retrieve your test result.</b> If you change your mind later, you can delete the test in the app."</string> + <string name="submission_consent_your_consent_subsection_first_point" translatable="false">"<b>"The app will retrieve your test result.</b> If you change your mind later, you can delete the test in the app."</string> <!-- YTXT: Body for consent sub section your consent subtext second point --> - <string name="submission_consent_your_consent_subsection_second_point" translatable="false">"<b>If you have tested positive for coronavirus, the app will share your test result in order to warn other users whom you have encountered. This applies to users of coronavirus apps from the above countries and users who were simultaneously checked in at the same event or place as you.\n\nIf you provide additional information about the onset of your symptoms, this will also be shared.</b>"</string> + <string name="submission_consent_your_consent_subsection_second_point" translatable="false">"<b>If you have tested positive for coronavirus, the app will share your test result in order to warn other users whom you have encountered. This applies to users of coronavirus apps from the above countries and users who were simultaneously checked in at the same event or place as you. In the case of rapid tests, only users of the Corona-Warn-App will be warned.</b>"</string> <!-- YTXT: Body for consent sub section your consent subtext third point --> - <string name="submission_consent_your_consent_subsection_third_point" translatable="false">"You can withdraw your consent at any time. The setting for this can be found under “Display Testâ€. Before sharing your test result, you will be reminded of your consent."</string> + <string name="submission_consent_your_consent_subsection_third_point" translatable="false">"<b>"If you provide additional information about the onset of your symptoms, this will also be shared."</b>"</string> + <!-- YTXT: Body for consent sub section your consent subtext fourth point --> + <string name="submission_consent_your_consent_subsection_fourth_point" translatable="false">"You can withdraw your consent at any time. The setting for this can be found under "Display Test". Before sharing your test result, you will be reminded of your consent."</string> <!-- YTXT: Body for your consent screen agreement share test results --> - <string name="submission_your_consent_agreement_share_test_results" translatable="false">"<b>If you have tested positive for coronavirus, the app will share your test result in order to warn other users whom you have encountered.</b>\n\n<b>The warning will reach users of the official coronavirus apps of the following participating countries:</b>"</string> + <string name="submission_your_consent_agreement_share_test_results" translatable="false">"<b>"If you have tested positive for coronavirus, the app will share your test result in order to warn other users whom you have encountered.</b>\n\n<b>This warning only affects users who were simultaneously checked in at the same event or place as you and users of coronavirus apps in the following countries:</b>"</string> <!-- YTXT: Body for your consent screen agreement share symptoms --> - <string name="submission_your_consent_agreement_share_symptoms" translatable="false">"<b>If you provide additional information about the onset of your symptoms, this will also be shared.</b>\n\nYou can withdraw your consent by disabling “Warn Others†above."</string> + <string name="submission_your_consent_agreement_share_symptoms" translatable="false">"<b>In the case of rapid tests, only users of the Corona-Warn-App will be warned. If you provide additional information about the onset of symptoms, this will be also shared.</b>\n\nYou can withdraw your consent by disabling “Warn Others†above."</string> <!-- YTXT: Body for your consent screen agreement share symptoms with additional hint for test result--> <string name="submission_your_consent_agreement_share_symptoms_2" translatable="false"><b>If you provide additional information about the onset of your symptoms, this will also be shared.</b>\n\nYou can withdraw your consent by disabling “Warn others†above. Your test result will then be shown to you.</string> <!-- YTXT: Body for keys submission no consent text first part--> diff --git a/Corona-Warn-App/src/main/res/values/release_info_strings.xml b/Corona-Warn-App/src/main/res/values/release_info_strings.xml index f480f402b927499db999d597b89dd70293fdc893..931baa1a0f23f04eea380f04b1788d0b9aab6c18 100644 --- a/Corona-Warn-App/src/main/res/values/release_info_strings.xml +++ b/Corona-Warn-App/src/main/res/values/release_info_strings.xml @@ -17,22 +17,34 @@ <!-- XHED: Titles for the release info screen bullet points --> <string-array name="new_release_title"> - <item>"QR code check-in to effectively track chains of infection"</item> + <item>Registrierung von Antigen-Schnelltests</item> + <item>Anzeige von Nachweisen</item> + <item>Technische Hotline nun auch aus dem Ausland erreichbar</item> + <item>TAN-Hotline nun auch aus dem Ausland erreichbar</item> </string-array> <!-- XTXT: Text bodies for the release info screen bullet points --> <string-array name="new_release_body"> - <item>"The CWA now enables you to check in to events and places. Event organizers and store owners can use the app to create a QR code. Guests can then scan this QR code when they arrive to register their presence. The app can also create a journal entry upon request. If someone who checked in to this event or place is diagnosed with coronavirus later, other people who were checked in at the same time can be warned automatically."</item> + <item>Sie können sich nun zusätzlich zu Ergebnissen von PCR-Tests auch Ergebnisse von Antigen-Schnelltests in der App anzeigen lassen und nutzen, um andere zu warnen.</item> + <item>Auf Wunsch können Sie über die App Ihren persönlichen Infektionsstatus nachweisen (z.B. negativer Schnelltest). Bitte beachten Sie, dass Sie grundsätzlich nicht zum Nachweis Ihres Infektionsstatus per App verpflichtet sind. Sie können Ihren Infektionsstatus im Rahmen der rechtlichen Bestimmungen an Ihrem Aufenthaltsort auch auf andere Weise nachweisen.</item> + <item>Sie können die technische Hotline nun auch aus dem Ausland unter der Nummer +49 30 498 75401 erreichen. Es fallen Gebühren des jeweiligen Telefonanbieters an.</item> + <item>Sie können bei einem positiven Befund nun auch aus dem Ausland eine TAN anfordern unter der Nummer +49 30 498 75402. Es fallen Gebühren des jeweiligen Telefonanbieters an.</item> </string-array> <!-- XTXT: Text labels that will be converted to Links --> <string-array name="new_release_linkified_labels"> - <item/> + <item></item> + <item></item> + <item></item> + <item></item> </string-array> <!-- XTXT: URL destinations for the lables in new_release_linkified_labels --> <string-array name="new_release_target_urls"> - <item/> + <item></item> + <item></item> + <item></item> + <item></item> </string-array> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 530da0471d5301ce113b1209ffc9c87584c6f478..2dcf9f6df2e4985bf0e7b4a3c6472a5423051205 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -413,6 +413,16 @@ <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 coronavirus 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 smartphones, whereby no personal data whatsoever is accessed."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_1">"More protection for you and for us all. By using the Corona-Warn-App we can break chains of infection much quicker."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_2">"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 coronavirus in the last 14 days."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_3">"Lassen Sie sich Ihre Testergebnisse (PCR-Test oder Antigen-Schnelltest) in der App anzeigen und warnen Sie andere, wenn Sie ein positives Testergebnis erhalten."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_4">"Auf Wunsch können Sie mit der App Ihren persönlichen Infektionsstatus nachweisen (z.B. negativer Schnelltest). Bitte beachten Sie, dass Sie grundsätzlich nicht zum Nachweis Ihres Infektionsstatus per App verpflichtet sind. Sie können Ihren Infektionsstatus im Rahmen der rechtlichen Bestimmungen an Ihrem Aufenthaltsort auch auf andere Weise nachweisen."</string> + <!-- YTXT: onboarding(together) - paragraph --> + <string name="onboarding_body_5">"The app logs encounters between individuals by exchanging encrypted, random IDs between their smartphones, whereby no personal data whatsoever is accessed."</string> <!-- XACT: onboarding(together) - illustraction description, header image --> <string name="onboarding_illustration_description">"A group of persons use their smartphones around town."</string> <!-- XACT: Onboarding (privacy) page title --> @@ -792,6 +802,14 @@ <string name="information_contact_subtitle_phone">"Technical hotline:"</string> <!-- XLNK: Button / hyperlink to phone call for technical contact and hotline information page --> <string name="information_contact_button_phone">"0800 7540001"</string> + <!-- XLNK: Button / hyperlink to international phone call for technical contact and hotline information page --> + <string name="information_contact_button_international_phone">"+49 30 498 75401"</string> + <!-- XLNK: Button / hyperlink to international phone call for technical contact and hotline TAN info page --> + <string name="submission_contact_button_international_phone">"+49 30 498 75402"</string> + <!-- XLNK: Description for national technical contact and hotline information page --> + <string name="information_contact_button_phone_description">"Für Anrufe innerhalb Deutschlands. Der Anruf ist kostenfrei."</string> + <!-- XLNK: Description for international technical contact and hotline information page --> + <string name="information_contact_button_international_phone_description">"Für Anrufe aus dem Ausland. Es fallen die Gebühren des jeweiligen Telefonanbieters an."</string> <!-- XBUT: CAUTION - ONLY UPDATE THE NUMBER IF NEEDED, ONLY NUMBERS AND NO SPECIAL CHARACTERS EXCEPT "+" and "space" ALLOWED IN THIS FIELD; --> <string name="information_contact_phone_call_number">"0800 7540001"</string> <!-- XTXT: Body text for technical contact and hotline information page --> @@ -973,7 +991,7 @@ <!-- XHED: Dialog title for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_title">"Do you want to cancel entering your symptoms?"</string> <!-- XMSG: Dialog body for keys submission process cancellation --> - <string name="submission_error_dialog_confirm_cancellation_body">"If you provide information about your symptoms, you can warn others more exactly."</string> + <string name="submission_error_dialog_confirm_cancellation_body">"If you provide information about your symptoms, you can warn others more accurately."</string> <!-- XBUT: Positive button for keys submission process cancellation --> <string name="submission_error_dialog_confirm_cancellation_button_positive">"Yes"</string> <!-- XBUT: Negative button for keys submission process cancellation --> @@ -1022,11 +1040,11 @@ <!-- YTXT: Body sub text 1 for Submission Consent call test result --> <string name="submission_consent_call_test_result_scan_your_test_only">"Now scan the QR code for your test and retrieve your test result."</string> <!-- YTXT: Body sub text 2 for Submission Consent call test result --> - <string name="submission_consent_call_test_result_scan_test_only_once">"Your test can only be scanned once. The app cannot manage multiple tests at the same time."</string> + <string name="submission_consent_call_test_result_scan_test_only_once">"Jeder Test kann nur einmal gescannt werden. Die App kann maximal einen Schnelltest und einen PCR-Test gleichzeitig verwalten."</string> <!-- XHED: Page subheadline for consent help by warning others --> <string name="submission_consent_help_by_warning_others_headline">"Please help others you have encountered by warning them!"</string> <!-- YTXT: Body for consent help by warning others --> - <string name="submission_consent_help_by_warning_others_body">"If you have been diagnosed with coronavirus, you can warn others through the app. The warning feature works in several countries. The following countries are currently participating:"</string> + <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. Bei einem Schnelltest funktioniert die Warnung nur in Deutschland, im Falle eines PCR-Tests funktioniert die Warnung in folgenden Ländern:"</string> <!-- YTXT: Body for consent help by warning others - info about events --> <string name="submission_consent_help_by_warning_others_body_event">"People who were checked in to the same events or places at the same time as you were will also be warned."</string> <!-- YTXT: Page bottom text for consent screen --> @@ -1999,4 +2017,22 @@ <string name="trace_location_organiser_poster_share">"Share"</string> <!-- XHED: Trace location poster title --> <string name="trace_location_organiser_poster_title">"Print Version"</string> + <!-- XHED: Trace location check-ins consent screen title --> + <string name="trace_location_attendee_consent_title">"Share Your Location Check-Ins?"</string> + <!-- XTXT: Trace location check-ins consent screen header description --> + <string name="trace_location_attendee_consent_header_description">"Warn others who checked in near you. Your personal data will not be shared."</string> + <!-- XBUT: Trace location check-ins consent screen header button --> + <string name="trace_location_attendee_consent_header_button">"Select all"</string> + <!-- XBUT: Trace location check-ins consent screen continue button --> + <string name="trace_location_attendee_consent_continue">"Submit"</string> + <!-- XBUT: Trace location check-ins consent screen skip button --> + <string name="trace_location_attendee_consent_skip">"Skip"</string> + <!-- XHED: Trace location check-ins consent screen dialog title --> + <string name="trace_location_attendee_consent_dialog_title">"Are you sure you don’t want to share your check-ins?"</string> + <!-- XTXT: Trace location check-ins consent screen dialog message --> + <string name="trace_location_attendee_consent_dialog_message">"Skipping will not warn any contacts who checked in near you."</string> + <!-- XBUT: Trace location check-ins consent screen dialog positive button --> + <string name="trace_location_attendee_consent_dialog_positive_button">"Share"</string> + <!-- XBUT: Trace location check-ins consent screen dialog negative button --> + <string name="trace_location_attendee_consent_dialog_negative_button">"Don’t Share"</string> </resources> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index ecacc05a638093304b440e6405031fdf834af0ea..927a1ef0dba0d35470c54b1d435a62e8ae7f8b88 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -155,6 +155,10 @@ <item name="android:textColor">@color/colorAccent</item> </style> + <style name="materialTextButton" parent="Widget.MaterialComponents.Button.TextButton.Dialog.Flush"> + <item name="android:textColor">@color/colorAccent</item> + </style> + <style name="buttonIcon"> <item name="android:background">@drawable/circle_ripple</item> <item name="android:backgroundTint">@color/button_back</item> @@ -188,6 +192,11 @@ <item name="android:textColor">@color/colorTextPrimary1</item> </style> + <style name="phoneNumber"> + <item name="android:textAllCaps">false</item> + <item name="android:background">?selectableItemBackground</item> + </style> + <style name="rowSettings" parent="@style/row"> <item name="android:paddingStart">0dp</item> </style> @@ -289,9 +298,13 @@ <item name="android:textColor">@color/colorTextPrimary1</item> </style> - <style name="subtitleBoldSixteen" parent="@style/TextAppearance.MaterialComponents.Subtitle1"> + + <style name="materialSubtitleSixteen" parent="@style/TextAppearance.MaterialComponents.Subtitle1"> <item name="android:textColor">@color/colorTextPrimary1</item> <item name="android:textSize">16sp</item> + </style> + + <style name="subtitleBoldSixteen" parent="materialSubtitleSixteen"> <item name="android:textStyle">bold</item> </style> 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 index d8199e64261e221a682a98b6c2e65ffecadf4055..39f8b0fb3004295c5011fb9ef0dc11ab5345dd70 100644 --- 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 @@ -34,6 +34,7 @@ class ConfigChangeDetectorTest : BaseTest() { every { taskController.submit(any()) } just Runs every { appConfigProvider.currentConfig } returns currentConfigFake coEvery { riskLevelStorage.clear() } just Runs + coEvery { riskLevelStorage.clearResults() } just Runs } private fun mockConfigId(id: String): ConfigData { @@ -59,7 +60,7 @@ class ConfigChangeDetectorTest : BaseTest() { coVerify(exactly = 0) { taskController.submit(any()) - riskLevelStorage.clear() + riskLevelStorage.clearResults() } } @@ -70,20 +71,25 @@ class ConfigChangeDetectorTest : BaseTest() { createInstance().launch() coVerifySequence { - riskLevelStorage.clear() + riskLevelStorage.clearResults() taskController.submit(any()) + taskController.submit(any()) + } + + coVerify(exactly = 0) { + riskLevelStorage.clear() } } @Test - fun `same idetifier results in no op`() { + fun `same identifier results in no op`() { every { riskLevelSettings.lastUsedConfigIdentifier } returns "initial" createInstance().launch() coVerify(exactly = 0) { taskController.submit(any()) - riskLevelStorage.clear() + riskLevelStorage.clearResults() } } @@ -96,10 +102,16 @@ class ConfigChangeDetectorTest : BaseTest() { currentConfigFake.value = mockConfigId("berry") coVerifySequence { - riskLevelStorage.clear() + riskLevelStorage.clearResults() + taskController.submit(any()) + taskController.submit(any()) + riskLevelStorage.clearResults() taskController.submit(any()) - riskLevelStorage.clear() taskController.submit(any()) } + + coVerify(exactly = 0) { + riskLevelStorage.clear() + } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt index 90bcddb7723145acd4131e3d4e6ba14343778a09..e6c3aa4a1df8bf4db40f25f4eb488cf3ce9150df 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/CensorInjectionTest.kt @@ -5,6 +5,7 @@ import dagger.Module import dagger.Provides import de.rki.coronawarnapp.bugreporting.BugReportingSharedModule import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.submission.SubmissionSettings import io.github.classgraph.ClassGraph import io.kotest.matchers.collections.shouldContainAll @@ -68,4 +69,8 @@ class MockProvider { @Singleton @Provides fun submissionSettings(): SubmissionSettings = mockk() + + @Singleton + @Provides + fun coronaTestRepository(): CoronaTestRepository = mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt index 284f8398badbfc1427c82fbfdaebc1bd6b488bc7..b3fde2e4884541ee4687eaa2afd73854c3e0d2bb 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/RegistrationTokenCensorTest.kt @@ -1,26 +1,34 @@ package de.rki.coronawarnapp.bugreporting.censors import de.rki.coronawarnapp.bugreporting.debuglog.LogLine -import de.rki.coronawarnapp.submission.SubmissionSettings +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.util.CWADebug import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.mockk import io.mockk.mockkObject import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import testhelpers.preferences.mockFlowPreference class RegistrationTokenCensorTest : BaseTest() { - @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var coronaTestRepository: CoronaTestRepository private val testToken = "63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f" - private val regtokenPreference = mockFlowPreference<String?>(testToken) + private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( + setOf( + mockk<CoronaTest>().apply { + every { registrationToken } returns testToken + } + ) + ) @BeforeEach fun setup() { @@ -29,11 +37,11 @@ class RegistrationTokenCensorTest : BaseTest() { mockkObject(CWADebug) every { CWADebug.isDeviceForTestersBuild } returns false - every { submissionSettings.registrationToken } returns regtokenPreference + every { coronaTestRepository.coronaTests } returns coronaTests } private fun createInstance() = RegistrationTokenCensor( - submissionSettings = submissionSettings + coronaTestRepository = coronaTestRepository ) @Test @@ -55,12 +63,13 @@ class RegistrationTokenCensorTest : BaseTest() { message = "I'm a shy registration token: ########-e0de-4bd4-90c1-17c2bb683a2f" ) - verify { regtokenPreference.value } + verify { coronaTestRepository.coronaTests } } @Test fun `censoring returns null if there is no token`() = runBlockingTest { - every { submissionSettings.registrationToken } returns mockFlowPreference(null) + coronaTests.value = emptySet() + val instance = createInstance() val filterMeNot = LogLine( timestamp = 1, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2d9478b797bb905bd6e88fab37518e52bd034c44 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt @@ -0,0 +1,32 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class CoronaTestQrCodeValidatorTest : BaseTest() { + + @Test + fun `valid codes are extracted by corresponding extractor`() { + val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + instance.validate(pcrQrCode1).type shouldBe CoronaTest.Type.PCR + instance.validate(pcrQrCode2).type shouldBe CoronaTest.Type.PCR + instance.validate(pcrQrCode3).type shouldBe CoronaTest.Type.PCR + instance.validate(raQrCode1).type shouldBe CoronaTest.Type.RAPID_ANTIGEN + instance.validate(raQrCode2).type shouldBe CoronaTest.Type.RAPID_ANTIGEN + instance.validate(raQrCode3).type shouldBe CoronaTest.Type.RAPID_ANTIGEN + } + + @Test + fun `invalid code throws exception`() { + val invalidCode = "HTTPS://somethingelse/?123456-12345678-1234-4DA7-B166-B86D85475064" + val instance = CoronaTestQrCodeValidator(RapidAntigenQrCodeExtractor(), PcrQrCodeExtractor()) + return try { + instance.validate(invalidCode) + false + } catch (e: InvalidQRCodeException) { + true + } shouldBe true + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt similarity index 61% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt index 44bf60cc1340c0eaa3bfc20fd0484a0d7ce8ffd6..e4537ff5a6113f3d0c46a6691851bdf94a3a9a69 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/ScanResultTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt @@ -1,15 +1,10 @@ -package de.rki.coronawarnapp.service.submission +package de.rki.coronawarnapp.coronatest.qrcode import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.mockkObject -import org.junit.Before import org.junit.Test import testhelpers.BaseTest -class ScanResultTest : BaseTest() { +class PcrQrCodeExtractorTest : BaseTest() { private val guidUpperCase = "123456-12345678-1234-4DA7-B166-B86D85475064" private val guidLowerCase = "123456-12345678-1234-4da7-b166-b86d85475064" private val guidMixedCase = "123456-12345678-1234-4dA7-b166-B86d85475064" @@ -17,19 +12,18 @@ class ScanResultTest : BaseTest() { private val localhostLowerCase = "https://localhost/?" private val localhostMixedCase = "https://LOCALHOST/?" - @MockK - private lateinit var scanResult: QRScanResult - - @Before - fun setUp() { - MockKAnnotations.init(this) - mockkObject(scanResult) - every { scanResult.isValid } returns false - } - private fun buildQRCodeCases(prefixString: String, guid: String, conditionToMatch: Boolean) { - scanResult = QRScanResult("$prefixString$guid") - scanResult.isValid shouldBe conditionToMatch + val extractor = PcrQrCodeExtractor() + try { + if (extractor.canHandle("$prefixString$guid")) { + extractor.extract("$prefixString$guid") + conditionToMatch shouldBe true + } else { + conditionToMatch shouldBe false + } + } catch (e: InvalidQRCodeException) { + conditionToMatch shouldBe false + } } @Test @@ -83,16 +77,16 @@ class ScanResultTest : BaseTest() { @Test fun extractGUID() { - QRScanResult("$localhostUpperCase$guidUpperCase").guid shouldBe guidUpperCase - QRScanResult("$localhostUpperCase$guidLowerCase").guid shouldBe guidLowerCase - QRScanResult("$localhostUpperCase$guidMixedCase").guid shouldBe guidMixedCase + PcrQrCodeExtractor().extract("$localhostUpperCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase + PcrQrCodeExtractor().extract("$localhostUpperCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase + PcrQrCodeExtractor().extract("$localhostUpperCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase - QRScanResult("$localhostLowerCase$guidUpperCase").guid shouldBe guidUpperCase - QRScanResult("$localhostLowerCase$guidLowerCase").guid shouldBe guidLowerCase - QRScanResult("$localhostLowerCase$guidMixedCase").guid shouldBe guidMixedCase + PcrQrCodeExtractor().extract("$localhostLowerCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase + PcrQrCodeExtractor().extract("$localhostLowerCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase + PcrQrCodeExtractor().extract("$localhostLowerCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase - QRScanResult("$localhostMixedCase$guidUpperCase").guid shouldBe guidUpperCase - QRScanResult("$localhostMixedCase$guidLowerCase").guid shouldBe guidLowerCase - QRScanResult("$localhostMixedCase$guidMixedCase").guid shouldBe guidMixedCase + PcrQrCodeExtractor().extract("$localhostMixedCase$guidUpperCase").qrCodeGUID shouldBe guidUpperCase + PcrQrCodeExtractor().extract("$localhostMixedCase$guidLowerCase").qrCodeGUID shouldBe guidLowerCase + PcrQrCodeExtractor().extract("$localhostMixedCase$guidMixedCase").qrCodeGUID shouldBe guidMixedCase } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..ac2fd0474a277dda83ceaeab4203437c593877d6 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt @@ -0,0 +1,45 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import io.kotest.matchers.shouldBe +import org.joda.time.Instant +import org.joda.time.LocalDate +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RapidAntigenQrCodeExtractorTest : BaseTest() { + + private val instance = RapidAntigenQrCodeExtractor() + + @Test + fun `valid codes are recognized`() { + listOf(raQrCode1, raQrCode2, raQrCode3, raQrCode4, raQrCode5, raQrCode6, raQrCode7, raQrCode8).forEach { + instance.canHandle(it) shouldBe true + } + } + + @Test + fun `invalid codes are rejected`() { + listOf(pcrQrCode1, pcrQrCode2, pcrQrCode3).forEach { + instance.canHandle(it) shouldBe false + } + } + + @Test + fun `extracting valid codes does not throw exception`() { + listOf(raQrCode1, raQrCode2, raQrCode3, raQrCode4, raQrCode5, raQrCode6, raQrCode7, raQrCode8).forEach { + instance.extract(it) + } + } + + @Test + fun `personal data is extracted`() { + val data = instance.extract(raQrCode3) + data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN + data.hash shouldBe "7b1c063e883063f8c33ffaa256aded506afd907f7446143b3da0f938a21967a9" + data.createdAt shouldBe Instant.ofEpochMilli(1618563782000) + data.dateOfBirth shouldBe LocalDate.parse("1962-01-08") + data.lastName shouldBe "Hayes" + data.firstName shouldBe "Alma" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt new file mode 100644 index 0000000000000000000000000000000000000000..d8ebbe36ea3f19ae5f5931d8e1b31294c6338ce2 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/TestQrCodes.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.coronatest.qrcode + +internal val pcrQrCode1 = "HTTPS://LOCALHOST/?123456-12345678-1234-4DA7-B166-B86D85475064" +internal val pcrQrCode2 = "https://localhost/?123456-12345678-1234-4DA7-B166-B86D85475064" +internal val pcrQrCode3 = "https://LOCALHOST/?123456-12345678-1234-4DA7-B166-B86D85475064" + +internal val raQrCode1 = + "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjA5NjQsInNhbHQiOiIwQ0ZEMUJCQzI2Q0FCODVCNkZFNDE5MTJFQjFBQUU1QUNEN0QyNjA0RTQwNTQyRUVEQjZEQUYyQkRBMDQ5QzRGIiwidGVzdElkIjoiYjM2YzUzN2ItZWQ5NC00Njc3LTkzZmQtODUwMTY4NjlkYjEwIiwiaGFzaCI6IjJiNTc0NjhlN2Q4MTkyMWQzOGM4OGI1NjExOWE0Y2ViMzYyNmI1MDM4ZWI5Njk3ZjkxOTQ4NmJjMzg0Y2U2M2UifQ" +internal val raQrCode2 = + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjA5NjQsInNhbHQiOiIwQ0ZEMUJCQzI2Q0FCODVCNkZFNDE5MTJFQjFBQUU1QUNEN0QyNjA0RTQwNTQyRUVEQjZEQUYyQkRBMDQ5QzRGIiwidGVzdElkIjoiYjM2YzUzN2ItZWQ5NC00Njc3LTkzZmQtODUwMTY4NjlkYjEwIiwiaGFzaCI6IjJiNTc0NjhlN2Q4MTkyMWQzOGM4OGI1NjExOWE0Y2ViMzYyNmI1MDM4ZWI5Njk3ZjkxOTQ4NmJjMzg0Y2U2M2UifQ" +internal val raQrCode3 = + "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM3ODIsInNhbHQiOiI1QTI3M0REREJCQTFEMkFDQUEzN0ExMDg4NjhGNkIwMjM3NjQzRjhBNjdCQTNENkQ3RUE3RkREQ0M0RDJGMjBEIiwidGVzdElkIjoiMGQ5ZTg0MzItZWI5MS00YzhmLTgyYWYtNWEwMWZiMWI2NzYwIiwiaGFzaCI6IjdiMWMwNjNlODgzMDYzZjhjMzNmZmFhMjU2YWRlZDUwNmFmZDkwN2Y3NDQ2MTQzYjNkYTBmOTM4YTIxOTY3YTkiLCJmbiI6IkFsbWEiLCJsbiI6IkhheWVzIiwiZG9iIjoiMTk2Mi0wMS0wOCJ9" +internal val raQrCode4 = + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM3ODIsInNhbHQiOiI1QTI3M0REREJCQTFEMkFDQUEzN0ExMDg4NjhGNkIwMjM3NjQzRjhBNjdCQTNENkQ3RUE3RkREQ0M0RDJGMjBEIiwidGVzdElkIjoiMGQ5ZTg0MzItZWI5MS00YzhmLTgyYWYtNWEwMWZiMWI2NzYwIiwiaGFzaCI6IjdiMWMwNjNlODgzMDYzZjhjMzNmZmFhMjU2YWRlZDUwNmFmZDkwN2Y3NDQ2MTQzYjNkYTBmOTM4YTIxOTY3YTkiLCJmbiI6IkFsbWEiLCJsbiI6IkhheWVzIiwiZG9iIjoiMTk2Mi0wMS0wOCJ9" +internal val raQrCode5 = + "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM4NDMsInNhbHQiOiJBNkVFRkZERDE2Qzk5RDFCODEyREE2NTc1NzgwMDM1QTQ0REI0OUM5NTBGODdCQkY0NDJBRkIwMDE5NkZCNDQ4IiwidGVzdElkIjoiNDhjOTc2ODgtY2U2ZC00MDFjLWEwMmMtMjU5MTE2YTRmODhmIiwiaGFzaCI6IjIyMTA4Y2FmNTQwNWM0MzI5Y2I3ZTEzNzg3MTMxMDJhMGNkNjY4OWM3YWFmMWJjOGViYzk3MDdiMjNjNTZhN2UifQ==" +internal val raQrCode6 = + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM4NDMsInNhbHQiOiJBNkVFRkZERDE2Qzk5RDFCODEyREE2NTc1NzgwMDM1QTQ0REI0OUM5NTBGODdCQkY0NDJBRkIwMDE5NkZCNDQ4IiwidGVzdElkIjoiNDhjOTc2ODgtY2U2ZC00MDFjLWEwMmMtMjU5MTE2YTRmODhmIiwiaGFzaCI6IjIyMTA4Y2FmNTQwNWM0MzI5Y2I3ZTEzNzg3MTMxMDJhMGNkNjY4OWM3YWFmMWJjOGViYzk3MDdiMjNjNTZhN2UifQ==" +internal val raQrCode7 = + "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM5MDIsInNhbHQiOiJGRkVERDZCMEM5MzZGRDVBQTg1M0NBNjgzOTY1QzYzMDI1NDRBMTIyNTY5MDAyQjg5OEI1NkM5OTY1NjRENTNGIiwidGVzdElkIjoiODYyNDcwYzItMzk3MS00M2Y0LWFjY2UtMjIzMzRlMjZiZTg3IiwiaGFzaCI6Ijc1ZmU5M2MyOWI1ZDI4NTU3NmJjZmM5NTZmYWIxMzllNTgxMWNhZWY1MTNiN2Y4MzhmNjY3NWJhYTU4MGM5YWUiLCJmbiI6IkJyeWFuIiwibG4iOiJNYXJzaCIsImRvYiI6IjE5OTAtMDQtMjgifQ==" +internal val raQrCode8 = + "https://s.coronawarn.app?v=1#eyJ0aW1lc3RhbXAiOjE2MTg1NjM5MDIsInNhbHQiOiJGRkVERDZCMEM5MzZGRDVBQTg1M0NBNjgzOTY1QzYzMDI1NDRBMTIyNTY5MDAyQjg5OEI1NkM5OTY1NjRENTNGIiwidGVzdElkIjoiODYyNDcwYzItMzk3MS00M2Y0LWFjY2UtMjIzMzRlMjZiZTg3IiwiaGFzaCI6Ijc1ZmU5M2MyOWI1ZDI4NTU3NmJjZmM5NTZmYWIxMzllNTgxMWNhZWY1MTNiN2Y4MzhmNjY3NWJhYTU4MGM5YWUiLCJmbiI6IkJyeWFuIiwibG4iOiJNYXJzaCIsImRvYiI6IjE5OTAtMDQtMjgifQ==" diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/server/VerificationApiV1Test.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt similarity index 98% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/server/VerificationApiV1Test.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt index 74f8ea11fe2cdd3f9d43aa91ee3368c7fc707065..dd428bd56dace884e575ca41863a832297089bec 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/server/VerificationApiV1Test.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt @@ -1,8 +1,7 @@ -package de.rki.coronawarnapp.verification.server +package de.rki.coronawarnapp.coronatest.server import android.content.Context import de.rki.coronawarnapp.http.HttpModule -import de.rki.coronawarnapp.verification.VerificationModule import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.every diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/VerificationModuleTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationModuleTest.kt similarity index 97% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/VerificationModuleTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationModuleTest.kt index 32eb9f446b80536621b2770298a932f9e8f932ff..1cc4d1f74f1e087776a206031615be33f35fd340 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/VerificationModuleTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationModuleTest.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.verification +package de.rki.coronawarnapp.coronatest.server import android.content.Context import io.kotest.assertions.throwables.shouldNotThrowAny diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/server/VerificationServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt similarity index 96% rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/server/VerificationServerTest.kt rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt index 20fa57386a359a277ae4f2ee6f4dd88e69828a9b..a7f311ba6a18f1b0107130993b5912513bc0ad1c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/verification/server/VerificationServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt @@ -1,9 +1,8 @@ -package de.rki.coronawarnapp.verification.server +package de.rki.coronawarnapp.coronatest.server import android.content.Context import de.rki.coronawarnapp.http.HttpModule import de.rki.coronawarnapp.util.headerSizeIgnoringContentLength -import de.rki.coronawarnapp.verification.VerificationModule import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -115,7 +114,7 @@ class VerificationServerTest : BaseIOTest() { VerificationApiV1.TestResultResponse(testResult = 2) } - server.retrieveTestResults("testRegistrationToken") shouldBe 2 + server.pollTestResult("testRegistrationToken") shouldBe CoronaTestResult.PCR_POSITIVE coVerify { verificationApi.getTestResult(any(), any(), any()) } } @@ -189,7 +188,7 @@ class VerificationServerTest : BaseIOTest() { api.retrieveRegistrationToken(teletanExample, VerificationKeyType.TELETAN) webServer.enqueue(MockResponse().setBody("{}")) - api.retrieveTestResults(registrationTokenExample) + api.pollTestResult(registrationTokenExample) webServer.enqueue(MockResponse().setBody("{}")) api.retrieveTan(registrationTokenExample) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..f1ea759196b4ee7f44df7006f8f1825cc45a730f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTestExtensionsTest.kt @@ -0,0 +1,212 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PCRCoronaTestExtensionsTest : BaseTest() { + + @Test + fun `state determination, unregistered test`() = runBlockingTest { + val test: PCRCoronaTest? = null + test.toSubmissionState() shouldBe SubmissionStatePCR.NoTest + } + +// @Test +// fun removeTestFromDeviceSucceeds() = runBlockingTest { +// val submissionRepository = createInstance(scope = this) +// +// val initialPollingForTestResultTimeStampSlot = slot<Long>() +// val isTestResultAvailableNotificationSent = slot<Boolean>() +// every { +// tracingSettings.initialPollingForTestResultTimeStamp = capture(initialPollingForTestResultTimeStampSlot) +// } answers {} +// every { +// tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) +// } answers {} +// +// every { submissionSettings.isAllowedToSubmitKeys = any() } just Runs +// every { submissionSettings.isSubmissionSuccessful = any() } just Runs +// +// submissionRepository.removeTestFromDevice() +// +// verify(exactly = 1) { +// testResultDataCollector.clear() +// registrationTokenPreference.update(any()) +// submissionSettings.devicePairingSuccessfulAt = null +// submissionSettings.initialTestResultReceivedAt = null +// submissionSettings.isAllowedToSubmitKeys = false +// submissionSettings.isSubmissionSuccessful = false +// } +// +// initialPollingForTestResultTimeStampSlot.captured shouldBe 0L +// isTestResultAvailableNotificationSent.captured shouldBe false +// } +// +// @Test +// fun registrationWithGUIDSucceeds() = runBlockingTest { +// coEvery { submissionService.asyncRegisterDeviceViaGUID(guid) } returns registrationData +// coEvery { analyticsKeySubmissionCollector.reportTestRegistered() } just Runs +// every { analyticsKeySubmissionCollector.reset() } just Runs +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.asyncRegisterDeviceViaGUID(guid) +// +// registrationTokenPreference.value shouldBe registrationToken +// submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() +// +// verify(exactly = 1) { +// registrationTokenPreference.update(any()) +// submissionSettings.devicePairingSuccessfulAt = any() +// backgroundNoise.scheduleDummyPattern() +// } +// +// coVerify { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } +// } +// +// @Test +// fun registrationWithTeleTANSucceeds() = runBlockingTest { +// coEvery { submissionService.asyncRegisterDeviceViaTAN(tan) } returns registrationData +// coEvery { analyticsKeySubmissionCollector.reportTestRegistered() } just Runs +// every { analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() } just Runs +// every { analyticsKeySubmissionCollector.reset() } just Runs +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.asyncRegisterDeviceViaTAN(tan) +// +// registrationTokenPreference.value shouldBe registrationToken +// submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() +// +// verify(exactly = 1) { +// registrationTokenPreference.update(any()) +// submissionSettings.devicePairingSuccessfulAt = any() +// backgroundNoise.scheduleDummyPattern() +// } +// +// coVerify(exactly = 0) { +// testResultDataCollector.saveTestResultAnalyticsSettings(any()) +// } +// } +// +// @Test +// fun `reset clears tek history and settings`() = runBlockingTest { +// val instance = createInstance(this) +// instance.reset() +// +// instance.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestIdle +// +// coVerifyOrder { +// tekHistoryStorage.clear() +// submissionSettings.clear() +// } +// } +// +// @Test +// fun `ui state is SUBMITTED_FINAL when submission was done`() = runBlockingTest { +// every { submissionSettings.isSubmissionSuccessful } returns true +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.refreshTest() +// submissionRepository.deviceUIStateFlow.first() shouldBe +// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) +// } +// +// @Test +// fun `ui state is UNPAIRED when no token is present`() = runBlockingTest { +// every { submissionSettings.isSubmissionSuccessful } returns false +// every { submissionSettings.registrationToken } returns mockFlowPreference(null) +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.refreshTest() +// submissionRepository.deviceUIStateFlow.first() shouldBe +// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) +// } +// +// @Test +// fun `ui state is PAIRED_POSITIVE when allowed to submit`() = runBlockingTest { +// every { submissionSettings.isSubmissionSuccessful } returns false +// every { submissionSettings.registrationToken } returns mockFlowPreference("token") +// coEvery { submissionSettings.isAllowedToSubmitKeys } returns true +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.refreshTest() +// submissionRepository.deviceUIStateFlow.first() shouldBe +// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE) +// } +// +// @Test +// fun `refresh when state is PAIRED_NO_RESULT`() = runBlockingTest { +// every { submissionSettings.isSubmissionSuccessful } returns false +// every { submissionSettings.registrationToken } returns mockFlowPreference("token") +// coEvery { submissionSettings.isAllowedToSubmitKeys } returns false +// coEvery { submissionService.asyncRequestTestResult(any()) } returns CoronaTestResult.PCR_OR_RAT_PENDING +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.refreshTest() +// submissionRepository.deviceUIStateFlow.first() shouldBe +// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT) +// +// coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } +// } +// +// @Test +// fun `refresh when state is UNPAIRED`() = runBlockingTest { +// every { submissionSettings.isSubmissionSuccessful } returns false +// every { submissionSettings.registrationToken } returns mockFlowPreference(null) +// coEvery { submissionSettings.isAllowedToSubmitKeys } returns false +// coEvery { submissionService.asyncRequestTestResult(any()) } returns CoronaTestResult.PCR_OR_RAT_PENDING +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.refreshTest() +// submissionRepository.deviceUIStateFlow.first() shouldBe +// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) +// +// every { submissionSettings.registrationToken } returns mockFlowPreference("token") +// +// submissionRepository.refreshTest() +// +// coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } +// } +// +// @Test +// fun `no refresh when state is SUBMITTED_FINAL`() = runBlockingTest { +// every { submissionSettings.isSubmissionSuccessful } returns true +// +// val submissionRepository = createInstance(scope = this) +// +// submissionRepository.refreshTest() +// +// submissionRepository.deviceUIStateFlow.first() shouldBe +// NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) +// +// submissionRepository.refreshTest() +// +// coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } +// } +// @Test +// fun `updateTestResult updates test result donor data`() = runBlockingTest { +// val submissionRepository = createInstance(scope = this) +// submissionRepository.updateTestResult(CoronaTestResult.PCR_NEGATIVE) +// +// verify { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } +// } + +// @Test +// fun `doDeviceRegistration calls TestResultDataCollector`() { +// val viewModel = createViewModel() +// val mockResult = mockk<QRScanResult>().apply { +// every { guid } returns "guid" +// } +// +// coEvery { submissionRepository.asyncRegisterDeviceViaGUID(any()) } returns CoronaTestResult.PCR_POSITIVE +// viewModel.doDeviceRegistration(mockResult) +// } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..39ee2b0fd03be3ed556a2059de6e0cc706a42e6b --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.coronatest.type.pcr + +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class PCRProcessorTest : BaseTest() { + + @Test + fun todo() { + // TODO + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..48486b70cbcda1ec67315615d65c64bccc53fa88 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenCoronaTestExtensionsTest.kt @@ -0,0 +1,15 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runBlockingTest +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RapidAntigenCoronaTestExtensionsTest : BaseTest() { + + @Test + fun `state determination, unregistered test`() = runBlockingTest { + val test: RACoronaTest? = null + test.toSubmissionState() shouldBe SubmissionStateRAT.NoTest + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..2396194a96a5bc2fe009c291304880aad27afc56 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt @@ -0,0 +1,12 @@ +package de.rki.coronawarnapp.coronatest.type.rapidantigen + +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class RapidAntigenProcessorTest : BaseTest() { + + @Test + fun todo() { + // TODO + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollectorTest.kt index fe6d8af0e238e196f949f8a58c52fff4485db9fb..c3146306200fc9ccb318127830add98f2c4c1ec2 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDataCollectorTest.kt @@ -1,11 +1,15 @@ package de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_INVALID +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_NEGATIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_OR_RAT_PENDING +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_POSITIVE +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.PCR_REDEEMED import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.risk.EwRiskLevelResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.formatter.TestResult import io.mockk.Called import io.mockk.MockKAnnotations import io.mockk.Runs @@ -19,7 +23,6 @@ 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 import testhelpers.preferences.mockFlowPreference @@ -50,7 +53,7 @@ class TestResultDataCollectorTest : BaseTest() { fun `saveTestResultAnalyticsSettings does not save anything when no user consent`() = runBlockingTest { every { analyticsSettings.analyticsEnabled } returns mockFlowPreference(false) - testResultDataCollector.saveTestResultAnalyticsSettings(TestResult.POSITIVE) + testResultDataCollector.saveTestResultAnalyticsSettings(PCR_POSITIVE) verify(exactly = 0) { testResultDonorSettings.saveTestResultDonorDataAtRegistration(any(), any()) @@ -66,9 +69,13 @@ class TestResultDataCollectorTest : BaseTest() { every { calculatedAt } returns Instant.now() every { wasSuccessfullyCalculated } returns true } - every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns flowOf(listOf(mockRiskLevelResult)) + every { riskLevelStorage.latestAndLastSuccessfulEwRiskLevelResult } returns flowOf( + listOf( + mockRiskLevelResult + ) + ) every { testResultDonorSettings.saveTestResultDonorDataAtRegistration(any(), any()) } just Runs - testResultDataCollector.saveTestResultAnalyticsSettings(TestResult.POSITIVE) + testResultDataCollector.saveTestResultAnalyticsSettings(PCR_POSITIVE) verify(exactly = 1) { testResultDonorSettings.saveTestResultDonorDataAtRegistration(any(), any()) @@ -79,7 +86,7 @@ class TestResultDataCollectorTest : BaseTest() { fun `saveTestResultAnalyticsSettings does not save data when TestResult is INVALID`() = runBlockingTest { every { analyticsSettings.analyticsEnabled } returns mockFlowPreference(false) - testResultDataCollector.saveTestResultAnalyticsSettings(TestResult.INVALID) + testResultDataCollector.saveTestResultAnalyticsSettings(PCR_INVALID) verify { analyticsSettings.analyticsEnabled wasNot Called @@ -90,7 +97,7 @@ class TestResultDataCollectorTest : BaseTest() { fun `saveTestResultAnalyticsSettings does not save data when TestResult is REDEEMED`() = runBlockingTest { every { analyticsSettings.analyticsEnabled } returns mockFlowPreference(false) - testResultDataCollector.saveTestResultAnalyticsSettings(TestResult.REDEEMED) + testResultDataCollector.saveTestResultAnalyticsSettings(PCR_REDEEMED) verify { analyticsSettings.analyticsEnabled wasNot Called @@ -100,10 +107,10 @@ class TestResultDataCollectorTest : BaseTest() { @Test fun `updatePendingTestResultReceivedTime doesn't update when TestResult isn't POS or NEG`() = runBlockingTest { - for (testResult in listOf(TestResult.REDEEMED, TestResult.INVALID, TestResult.PENDING)) { + for (testResult in listOf(PCR_REDEEMED, PCR_INVALID, PCR_OR_RAT_PENDING)) { every { analyticsSettings.analyticsEnabled } returns mockFlowPreference(true) every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.PENDING) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(PCR_OR_RAT_PENDING) testResultDataCollector.updatePendingTestResultReceivedTime(testResult) verify { @@ -121,8 +128,8 @@ class TestResultDataCollectorTest : BaseTest() { runBlockingTest { every { analyticsSettings.analyticsEnabled } returns mockFlowPreference(true) every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(false) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.PENDING) - testResultDataCollector.updatePendingTestResultReceivedTime(TestResult.NEGATIVE) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(PCR_OR_RAT_PENDING) + testResultDataCollector.updatePendingTestResultReceivedTime(PCR_NEGATIVE) verify { analyticsSettings.analyticsEnabled @@ -136,10 +143,10 @@ class TestResultDataCollectorTest : BaseTest() { @Test fun `updatePendingTestResultReceivedTime update when TestResult is POS or NEG`() = runBlockingTest { - for (testResult in listOf(TestResult.NEGATIVE, TestResult.POSITIVE)) { + for (testResult in listOf(PCR_NEGATIVE, PCR_POSITIVE)) { every { analyticsSettings.analyticsEnabled } returns mockFlowPreference(true) every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.PENDING) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(PCR_OR_RAT_PENDING) every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(Instant.EPOCH) testResultDataCollector.updatePendingTestResultReceivedTime(testResult) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt index 1b41fed797c94414bdfdaf339fca5ed88297401d..7ac52f2863e56274ceaa76cace64b9726d85efd8 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/registeredtest/TestResultDonorTest.kt @@ -2,12 +2,14 @@ package de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest import de.rki.coronawarnapp.appconfig.AnalyticsConfig import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.DonorModule import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.server.protocols.internal.ppdd.PpaData -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.formatter.TestResult import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf import io.mockk.MockKAnnotations @@ -18,6 +20,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.unmockkAll import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Duration import org.joda.time.Instant @@ -30,12 +33,22 @@ import testhelpers.preferences.mockFlowPreference class TestResultDonorTest : BaseTest() { @MockK lateinit var testResultDonorSettings: TestResultDonorSettings @MockK lateinit var timeStamper: TimeStamper - @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var coronaTestRepository: CoronaTestRepository private lateinit var testResultDonor: TestResultDonor private val baseTime = Instant.ofEpochMilli(101010101) + private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( + setOf( + mockk<PCRCoronaTest>().apply { + every { registeredAt } returns baseTime + every { testResultReceivedAt } returns baseTime + every { type } returns CoronaTest.Type.PCR + } + ) + ) + @BeforeEach fun setUp() { MockKAnnotations.init(this, true) @@ -45,12 +58,12 @@ class TestResultDonorTest : BaseTest() { every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_LOW) } every { timeStamper.nowUTC } returns baseTime - every { submissionSettings.initialTestResultReceivedAt } returns baseTime + every { coronaTestRepository.coronaTests } returns coronaTests testResultDonor = TestResultDonor( testResultDonorSettings, timeStamper, - submissionSettings + coronaTestRepository = coronaTestRepository ) } @@ -68,21 +81,21 @@ class TestResultDonorTest : BaseTest() { @Test fun `No donation when timestamp at registration is missing`() = runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { submissionSettings.initialTestResultReceivedAt } returns null + coronaTests.value = emptySet() testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution } @Test fun `No donation when test result is INVALID`() = runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.INVALID) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_INVALID) testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution } @Test fun `No donation when test result is REDEEMED`() = runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.REDEEMED) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_REDEEMED) testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution } @@ -90,7 +103,7 @@ class TestResultDonorTest : BaseTest() { fun `No donation when test result is PENDING and hours isn't greater or equal to config hours`() { runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.PENDING) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_OR_RAT_PENDING) testResultDonor.beginDonation(TestRequest) shouldBe TestResultDonor.TestResultMetadataNoContribution } @@ -100,10 +113,16 @@ class TestResultDonorTest : BaseTest() { fun `Donation is collected when test result is PENDING and hours is greater or equal to config hours`() { runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.PENDING) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_OR_RAT_PENDING) val timeDayBefore = baseTime.minus(Duration.standardDays(1)) - every { submissionSettings.initialTestResultReceivedAt } returns timeDayBefore + coronaTests.value = setOf( + mockk<PCRCoronaTest>().apply { + every { registeredAt } returns timeDayBefore + every { testResultReceivedAt } returns baseTime + every { type } returns CoronaTest.Type.PCR + } + ) every { testResultDonorSettings.mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference( timeDayBefore ) @@ -124,7 +143,7 @@ class TestResultDonorTest : BaseTest() { fun `Donation is collected when test result is POSITIVE`() { runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_POSITIVE) every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(baseTime) val donation = testResultDonor.beginDonation(TestRequest) @@ -144,7 +163,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_POSITIVE) every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) every { riskLevelTurnedRedTime } returns mockFlowPreference(null) every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) @@ -157,7 +176,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_NEGATIVE) every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) every { riskLevelTurnedRedTime } returns mockFlowPreference(null) every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_HIGH) @@ -170,7 +189,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_POSITIVE) every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) every { riskLevelTurnedRedTime } returns mockFlowPreference(baseTime) every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) @@ -189,7 +208,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_NEGATIVE) every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) every { riskLevelTurnedRedTime } returns mockFlowPreference(baseTime) every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) @@ -207,7 +226,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_POSITIVE) every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) every { riskLevelTurnedRedTime } returns mockFlowPreference(null) every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) @@ -225,7 +244,7 @@ class TestResultDonorTest : BaseTest() { runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_NEGATIVE) every { finalTestResultReceivedAt } returns mockFlowPreference(baseTime) every { riskLevelTurnedRedTime } returns mockFlowPreference(null) every { mostRecentDateWithHighOrLowRiskLevel } returns mockFlowPreference(null) @@ -241,7 +260,7 @@ class TestResultDonorTest : BaseTest() { fun `Donation is collected when test result is NEGATIVE`() { runBlockingTest { every { testResultDonorSettings.testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { testResultDonorSettings.testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_NEGATIVE) every { testResultDonorSettings.finalTestResultReceivedAt } returns mockFlowPreference(baseTime) val donation = testResultDonor.beginDonation(TestRequest) @@ -260,7 +279,7 @@ class TestResultDonorTest : BaseTest() { fun `Scenario 1 LowRisk`() = runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.NEGATIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_NEGATIVE) every { finalTestResultReceivedAt } returns mockFlowPreference( Instant.parse("2021-03-20T20:00:00Z") ) @@ -270,7 +289,13 @@ class TestResultDonorTest : BaseTest() { every { riskLevelAtTestRegistration } returns mockFlowPreference(PpaData.PPARiskLevel.RISK_LEVEL_LOW) } every { timeStamper.nowUTC } returns Instant.parse("2021-03-20T00:00:00Z") - every { submissionSettings.initialTestResultReceivedAt } returns Instant.parse("2021-03-20T00:00:00Z") + coronaTests.value = setOf( + mockk<PCRCoronaTest>().apply { + every { testResultReceivedAt } returns baseTime + every { registeredAt } returns Instant.parse("2021-03-20T00:00:00Z") + every { type } returns CoronaTest.Type.PCR + } + ) val donation = testResultDonor.beginDonation(TestRequest) donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() @@ -287,7 +312,7 @@ class TestResultDonorTest : BaseTest() { fun `Scenario 2 HighRisk`() = runBlockingTest { with(testResultDonorSettings) { every { testScannedAfterConsent } returns mockFlowPreference(true) - every { testResultAtRegistration } returns mockFlowPreference(TestResult.POSITIVE) + every { testResultAtRegistration } returns mockFlowPreference(CoronaTestResult.PCR_POSITIVE) every { finalTestResultReceivedAt } returns mockFlowPreference( Instant.parse("2021-03-20T20:00:00Z") ) @@ -298,7 +323,13 @@ class TestResultDonorTest : BaseTest() { } every { timeStamper.nowUTC } returns Instant.parse("2021-03-20T00:00:00Z") - every { submissionSettings.initialTestResultReceivedAt } returns Instant.parse("2021-03-20T00:00:00Z") + coronaTests.value = setOf( + mockk<PCRCoronaTest>().apply { + every { testResultReceivedAt } returns baseTime + every { registeredAt } returns Instant.parse("2021-03-20T00:00:00Z") + every { type } returns CoronaTest.Type.PCR + } + ) val donation = testResultDonor.beginDonation(TestRequest) donation.shouldBeInstanceOf<TestResultDonor.TestResultMetadataContribution>() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt index 3201a04a7c24b83bb2c573c35c9982a59ac3c585..3ad5c90bc8cf3913cd7ca78cc6faa48373a810c5 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/diagnosiskeys/download/DownloadDiagnosisKeysTaskTest.kt @@ -3,12 +3,13 @@ package de.rki.coronawarnapp.diagnosiskeys.download import com.google.android.gms.nearby.exposurenotification.DiagnosisKeysDataMapping import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.environment.EnvironmentSetup import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.nearby.modules.detectiontracker.TrackedExposureDetection -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import io.mockk.MockKAnnotations import io.mockk.Runs @@ -18,8 +19,10 @@ 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.mockkObject import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Duration @@ -46,7 +49,13 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { @MockK lateinit var newKey1: CachedKey @MockK lateinit var latestTrackedDetection: TrackedExposureDetection - @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var coronaTestRepository: CoronaTestRepository + + private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( + setOf( + mockk<CoronaTest>().apply { every { isSubmissionAllowed } returns false } + ) + ) @BeforeEach fun setup() { @@ -54,7 +63,8 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { mockkObject(BuildConfigWrap) every { BuildConfigWrap.VERSION_CODE } returns 1080005 - every { submissionSettings.isAllowedToSubmitKeys } returns false + + every { coronaTestRepository.coronaTests } returns coronaTests availableKey1.apply { every { path } returns File("availableKey1") @@ -102,7 +112,7 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { keyPackageSyncTool = keyPackageSyncTool, timeStamper = timeStamper, settings = downloadSettings, - submissionSettings = submissionSettings + coronaTestRepository = coronaTestRepository, ) @Test @@ -230,7 +240,9 @@ class DownloadDiagnosisKeysTaskTest : BaseTest() { @Test fun `we do not submit keys if user got positive test results`() = runBlockingTest { - every { submissionSettings.isAllowedToSubmitKeys } returns true + coronaTests.value = setOf( + mockk<CoronaTest>().apply { every { isSubmissionAllowed } returns true } + ) createInstance().run(DownloadDiagnosisKeysTask.Arguments()) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt index f8701565cac240c69b8fc9c2ce987e6c8a1978d6..cdd16544d8379ca907355ca249c0bf964f5b3875 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt @@ -1,13 +1,13 @@ package de.rki.coronawarnapp.http.playbook +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.coronatest.server.VerificationServer import de.rki.coronawarnapp.exception.TanPairingException import de.rki.coronawarnapp.exception.http.BadRequestException import de.rki.coronawarnapp.playbook.DefaultPlaybook import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.submission.server.SubmissionServer -import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.verification.server.VerificationKeyType -import de.rki.coronawarnapp.verification.server.VerificationServer import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf @@ -32,7 +32,7 @@ class DefaultPlaybookTest : BaseTest() { MockKAnnotations.init(this) coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns "token" - coEvery { verificationServer.retrieveTestResults(any()) } returns 0 + coEvery { verificationServer.pollTestResult(any()) } returns CoronaTestResult.PCR_OR_RAT_PENDING coEvery { verificationServer.retrieveTanFake() } returns mockk() coEvery { verificationServer.retrieveTan(any()) } returns "tan" @@ -54,7 +54,7 @@ class DefaultPlaybookTest : BaseTest() { coVerifySequence { // ensure request order is 2x verification and 1x submission verificationServer.retrieveRegistrationToken(any(), any()) - verificationServer.retrieveTestResults(any()) + verificationServer.pollTestResult(any()) submissionServer.submitFakePayload() } } @@ -166,13 +166,11 @@ class DefaultPlaybookTest : BaseTest() { @Test fun `test result retrieval matches pattern`(): Unit = runBlocking { - coEvery { verificationServer.retrieveTestResults(any()) } returns 0 - createPlaybook().testResult("token") coVerifySequence { // ensure request order is 2x verification and 1x submission - verificationServer.retrieveTestResults(any()) + verificationServer.pollTestResult(any()) verificationServer.retrieveTanFake() submissionServer.submitFakePayload() } @@ -194,8 +192,8 @@ class DefaultPlaybookTest : BaseTest() { fun `failures during dummy requests should be ignored`(): Unit = runBlocking { val expectedToken = "token" coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns expectedToken - val expectedResult = TestResult.PENDING - coEvery { verificationServer.retrieveTestResults(expectedToken) } returns expectedResult.value + val expectedResult = CoronaTestResult.PCR_OR_RAT_PENDING + coEvery { verificationServer.pollTestResult(expectedToken) } returns expectedResult coEvery { submissionServer.submitFakePayload() } throws TestException() val (registrationToken, testResult) = createPlaybook() @@ -224,7 +222,7 @@ class DefaultPlaybookTest : BaseTest() { @Test fun `registration pattern matches despite test result failure`(): Unit = runBlocking { - coEvery { verificationServer.retrieveTestResults(any()) } throws TestException() + coEvery { verificationServer.pollTestResult(any()) } throws TestException() shouldThrow<TestException> { createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN) @@ -233,14 +231,14 @@ class DefaultPlaybookTest : BaseTest() { coVerifySequence { // ensure request order is 2x verification and 1x submission verificationServer.retrieveRegistrationToken(any(), any()) - verificationServer.retrieveTestResults(any()) + verificationServer.pollTestResult(any()) submissionServer.submitFakePayload() } } @Test fun `test result pattern matches despite failure`(): Unit = runBlocking { - coEvery { verificationServer.retrieveTestResults(any()) } throws TestException() + coEvery { verificationServer.pollTestResult(any()) } throws TestException() shouldThrow<TestException> { createPlaybook().testResult("token") @@ -248,7 +246,7 @@ class DefaultPlaybookTest : BaseTest() { coVerifySequence { // ensure request order is 2x verification and 1x submission - verificationServer.retrieveTestResults(any()) + verificationServer.pollTestResult(any()) verificationServer.retrieveTanFake() submissionServer.submitFakePayload() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt index 21548c0e148c7051f00e1e8987d3627fd0f19b5b..fa2a64983a5bdfba1a7fa9d4d8ed9197dcaa646c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/main/MainActivityViewModelTest.kt @@ -1,10 +1,12 @@ package de.rki.coronawarnapp.main import de.rki.coronawarnapp.contactdiary.ui.ContactDiarySettings +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.presencetracing.TraceLocationSettings import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository -import de.rki.coronawarnapp.playbook.BackgroundNoise import de.rki.coronawarnapp.storage.OnboardingSettings import de.rki.coronawarnapp.ui.main.MainActivityViewModel import de.rki.coronawarnapp.util.CWADebug @@ -33,6 +35,8 @@ class MainActivityViewModelTest : BaseTest() { @MockK lateinit var onboardingSettings: OnboardingSettings @MockK lateinit var traceLocationSettings: TraceLocationSettings @MockK lateinit var checkInRepository: CheckInRepository + @MockK lateinit var deadManScheduler: DeadmanNotificationScheduler + @MockK lateinit var coronaTestRepository: CoronaTestRepository @BeforeEach fun setup() { @@ -55,7 +59,9 @@ class MainActivityViewModelTest : BaseTest() { backgroundNoise = backgroundNoise, onboardingSettings = onboardingSettings, checkInRepository = checkInRepository, - traceLocationSettings = traceLocationSettings + traceLocationSettings = traceLocationSettings, + deadmanScheduler = deadManScheduler, + coronaTestRepository = coronaTestRepository, ) @Test 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 045f24aed0ab8ef9334aa47abe206b4b0a8ea703..a5a8653dd65751954d95be2c1057f26dd60f0a42 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,9 @@ package de.rki.coronawarnapp.main.home import android.content.Context import de.rki.coronawarnapp.appconfig.AppConfigProvider +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.environment.BuildConfigWrap import de.rki.coronawarnapp.main.CWASettings @@ -10,19 +13,14 @@ import de.rki.coronawarnapp.statistics.source.StatisticsProvider import de.rki.coronawarnapp.storage.TracingRepository import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionDone -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider import de.rki.coronawarnapp.tracing.GeneralTracingStatus import de.rki.coronawarnapp.tracing.GeneralTracingStatus.Status import de.rki.coronawarnapp.tracing.states.LowRisk import de.rki.coronawarnapp.tracing.states.TracingStateProvider import de.rki.coronawarnapp.tracing.ui.statusbar.TracingHeaderState -import de.rki.coronawarnapp.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.ui.main.home.HomeFragmentEvents import de.rki.coronawarnapp.ui.main.home.HomeFragmentViewModel -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.ui.presencetracing.organizer.TraceLocationOrganizerSettings import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool import de.rki.coronawarnapp.util.shortcuts.AppShortcutsHelper import io.kotest.matchers.shouldBe @@ -56,7 +54,7 @@ class HomeFragmentViewModelTest : BaseTest() { @MockK lateinit var errorResetTool: EncryptionErrorResetTool @MockK lateinit var tracingStateProvider: TracingStateProvider @MockK lateinit var tracingStateProviderFactory: TracingStateProvider.Factory - @MockK lateinit var submissionStateProvider: SubmissionStateProvider + @MockK lateinit var coronaTestRepository: CoronaTestRepository @MockK lateinit var tracingRepository: TracingRepository @MockK lateinit var shareTestResultNotificationService: ShareTestResultNotificationService @MockK lateinit var submissionRepository: SubmissionRepository @@ -77,9 +75,7 @@ class HomeFragmentViewModelTest : BaseTest() { every { tracingStateProviderFactory.create(isDetailsMode = false) } returns tracingStateProvider every { tracingStateProvider.state } returns flowOf(mockk<LowRisk>()) - every { submissionStateProvider.state } returns flowOf(mockk<SubmissionDone>()) - - every { submissionRepository.hasViewedTestResult } returns flowOf(true) + every { coronaTestRepository.coronaTests } returns emptyFlow() coEvery { appConfigProvider.currentConfig } returns emptyFlow() coEvery { statisticsProvider.current } returns emptyFlow() @@ -92,7 +88,7 @@ class HomeFragmentViewModelTest : BaseTest() { tracingRepository = tracingRepository, shareTestResultNotificationService = shareTestResultNotificationService, submissionRepository = submissionRepository, - submissionStateProvider = submissionStateProvider, + coronaTestRepository = coronaTestRepository, tracingStateProviderFactory = tracingStateProviderFactory, cwaSettings = cwaSettings, appConfigProvider = appConfigProvider, @@ -144,15 +140,21 @@ class HomeFragmentViewModelTest : BaseTest() { this.homeItems.observeForTesting { } coVerify { tracingStateProvider.state - submissionStateProvider.state + coronaTestRepository.coronaTests } } } @Test fun `positive test result notification is triggered on positive QR code result`() { - every { submissionRepository.deviceUIStateFlow } returns flowOf( - NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE) +// every { submissionRepository.deviceUIStateFlow } returns flowOf( +// NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE) +// ) + every { submissionRepository.pcrTest } returns flowOf( + mockk<PCRCoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_POSITIVE + every { lastError } returns null + } ) every { shareTestResultNotificationService.scheduleSharePositiveTestResultReminder() } returns Unit @@ -166,8 +168,14 @@ class HomeFragmentViewModelTest : BaseTest() { @Test fun `positive test result notification is triggered on positive TeleTan code result`() { - every { submissionRepository.deviceUIStateFlow } returns flowOf( - NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE_TELETAN) +// every { submissionRepository.deviceUIStateFlow } returns flowOf( +// NetworkRequestWrapper.RequestSuccessful(PAIRED_POSITIVE_TELETAN) +// ) + every { submissionRepository.pcrTest } returns flowOf( + mockk<PCRCoronaTest>().apply { + every { testResult } returns CoronaTestResult.PCR_POSITIVE + every { lastError } returns null + } ) every { shareTestResultNotificationService.scheduleSharePositiveTestResultReminder() } returns Unit diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt index b57db26b647333db5577520216f1e1b985999007..96a785177817e7ca7dfc02ada3aec020d859fb44 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/notification/TestResultAvailableNotificationServiceTest.kt @@ -6,9 +6,9 @@ import android.content.Context import androidx.navigation.NavDeepLinkBuilder import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult import de.rki.coronawarnapp.main.CWASettings import de.rki.coronawarnapp.util.device.ForegroundState -import de.rki.coronawarnapp.util.formatter.TestResult import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -63,13 +63,13 @@ class TestResultAvailableNotificationServiceTest : BaseTest() { @Test fun `check destination`() { - val negative = createInstance().getNotificationDestination(TestResult.NEGATIVE) + val negative = createInstance().getNotificationDestination(CoronaTestResult.PCR_NEGATIVE) negative shouldBe (R.id.submissionTestResultPendingFragment) - val invalid = createInstance().getNotificationDestination(TestResult.INVALID) + val invalid = createInstance().getNotificationDestination(CoronaTestResult.PCR_INVALID) invalid shouldBe (R.id.submissionTestResultPendingFragment) - val positive = createInstance().getNotificationDestination(TestResult.POSITIVE) + val positive = createInstance().getNotificationDestination(CoronaTestResult.PCR_POSITIVE) positive shouldBe (R.id.submissionTestResultPendingFragment) } @@ -77,7 +77,7 @@ class TestResultAvailableNotificationServiceTest : BaseTest() { fun `test notification in foreground`() = runBlockingTest { coEvery { foregroundState.isInForeground } returns flow { emit(true) } - createInstance().showTestResultAvailableNotification(TestResult.POSITIVE) + createInstance().showTestResultAvailableNotification(CoronaTestResult.PCR_POSITIVE) verify(exactly = 0) { navDeepLinkBuilderProvider.get() } } @@ -94,11 +94,11 @@ class TestResultAvailableNotificationServiceTest : BaseTest() { val instance = createInstance() - instance.showTestResultAvailableNotification(TestResult.POSITIVE) + instance.showTestResultAvailableNotification(CoronaTestResult.PCR_POSITIVE) verifyOrder { navDeepLinkBuilderProvider.get() - instance.getNotificationDestination(TestResult.POSITIVE) + instance.getNotificationDestination(CoronaTestResult.PCR_POSITIVE) context.getString(R.string.notification_headline_test_result_ready) context.getString(R.string.notification_body_test_result_ready) notificationHelper.sendNotification( @@ -114,7 +114,7 @@ class TestResultAvailableNotificationServiceTest : BaseTest() { every { cwaSettings.isNotificationsTestEnabled.value } returns false createInstance().apply { - showTestResultAvailableNotification(TestResult.POSITIVE) + showTestResultAvailableNotification(CoronaTestResult.PCR_POSITIVE) verify(exactly = 0) { notificationHelper.sendNotification( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/attendee/confirm/ConfirmCheckInViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/attendee/confirm/ConfirmCheckInViewModelTest.kt index 51a12fe4489dfa0671daf297a37a49e60ab2751b..494ff184a0dfa3d16c230bf6788e39b39854cbae 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/attendee/confirm/ConfirmCheckInViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/attendee/confirm/ConfirmCheckInViewModelTest.kt @@ -4,6 +4,7 @@ import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.presencetracing.checkins.qrcode.TraceLocation import de.rki.coronawarnapp.presencetracing.checkins.qrcode.VerifiedTraceLocation import de.rki.coronawarnapp.server.protocols.internal.pt.TraceLocationOuterClass +import de.rki.coronawarnapp.ui.presencetracing.attendee.TraceLocationAttendeeSettings import de.rki.coronawarnapp.ui.presencetracing.attendee.confirm.ConfirmCheckInNavigation import de.rki.coronawarnapp.ui.presencetracing.attendee.confirm.ConfirmCheckInViewModel import de.rki.coronawarnapp.util.TimeAndDateExtensions.secondsToInstant @@ -13,6 +14,9 @@ import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every import io.mockk.impl.annotations.MockK +import io.mockk.just +import io.mockk.runs +import kotlinx.coroutines.flow.flowOf import okio.ByteString.Companion.decodeBase64 import org.joda.time.Duration import org.joda.time.Instant @@ -29,6 +33,7 @@ class ConfirmCheckInViewModelTest : BaseTest() { @MockK lateinit var verifiedTraceLocation: VerifiedTraceLocation @MockK lateinit var checkInRepository: CheckInRepository @MockK lateinit var timeStamper: TimeStamper + @MockK lateinit var traceLocationAttendeeSettings: TraceLocationAttendeeSettings private val traceLocation = TraceLocation( id = 1, @@ -50,12 +55,15 @@ class ConfirmCheckInViewModelTest : BaseTest() { coEvery { checkInRepository.addCheckIn(any()) } returns 1L every { verifiedTraceLocation.traceLocation } returns traceLocation every { timeStamper.nowUTC } returns Instant.parse("2021-03-04T10:30:00Z") + every { traceLocationAttendeeSettings.createJournalEntryCheckedState } returns flowOf(true) + every { traceLocationAttendeeSettings.setCreateJournalEntryCheckedState(any()) } just runs } private fun createInstance() = ConfirmCheckInViewModel( verifiedTraceLocation = verifiedTraceLocation, checkInRepository = checkInRepository, - timeStamper = timeStamper + timeStamper = timeStamper, + traceLocationAttendeeSettings = traceLocationAttendeeSettings ) @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepositoryTest.kt index f5e3d4202e2587dad2ba7589ec22999e4529d99b..9763fe7788a5ea216e9901db0532c2483180eb48 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/checkins/CheckInRepositoryTest.kt @@ -115,6 +115,7 @@ class CheckInRepositoryTest : BaseTest() { completed = false, createJournalEntry = false, isSubmitted = true, + hasSubmissionConsent = false, ) ) } @@ -160,6 +161,7 @@ class CheckInRepositoryTest : BaseTest() { completed = false, createJournalEntry = false, isSubmitted = true, + hasSubmissionConsent = true, ) ) runBlockingTest { @@ -181,6 +183,7 @@ class CheckInRepositoryTest : BaseTest() { completed = false, createJournalEntry = false, isSubmitted = true, + hasSubmissionConsent = true, ) ) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt index dd07536e331c18350214ed1ed23f8f70ca1898bf..341d5b9bb05c03111c75cb9573ab2c72df2f3af7 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/calculation/PresenceTracingRiskMapperTest.kt @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParamete import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK import kotlinx.coroutines.flow.flowOf @@ -50,7 +51,7 @@ class PresenceTracingRiskMapperTest : BaseTest() { .setRiskLevel(RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.HIGH) .build() - val container = PresenceTracingRiskCalculationParamContainer( + private val container = PresenceTracingRiskCalculationParamContainer( transmissionRiskValueMapping = listOf(transmissionRiskValueMapping), normalizedTimePerCheckInToRiskLevelMapping = listOf(normalizedTimeMappingLow, normalizedTimeMappingHigh), normalizedTimePerDayToRiskLevelMapping = listOf(normalizedTimeMappingLow, normalizedTimeMappingHigh) @@ -100,5 +101,26 @@ class PresenceTracingRiskMapperTest : BaseTest() { } } + @Test + fun `config is requested only once`() { + runBlockingTest { + val mapper = createInstance() + mapper.lookupRiskStatePerDay(30.0) + mapper.lookupRiskStatePerDay(60.0) + coVerify(exactly = 1) { configProvider.currentConfig } + } + } + + @Test + fun `config is requested again after reset`() { + runBlockingTest { + val mapper = createInstance() + mapper.lookupRiskStatePerDay(30.0) + mapper.clearConfig() + mapper.lookupRiskStatePerDay(60.0) + coVerify(exactly = 2) { configProvider.currentConfig } + } + } + private fun createInstance() = PresenceTracingRiskMapper(configProvider) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt index f9722d08a76a6568cd5d3b4782661a5b98b17637..57b4cfb409c03ec328048fc8b45bf0824cc33c0e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/presencetracing/risk/execution/PresenceTracingWarningTaskTest.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.presencetracing.risk.execution import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.presencetracing.risk.calculation.CheckInWarningMatcher +import de.rki.coronawarnapp.presencetracing.risk.calculation.PresenceTracingRiskMapper import de.rki.coronawarnapp.presencetracing.risk.calculation.createCheckIn import de.rki.coronawarnapp.presencetracing.risk.calculation.createWarning import de.rki.coronawarnapp.presencetracing.risk.storage.PresenceTracingRiskRepository @@ -40,6 +41,7 @@ class PresenceTracingWarningTaskTest : BaseTest() { @MockK lateinit var presenceTracingRiskRepository: PresenceTracingRiskRepository @MockK lateinit var traceWarningRepository: TraceWarningRepository @MockK lateinit var checkInsRepository: CheckInRepository + @MockK lateinit var presenceTracingRiskMapper: PresenceTracingRiskMapper @BeforeEach fun setup() { @@ -71,6 +73,8 @@ class PresenceTracingWarningTaskTest : BaseTest() { coEvery { deleteStaleData() } just Runs coEvery { reportCalculation(any(), any()) } just Runs } + + coEvery { presenceTracingRiskMapper.clearConfig() } just Runs } private fun createInstance() = PresenceTracingWarningTask( @@ -80,6 +84,7 @@ class PresenceTracingWarningTaskTest : BaseTest() { presenceTracingRiskRepository = presenceTracingRiskRepository, traceWarningRepository = traceWarningRepository, checkInsRepository = checkInsRepository, + presenceTracingRiskMapper = presenceTracingRiskMapper ) @Test @@ -102,6 +107,27 @@ class PresenceTracingWarningTaskTest : BaseTest() { } } + @Test + fun `happy path with config change`() = runBlockingTest { + createInstance().run(mockk()) shouldNotBe null + + coVerifySequence { + presenceTracingRiskMapper.clearConfig() + syncTool.syncPackages() + presenceTracingRiskRepository.deleteStaleData() + checkInsRepository.checkInsWithinRetention + traceWarningRepository.unprocessedWarningPackages + + checkInWarningMatcher.process(any(), any()) + + presenceTracingRiskRepository.reportCalculation( + successful = true, + overlaps = any() + ) + traceWarningRepository.markPackagesProcessed(listOf(WARNING_PKG.packageId)) + } + } + @Test fun `overall task errors lead to a reported failed calculation`() = runBlockingTest { coEvery { syncTool.syncPackages() } throws IOException("Unexpected") diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt index 225b325d98625085679e70b2392d65944b53f471..a88a3a777beda43d72e3800e7bbf588471b4a511 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/risk/RiskLevelChangeDetectorTest.kt @@ -5,6 +5,8 @@ import android.content.Context import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.android.gms.nearby.exposurenotification.ExposureWindow +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.storage.TestResultDonorSettings import de.rki.coronawarnapp.datadonation.survey.Surveys import de.rki.coronawarnapp.notification.GeneralNotifications @@ -15,7 +17,6 @@ import de.rki.coronawarnapp.risk.RiskState.LOW_RISK import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.device.ForegroundState import de.rki.coronawarnapp.util.notifications.setContentTextExpandable @@ -30,6 +31,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Instant @@ -47,19 +49,25 @@ class RiskLevelChangeDetectorTest : BaseTest() { @MockK lateinit var riskLevelSettings: RiskLevelSettings @MockK lateinit var notificationHelper: GeneralNotifications @MockK lateinit var surveys: Surveys - @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var coronaTestRepository: CoronaTestRepository @MockK lateinit var tracingSettings: TracingSettings @MockK lateinit var testResultDonorSettings: TestResultDonorSettings @MockK lateinit var builder: NotificationCompat.Builder @MockK lateinit var notification: Notification + private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( + setOf( + mockk<CoronaTest>().apply { every { isSubmitted } returns false } + ) + ) + @BeforeEach fun setup() { MockKAnnotations.init(this) every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockFlowPreference(false) - every { submissionSettings.isSubmissionSuccessful } returns false + every { coronaTestRepository.coronaTests } returns coronaTests every { foregroundState.isInForeground } returns flowOf(false) every { notificationManagerCompat.areNotificationsEnabled() } returns true @@ -126,7 +134,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { riskLevelSettings = riskLevelSettings, notificationHelper = notificationHelper, surveys = surveys, - submissionSettings = submissionSettings, + coronaTestRepository = coronaTestRepository, tracingSettings = tracingSettings, testResultDonorSettings = testResultDonorSettings ) @@ -204,7 +212,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - submissionSettings.isSubmissionSuccessful + coronaTestRepository.coronaTests foregroundState.isInForeground notificationHelper.newBaseBuilder() notificationHelper.sendNotification(any(), any()) @@ -235,7 +243,7 @@ class RiskLevelChangeDetectorTest : BaseTest() { advanceUntilIdle() coVerifySequence { - submissionSettings.isSubmissionSuccessful + coronaTestRepository.coronaTests foregroundState.isInForeground notificationHelper.newBaseBuilder() notificationHelper.sendNotification(any(), any()) 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 index ffc2478a2edc4d10d4e4d1265e78b59c0de45deb..77e65bd863c06f9f0ae662767e22adf81cd63c3a 100644 --- 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 @@ -6,6 +6,8 @@ import android.net.Network import android.net.NetworkCapabilities import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.exposurewindows.AnalyticsExposureWindowCollector import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKey import de.rki.coronawarnapp.diagnosiskeys.storage.CachedKeyInfo @@ -14,7 +16,6 @@ import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut import de.rki.coronawarnapp.risk.result.EwAggregatedRiskResult import de.rki.coronawarnapp.risk.storage.RiskLevelStorage -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.task.Task import de.rki.coronawarnapp.task.TaskCancellationException import de.rki.coronawarnapp.util.TimeStamper @@ -29,6 +30,7 @@ import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest import org.joda.time.DateTime @@ -50,19 +52,28 @@ class RiskLevelTaskTest : BaseTest() { @MockK lateinit var appConfigProvider: AppConfigProvider @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var keyCacheRepository: KeyCacheRepository - @MockK lateinit var submissionSettings: SubmissionSettings + @MockK lateinit var coronaTestRepository: CoronaTestRepository @MockK lateinit var analyticsExposureWindowCollector: AnalyticsExposureWindowCollector @MockK lateinit var autoCheckOut: AutoCheckOut private val arguments: Task.Arguments = object : Task.Arguments {} + private val coronaTests: MutableStateFlow<Set<CoronaTest>> = MutableStateFlow( + setOf( + mockk<CoronaTest>().apply { + every { isSubmissionAllowed } returns false + every { isViewed } returns false + } + ) + ) + @BeforeEach fun setup() { MockKAnnotations.init(this) mockkObject(TimeVariables) - every { submissionSettings.isAllowedToSubmitKeys } returns false + every { coronaTestRepository.coronaTests } returns coronaTests every { configData.isDeviceTimeCorrect } returns true every { backgroundModeStatus.isAutoModeEnabled } returns flowOf(true) coEvery { appConfigProvider.getAppConfig() } returns configData @@ -99,7 +110,7 @@ class RiskLevelTaskTest : BaseTest() { appConfigProvider = appConfigProvider, riskLevelStorage = riskLevelStorage, keyCacheRepository = keyCacheRepository, - submissionSettings = submissionSettings, + coronaTestRepository = coronaTestRepository, analyticsExposureWindowCollector = analyticsExposureWindowCollector, autoCheckOut = autoCheckOut ) @@ -221,8 +232,13 @@ class RiskLevelTaskTest : BaseTest() { coEvery { keyCacheRepository.getAllCachedKeys() } returns listOf(cachedKey) every { backgroundModeStatus.isAutoModeEnabled } returns flowOf(false) every { timeStamper.nowUTC } returns now - every { submissionSettings.isAllowedToSubmitKeys } returns true - every { submissionSettings.hasViewedTestResult.value } returns true + + coronaTests.value = setOf( + mockk<CoronaTest>().apply { + every { isSubmissionAllowed } returns true + every { isViewed } returns true + } + ) createTask().run(arguments) shouldBe EwRiskLevelTaskResult( calculatedAt = now, @@ -248,8 +264,13 @@ class RiskLevelTaskTest : BaseTest() { every { riskLevels.aggregateResults(any(), any()) } returns aggregatedRiskResult every { timeStamper.nowUTC } returns now coEvery { analyticsExposureWindowCollector.reportRiskResultsPerWindow(any()) } just Runs - every { submissionSettings.isAllowedToSubmitKeys } returns true - every { submissionSettings.hasViewedTestResult.value } returns false + + coronaTests.value = setOf( + mockk<CoronaTest>().apply { + every { isSubmissionAllowed } returns true + every { isViewed } returns false + } + ) createTask().run(arguments) shouldBe EwRiskLevelTaskResult( calculatedAt = now, 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 e58c535688d11c39bbb8504d9c379c220060a544..3b74fd8ea4034bc728b7fd5ec7f1f77d479b2e82 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,10 +1,12 @@ package de.rki.coronawarnapp.service.submission +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.server.VerificationKeyType +import de.rki.coronawarnapp.coronatest.type.CoronaTestService +import de.rki.coronawarnapp.deniability.NoiseScheduler import de.rki.coronawarnapp.playbook.Playbook 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.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -25,8 +27,9 @@ class SubmissionServiceTest : BaseTest() { @MockK lateinit var mockPlaybook: Playbook @MockK lateinit var appComponent: ApplicationComponent + @MockK lateinit var noiseScheduler: NoiseScheduler - lateinit var submissionService: SubmissionService + lateinit var submissionService: CoronaTestService @BeforeEach fun setUp() { @@ -35,14 +38,17 @@ class SubmissionServiceTest : BaseTest() { every { AppInjector.component } returns appComponent every { appComponent.playbook } returns mockPlaybook - submissionService = SubmissionService(mockPlaybook) + submissionService = CoronaTestService( + playbook = mockPlaybook, + noiseScheduler = noiseScheduler + ) } @Test fun registrationWithGUIDSucceeds() { coEvery { mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID) - } returns (registrationToken to TestResult.PENDING) + } returns (registrationToken to CoronaTestResult.PCR_OR_RAT_PENDING) runBlocking { submissionService.asyncRegisterDeviceViaGUID(guid) @@ -57,7 +63,7 @@ class SubmissionServiceTest : BaseTest() { fun registrationWithTeleTANSucceeds() { coEvery { mockPlaybook.initialRegistration(any(), VerificationKeyType.TELETAN) - } returns (registrationToken to TestResult.PENDING) + } returns (registrationToken to CoronaTestResult.PCR_OR_RAT_PENDING) runBlocking { submissionService.asyncRegisterDeviceViaTAN(tan) @@ -70,10 +76,10 @@ class SubmissionServiceTest : BaseTest() { @Test fun requestTestResultSucceeds() { - coEvery { mockPlaybook.testResult(registrationToken) } returns TestResult.NEGATIVE + coEvery { mockPlaybook.testResult(registrationToken) } returns CoronaTestResult.PCR_NEGATIVE runBlocking { - submissionService.asyncRequestTestResult(registrationToken) shouldBe TestResult.NEGATIVE + submissionService.asyncRequestTestResult(registrationToken) shouldBe CoronaTestResult.PCR_NEGATIVE } coVerify(exactly = 1) { mockPlaybook.testResult(registrationToken) 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 index 38282aaf96e40847a257eaca7047f53b675770d1..ae77a7e3ca94f391438e9a5f0aedc71cbcc48fe8 100644 --- 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 @@ -1,361 +1,36 @@ package de.rki.coronawarnapp.storage -import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector -import de.rki.coronawarnapp.datadonation.analytics.modules.registeredtest.TestResultDataCollector -import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler -import de.rki.coronawarnapp.playbook.BackgroundNoise -import de.rki.coronawarnapp.service.submission.SubmissionService +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.SubmissionSettings -import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryStorage -import de.rki.coronawarnapp.task.TaskController -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper -import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.di.AppInjector -import de.rki.coronawarnapp.util.di.ApplicationComponent -import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory -import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool -import de.rki.coronawarnapp.util.formatter.TestResult -import de.rki.coronawarnapp.worker.BackgroundWorkScheduler -import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations -import io.mockk.Runs -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.just -import io.mockk.mockkObject -import io.mockk.slot -import io.mockk.verify import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first -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 -import testhelpers.preferences.mockFlowPreference class SubmissionRepositoryTest : BaseTest() { @MockK lateinit var submissionSettings: SubmissionSettings - @MockK lateinit var submissionService: SubmissionService - - @MockK lateinit var backgroundNoise: BackgroundNoise - @MockK lateinit var appComponent: ApplicationComponent - @MockK lateinit var taskController: TaskController @MockK lateinit var tekHistoryStorage: TEKHistoryStorage - @MockK lateinit var timeStamper: TimeStamper - - @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory - @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool - @MockK lateinit var deadmanNotificationScheduler: DeadmanNotificationScheduler - @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector - @MockK lateinit var tracingSettings: TracingSettings - @MockK lateinit var testResultDataCollector: TestResultDataCollector - @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler - - 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) - - private val registrationTokenPreference = mockFlowPreference<String?>(null) - private val resultReceivedTimeStamp = Instant.ofEpochMilli(101010101) + @MockK lateinit var coronaTestRepository: CoronaTestRepository @BeforeEach fun setUp() { MockKAnnotations.init(this) - - mockkObject(AppInjector) - every { AppInjector.component } returns appComponent - every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory - every { appComponent.errorResetTool } returns encryptionErrorResetTool - - every { backgroundNoise.scheduleDummyPattern() } just Runs - - every { submissionSettings.registrationToken } returns registrationTokenPreference - - every { submissionSettings.devicePairingSuccessfulAt = any() } just Runs - every { submissionSettings.initialTestResultReceivedAt } returns resultReceivedTimeStamp - every { submissionSettings.initialTestResultReceivedAt = any() } just Runs - - every { submissionSettings.hasGivenConsent } returns mockFlowPreference(false) - every { submissionSettings.hasViewedTestResult } returns mockFlowPreference(false) - every { submissionSettings.symptoms } returns mockFlowPreference(Symptoms.NO_INFO_GIVEN) - every { submissionSettings.clear() } just Runs - - every { submissionSettings.devicePairingSuccessfulAt } returns null - - every { taskController.tasks } returns emptyFlow() - - coEvery { tekHistoryStorage.clear() } just Runs - - every { timeStamper.nowUTC } returns Instant.EPOCH - every { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } just Runs - coEvery { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } just Runs - every { testResultDataCollector.clear() } just Runs - - backgroundWorkScheduler.apply { - every { startWorkScheduler() } just Runs - } } fun createInstance(scope: CoroutineScope) = SubmissionRepository( scope = scope, submissionSettings = submissionSettings, - submissionService = submissionService, - timeStamper = timeStamper, tekHistoryStorage = tekHistoryStorage, - deadmanNotificationScheduler = deadmanNotificationScheduler, - backgroundNoise = backgroundNoise, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, - tracingSettings = tracingSettings, - testResultDataCollector = testResultDataCollector, - backgroundWorkScheduler = backgroundWorkScheduler + coronaTestRepository = coronaTestRepository, ) @Test - fun removeTestFromDeviceSucceeds() = runBlockingTest { - val submissionRepository = createInstance(scope = this) - - val initialPollingForTestResultTimeStampSlot = slot<Long>() - val isTestResultAvailableNotificationSent = slot<Boolean>() - every { - tracingSettings.initialPollingForTestResultTimeStamp = capture(initialPollingForTestResultTimeStampSlot) - } answers {} - every { - tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) - } answers {} - - every { submissionSettings.isAllowedToSubmitKeys = any() } just Runs - every { submissionSettings.isSubmissionSuccessful = any() } just Runs - - submissionRepository.removeTestFromDevice() - - verify(exactly = 1) { - testResultDataCollector.clear() - registrationTokenPreference.update(any()) - submissionSettings.devicePairingSuccessfulAt = null - submissionSettings.initialTestResultReceivedAt = null - submissionSettings.isAllowedToSubmitKeys = false - submissionSettings.isSubmissionSuccessful = false - } - - initialPollingForTestResultTimeStampSlot.captured shouldBe 0L - isTestResultAvailableNotificationSent.captured shouldBe false - } - - @Test - fun registrationWithGUIDSucceeds() = runBlockingTest { - coEvery { submissionService.asyncRegisterDeviceViaGUID(guid) } returns registrationData - coEvery { analyticsKeySubmissionCollector.reportTestRegistered() } just Runs - every { analyticsKeySubmissionCollector.reset() } just Runs - - val submissionRepository = createInstance(scope = this) - - submissionRepository.asyncRegisterDeviceViaGUID(guid) - - registrationTokenPreference.value shouldBe registrationToken - submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() - - verify(exactly = 1) { - registrationTokenPreference.update(any()) - submissionSettings.devicePairingSuccessfulAt = any() - backgroundNoise.scheduleDummyPattern() - } - - coVerify { testResultDataCollector.saveTestResultAnalyticsSettings(any()) } - } - - @Test - fun registrationWithTeleTANSucceeds() = runBlockingTest { - coEvery { submissionService.asyncRegisterDeviceViaTAN(tan) } returns registrationData - coEvery { analyticsKeySubmissionCollector.reportTestRegistered() } just Runs - every { analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN() } just Runs - every { analyticsKeySubmissionCollector.reset() } just Runs - - val submissionRepository = createInstance(scope = this) - - submissionRepository.asyncRegisterDeviceViaTAN(tan) - - registrationTokenPreference.value shouldBe registrationToken - submissionRepository.testResultReceivedDateFlow.first() shouldBe resultReceivedTimeStamp.toDate() - - verify(exactly = 1) { - registrationTokenPreference.update(any()) - submissionSettings.devicePairingSuccessfulAt = any() - backgroundNoise.scheduleDummyPattern() - } - - coVerify(exactly = 0) { - testResultDataCollector.saveTestResultAnalyticsSettings(any()) - } - } - - @Test - fun `reset clears tek history and settings`() = runBlockingTest { - val instance = createInstance(this) - instance.reset() - - instance.deviceUIStateFlow.first() shouldBe NetworkRequestWrapper.RequestIdle - - coVerifyOrder { - tekHistoryStorage.clear() - submissionSettings.clear() - } - } - - @Test - fun `ui state is SUBMITTED_FINAL when submission was done`() = runBlockingTest { - every { submissionSettings.isSubmissionSuccessful } returns true - - val submissionRepository = createInstance(scope = this) - - submissionRepository.refreshDeviceUIState() - submissionRepository.deviceUIStateFlow.first() shouldBe - NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) - } - - @Test - fun `ui state is UNPAIRED when no token is present`() = runBlockingTest { - every { submissionSettings.isSubmissionSuccessful } returns false - every { submissionSettings.registrationToken } returns mockFlowPreference(null) - - val submissionRepository = createInstance(scope = this) - - submissionRepository.refreshDeviceUIState() - submissionRepository.deviceUIStateFlow.first() shouldBe - NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) - } - - @Test - fun `ui state is PAIRED_POSITIVE when allowed to submit`() = runBlockingTest { - every { submissionSettings.isSubmissionSuccessful } returns false - every { submissionSettings.registrationToken } returns mockFlowPreference("token") - coEvery { submissionSettings.isAllowedToSubmitKeys } returns true - - val submissionRepository = createInstance(scope = this) - - submissionRepository.refreshDeviceUIState() - submissionRepository.deviceUIStateFlow.first() shouldBe - NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE) - } - - @Test - fun `refresh when state is PAIRED_NO_RESULT`() = runBlockingTest { - every { submissionSettings.isSubmissionSuccessful } returns false - every { submissionSettings.registrationToken } returns mockFlowPreference("token") - coEvery { submissionSettings.isAllowedToSubmitKeys } returns false - coEvery { submissionService.asyncRequestTestResult(any()) } returns TestResult.PENDING - - val submissionRepository = createInstance(scope = this) - - submissionRepository.refreshDeviceUIState() - submissionRepository.deviceUIStateFlow.first() shouldBe - NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT) - - coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } - } - - @Test - fun `refresh when state is UNPAIRED`() = runBlockingTest { - every { submissionSettings.isSubmissionSuccessful } returns false - every { submissionSettings.registrationToken } returns mockFlowPreference(null) - coEvery { submissionSettings.isAllowedToSubmitKeys } returns false - coEvery { submissionService.asyncRequestTestResult(any()) } returns TestResult.PENDING - - val submissionRepository = createInstance(scope = this) - - submissionRepository.refreshDeviceUIState() - submissionRepository.deviceUIStateFlow.first() shouldBe - NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED) - - every { submissionSettings.registrationToken } returns mockFlowPreference("token") - - submissionRepository.refreshDeviceUIState() - - coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } - } - - @Test - fun `no refresh when state is SUBMITTED_FINAL`() = runBlockingTest { - every { submissionSettings.isSubmissionSuccessful } returns true - - val submissionRepository = createInstance(scope = this) - - submissionRepository.refreshDeviceUIState() - - submissionRepository.deviceUIStateFlow.first() shouldBe - NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL) - - submissionRepository.refreshDeviceUIState() - - coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - } - - @Test - fun `EXPOSUREAPP-4484 is fixed`() = runBlockingTest { - every { timeStamper.nowUTC } returns Instant.EPOCH - - var initialTimeStamp = Instant.EPOCH.plus(9999) - every { submissionSettings.initialTestResultReceivedAt } answers { initialTimeStamp } - every { submissionSettings.initialTestResultReceivedAt = any() } answers { initialTimeStamp = arg(0) } - - every { submissionSettings.registrationToken } returns mockFlowPreference("token") - every { submissionSettings.devicePairingSuccessfulAt } returns null - - val submissionRepository = createInstance(scope = this) - - submissionRepository.updateTestResult(TestResult.NEGATIVE) - - verify { - submissionSettings.initialTestResultReceivedAt = null - submissionSettings.initialTestResultReceivedAt = Instant.EPOCH - } - - initialTimeStamp shouldBe Instant.EPOCH - } - - @Test - fun `EXPOSUREAPP-4484 has specific conditions`() = runBlockingTest { - val submissionRepository = createInstance(scope = this) - - every { submissionSettings.initialTestResultReceivedAt } returns Instant.ofEpochMilli(1234) - every { submissionSettings.registrationToken } returns mockFlowPreference("token") - // This needs to be null to trigger the fix - every { submissionSettings.devicePairingSuccessfulAt } returns Instant.ofEpochMilli(5678) - - submissionRepository.updateTestResult(TestResult.NEGATIVE) - - every { submissionSettings.initialTestResultReceivedAt } returns Instant.ofEpochMilli(1234) - // This needs to be non null to trigger the fix - every { submissionSettings.registrationToken } returns mockFlowPreference(null) - every { submissionSettings.devicePairingSuccessfulAt } returns null - - submissionRepository.updateTestResult(TestResult.NEGATIVE) - - // This needs to be non null to trigger the fix - every { submissionSettings.initialTestResultReceivedAt } returns null - every { submissionSettings.registrationToken } returns mockFlowPreference("token") - every { submissionSettings.devicePairingSuccessfulAt } returns null - - submissionRepository.updateTestResult(TestResult.NEGATIVE) - - verify(exactly = 0) { submissionSettings.initialTestResultReceivedAt = null } - } - - @Test - fun `updateTestResult updates test result donor data`() = runBlockingTest { - val submissionRepository = createInstance(scope = this) - submissionRepository.updateTestResult(TestResult.NEGATIVE) - - verify { testResultDataCollector.updatePendingTestResultReceivedTime(any()) } + fun todo() { + // TODO } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/data/SubmissionSettingsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/data/SubmissionSettingsTest.kt index b1b1f7c5df7e50acbf4f1de90f1619f596a93a1a..51f0d5706abcbe23e682f94a644ad3a5369b6335 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/data/SubmissionSettingsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/data/SubmissionSettingsTest.kt @@ -39,15 +39,6 @@ class SubmissionSettingsTest { baseGson = baseGson ) - @Test - fun consentIsPersisted() { - createInstance().apply { - hasGivenConsent.value shouldBe false - hasGivenConsent.update { true } - hasGivenConsent.value shouldBe true - } - } - @Test fun `persist symptoms`() { createInstance().apply { diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index 1ebc52af489fd60fa81b8470ff6b12cfb78af9a2..9a10b45206fd8216eb1325d4da42275e83f3341f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt @@ -3,14 +3,15 @@ package de.rki.coronawarnapp.submission.task import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.appconfig.ConfigData +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector -import de.rki.coronawarnapp.presencetracing.checkins.CheckIn -import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository -import de.rki.coronawarnapp.presencetracing.checkins.CheckInsTransformer -import de.rki.coronawarnapp.exception.NoRegistrationTokenSetException import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook +import de.rki.coronawarnapp.presencetracing.checkins.CheckIn +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.presencetracing.checkins.CheckInsTransformer import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.Symptoms @@ -32,6 +33,7 @@ import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runBlockingTest @@ -64,17 +66,28 @@ class SubmissionTaskTest : BaseTest() { @MockK lateinit var checkInsTransformer: CheckInsTransformer @MockK lateinit var checkInRepository: CheckInRepository @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler + @MockK lateinit var coronaTestRepository: CoronaTestRepository private lateinit var settingSymptomsPreference: FlowPreference<Symptoms?> - private val registrationToken: FlowPreference<String?> = mockFlowPreference("regtoken") - private val settingHasGivenConsent: FlowPreference<Boolean> = mockFlowPreference(true) private val settingAutoSubmissionAttemptsCount: FlowPreference<Int> = mockFlowPreference(0) private val settingAutoSubmissionAttemptsLast: FlowPreference<Instant> = mockFlowPreference(Instant.EPOCH) private val settingLastUserActivityUTC: FlowPreference<Instant> = mockFlowPreference(Instant.EPOCH.plus(1)) - private val testCheckIn1 = CheckIn( + private val coronaTestsFlow = MutableStateFlow( + setOf( + mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + every { isSubmissionAllowed } returns true + every { isSubmitted } returns false + every { registrationToken } returns "regtoken" + every { identifier } returns "coronatest-identifier" + } + ) + ) + + private val validCheckIn = CheckIn( id = 1L, traceLocationId = mockk(), version = 1, @@ -88,17 +101,24 @@ class SubmissionTaskTest : BaseTest() { cnPublicKey = "cnPublicKey", checkInStart = Instant.EPOCH, checkInEnd = Instant.EPOCH.plus(9000), - completed = false, + completed = true, createJournalEntry = false, - isSubmitted = true + isSubmitted = false, + hasSubmissionConsent = true ) + private val invalidCheckIn1 = validCheckIn.copy(id = 2L, completed = false) + private val invalidCheckIn2 = validCheckIn.copy(id = 3L, isSubmitted = true) + private val invalidCheckIn3 = validCheckIn.copy(id = 4L, hasSubmissionConsent = false) + @BeforeEach fun setup() { MockKAnnotations.init(this) - every { submissionSettings.registrationToken } returns registrationToken - every { submissionSettings.isSubmissionSuccessful = any() } just Runs + coronaTestRepository.apply { + every { coronaTests } returns coronaTestsFlow + coEvery { markAsSubmitted("coronatest-identifier") } just Runs + } every { backgroundWorkScheduler.stopWorkScheduler() } just Runs every { backgroundWorkScheduler.startWorkScheduler() } just Runs @@ -113,7 +133,6 @@ class SubmissionTaskTest : BaseTest() { settingSymptomsPreference = mockFlowPreference(userSymptoms) every { submissionSettings.symptoms } returns settingSymptomsPreference - every { submissionSettings.hasGivenConsent } returns settingHasGivenConsent every { submissionSettings.lastSubmissionUserActivityUTC } returns settingLastUserActivityUTC every { submissionSettings.autoSubmissionAttemptsCount } returns settingAutoSubmissionAttemptsCount every { submissionSettings.autoSubmissionAttemptsLast } returns settingAutoSubmissionAttemptsLast @@ -133,7 +152,18 @@ class SubmissionTaskTest : BaseTest() { every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1)) - every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(testCheckIn1)) + checkInRepository.apply { + every { checkInsWithinRetention } returns flowOf( + listOf( + validCheckIn, + invalidCheckIn1, + invalidCheckIn2, + invalidCheckIn3 + ) + ) + coEvery { updatePostSubmissionFlags(any()) } just Runs + } + coEvery { checkInsTransformer.transform(any(), any()) } returns emptyList() } @@ -151,6 +181,7 @@ class SubmissionTaskTest : BaseTest() { checkInsRepository = checkInRepository, checkInsTransformer = checkInsTransformer, backgroundWorkScheduler = backgroundWorkScheduler, + coronaTestRepository = coronaTestRepository, ) @Test @@ -163,16 +194,14 @@ class SubmissionTaskTest : BaseTest() { coVerifySequence { submissionSettings.lastSubmissionUserActivityUTC settingLastUserActivityUTC.value - submissionSettings.hasGivenConsent - settingHasGivenConsent.value + coronaTestRepository.coronaTests submissionSettings.autoSubmissionAttemptsCount submissionSettings.autoSubmissionAttemptsLast submissionSettings.autoSubmissionAttemptsCount submissionSettings.autoSubmissionAttemptsLast - submissionSettings.registrationToken - registrationToken.value + coronaTestRepository.coronaTests tekHistoryStorage.tekData submissionSettings.symptoms settingSymptomsPreference.value @@ -199,17 +228,23 @@ class SubmissionTaskTest : BaseTest() { submissionSettings.symptoms settingSymptomsPreference.update(match { it.invoke(mockk()) == null }) - checkInRepository.markCheckInAsSubmitted(testCheckIn1.id) + checkInRepository.updatePostSubmissionFlags(validCheckIn.id) autoSubmission.updateMode(AutoSubmission.Mode.DISABLED) backgroundWorkScheduler.stopWorkScheduler() - submissionSettings.isSubmissionSuccessful = true + coronaTestRepository.markAsSubmitted(any()) backgroundWorkScheduler.startWorkScheduler() shareTestResultNotificationService.cancelSharePositiveTestResultNotification() testResultAvailableNotificationService.cancelTestResultAvailableNotification() } + + coVerify(exactly = 0) { + checkInRepository.updatePostSubmissionFlags(invalidCheckIn1.id) + checkInRepository.updatePostSubmissionFlags(invalidCheckIn2.id) + checkInRepository.updatePostSubmissionFlags(invalidCheckIn3.id) + } } @Test @@ -236,9 +271,8 @@ class SubmissionTaskTest : BaseTest() { } coVerifySequence { - settingHasGivenConsent.value - - registrationToken.value + coronaTestRepository.coronaTests // Consent + coronaTestRepository.coronaTests // regToken tekHistoryStorage.tekData settingSymptomsPreference.value @@ -267,10 +301,16 @@ class SubmissionTaskTest : BaseTest() { @Test fun `task throws if no registration token is available`() = runBlockingTest { - every { submissionSettings.registrationToken } returns mockFlowPreference(null) + coronaTestsFlow.value = setOf( + mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + every { isSubmissionAllowed } returns false + every { isSubmitted } returns false + } + ) val task = createTask() - shouldThrow<NoRegistrationTokenSetException> { + shouldThrow<IllegalStateException> { task.run(SubmissionTask.Arguments()) } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/testresult/pending/SubmissionTestResultPendingViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/testresult/pending/SubmissionTestResultPendingViewModelTest.kt index 7898f97ead95c935255b9b58703bfb809607c1fc..c8430ea2a07a0a8ee28b306d979c7157397ab775 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/testresult/pending/SubmissionTestResultPendingViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/testresult/pending/SubmissionTestResultPendingViewModelTest.kt @@ -1,18 +1,19 @@ package de.rki.coronawarnapp.submission.testresult.pending +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.exception.http.CwaWebException import de.rki.coronawarnapp.notification.ShareTestResultNotificationService import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.testresult.pending.SubmissionTestResultPendingViewModel -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestCoroutineScope import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -27,14 +28,15 @@ class SubmissionTestResultPendingViewModelTest : BaseTest() { @MockK lateinit var shareTestResultNotificationService: ShareTestResultNotificationService @MockK lateinit var submissionRepository: SubmissionRepository + private val testFlow = MutableStateFlow<CoronaTest?>(null) + @BeforeEach fun setup() { MockKAnnotations.init(this) submissionRepository.apply { - every { hasGivenConsentToSubmission } returns emptyFlow() - every { deviceUIStateFlow } returns emptyFlow() - every { testResultReceivedDateFlow } returns emptyFlow() + every { testForType(any()) } returns testFlow + every { setViewedTestResult(any()) } just Runs } } @@ -46,19 +48,17 @@ class SubmissionTestResultPendingViewModelTest : BaseTest() { @Test fun `web exception handling`() { - val expectedType = NetworkRequestWrapper.RequestFailed<DeviceUIState, CwaWebException>( - CwaWebException(statusCode = 1, message = "message") - ) - val unexpectedType = - NetworkRequestWrapper.RequestFailed<DeviceUIState, Throwable>(UnsupportedOperationException()) - val deviceUI = MutableStateFlow<NetworkRequestWrapper<DeviceUIState, Throwable>>(expectedType) - every { submissionRepository.deviceUIStateFlow } returns deviceUI + val expectedError = CwaWebException(statusCode = 1, message = "message") + val unexpectedError = UnsupportedOperationException() + + testFlow.value = mockk<CoronaTest>().apply { every { lastError } returns expectedError } + createInstance().apply { cwaWebExceptionLiveData.observeForever {} - cwaWebExceptionLiveData.value shouldBe expectedType.error + cwaWebExceptionLiveData.value shouldBe expectedError - deviceUI.value = unexpectedType - cwaWebExceptionLiveData.value shouldBe unexpectedType.error + testFlow.value = mockk<CoronaTest>().apply { every { lastError } returns unexpectedError } + cwaWebExceptionLiveData.value shouldBe unexpectedError } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt deleted file mode 100644 index dee497b2ccdd3f88fa5a838b3480616fd2940ce5..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/tracing/ui/homecards/SubmissionStateProviderTest.kt +++ /dev/null @@ -1,64 +0,0 @@ -package de.rki.coronawarnapp.tracing.ui.homecards - -import android.content.Context -import de.rki.coronawarnapp.submission.SubmissionRepository -import de.rki.coronawarnapp.submission.SubmissionSettings -import de.rki.coronawarnapp.submission.ui.homecards.NoTest -import de.rki.coronawarnapp.submission.ui.homecards.SubmissionStateProvider -import de.rki.coronawarnapp.util.DeviceUIState -import de.rki.coronawarnapp.util.NetworkRequestWrapper -import io.kotest.matchers.shouldBe -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.verifySequence -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.test.runBlockingTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import testhelpers.BaseTest -import testhelpers.extensions.CoroutinesTestExtension -import testhelpers.extensions.InstantExecutorExtension -import testhelpers.preferences.mockFlowPreference -import java.util.Date - -@ExtendWith(InstantExecutorExtension::class, CoroutinesTestExtension::class) -class SubmissionStateProviderTest : BaseTest() { - - @MockK lateinit var context: Context - @MockK lateinit var submissionRepository: SubmissionRepository - @MockK lateinit var submissionSettings: SubmissionSettings - - @BeforeEach - fun setup() { - MockKAnnotations.init(this) - - every { submissionRepository.hasViewedTestResult } returns flow { emit(true) } - every { submissionRepository.deviceUIStateFlow } returns flow { - emit(NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>(DeviceUIState.PAIRED_POSITIVE)) - } - every { submissionRepository.testResultReceivedDateFlow } returns flow { emit(Date()) } - every { submissionSettings.registrationToken } returns mockFlowPreference(null) - } - - private fun createInstance() = SubmissionStateProvider( - submissionRepository = submissionRepository, - submissionSettings = submissionSettings - ) - - @Test - fun `state determination, unregistered test`() = runBlockingTest { - createInstance().apply { - state.first() shouldBe NoTest - - verifySequence { - submissionRepository.deviceUIStateFlow - submissionRepository.hasViewedTestResult - submissionRepository.testResultReceivedDateFlow - submissionSettings.registrationToken - } - } - } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..0012a6ae7af09003d77c24227dd99432abb9d0ac --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt @@ -0,0 +1,345 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import androidx.lifecycle.SavedStateHandle +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.presencetracing.checkins.CheckIn +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.auto.AutoSubmission +import io.kotest.matchers.shouldBe +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.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import okio.ByteString.Companion.decodeBase64 +import okio.ByteString.Companion.encode +import org.joda.time.Instant +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.InstantExecutorExtension +import testhelpers.extensions.getOrAwaitValue + +@ExtendWith(InstantExecutorExtension::class) +class CheckInsConsentViewModelTest : BaseTest() { + + @MockK lateinit var savedState: SavedStateHandle + @MockK lateinit var checkInRepository: CheckInRepository + @MockK lateinit var submissionRepository: SubmissionRepository + @MockK lateinit var autoSubmission: AutoSubmission + + private val checkIn1 = CheckIn( + id = 1L, + traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".decodeBase64()!!, + version = 1, + type = 2, + description = "brothers birthday", + address = "Malibu", + traceLocationStart = Instant.EPOCH, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + cryptographicSeed = "cryptographicSeed".encode(), + cnPublicKey = "cnPublicKey", + checkInStart = Instant.EPOCH, + checkInEnd = Instant.EPOCH, + completed = true, + createJournalEntry = false + ) + + private val checkIn2 = CheckIn( + id = 2L, + traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".decodeBase64()!!, + version = 1, + type = 2, + description = "brothers birthday", + address = "Malibu", + traceLocationStart = Instant.EPOCH, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + cryptographicSeed = "cryptographicSeed".encode(), + cnPublicKey = "cnPublicKey", + checkInStart = Instant.EPOCH, + checkInEnd = Instant.EPOCH, + completed = true, + createJournalEntry = false + ) + + private val checkIn3 = CheckIn( + id = 3L, + traceLocationId = "41da2115-eba2-49bd-bf17-adb3d635ddaf".decodeBase64()!!, + version = 1, + type = 2, + description = "brothers birthday", + address = "Malibu", + traceLocationStart = Instant.EPOCH, + traceLocationEnd = null, + defaultCheckInLengthInMinutes = null, + cryptographicSeed = "cryptographicSeed".encode(), + cnPublicKey = "cnPublicKey", + checkInStart = Instant.EPOCH, + checkInEnd = Instant.EPOCH, + completed = false, + createJournalEntry = false + ) + + private val coronaTestFlow = MutableStateFlow( + mockk<CoronaTest>().apply { every { isViewed } returns false } + ) + + @BeforeEach + fun setUp() { + MockKAnnotations.init(this) + + every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(checkIn1, checkIn2, checkIn3)) + coEvery { checkInRepository.updateSubmissionConsents(any(), true) } just Runs + coEvery { checkInRepository.updateSubmissionConsents(any(), false) } just Runs + every { savedState.set(any(), any<Set<Long>>()) } just Runs + every { autoSubmission.updateMode(any()) } just Runs + every { submissionRepository.testForType(any()) } returns coronaTestFlow + every { savedState.get<Set<Long>>(any()) } returns emptySet() + } + + @Test + fun `Nothing is selected initially`() { + every { savedState.get<Set<Long>>(any()) } returns emptySet() + + val viewModel = createViewModel() + viewModel.checkIns.getOrAwaitValue().apply { + size shouldBe 3 + get(0).apply { + this is HeaderCheckInsVH.Item + } + + get(1).apply { + this as SelectableCheckInVH.Item + this.checkIn.hasSubmissionConsent shouldBe false + } + + get(2).apply { + this as SelectableCheckInVH.Item + this.checkIn.hasSubmissionConsent shouldBe false + } + } + } + + @Test + fun `Saved state is restored`() { + every { savedState.get<Set<Long>>(any()) } returns setOf(1L) + val viewModel = createViewModel() + viewModel.checkIns.getOrAwaitValue().apply { + size shouldBe 3 + get(0).apply { + this is HeaderCheckInsVH.Item + } + + get(1).apply { + this as SelectableCheckInVH.Item + this.checkIn.hasSubmissionConsent shouldBe true + } + + get(2).apply { + this as SelectableCheckInVH.Item + this.checkIn.hasSubmissionConsent shouldBe false + } + } + } + + @Test + fun `Select all`() { + every { savedState.get<Set<Long>>(any()) } returns emptySet() + val viewModel = createViewModel() + viewModel.checkIns.getOrAwaitValue().apply { + size shouldBe 3 + get(0).apply { + this as HeaderCheckInsVH.Item + (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false + (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false + + this.selectAll() + + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + } + } + } + } + + @Test + fun `Select all does not un-select all`() { + every { savedState.get<Set<Long>>(any()) } returns emptySet() + val viewModel = createViewModel() + viewModel.checkIns.getOrAwaitValue().apply { + size shouldBe 3 + get(0).apply { + this as HeaderCheckInsVH.Item + (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false + (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe false + + this.selectAll() + + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + } + + this.selectAll() + + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + (get(2) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + } + } + } + } + + @Test + fun `Single selection`() { + every { savedState.get<Set<Long>>(any()) } returns emptySet() + val viewModel = createViewModel() + viewModel.checkIns.getOrAwaitValue().apply { + + (get(1) as SelectableCheckInVH.Item).apply { + checkIn.hasSubmissionConsent shouldBe false + onItemSelected(checkIn) + } + + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).checkIn.hasSubmissionConsent shouldBe true + } + } + } + + @Test + fun `Single deselection`() { + every { savedState.get<Set<Long>>(any()) } returns emptySet() + val viewModel = createViewModel() + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).apply { + checkIn.hasSubmissionConsent shouldBe false + onItemSelected(checkIn) + } + + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).apply { + checkIn.hasSubmissionConsent shouldBe true + onItemSelected(checkIn) + } + } + + viewModel.checkIns.getOrAwaitValue().apply { + (get(1) as SelectableCheckInVH.Item).apply { + checkIn.hasSubmissionConsent shouldBe false + } + } + } + } + + @Test + fun `Confirming cancel goes to home screen`() { + createViewModel().apply { + onCancelConfirmed() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToHomeFragment + } + } + + @Test + fun `Skip opens skipDialog`() { + createViewModel().apply { + onSkipClick() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenSkipDialog + } + } + + @Test + fun `Close opens skipDialog when test result has been shown`() { + coronaTestFlow.value = mockk<CoronaTest>().apply { every { isViewed } returns true } + createViewModel().apply { + onCloseClick() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenSkipDialog + } + } + + @Test + fun `Close opens closeDialog when test result has not been shown`() { + coronaTestFlow.value = mockk<CoronaTest>().apply { every { isViewed } returns false } + createViewModel().apply { + onCloseClick() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenCloseDialog + } + } + + @Test + fun `shareSelectedCheckIns when test result has been shown`() { + coronaTestFlow.value = mockk<CoronaTest>().apply { every { isViewed } returns true } + createViewModel().apply { + shareSelectedCheckIns() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionResultReadyFragment + } + + coVerify { + checkInRepository.updateSubmissionConsents(any(), false) + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + checkInRepository.updateSubmissionConsents(any(), true) + } + } + + @Test + fun `shareSelectedCheckIns when test result has not been shown`() { + coronaTestFlow.value = mockk<CoronaTest>().apply { every { isViewed } returns false } + createViewModel().apply { + shareSelectedCheckIns() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment + } + + coVerify { + checkInRepository.updateSubmissionConsents(any(), false) + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + checkInRepository.updateSubmissionConsents(any(), true) + } + } + + @Test + fun `doNotShareCheckIns when test result has been shown`() { + coronaTestFlow.value = mockk<CoronaTest>().apply { every { isViewed } returns true } + createViewModel().apply { + doNotShareCheckIns() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionResultReadyFragment + } + + coVerify { + checkInRepository.updateSubmissionConsents(any(), false) + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + } + } + + @Test + fun `doNotShareCheckIns when test result has not been shown`() { + coronaTestFlow.value = mockk<CoronaTest>().apply { every { isViewed } returns false } + createViewModel().apply { + doNotShareCheckIns() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment + } + + coVerify { + checkInRepository.updateSubmissionConsents(any(), false) + autoSubmission.updateMode(AutoSubmission.Mode.MONITOR) + } + } + + private fun createViewModel() = CheckInsConsentViewModel( + savedState = savedState, + dispatcherProvider = TestDispatcherProvider(), + checkInRepository = checkInRepository, + submissionRepository = submissionRepository, + autoSubmission = autoSubmission + ) +} 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 index 5e1f894d73b3e3296a5664e021376872cef265b7..8a3ef38c00f8fc402969356ee5b861921c3d73bc 100644 --- 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 @@ -16,7 +16,6 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk -import io.mockk.verify import kotlinx.coroutines.flow.MutableStateFlow import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -41,7 +40,7 @@ class SubmissionConsentViewModelTest { fun setUp() { MockKAnnotations.init(this) every { interoperabilityRepository.countryList } returns MutableStateFlow(countryList) - every { submissionRepository.giveConsentToSubmission() } just Runs + every { submissionRepository.giveConsentToSubmission(any()) } just Runs every { analyticsKeySubmissionCollector.reportAdvancedConsentGiven() } just Runs viewModel = SubmissionConsentViewModel( submissionRepository, @@ -55,7 +54,8 @@ class SubmissionConsentViewModelTest { @Test fun testOnConsentButtonClick() { viewModel.onConsentButtonClick() - verify(exactly = 1) { submissionRepository.giveConsentToSubmission() } + // TODO doesn't happen here anymore, we don't have a CoronaTest instance to store it with, see QR Code VM +// verify(exactly = 1) { submissionRepository.giveConsentToSubmission(any()) } } @Test @@ -92,7 +92,8 @@ class SubmissionConsentViewModelTest { @Test fun `onConsentButtonClick sets normal consent and request new Google consent Api`() { viewModel.onConsentButtonClick() - verify(exactly = 1) { submissionRepository.giveConsentToSubmission() } + // TODO doesn't happen here anymore, we don't have a CoronaTest instance to store it with, see QR Code VM +// verify(exactly = 1) { submissionRepository.giveConsentToSubmission(any()) } coVerify(exactly = 1) { tekHistoryProvider.preAuthorizeExposureKeyHistory() } } 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 a20bc57af4ef729bcd28897e8c3c6c18bedb6cf9..02ed331db2b6e0bea748693db91616d7b058694b 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 @@ -1,10 +1,12 @@ package de.rki.coronawarnapp.ui.submission.qrcode.scan import de.rki.coronawarnapp.bugreporting.censors.QRCodeCensor -import de.rki.coronawarnapp.service.submission.QRScanResult +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQRCode +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestQrCodeValidator +import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.submission.ScanStatus -import de.rki.coronawarnapp.util.formatter.TestResult import de.rki.coronawarnapp.util.permission.CameraSettings import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -26,6 +28,7 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { @MockK lateinit var submissionRepository: SubmissionRepository @MockK lateinit var cameraSettings: CameraSettings + @MockK lateinit var qrCodeValidator: CoronaTestQrCodeValidator @BeforeEach fun setUp() { @@ -34,11 +37,23 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { private fun createViewModel() = SubmissionQRCodeScanViewModel( submissionRepository, - cameraSettings + cameraSettings, + qrCodeValidator ) @Test fun scanStatusValid() { + // valid guid + val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" + val coronaTestQRCode = CoronaTestQRCode.PCR( + qrCodeGUID = guid + ) + + val validQrCode = "https://localhost/?$guid" + val invalidQrCode = "https://no-guid-here" + + every { qrCodeValidator.validate(validQrCode) } returns coronaTestQRCode + every { qrCodeValidator.validate(invalidQrCode) } throws InvalidQRCodeException() val viewModel = createViewModel() // start @@ -48,25 +63,23 @@ class SubmissionQRCodeScanViewModelTest : BaseTest() { QRCodeCensor.lastGUID = null - // valid guid - val guid = "123456-12345678-1234-4DA7-B166-B86D85475064" - viewModel.validateTestGUID("https://localhost/?$guid") + viewModel.validateTestGUID(validQrCode) viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.SUCCESS, it.value) } QRCodeCensor.lastGUID = guid // invalid guid - viewModel.validateTestGUID("https://no-guid-here") + viewModel.validateTestGUID(invalidQrCode) viewModel.scanStatusValue.let { Assert.assertEquals(ScanStatus.INVALID, it.value) } } @Test fun `doDeviceRegistration calls TestResultDataCollector`() { val viewModel = createViewModel() - val mockResult = mockk<QRScanResult>().apply { - every { guid } returns "guid" + val mockResult = mockk<CoronaTestQRCode>().apply { + every { registrationIdentifier } returns "guid" } - - coEvery { submissionRepository.asyncRegisterDeviceViaGUID(any()) } returns TestResult.POSITIVE + val mockTest = mockk<CoronaTest>() + coEvery { submissionRepository.registerTest(any()) } returns mockTest viewModel.doDeviceRegistration(mockResult) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt index f06833355235b74b2ecc8746f09cd475bf9fa929..d9c1e6ea258382161a12a535ae48bb3b39a78e79 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt @@ -1,6 +1,9 @@ package de.rki.coronawarnapp.ui.submission.testavailable +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater @@ -12,9 +15,9 @@ 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 kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -31,17 +34,27 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() { @MockK lateinit var tekHistoryUpdater: TEKHistoryUpdater @MockK lateinit var tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var coronaTestRepository: CoronaTestRepository + @MockK lateinit var checkInRepository: CheckInRepository + + private val coronaTestFlow = MutableStateFlow( + mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + } + ) @BeforeEach fun setUp() { MockKAnnotations.init(this) - every { submissionRepository.hasGivenConsentToSubmission } returns flowOf(true) every { tekHistoryUpdaterFactory.create(any()) } returns tekHistoryUpdater every { tekHistoryUpdater.updateTEKHistoryOrRequestPermission() } just Runs // TODO Check specific behavior - every { submissionRepository.refreshDeviceUIState(any()) } just Runs + submissionRepository.apply { + every { refreshTest(any()) } just Runs + every { testForType(type = any()) } returns coronaTestFlow + } } private fun createViewModel(): SubmissionTestResultAvailableViewModel = SubmissionTestResultAvailableViewModel( @@ -49,20 +62,24 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() { dispatcherProvider = TestDispatcherProvider(), tekHistoryUpdaterFactory = tekHistoryUpdaterFactory, autoSubmission = autoSubmission, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + checkInRepository = checkInRepository ) @Test fun `consent repository changed`() { - val consentMutable = MutableStateFlow(false) - every { submissionRepository.hasGivenConsentToSubmission } returns consentMutable + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns false + } val viewModel = createViewModel() viewModel.consent.observeForever { } viewModel.consent.value shouldBe false - consentMutable.value = true + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + } viewModel.consent.value shouldBe true } @@ -96,8 +113,11 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() { @Test fun `go to test result without updating TEK history if NO consent is given`() { - every { submissionRepository.hasGivenConsentToSubmission } returns flowOf(false) + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns false + } every { analyticsKeySubmissionCollector.reportConsentWithdrawn() } just Runs + val viewModel = createViewModel() viewModel.proceed() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt index 6b3e2fdc42a2b7829f254c762b2bf1e682637b0d..40080f1f514b7f5af1865eb5243bc9c837b0b365 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModelTest.kt @@ -1,7 +1,9 @@ package de.rki.coronawarnapp.ui.submission.warnothers +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.nearby.ENFClient +import de.rki.coronawarnapp.presencetracing.checkins.CheckInRepository import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission @@ -11,6 +13,7 @@ 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 kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow @@ -33,6 +36,13 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() { @MockK lateinit var interoperabilityRepository: InteroperabilityRepository @MockK lateinit var enfClient: ENFClient @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var checkInRepository: CheckInRepository + + private val coronaTestFlow = MutableStateFlow( + mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + } + ) @BeforeEach fun setUp() { @@ -41,7 +51,11 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() { every { tekHistoryUpdater.updateTEKHistoryOrRequestPermission() } just Runs every { interoperabilityRepository.countryList } returns emptyFlow() - every { submissionRepository.giveConsentToSubmission() } just Runs + + submissionRepository.apply { + every { giveConsentToSubmission(any()) } just Runs + every { testForType(any()) } returns coronaTestFlow + } every { enfClient.isTracingEnabled } returns flowOf(true) } @@ -53,19 +67,21 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() { enfClient = enfClient, interoperabilityRepository = interoperabilityRepository, submissionRepository = submissionRepository, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + checkInRepository = checkInRepository ) @Test fun `consent is stored and tek history updated`() { - val consentMutable = MutableStateFlow(false) - every { submissionRepository.hasGivenConsentToSubmission } returns consentMutable + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns false + } val viewModel = createViewModel() viewModel.onConsentButtonClicked() - verify { submissionRepository.giveConsentToSubmission() } + verify { submissionRepository.giveConsentToSubmission(any()) } verify { tekHistoryUpdater.updateTEKHistoryOrRequestPermission() } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt index 30797a6da0077966f6444c60504b416bb256dabc..e11349903be08c2f7d039efc891d1b868519aa7f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/yourconsent/SubmissionYourConsentViewModelTest.kt @@ -1,18 +1,18 @@ package de.rki.coronawarnapp.ui.submission.yourconsent +import de.rki.coronawarnapp.coronatest.type.CoronaTest import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.ui.Country import io.kotest.matchers.shouldBe 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.verify import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -29,13 +29,19 @@ class SubmissionYourConsentViewModelTest : BaseTest() { private val countryList = Country.values().toList() + private val coronaTestFlow = MutableStateFlow( + mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + } + ) + @BeforeEach fun setUp() { MockKAnnotations.init(this) + every { submissionRepository.testForType(any()) } returns coronaTestFlow every { interoperabilityRepository.countryList } returns MutableStateFlow(countryList) - every { submissionRepository.hasGivenConsentToSubmission } returns flowOf(true) - every { submissionRepository.giveConsentToSubmission() } just Runs - every { submissionRepository.revokeConsentToSubmission() } just Runs + every { submissionRepository.giveConsentToSubmission(any()) } just Runs + every { submissionRepository.revokeConsentToSubmission(any()) } just Runs } private fun createViewModel(): SubmissionYourConsentViewModel = SubmissionYourConsentViewModel( @@ -62,33 +68,38 @@ class SubmissionYourConsentViewModelTest : BaseTest() { @Test fun `consent removed`() { - val viewModel = createViewModel() + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + } - coEvery { submissionRepository.hasGivenConsentToSubmission } returns flowOf(true) - viewModel.switchConsent() - verify(exactly = 1) { submissionRepository.revokeConsentToSubmission() } + createViewModel().switchConsent() + verify(exactly = 1) { submissionRepository.revokeConsentToSubmission(any()) } } @Test fun `consent given`() { - val viewModel = createViewModel() + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns false + } - coEvery { submissionRepository.hasGivenConsentToSubmission } returns flowOf(false) - viewModel.switchConsent() - verify(exactly = 1) { submissionRepository.giveConsentToSubmission() } + createViewModel().switchConsent() + verify(exactly = 1) { submissionRepository.giveConsentToSubmission(any()) } } @Test fun `consent repository changed`() { - val consentMutable = MutableStateFlow(false) - every { submissionRepository.hasGivenConsentToSubmission } returns consentMutable + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns false + } val viewModel = createViewModel() viewModel.consent.observeForever { } viewModel.consent.value shouldBe false - consentMutable.value = true + coronaTestFlow.value = mockk<CoronaTest>().apply { + every { isAdvancedConsentGiven } returns true + } viewModel.consent.value shouldBe true } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt index 38c405c76885fa2c6c6c7927832ef032bcd8269b..b78a603a9f5ecca262d3ec469086c2a4848f998a 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt @@ -5,6 +5,7 @@ import de.rki.coronawarnapp.appconfig.AppConfigProvider import de.rki.coronawarnapp.bugreporting.BugReportingSettings import de.rki.coronawarnapp.contactdiary.storage.ContactDiaryPreferences import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository +import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.datadonation.analytics.Analytics import de.rki.coronawarnapp.datadonation.analytics.storage.AnalyticsSettings import de.rki.coronawarnapp.datadonation.survey.SurveySettings @@ -56,6 +57,7 @@ internal class DataResetTest : BaseTest() { @MockK lateinit var traceWarningRepository: TraceWarningRepository @MockK lateinit var checkInRepository: CheckInRepository @MockK lateinit var traceLocationSettings: TraceLocationSettings + @MockK lateinit var coronaTestRepository: CoronaTestRepository @BeforeEach fun setUp() { @@ -85,7 +87,8 @@ internal class DataResetTest : BaseTest() { traceLocationRepository = traceLocationRepository, checkInRepository = checkInRepository, traceLocationSettings = traceLocationSettings, - traceWarningRepository = traceWarningRepository + traceWarningRepository = traceWarningRepository, + coronaTestRepository = coronaTestRepository ) @Test @@ -118,5 +121,6 @@ internal class DataResetTest : BaseTest() { coVerify(exactly = 1) { traceWarningRepository.clear() } coVerify(exactly = 1) { traceLocationRepository.deleteAllTraceLocations() } coVerify(exactly = 1) { checkInRepository.clear() } + coVerify(exactly = 1) { coronaTestRepository.clear() } } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt index 4c7411bca6a60422e7bdd71bb58d47e0c792f4e1..876272834a33adb138e10e30d164f15c5dc3614b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/TimeAndDateExtensionsTest.kt @@ -2,7 +2,6 @@ package de.rki.coronawarnapp.util import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.util.TimeAndDateExtensions.ageInDays -import de.rki.coronawarnapp.util.TimeAndDateExtensions.calculateDays import de.rki.coronawarnapp.util.TimeAndDateExtensions.derive10MinutesInterval import de.rki.coronawarnapp.util.TimeAndDateExtensions.deriveHourInterval import de.rki.coronawarnapp.util.TimeAndDateExtensions.getCurrentHourUTC @@ -20,7 +19,6 @@ import org.joda.time.LocalDate import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import testhelpers.BaseTest -import java.util.concurrent.TimeUnit /** * TimeAndDateExtensions test. @@ -40,15 +38,6 @@ class TimeAndDateExtensionsTest : BaseTest() { MatcherAssert.assertThat(result, CoreMatchers.`is`(DateTime(Instant.now(), DateTimeZone.UTC).hourOfDay().get())) } - @Test - fun calculateDaysTest() { - val lFirstDate = DateTime(2019, 1, 1, 1, 1).millis - val lSecondDate = DateTime(2020, 1, 1, 1, 1).millis - - val result = calculateDays(firstDate = lFirstDate, secondDate = lSecondDate) - MatcherAssert.assertThat(result, CoreMatchers.`is`(TimeUnit.MILLISECONDS.toDays(lSecondDate - lFirstDate))) - } - @Test fun test_daysAgo() { LocalDate(2012, 3, 4).ageInDays( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt index bc18a0cf95c56e371fc70394f66f767ce40f307d..f3585f5d8fe1a1cab0d9a359b7d6b780daf782dc 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/encryptionmigration/EncryptedPreferencesMigrationTest.kt @@ -15,6 +15,7 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk +import io.mockk.verify import org.joda.time.Instant import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -24,6 +25,7 @@ import testhelpers.preferences.MockSharedPreferences import testhelpers.preferences.mockFlowPreference import java.io.File +@Suppress("DEPRECATION") class EncryptedPreferencesMigrationTest : BaseIOTest() { @MockK lateinit var context: Context @MockK lateinit var encryptedPreferencesHelper: EncryptedPreferencesHelper @@ -111,19 +113,18 @@ class EncryptedPreferencesMigrationTest : BaseIOTest() { every { onboardingSettings.isBackgroundCheckDone = true } just Runs // TracingLocalData - every { tracingSettings.initialPollingForTestResultTimeStamp = 10101010L } just Runs - every { tracingSettings.isTestResultAvailableNotificationSent = true } just Runs + every { tracingSettings.initialPollingForTestResultTimeStampMigration = 10101010L } just Runs + every { tracingSettings.isTestResultAvailableNotificationSentMigration = true } just Runs val mockNotificationPreference = mockFlowPreference(false) every { tracingSettings.isUserToBeNotifiedOfLoweredRiskLevel } returns mockNotificationPreference every { tracingSettings.isConsentGiven = true } just Runs // SubmissionLocalData - val mockRegtokenPreference = mockFlowPreference<String?>(null) - every { submissionSettings.registrationToken } returns mockRegtokenPreference - every { submissionSettings.initialTestResultReceivedAt = Instant.ofEpochMilli(10101010L) } just Runs - every { submissionSettings.devicePairingSuccessfulAt = Instant.ofEpochMilli(10101010L) } just Runs - every { submissionSettings.isSubmissionSuccessful = true } just Runs - every { submissionSettings.isAllowedToSubmitKeys = true } just Runs + every { submissionSettings.registrationTokenMigration = any() } just Runs + every { submissionSettings.initialTestResultReceivedAtMigration = Instant.ofEpochMilli(10101010L) } just Runs + every { submissionSettings.devicePairingSuccessfulAtMigration = Instant.ofEpochMilli(10101010L) } just Runs + every { submissionSettings.isSubmissionSuccessfulMigration = true } just Runs + every { submissionSettings.isAllowedToSubmitKeysMigration = true } just Runs val migrationInstance = createInstance() @@ -137,7 +138,7 @@ class EncryptedPreferencesMigrationTest : BaseIOTest() { mockNotificationPreference.value shouldBe true // SubmissionLocalData - mockRegtokenPreference.value shouldBe "super_secret_token" + verify { submissionSettings.registrationTokenMigration = "super_secret_token" } } @Test 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 ec822d947365799b5a2a69c65924eb56d926e457..54d8d584e583125d432175bc799c7845a1a6e17f 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 @@ -5,14 +5,12 @@ import android.graphics.drawable.Drawable import android.text.Spannable 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.util.ContextExtensions import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat import de.rki.coronawarnapp.util.ContextExtensions.getDrawableCompat 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 @@ -78,7 +76,7 @@ class FormatterSubmissionHelperTest : BaseTest() { } private fun formatTestResultStatusTextBase( - oUiState: NetworkRequestWrapper<DeviceUIState, Throwable>?, + oUiState: DeviceUIState, iResult: String ) { val result = formatTestResultStatusText(context = context, uiState = oUiState) @@ -86,35 +84,19 @@ class FormatterSubmissionHelperTest : BaseTest() { } private fun formatTestResultStatusColorBase( - oUiState: NetworkRequestWrapper<DeviceUIState, Throwable>?, + oUiState: DeviceUIState, iResult: Int ) { val result = formatTestResultStatusColor(context = context, uiState = oUiState) assertThat(result, `is`(iResult)) } - private fun formatTestStatusIconBase(oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?) { + private fun formatTestStatusIconBase(oUiState: DeviceUIState) { val result = formatTestStatusIcon(context = context, uiState = oUiState) assertThat(result, `is`(drawable)) } - private fun formatTestResultPendingStepsVisibleBase( - oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?, - iResult: Int - ) { - val result = formatTestResultPendingStepsVisible(uiState = oUiState) - assertThat(result, `is`(iResult)) - } - - private fun formatTestResultInvalidStepsVisibleBase( - oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?, - iResult: Int - ) { - val result = formatTestResultInvalidStepsVisible(uiState = oUiState) - assertThat(result, `is`(iResult)) - } - - private fun formatTestResultBase(oUiState: NetworkRequestWrapper.RequestSuccessful<DeviceUIState, Throwable>?) { + private fun formatTestResultBase(oUiState: DeviceUIState) { mockkConstructor(SpannableStringBuilder::class) val spannableStringBuilder1 = @@ -141,39 +123,35 @@ class FormatterSubmissionHelperTest : BaseTest() { @Test fun formatTestResultStatusText() { formatTestResultStatusTextBase( - oUiState = null, - iResult = context.getString(R.string.test_result_card_status_invalid) - ) - formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), + oUiState = DeviceUIState.PAIRED_NEGATIVE, iResult = context.getString(R.string.test_result_card_status_negative) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), + oUiState = DeviceUIState.PAIRED_ERROR, iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), + oUiState = DeviceUIState.PAIRED_NO_RESULT, iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), + oUiState = DeviceUIState.PAIRED_POSITIVE, iResult = context.getString(R.string.test_result_card_status_positive) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), + oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, iResult = context.getString(R.string.test_result_card_status_positive) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), + oUiState = DeviceUIState.SUBMITTED_FINAL, iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), + oUiState = DeviceUIState.SUBMITTED_INITIAL, iResult = context.getString(R.string.test_result_card_status_invalid) ) formatTestResultStatusTextBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), + oUiState = DeviceUIState.UNPAIRED, iResult = context.getString(R.string.test_result_card_status_invalid) ) } @@ -181,140 +159,60 @@ class FormatterSubmissionHelperTest : BaseTest() { @Test fun formatTestResultStatusColor() { formatTestResultStatusColorBase( - oUiState = null, - iResult = context.getColorCompat(R.color.colorTextSemanticRed) - ) - formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), + oUiState = DeviceUIState.PAIRED_NEGATIVE, iResult = context.getColorCompat(R.color.colorTextSemanticGreen) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), + oUiState = DeviceUIState.PAIRED_ERROR, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), + oUiState = DeviceUIState.PAIRED_NO_RESULT, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), + oUiState = DeviceUIState.PAIRED_POSITIVE, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), + oUiState = DeviceUIState.PAIRED_POSITIVE_TELETAN, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), + oUiState = DeviceUIState.SUBMITTED_FINAL, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), + oUiState = DeviceUIState.SUBMITTED_INITIAL, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) formatTestResultStatusColorBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), + oUiState = DeviceUIState.UNPAIRED, iResult = context.getColorCompat(R.color.colorTextSemanticRed) ) } @Test fun formatTestStatusIcon() { - formatTestStatusIconBase(oUiState = null) - 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 = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), - iResult = View.GONE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), - iResult = View.GONE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), - iResult = View.VISIBLE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), - iResult = View.GONE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), - iResult = View.GONE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), - iResult = View.GONE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), - iResult = View.GONE - ) - formatTestResultPendingStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), - iResult = View.GONE - ) - } - - @Test - fun formatTestResultInvalidStepsVisible() { - formatTestResultInvalidStepsVisibleBase(oUiState = null, iResult = View.GONE) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NEGATIVE), - iResult = View.GONE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_ERROR), - iResult = View.VISIBLE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_NO_RESULT), - iResult = View.GONE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE), - iResult = View.GONE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.PAIRED_POSITIVE_TELETAN), - iResult = View.GONE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_FINAL), - iResult = View.GONE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.SUBMITTED_INITIAL), - iResult = View.GONE - ) - formatTestResultInvalidStepsVisibleBase( - oUiState = NetworkRequestWrapper.RequestSuccessful(DeviceUIState.UNPAIRED), - iResult = View.GONE - ) + 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) } @Test fun formatTestResult() { - formatTestResultBase(oUiState = null) - 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)) + 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) } } 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 5f94538cd6fd726a3bc983a0dde5227b696bfe0e..da4f8b70e31ad2b531ee73da732b52d8b1d83dc8 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,20 +6,24 @@ import com.google.gson.Gson import dagger.Component import dagger.Module import dagger.Provides +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler import de.rki.coronawarnapp.datadonation.analytics.Analytics import de.rki.coronawarnapp.datadonation.analytics.worker.DataDonationAnalyticsScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationScheduler import de.rki.coronawarnapp.deadman.DeadmanNotificationSender -import de.rki.coronawarnapp.presencetracing.storage.repo.TraceLocationRepository -import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpScheduler +import de.rki.coronawarnapp.deniability.NoiseScheduler import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService import de.rki.coronawarnapp.playbook.Playbook import de.rki.coronawarnapp.presencetracing.checkins.checkout.CheckOutNotification import de.rki.coronawarnapp.presencetracing.checkins.checkout.auto.AutoCheckOut +import de.rki.coronawarnapp.presencetracing.storage.repo.TraceLocationRepository +import de.rki.coronawarnapp.presencetracing.storage.retention.TraceLocationDbCleanUpScheduler import de.rki.coronawarnapp.risk.execution.RiskWorkScheduler import de.rki.coronawarnapp.risk.storage.RiskLevelStorage +import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.serialization.BaseGson @@ -147,4 +151,16 @@ class MockProvider { @Provides fun riskWorkScheduler(): RiskWorkScheduler = mockk() + + @Provides + fun submissionRepository(): SubmissionRepository = mockk() + + @Provides + fun coronaTestRepository(): CoronaTestRepository = mockk() + + @Provides + fun noiseScheduler(): NoiseScheduler = mockk() + + @Provides + fun testResultScheduler(): TestResultScheduler = mockk() } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt index a2247300dd4871e0a949b7bfaba310237c2862b3..899fc46235d9c8adbb16c326d5238e873c61211e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/BackgroundConstantsTest.kt @@ -10,7 +10,6 @@ class BackgroundConstantsTest { Assert.assertEquals(BackgroundConstants.MINUTES_IN_DAY, 1440) Assert.assertEquals(BackgroundConstants.DIAGNOSIS_TEST_RESULT_RETRIEVAL_TRIES_PER_DAY, 12) Assert.assertEquals(BackgroundConstants.KIND_DELAY, 1L) - Assert.assertEquals(BackgroundConstants.DIAGNOSIS_TEST_RESULT_PERIODIC_INITIAL_DELAY, 10L) Assert.assertEquals(BackgroundConstants.WORKER_RETRY_COUNT_THRESHOLD, 2) Assert.assertEquals(BackgroundConstants.POLLING_VALIDITY_MAX_DAYS, 21) Assert.assertEquals(BackgroundConstants.BACKOFF_INITIAL_DELAY, 8L) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt index a59702bd5d771dbc90c8fe80cf09ec523e6f83bb..aa99fa078ddeaf064e3f154ba7e717bbdc8b2398 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/worker/DiagnosisTestResultRetrievalPeriodicWorkerTest.kt @@ -4,19 +4,20 @@ import android.content.Context import androidx.work.ListenableWorker import androidx.work.WorkRequest import androidx.work.WorkerParameters +import de.rki.coronawarnapp.coronatest.CoronaTestRepository +import de.rki.coronawarnapp.coronatest.execution.TestResultScheduler +import de.rki.coronawarnapp.coronatest.server.CoronaTestResult +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.pcr.PCRCoronaTest import de.rki.coronawarnapp.notification.GeneralNotifications import de.rki.coronawarnapp.notification.NotificationConstants import de.rki.coronawarnapp.notification.TestResultAvailableNotificationService -import de.rki.coronawarnapp.service.submission.SubmissionService -import de.rki.coronawarnapp.storage.TracingSettings -import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.util.TimeAndDateExtensions.daysToMilliseconds import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.di.AppInjector import de.rki.coronawarnapp.util.di.ApplicationComponent import de.rki.coronawarnapp.util.encryptionmigration.EncryptedPreferencesFactory import de.rki.coronawarnapp.util.encryptionmigration.EncryptionErrorResetTool -import de.rki.coronawarnapp.util.formatter.TestResult import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations import io.mockk.Runs @@ -26,232 +27,230 @@ import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.RelaxedMockK import io.mockk.just +import io.mockk.mockk import io.mockk.mockkObject -import io.mockk.slot import io.mockk.verify +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runBlockingTest import org.joda.time.Instant import org.junit.Before import org.junit.Test import testhelpers.BaseTest -import testhelpers.preferences.mockFlowPreference class DiagnosisTestResultRetrievalPeriodicWorkerTest : BaseTest() { @MockK lateinit var context: Context @MockK lateinit var request: WorkRequest - @MockK lateinit var submissionSettings: SubmissionSettings - @MockK lateinit var submissionService: SubmissionService @MockK lateinit var testResultAvailableNotificationService: TestResultAvailableNotificationService @MockK lateinit var notificationHelper: GeneralNotifications @MockK lateinit var appComponent: ApplicationComponent @MockK lateinit var encryptedPreferencesFactory: EncryptedPreferencesFactory @MockK lateinit var encryptionErrorResetTool: EncryptionErrorResetTool @MockK lateinit var timeStamper: TimeStamper - @MockK lateinit var tracingSettings: TracingSettings - @MockK lateinit var backgroundWorkScheduler: BackgroundWorkScheduler + @MockK lateinit var coronaTestRepository: CoronaTestRepository + @MockK lateinit var testResultScheduler: TestResultScheduler @RelaxedMockK lateinit var workerParams: WorkerParameters private val currentInstant = Instant.ofEpochSecond(1611764225) - private val registrationToken = "test token" + private val testToken = "test token" + + private val coronaTestFlow = MutableStateFlow(emptySet<CoronaTest>()) @Before fun setUp() { MockKAnnotations.init(this) - every { submissionSettings.hasViewedTestResult.value } returns false every { timeStamper.nowUTC } returns currentInstant - every { tracingSettings.initialPollingForTestResultTimeStamp } returns currentInstant.millis - every { tracingSettings.isTestResultAvailableNotificationSent } returns false - every { tracingSettings.initialPollingForTestResultTimeStamp = capture(slot()) } answers {} - every { tracingSettings.isTestResultAvailableNotificationSent = capture(slot()) } answers {} mockkObject(AppInjector) every { AppInjector.component } returns appComponent every { appComponent.encryptedPreferencesFactory } returns encryptedPreferencesFactory every { appComponent.errorResetTool } returns encryptionErrorResetTool - every { submissionSettings.registrationToken } returns mockFlowPreference(registrationToken) + every { testResultScheduler.setPeriodicTestPolling(enabled = any()) } just Runs + + every { notificationHelper.cancelCurrentNotification(any()) } just Runs - every { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } just Runs + coronaTestRepository.apply { + every { coronaTests } answers { coronaTestFlow } + coEvery { refresh(any()) } coAnswers { coronaTestFlow.first() } + coEvery { updateResultNotification(identifier = any(), sent = any()) } just Runs + } } - @Test - fun testStopWorkerWhenResultHasBeenViewed() { - runBlockingTest { - every { submissionSettings.hasViewedTestResult.value } returns true - val worker = createWorker() - val result = worker.doWork() - coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } - result shouldBe ListenableWorker.Result.success() + private fun newCoronaTest( + registered: Instant = currentInstant, + viewed: Boolean = false, + result: CoronaTestResult = CoronaTestResult.PCR_POSITIVE, + isNotificationSent: Boolean = false, + ): CoronaTest { + return mockk<PCRCoronaTest>().apply { + every { identifier } returns "" + every { type } returns CoronaTest.Type.PCR + every { registeredAt } returns registered + every { isViewed } returns viewed + every { testResult } returns result + every { registrationToken } returns testToken + every { isResultAvailableNotificationSent } returns isNotificationSent } } + private fun createWorker() = DiagnosisTestResultRetrievalPeriodicWorker( + context = context, + workerParams = workerParams, + testResultAvailableNotificationService = testResultAvailableNotificationService, + notificationHelper = notificationHelper, + coronaTestRepository = coronaTestRepository, + testResultScheduler = testResultScheduler, + timeStamper = timeStamper, + ) + @Test - fun testStopWorkerWhenNotificationSent() { - runBlockingTest { - every { tracingSettings.isTestResultAvailableNotificationSent } returns true - val worker = createWorker() - val result = worker.doWork() - coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } - result shouldBe ListenableWorker.Result.success() - } + fun testStopWorkerWhenResultHasBeenViewed() = runBlockingTest { + coronaTestFlow.value = setOf(newCoronaTest(viewed = true)) + + val result = createWorker().doWork() + + coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) } + verify(exactly = 1) { testResultScheduler.setPeriodicTestPolling(enabled = false) } + result shouldBe ListenableWorker.Result.success() } @Test - fun testStopWorkerWhenMaxDaysExceeded() { - runBlockingTest { - val past = - currentInstant - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() + 1).daysToMilliseconds() - every { tracingSettings.initialPollingForTestResultTimeStamp } returns past.millis - val worker = createWorker() - val result = worker.doWork() - coVerify(exactly = 0) { submissionService.asyncRequestTestResult(any()) } - verify(exactly = 1) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } - result shouldBe ListenableWorker.Result.success() - } + fun testStopWorkerWhenNotificationSent() = runBlockingTest { + coronaTestFlow.value = setOf(newCoronaTest(isNotificationSent = true)) + + val result = createWorker().doWork() + + coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) } + verify(exactly = 1) { testResultScheduler.setPeriodicTestPolling(enabled = false) } + result shouldBe ListenableWorker.Result.success() } @Test - fun testSendNotificationWhenPositive() { - val isTestResultAvailableNotificationSent = slot<Boolean>() - every { - tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) - } answers {} - - runBlockingTest { - val testResult = TestResult.POSITIVE - coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult - coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs - coEvery { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } just Runs - val worker = createWorker() - val result = worker.doWork() - coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } - coVerify { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } - result shouldBe ListenableWorker.Result.success() - isTestResultAvailableNotificationSent.captured shouldBe true - } + fun testStopWorkerWhenMaxDaysExceeded() = runBlockingTest { + val past = + currentInstant - (BackgroundConstants.POLLING_VALIDITY_MAX_DAYS.toLong() + 1).daysToMilliseconds() + coronaTestFlow.value = setOf(newCoronaTest(registered = past)) + + val result = createWorker().doWork() + + coVerify(exactly = 0) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) } + verify(exactly = 1) { testResultScheduler.setPeriodicTestPolling(enabled = false) } + result shouldBe ListenableWorker.Result.success() } @Test - fun testSendNotificationWhenNegative() { - val isTestResultAvailableNotificationSent = slot<Boolean>() - every { - tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) - } answers {} - - runBlockingTest { - val testResult = TestResult.NEGATIVE - coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult - coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs - coEvery { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } just Runs - val worker = createWorker() - val result = worker.doWork() - coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } - coVerify { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } - result shouldBe ListenableWorker.Result.success() - isTestResultAvailableNotificationSent.captured shouldBe true + fun testSendNotificationWhenPositive() = runBlockingTest { + val testResult = CoronaTestResult.PCR_POSITIVE + coronaTestFlow.value = setOf(newCoronaTest(result = testResult)) + + coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs + coEvery { + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } just Runs + + val result = createWorker().doWork() + + coVerify { + coronaTestRepository.refresh(type = CoronaTest.Type.PCR) + testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + coronaTestRepository.updateResultNotification(any(), sent = true) } + + result shouldBe ListenableWorker.Result.success() } @Test - fun testSendNotificationWhenInvalid() { - val isTestResultAvailableNotificationSent = slot<Boolean>() - every { - tracingSettings.isTestResultAvailableNotificationSent = capture(isTestResultAvailableNotificationSent) - } answers {} - - runBlockingTest { - val testResult = TestResult.INVALID - coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult - coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs - coEvery { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } just Runs - val worker = createWorker() - val result = worker.doWork() - coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } - coVerify { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } - result shouldBe ListenableWorker.Result.success() - isTestResultAvailableNotificationSent.captured shouldBe true + fun testSendNotificationWhenNegative() = runBlockingTest { + val testResult = CoronaTestResult.PCR_NEGATIVE + coronaTestFlow.value = setOf(newCoronaTest(result = testResult)) + coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs + coEvery { + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } just Runs + + val result = createWorker().doWork() + + coVerify { + coronaTestRepository.refresh(type = CoronaTest.Type.PCR) + testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + coronaTestRepository.updateResultNotification(any(), sent = true) } + + result shouldBe ListenableWorker.Result.success() } @Test - fun testSendNoNotificationWhenPending() { - runBlockingTest { - val testResult = TestResult.PENDING - coEvery { submissionService.asyncRequestTestResult(registrationToken) } returns testResult - coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs - coEvery { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } just Runs - val worker = createWorker() - val result = worker.doWork() - coVerify { submissionService.asyncRequestTestResult(registrationToken) } - coVerify(exactly = 0) { - testResultAvailableNotificationService.showTestResultAvailableNotification( - testResult - ) - } - coVerify(exactly = 0) { - notificationHelper.cancelCurrentNotification( - NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID - ) - } - coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } - result shouldBe ListenableWorker.Result.success() + fun testSendNotificationWhenInvalid() = runBlockingTest { + val testResult = CoronaTestResult.PCR_INVALID + coronaTestFlow.value = setOf(newCoronaTest(result = testResult)) + coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs + coEvery { + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } just Runs + + val result = createWorker().doWork() + + coVerify { + coronaTestRepository.refresh(type = CoronaTest.Type.PCR) + testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + coronaTestRepository.updateResultNotification(any(), sent = true) } + + result shouldBe ListenableWorker.Result.success() } @Test - fun testRetryWhenExceptionIsThrown() { - runBlockingTest { - coEvery { submissionService.asyncRequestTestResult(registrationToken) } throws Exception() - val worker = createWorker() - val result = worker.doWork() - coVerify(exactly = 1) { submissionService.asyncRequestTestResult(any()) } - coVerify(exactly = 0) { backgroundWorkScheduler.stopDiagnosisTestResultPeriodicWork() } - result shouldBe ListenableWorker.Result.retry() + fun testSendNoNotificationWhenPending() = runBlockingTest { + val testResult = CoronaTestResult.PCR_OR_RAT_PENDING + coronaTestFlow.value = setOf(newCoronaTest(result = testResult)) + coEvery { testResultAvailableNotificationService.showTestResultAvailableNotification(testResult) } just Runs + coEvery { + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + } just Runs + + val result = createWorker().doWork() + + coVerify { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) } + coVerify(exactly = 0) { + testResultAvailableNotificationService.showTestResultAvailableNotification( + testResult + ) + notificationHelper.cancelCurrentNotification( + NotificationConstants.NEW_MESSAGE_RISK_LEVEL_SCORE_NOTIFICATION_ID + ) + testResultScheduler.setPeriodicTestPolling(enabled = false) } + + result shouldBe ListenableWorker.Result.success() } - private fun createWorker() = DiagnosisTestResultRetrievalPeriodicWorker( - context, - workerParams, - testResultAvailableNotificationService, - notificationHelper, - submissionSettings, - submissionService, - timeStamper, - tracingSettings, - backgroundWorkScheduler, - ) + @Test + fun testRetryWhenExceptionIsThrown() = runBlockingTest { + coronaTestFlow.value = setOf(newCoronaTest()) + coEvery { coronaTestRepository.refresh(any()) } throws Exception() + + val result = createWorker().doWork() + + coVerify(exactly = 1) { coronaTestRepository.refresh(type = CoronaTest.Type.PCR) } + coVerify(exactly = 0) { testResultScheduler.setPeriodicTestPolling(any()) } + result shouldBe ListenableWorker.Result.retry() + } }