From 24ba3c02ea5c6c672b562fe8f7b669cd0199af50 Mon Sep 17 00:00:00 2001 From: Mohamed <mohamed.metwalli@sap.com> Date: Mon, 19 Apr 2021 15:38:00 +0200 Subject: [PATCH] Check-in selection in submission flow (EXPOSUREAPP-6514) (#2851) * Pump version patch * CheckIns Consent (EXPOSUREAPP-6530) (#2855) * Initial implementation * Bind selectable check-ins item * Select items * Update strings * lint * show dialog * Update strings.xml * Delegate to ViewModel * Correct text * Consider pre-consent * Move screen to actual graph * Add unit tests * Select All behaviour * Tweak padding and background color in darkmode. Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> * Add submission consent property to checkin database (EXPOSUREAPP-6514) (#2859) * Add new Check-In property for submission consent. * Fix unit tests. * Wire up new consent (EXPOSUREAPP-6532) (#2860) * Wire CheckIns consent * Move to business logic * Handle back navigation * Fix test * Use completedCheckIns extension * Update CheckIns for submission * Renaming * Extend unit tests, test for CheckIn validity. * Fix KLINT * More unit tests * Lint * PR Comments Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> * Reset old selection (DEV) (#2870) Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> Co-authored-by: harambasicluka <64483219+harambasicluka@users.noreply.github.com> --- .../2.json | 204 +++++++++++ .../storage/CheckInDatabaseData.kt | 2 + .../PresenceTracingDatabaseMigrationTest.kt | 133 +++++++ ...bmissionTestResultAvailableFragmentTest.kt | 13 +- .../ui/PresenceTracingTestFragment.kt | 5 + .../layout/fragment_test_presence_tracing.xml | 11 +- .../eventregistration/checkins/CheckIn.kt | 2 + .../checkins/CheckInRepository.kt | 19 +- .../storage/TraceLocationDatabase.kt | 8 +- .../storage/dao/CheckInDao.kt | 3 + .../entity/TraceLocationCheckInEntity.kt | 11 +- .../PresenceTracingDatabaseMigration1To2.kt | 38 ++ .../common/CheckInRepositoryExtension.kt | 13 + .../submission/task/SubmissionTask.kt | 7 +- .../EventRegistrationUIModule.kt | 5 + .../attendee/checkins/items/PastCheckInVH.kt | 15 +- .../checkins/common/CompletedCheckIn.kt | 16 + .../consent/CheckInsConsentAdapter.kt | 37 ++ .../consent/CheckInsConsentFragment.kt | 106 ++++++ .../consent/CheckInsConsentFragmentModule.kt | 19 + .../checkins/consent/CheckInsConsentItem.kt | 5 + .../consent/CheckInsConsentNavigation.kt | 9 + .../consent/CheckInsConsentViewModel.kt | 165 +++++++++ .../checkins/consent/HeaderCheckInsVH.kt | 31 ++ .../checkins/consent/SelectableCheckInVH.kt | 44 +++ .../SubmissionTestResultAvailableViewModel.kt | 34 +- ...tPositiveOtherWarningNoConsentViewModel.kt | 39 +- .../res/layout/check_ins_consent_fragment.xml | 56 +++ ...trace_location_attendee_consent_header.xml | 25 ++ ...n_attendee_consent_selectable_check_in.xml | 67 ++++ .../src/main/res/navigation/nav_graph.xml | 34 ++ .../src/main/res/values/styles.xml | 10 +- .../checkins/CheckInRepositoryTest.kt | 3 + .../submission/task/SubmissionTaskTest.kt | 28 +- .../consent/CheckInsConsentViewModelTest.kt | 338 ++++++++++++++++++ ...missionTestResultAvailableViewModelTest.kt | 5 +- ...itiveOtherWarningNoConsentViewModelTest.kt | 5 +- gradle.properties | 2 +- 38 files changed, 1510 insertions(+), 57 deletions(-) create mode 100644 Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json create mode 100644 Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/presencetracing/checkins/common/CheckInRepositoryExtension.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/common/CompletedCheckIn.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentAdapter.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragment.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentFragmentModule.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentItem.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentNavigation.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/HeaderCheckInsVH.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/SelectableCheckInVH.kt create mode 100644 Corona-Warn-App/src/main/res/layout/check_ins_consent_fragment.xml create mode 100644 Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_header.xml create mode 100644 Corona-Warn-App/src/main/res/layout/trace_location_attendee_consent_selectable_check_in.xml create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.storage.TraceLocationDatabase/2.json new file mode 100644 index 000000000..006ae7f97 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.eventregistration.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/eventregistration/storage/CheckInDatabaseData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt index 1ab9a7725..bcc28c18c 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/storage/CheckInDatabaseData.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/eventregistration/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/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/presencetracing/migration/PresenceTracingDatabaseMigrationTest.kt new file mode 100644 index 000000000..dcb7f5e09 --- /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.eventregistration.storage.TraceLocationDatabase +import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity +import de.rki.coronawarnapp.eventregistration.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.eventCheckInDao().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/ui/submission/SubmissionTestResultAvailableFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/submission/SubmissionTestResultAvailableFragmentTest.kt index 33dc537e4..b92a122c6 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.eventregistration.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 @@ -57,11 +59,12 @@ class SubmissionTestResultAvailableFragmentTest : BaseUITest() { 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/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 8e54311d1..d80da8bff 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.eventregistration.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/res/layout/fragment_test_presence_tracing.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_presence_tracing.xml index dba2acc1b..19c37f7ec 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/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt index 8fea0b27a..36db8dd52 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckIn.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/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/eventregistration/checkins/CheckInRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt index ea9d8b74b..a1a12c842 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/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/eventregistration/storage/TraceLocationDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt index 30e508cf4..8a5232531 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/TraceLocationDatabase.kt @@ -10,6 +10,7 @@ import de.rki.coronawarnapp.eventregistration.storage.dao.TraceLocationDao import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationCheckInEntity import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationConverters import de.rki.coronawarnapp.eventregistration.storage.entity.TraceLocationEntity +import de.rki.coronawarnapp.eventregistration.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/eventregistration/storage/dao/CheckInDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt index 56ed45b2a..fd75d2f76 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/dao/CheckInDao.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/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/eventregistration/storage/entity/TraceLocationCheckInEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt index 8ddcadd2a..0017cb099 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/entity/TraceLocationCheckInEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/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/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt new file mode 100644 index 000000000..e055de482 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/eventregistration/storage/migration/PresenceTracingDatabaseMigration1To2.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.eventregistration.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/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 000000000..811c995ea --- /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.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.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/submission/task/SubmissionTask.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/submission/task/SubmissionTask.kt index 5f801d507..718359832 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 @@ -10,6 +10,7 @@ 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.common.completedCheckIns import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.submission.Symptoms import de.rki.coronawarnapp.submission.auto.AutoSubmission @@ -143,7 +144,9 @@ 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) @@ -171,7 +174,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") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt index 07b90907c..d97eb3591 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/EventRegistrationUIModule.kt @@ -24,6 +24,8 @@ import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFr import de.rki.coronawarnapp.ui.eventregistration.organizer.poster.QrCodePosterFragmentModule import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragment import de.rki.coronawarnapp.ui.eventregistration.organizer.qrinfo.TraceLocationQRInfoFragmentModule +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragment +import de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent.CheckInsConsentFragmentModule @Module internal abstract class EventRegistrationUIModule { @@ -60,4 +62,7 @@ internal abstract class EventRegistrationUIModule { @ContributesAndroidInjector(modules = [QrCodeDetailFragmentModule::class]) abstract fun showEventDetail(): QrCodeDetailFragment + + @ContributesAndroidInjector(modules = [CheckInsConsentFragmentModule::class]) + abstract fun checkInsConsentFragment(): CheckInsConsentFragment } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt index 4ae061dcb..8f8a84313 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/attendee/checkins/items/PastCheckInVH.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/eventregistration/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.eventregistration.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/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 000000000..37acd4f5e --- /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.eventregistration.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 000000000..fbd39ba15 --- /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 000000000..048e2edd9 --- /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.fragment.app.Fragment +import androidx.activity.OnBackPressedCallback +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 000000000..d7fd545fd --- /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 000000000..53f9a7859 --- /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 000000000..ff3bbef59 --- /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 000000000..0945210df --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModel.kt @@ -0,0 +1,165 @@ +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.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository +import de.rki.coronawarnapp.submission.SubmissionRepository +import de.rki.coronawarnapp.submission.auto.AutoSubmission +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns +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.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) { + + private val selectedSetFlow = MutableStateFlow(initialSet()) + + 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 (submissionRepository.hasViewedTestResult.first()) { + 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 (submissionRepository.hasViewedTestResult.first()) { + Timber.d("Navigate to SubmissionResultReadyFragment") + CheckInsConsentNavigation.ToSubmissionResultReadyFragment + } else { + Timber.d("Navigate to SubmissionTestResultConsentGivenFragment") + CheckInsConsentNavigation.ToSubmissionTestResultConsentGivenFragment + } + events.postValue(event) + } + + fun onCloseClick() = launch { + val event = if (submissionRepository.hasViewedTestResult.first()) { + 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 000000000..397b621ac --- /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 000000000..ae2060f42 --- /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.eventregistration.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/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/resultavailable/SubmissionTestResultAvailableViewModel.kt index 3e5d0a7cb..78d338084 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 @@ -8,11 +8,13 @@ import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -24,13 +26,14 @@ 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) { val routeToScreen = SingleLiveEvent<NavDirections>() - val consentFlow = submissionRepository.hasGivenConsentToSubmission + private val consentFlow = submissionRepository.hasGivenConsentToSubmission val consent = consentFlow.asLiveData(dispatcherProvider.Default) val showPermissionRequest = SingleLiveEvent<(Activity) -> Unit>() val showCloseDialog = SingleLiveEvent<Unit>() @@ -39,14 +42,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 +69,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) } @@ -109,10 +119,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 +140,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/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/ui/submission/warnothers/SubmissionResultPositiveOtherWarningNoConsentViewModel.kt index da6c4e514..0b6bee532 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 @@ -9,6 +9,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.Screen +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.nearby.ENFClient @@ -16,6 +17,7 @@ import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater +import de.rki.coronawarnapp.presencetracing.checkins.common.completedCheckIns import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -30,6 +32,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory, interoperabilityRepository: InteroperabilityRepository, private val submissionRepository: SubmissionRepository, + private val checkInRepository: CheckInRepository, private val analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -48,36 +51,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.") } @@ -96,10 +107,10 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con submissionRepository.giveConsentToSubmission() 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 +118,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModel @AssistedInject con } fun onDataPrivacyClick() { + Timber.tag(TAG).d("onDataPrivacyClick") routeToScreen.postValue( SubmissionResultPositiveOtherWarningNoConsentFragmentDirections .actionSubmissionResultPositiveOtherWarningNoConsentFragmentToInformationPrivacyFragment() @@ -114,6 +126,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 +139,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/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 000000000..37cd803b9 --- /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/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 000000000..04f98963f --- /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 000000000..703729940 --- /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/navigation/nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/nav_graph.xml index 6a025e839..44416b8d9 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/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index ecacc05a6..263879c3f 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> @@ -289,9 +293,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/eventregistration/checkins/CheckInRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt index 23ea1802a..393a07fcf 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/checkins/CheckInRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/eventregistration/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/submission/task/SubmissionTaskTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/submission/task/SubmissionTaskTest.kt index 778d26c72..e36271385 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 @@ -74,7 +74,7 @@ class SubmissionTaskTest : BaseTest() { private val settingLastUserActivityUTC: FlowPreference<Instant> = mockFlowPreference(Instant.EPOCH.plus(1)) - private val testCheckIn1 = CheckIn( + private val validCheckIn = CheckIn( id = 1L, traceLocationId = mockk(), version = 1, @@ -88,11 +88,16 @@ 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) @@ -133,7 +138,14 @@ class SubmissionTaskTest : BaseTest() { every { timeStamper.nowUTC } returns Instant.EPOCH.plus(Duration.standardHours(1)) - every { checkInRepository.checkInsWithinRetention } returns flowOf(listOf(testCheckIn1)) + every { checkInRepository.checkInsWithinRetention } returns flowOf( + listOf( + validCheckIn, + invalidCheckIn1, + invalidCheckIn2, + invalidCheckIn3 + ) + ) coEvery { checkInsTransformer.transform(any(), any()) } returns emptyList() } @@ -199,7 +211,7 @@ 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) @@ -210,6 +222,12 @@ class SubmissionTaskTest : BaseTest() { shareTestResultNotificationService.cancelSharePositiveTestResultNotification() testResultAvailableNotificationService.cancelTestResultAvailableNotification() } + + coVerify(exactly = 0) { + checkInRepository.updatePostSubmissionFlags(invalidCheckIn1.id) + checkInRepository.updatePostSubmissionFlags(invalidCheckIn2.id) + checkInRepository.updatePostSubmissionFlags(invalidCheckIn3.id) + } } @Test 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 000000000..6f8c68bbd --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/presencetracing/attendee/checkins/consent/CheckInsConsentViewModelTest.kt @@ -0,0 +1,338 @@ +package de.rki.coronawarnapp.ui.presencetracing.attendee.checkins.consent + +import androidx.lifecycle.SavedStateHandle +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.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 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 + ) + + @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.hasViewedTestResult } returns flowOf(false) + 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`() { + every { submissionRepository.hasViewedTestResult } returns flowOf(true) + createViewModel().apply { + onCloseClick() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenSkipDialog + } + } + + @Test + fun `Close opens closeDialog when test result has not been shown`() { + every { submissionRepository.hasViewedTestResult } returns flowOf(false) + createViewModel().apply { + onCloseClick() + events.getOrAwaitValue() shouldBe CheckInsConsentNavigation.OpenCloseDialog + } + } + + @Test + fun `shareSelectedCheckIns when test result has been shown`() { + every { submissionRepository.hasViewedTestResult } returns flowOf(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`() { + every { submissionRepository.hasViewedTestResult } returns flowOf(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`() { + every { submissionRepository.hasViewedTestResult } returns flowOf(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`() { + every { submissionRepository.hasViewedTestResult } returns flowOf(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/testavailable/SubmissionTestResultAvailableViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/ui/submission/testavailable/SubmissionTestResultAvailableViewModelTest.kt index f06833355..18f920516 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,7 @@ package de.rki.coronawarnapp.ui.submission.testavailable import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.auto.AutoSubmission import de.rki.coronawarnapp.submission.data.tekhistory.TEKHistoryUpdater @@ -31,6 +32,7 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() { @MockK lateinit var tekHistoryUpdater: TEKHistoryUpdater @MockK lateinit var tekHistoryUpdaterFactory: TEKHistoryUpdater.Factory @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var checkInRepository: CheckInRepository @BeforeEach fun setUp() { @@ -49,7 +51,8 @@ class SubmissionTestResultAvailableViewModelTest : BaseTest() { dispatcherProvider = TestDispatcherProvider(), tekHistoryUpdaterFactory = tekHistoryUpdaterFactory, autoSubmission = autoSubmission, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + checkInRepository = checkInRepository ) @Test 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 6b3e2fdc4..e4aab954d 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,6 +1,7 @@ package de.rki.coronawarnapp.ui.submission.warnothers import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.nearby.ENFClient import de.rki.coronawarnapp.storage.interoperability.InteroperabilityRepository import de.rki.coronawarnapp.submission.SubmissionRepository @@ -33,6 +34,7 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() { @MockK lateinit var interoperabilityRepository: InteroperabilityRepository @MockK lateinit var enfClient: ENFClient @MockK lateinit var analyticsKeySubmissionCollector: AnalyticsKeySubmissionCollector + @MockK lateinit var checkInRepository: CheckInRepository @BeforeEach fun setUp() { @@ -53,7 +55,8 @@ class SubmissionResultPositiveOtherWarningNoConsentViewModelTest : BaseTest() { enfClient = enfClient, interoperabilityRepository = interoperabilityRepository, submissionRepository = submissionRepository, - analyticsKeySubmissionCollector = analyticsKeySubmissionCollector + analyticsKeySubmissionCollector = analyticsKeySubmissionCollector, + checkInRepository = checkInRepository ) @Test diff --git a/gradle.properties b/gradle.properties index d80947505..c87a56f93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,5 +19,5 @@ org.gradle.dependency.verification.console=verbose # Versioning, this is used by the app & pipelines to calculate the current versionCode & versionName VERSION_MAJOR=2 VERSION_MINOR=0 -VERSION_PATCH=2 +VERSION_PATCH=3 VERSION_BUILD=0 -- GitLab