From 310db328d6ae33e5eb7834fa5968b88de062cf98 Mon Sep 17 00:00:00 2001 From: Juraj Kusnier <jurajkusnier@users.noreply.github.com> Date: Tue, 25 May 2021 10:44:14 +0200 Subject: [PATCH] Contact Journal Extension - Store RAT and PCR Tests results (EXPOSUREAPP-6041) (#3238) * prepare new TestResultRepository * Code refactoring * Update tests * Remove old files * Code refactoring * Move TestJournal Table to ContactDiaryDatabase * remove unused files * revert DataReset.kt * rename ContactDiaryTest > ContactDiaryCoronaTest * rename ContactDiaryTestEntity > ContactDiaryCoronaTestEntity * add some tests --- .../4.json | 282 ++++++++++++++++++ .../ContactDiaryDatabaseMigrationTest.kt | 73 +++++ .../storage/ContactDiaryDatabaseTest.kt | 37 +++ .../coronatest/ui/CoronaTestTestFragment.kt | 22 +- .../ui/CoronaTestTestFragmentViewModel.kt | 8 + .../res/layout/fragment_test_coronatest.xml | 27 ++ .../storage/ContactDiaryDatabase.kt | 15 +- .../storage/dao/ContactDiaryCoronaTestDao.kt | 21 ++ .../entity/ContactDiaryCoronaTestEntity.kt | 56 ++++ .../converters/ContactDiaryRoomConverters.kt | 15 + .../ContactDiaryDatabaseMigration3To4.kt | 38 +++ .../storage/repo/ContactDiaryRepository.kt | 7 + .../repo/DefaultContactDiaryRepository.kt | 20 ++ .../coronatest/CoronaTestRepository.kt | 3 + .../coronatest/type/CoronaTest.kt | 4 +- .../coronatest/type/pcr/PCRCoronaTest.kt | 6 +- .../type/rapidantigen/RACoronaTest.kt | 6 +- .../storage/CoronaTestStorageTest.kt | 4 - .../rki/coronawarnapp/util/DataResetTest.kt | 2 +- 19 files changed, 620 insertions(+), 26 deletions(-) create mode 100644 Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json new file mode 100644 index 000000000..393379694 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/4.json @@ -0,0 +1,282 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "3ed51cebcbafb1960bf5194f27748e12", + "entities": [ + { + "tableName": "locations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationName` TEXT NOT NULL, `phoneNumber` TEXT, `emailAddress` TEXT, `traceLocationID` TEXT)", + "fields": [ + { + "fieldPath": "locationId", + "columnName": "locationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "locationName", + "columnName": "locationName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phoneNumber", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emailAddress", + "columnName": "emailAddress", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "traceLocationID", + "columnName": "traceLocationID", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "locationId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "locationvisits", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` TEXT NOT NULL, `fkLocationId` INTEGER NOT NULL, `duration` INTEGER, `circumstances` TEXT, `checkInID` INTEGER, FOREIGN KEY(`fkLocationId`) REFERENCES `locations`(`locationId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fkLocationId", + "columnName": "fkLocationId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "circumstances", + "columnName": "circumstances", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "checkInID", + "columnName": "checkInID", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_locationvisits_fkLocationId", + "unique": false, + "columnNames": [ + "fkLocationId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_locationvisits_fkLocationId` ON `${TABLE_NAME}` (`fkLocationId`)" + } + ], + "foreignKeys": [ + { + "table": "locations", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "fkLocationId" + ], + "referencedColumns": [ + "locationId" + ] + } + ] + }, + { + "tableName": "persons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`personId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fullName` TEXT NOT NULL, `phoneNumber` TEXT, `emailAddress` TEXT)", + "fields": [ + { + "fieldPath": "personId", + "columnName": "personId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullName", + "columnName": "fullName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phoneNumber", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "emailAddress", + "columnName": "emailAddress", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "personId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "personencounters", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` TEXT NOT NULL, `fkPersonId` INTEGER NOT NULL, `durationClassification` TEXT, `withMask` INTEGER, `wasOutside` INTEGER, `circumstances` TEXT, FOREIGN KEY(`fkPersonId`) REFERENCES `persons`(`personId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fkPersonId", + "columnName": "fkPersonId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "durationClassification", + "columnName": "durationClassification", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "withMask", + "columnName": "withMask", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "wasOutside", + "columnName": "wasOutside", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "circumstances", + "columnName": "circumstances", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_personencounters_fkPersonId", + "unique": false, + "columnNames": [ + "fkPersonId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_personencounters_fkPersonId` ON `${TABLE_NAME}` (`fkPersonId`)" + } + ], + "foreignKeys": [ + { + "table": "persons", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "fkPersonId" + ], + "referencedColumns": [ + "personId" + ] + } + ] + }, + { + "tableName": "corona_tests", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `testType` TEXT NOT NULL, `result` TEXT NOT NULL, `time` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "testType", + "columnName": "testType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "result", + "columnName": "result", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "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, '3ed51cebcbafb1960bf5194f27748e12')" + ] + } +} \ No newline at end of file diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt index ca8693d3e..17c6963cb 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt @@ -16,6 +16,7 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEncoun import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration1To2 import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration2To3 +import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration3To4 import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import kotlinx.coroutines.flow.first @@ -231,6 +232,78 @@ class ContactDiaryDatabaseMigrationTest : BaseTestInstrumentation() { } } + @Test + fun migrate3To4() { + val location = ContactDiaryLocationEntity( + locationId = 1, + locationName = "My Location Name", + phoneNumber = "1234567890", + emailAddress = "email@address.com", + traceLocationID = null + ) + + val locationVisit = ContactDiaryLocationVisitEntity( + id = 2, + date = LocalDate.parse("2020-12-31"), + fkLocationId = 1, + duration = Duration.standardMinutes(13), + circumstances = "N/A", + checkInID = null + ) + + val locationAfter = location.copy(traceLocationID = "jshrgu-aifhioaio-aofsjof-samofp-kjsadngsgf".decodeBase64()) + val locationVisitAfter = locationVisit.copy(checkInID = 101) + + val locationValues = ContentValues().apply { + put("locationId", location.locationId) + put("locationName", location.locationName) + put("phoneNumber", location.phoneNumber) + put("emailAddress", location.emailAddress) + } + + val locationVisitValues = ContentValues().apply { + put("id", locationVisit.id) + put("date", locationVisit.date.toString()) + put("fkLocationId", locationVisit.fkLocationId) + put("duration", locationVisit.duration?.millis) + put("circumstances", locationVisit.circumstances) + } + + helper.createDatabase(DB_NAME, 3).apply { + insert("locations", SQLiteDatabase.CONFLICT_FAIL, locationValues) + insert("locationvisits", SQLiteDatabase.CONFLICT_FAIL, locationVisitValues) + close() + } + + // Run migration + helper.runMigrationsAndValidate( + DB_NAME, + 4, + true, + ContactDiaryDatabaseMigration3To4 + ) + + val daoDb = ContactDiaryDatabase.Factory( + ctx = ApplicationProvider.getApplicationContext() + ).create(databaseName = DB_NAME) + + runBlocking { + daoDb.locationVisitDao().allEntries().first().single() shouldBe ContactDiaryLocationVisitWrapper( + contactDiaryLocationEntity = location, + contactDiaryLocationVisitEntity = locationVisit + ) + + // Test if new attributes are added correctly + daoDb.locationDao().update(locationAfter) + daoDb.locationVisitDao().update(locationVisitAfter) + + daoDb.locationVisitDao().allEntries().first().single() shouldBe ContactDiaryLocationVisitWrapper( + contactDiaryLocationEntity = locationAfter, + contactDiaryLocationVisitEntity = locationVisitAfter + ) + } + } + @Test fun migrate1To2_failure_throws() { helper.createDatabase(DB_NAME, 1).apply { diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt index 6cdc211e2..fdbd5777c 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseTest.kt @@ -4,6 +4,11 @@ import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.NEGATIVE +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.POSITIVE +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.ANTIGEN +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.PCR import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitWrapper @@ -16,6 +21,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking import okio.ByteString.Companion.decodeBase64 import org.joda.time.Duration +import org.joda.time.Instant import org.joda.time.LocalDate import org.junit.After import org.junit.Test @@ -57,6 +63,12 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() { circumstances = "I had to buy snacks.", checkInID = 101 ) + private val coronaTest = ContactDiaryCoronaTestEntity( + id = "123-456-7890", + testType = PCR, + result = POSITIVE, + time = Instant.now() + ) // DB private val contactDiaryDatabase: ContactDiaryDatabase = Room.inMemoryDatabaseBuilder( @@ -68,6 +80,7 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() { private val locationDao = contactDiaryDatabase.locationDao() private val personEncounterDao = contactDiaryDatabase.personEncounterDao() private val locationVisitDao = contactDiaryDatabase.locationVisitDao() + private val coronaTestsDao = contactDiaryDatabase.coronaTestDao() private fun List<ContactDiaryPersonEncounterWrapper>.toContactDiaryPersonEncounterEntityList(): List<ContactDiaryPersonEncounterEntity> = this.map { it.contactDiaryPersonEncounterEntity } @@ -229,4 +242,28 @@ class ContactDiaryDatabaseTest : BaseTestInstrumentation() { personEncounterDao.update(updatedEncounter) personEncounterFlow.first().single() shouldBe updatedEncounter } + + @Test + fun updatingCoronaTests(): Unit = runBlocking { + val coronaTestsFlow = coronaTestsDao.allTests() + + coronaTestsDao.insertTest(coronaTest) + coronaTestsFlow.first().single() shouldBe coronaTest + + val updatedTest = coronaTest.copy( + time = Instant.now(), + result = NEGATIVE, + testType = ANTIGEN + ) + + coronaTestsDao.insertTest(updatedTest) + coronaTestsFlow.first().single() shouldBe coronaTest + + val newTest = coronaTest.copy( + id = "AAAAA-AAAAA-AAAAAA" + ) + + coronaTestsDao.insertTest(newTest) + coronaTestsFlow.first().containsAll(listOf(coronaTest, newTest)) shouldBe true + } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt index 1209ca39b..4eaae484d 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragment.kt @@ -26,7 +26,7 @@ import javax.inject.Inject class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), AutoInject { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val vm: CoronaTestTestFragmentViewModel by cwaViewModels { viewModelFactory } + private val viewModel: CoronaTestTestFragmentViewModel by cwaViewModels { viewModelFactory } private val binding: FragmentTestCoronatestBinding by viewBinding() @@ -50,7 +50,7 @@ class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), Auto qrcodeScanContainer.isVisible = true qrcodeScanPreview.resume() qrcodeScanPreview.decodeSingle { result -> - vm.onQRCodeScanner(result) + viewModel.onQRCodeScanner(result) stop() } } @@ -65,26 +65,30 @@ class CoronaTestTestFragment : Fragment(R.layout.fragment_test_coronatest), Auto qrcodeScanViewfinder.setCameraPreview(binding.qrcodeScanPreview) } - vm.pcrtState.observe2(this) { + viewModel.pcrtState.observe2(this) { binding.pcrtData.text = it.getNiceTextForHumans() } binding.apply { - pcrtDeleteAction.setOnClickListener { vm.deletePCRT() } - pcrtRefreshAction.setOnClickListener { vm.refreshPCRT() } + pcrtDeleteAction.setOnClickListener { viewModel.deletePCRT() } + pcrtRefreshAction.setOnClickListener { viewModel.refreshPCRT() } } - vm.ratState.observe2(this) { + viewModel.ratState.observe2(this) { binding.ratData.text = it.getNiceTextForHumans() } binding.apply { - ratDeleteAction.setOnClickListener { vm.deleteRAT() } - ratRefreshAction.setOnClickListener { vm.refreshRAT() } + ratDeleteAction.setOnClickListener { viewModel.deleteRAT() } + ratRefreshAction.setOnClickListener { viewModel.refreshRAT() } } - vm.errorEvents.observe2(this) { + viewModel.errorEvents.observe2(this) { val error = it.tryHumanReadableError(requireContext()) Toast.makeText(requireContext(), error.description, Toast.LENGTH_LONG).show() } + + viewModel.testsInContactDiary.observe2(this) { + binding.testsOutput.text = it + } } companion object { diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt index df524f3e1..544158248 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/coronatest/ui/CoronaTestTestFragmentViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.asLiveData import com.journeyapps.barcodescanner.BarcodeResult import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.coronatest.CoronaTestRepository import de.rki.coronawarnapp.coronatest.latestPCRT import de.rki.coronawarnapp.coronatest.latestRAT @@ -23,6 +24,7 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, private val coronaTestRepository: CoronaTestRepository, private val coronaTestQrCodeValidator: CoronaTestQrCodeValidator, + contactDiaryRepository: ContactDiaryRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { val errorEvents = SingleLiveEvent<Throwable>() @@ -38,6 +40,12 @@ class CoronaTestTestFragmentViewModel @AssistedInject constructor( ) }.asLiveData(context = dispatcherProvider.Default) + val testsInContactDiary = contactDiaryRepository.testResults.map { + it.foldIndexed(StringBuilder()) { id, buffer, item -> + buffer.append(id).append(":\n").append(item).append("\n") + }.toString() + }.asLiveData(context = dispatcherProvider.Default) + fun refreshPCRT() = launch { try { Timber.d("Refreshing PCR") diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml index 3d0c99217..04454809e 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_coronatest.xml @@ -168,5 +168,32 @@ </androidx.constraintlayout.widget.ConstraintLayout> + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/contact_diary_tests_container" + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/spacing_tiny"> + + <TextView + android:id="@+id/contact_diary_tests_title" + style="@style/headline6Sixteen" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:text="Contact diary tests" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:layout_marginTop="8dp" + android:id="@+id/tests_output" + app:layout_constraintTop_toBottomOf="@id/contact_diary_tests_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + </LinearLayout> </androidx.core.widget.NestedScrollView> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt index e7081eac5..8a99d68de 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabase.kt @@ -9,13 +9,16 @@ import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationDao import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationVisitDao import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonDao import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonEncounterDao +import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryCoronaTestDao import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEncounterEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity import de.rki.coronawarnapp.contactdiary.storage.internal.converters.ContactDiaryRoomConverters import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration1To2 import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration2To3 +import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration3To4 import de.rki.coronawarnapp.util.database.CommonConverters import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Inject @@ -25,9 +28,10 @@ import javax.inject.Inject ContactDiaryLocationEntity::class, ContactDiaryLocationVisitEntity::class, ContactDiaryPersonEntity::class, - ContactDiaryPersonEncounterEntity::class + ContactDiaryPersonEncounterEntity::class, + ContactDiaryCoronaTestEntity::class ], - version = 3, + version = 4, exportSchema = true ) @TypeConverters(CommonConverters::class, ContactDiaryRoomConverters::class) @@ -37,11 +41,16 @@ abstract class ContactDiaryDatabase : RoomDatabase() { abstract fun locationVisitDao(): ContactDiaryLocationVisitDao abstract fun personDao(): ContactDiaryPersonDao abstract fun personEncounterDao(): ContactDiaryPersonEncounterDao + abstract fun coronaTestDao(): ContactDiaryCoronaTestDao class Factory @Inject constructor(@AppContext private val ctx: Context) { fun create(databaseName: String = CONTACT_DIARY_DATABASE_NAME): ContactDiaryDatabase = Room .databaseBuilder(ctx, ContactDiaryDatabase::class.java, databaseName) - .addMigrations(ContactDiaryDatabaseMigration1To2, ContactDiaryDatabaseMigration2To3) + .addMigrations( + ContactDiaryDatabaseMigration1To2, + ContactDiaryDatabaseMigration2To3, + ContactDiaryDatabaseMigration3To4 + ) .build() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt new file mode 100644 index 000000000..6e12d9afb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/dao/ContactDiaryCoronaTestDao.kt @@ -0,0 +1,21 @@ +package de.rki.coronawarnapp.contactdiary.storage.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ContactDiaryCoronaTestDao { + + @Query("SELECT * FROM corona_tests") + fun allTests(): Flow<List<ContactDiaryCoronaTestEntity>> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertTest(contactDiaryCoronaTestEntity: ContactDiaryCoronaTestEntity) + + @Query("DELETE FROM corona_tests") + suspend fun deleteAll() +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt new file mode 100644 index 000000000..70f6e1104 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryCoronaTestEntity.kt @@ -0,0 +1,56 @@ +package de.rki.coronawarnapp.contactdiary.storage.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.gson.annotations.SerializedName +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID +import de.rki.coronawarnapp.coronatest.type.CoronaTest +import de.rki.coronawarnapp.coronatest.type.rapidantigen.RACoronaTest +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.PCR +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestType.ANTIGEN +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.POSITIVE +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity.TestResult.NEGATIVE +import org.joda.time.Instant + +@Entity(tableName = "corona_tests") +data class ContactDiaryCoronaTestEntity( + @PrimaryKey @ColumnInfo(name = "id") val id: String, + @ColumnInfo(name = "testType") val testType: TestType, + @ColumnInfo(name = "result") val result: TestResult, + @ColumnInfo(name = "time") val time: Instant +) { + enum class TestType(val raw: String) { + @SerializedName("PCR") + PCR("pcr"), + + @SerializedName("ANTIGEN") + ANTIGEN("antigen"); + } + + enum class TestResult(val raw: String) { + @SerializedName("POSITIVE") + POSITIVE("POSITIVE"), + + @SerializedName("NEGATIVE") + NEGATIVE("NEGATIVE"); + } +} + +fun CoronaTest.canBeAddedToJournal(): Boolean { + return isViewed && (isNegative || isPositive) +} + +fun Map.Entry<CoronaTestGUID, CoronaTest>.asTestResultEntity(): ContactDiaryCoronaTestEntity { + return with(value) { + ContactDiaryCoronaTestEntity( + id = key, + testType = if (type == CoronaTest.Type.PCR) PCR else ANTIGEN, + result = if (isPositive) POSITIVE else NEGATIVE, + time = when (this) { + is RACoronaTest -> testedAt + else -> registeredAt + } + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt index 95821990d..b7c034529 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.contactdiary.storage.internal.converters import androidx.room.TypeConverter import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity import org.joda.time.Duration class ContactDiaryRoomConverters { @@ -25,4 +26,18 @@ class ContactDiaryRoomConverters { fun fromJodaDuration(duration: Duration?): Long? { return duration?.millis } + + @TypeConverter + fun toTestType(value: String?): ContactDiaryCoronaTestEntity.TestType? = + ContactDiaryCoronaTestEntity.TestType.values().singleOrNull { it.raw == value } + + @TypeConverter + fun fromTestType(type: ContactDiaryCoronaTestEntity.TestType?): String? = type?.raw + + @TypeConverter + fun toTestResult(value: String?): ContactDiaryCoronaTestEntity.TestResult? = + ContactDiaryCoronaTestEntity.TestResult.values().singleOrNull { it.raw == value } + + @TypeConverter + fun fromTestResult(type: ContactDiaryCoronaTestEntity.TestResult?): String? = type?.raw } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt new file mode 100644 index 000000000..a789eed65 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration3To4.kt @@ -0,0 +1,38 @@ +package de.rki.coronawarnapp.contactdiary.storage.internal.migrations + +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 contact diary database from schema version 3 to schema version 4. + * We are adding additional table for storing test results + */ +@Suppress("MaxLineLength") +object ContactDiaryDatabaseMigration3To4 : Migration(3, 4) { + + override fun migrate(database: SupportSQLiteDatabase) { + try { + Timber.i("Attempting migration 3->4...") + performMigration(database) + Timber.i("Migration 3->4 successful.") + } catch (e: Exception) { + Timber.e(e, "Migration 3->4 failed") + e.report(ExceptionCategory.INTERNAL, "ContactDiary database migration failed.") + throw e + } + } + + private fun performMigration(database: SupportSQLiteDatabase) = with(database) { + Timber.d("Running MIGRATION_3_4") + + migrateTestTable() + } + + private val migrateTestTable: SupportSQLiteDatabase.() -> Unit = { + Timber.d("Create 'corona_tests' table") + execSQL("CREATE TABLE IF NOT EXISTS corona_tests (`id` TEXT NOT NULL, `testType` TEXT NOT NULL, `result` TEXT NOT NULL, `time` TEXT NOT NULL, PRIMARY KEY(`id`))") + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt index c37acddbe..bd2b7c365 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/ContactDiaryRepository.kt @@ -4,6 +4,9 @@ import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID +import de.rki.coronawarnapp.coronatest.type.CoronaTest import kotlinx.coroutines.flow.Flow import org.joda.time.LocalDate @@ -52,6 +55,10 @@ interface ContactDiaryRepository { suspend fun deletePersonEncounters(contactDiaryPersonEncounters: List<ContactDiaryPersonEncounter>) suspend fun deleteAllPersonEncounters() + // Tests + val testResults: Flow<List<ContactDiaryCoronaTestEntity>> + suspend fun updateTests(tests: Map<CoronaTestGUID, CoronaTest>) + // Clean suspend fun clear() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt index e94102ed8..279192f28 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/repo/DefaultContactDiaryRepository.kt @@ -10,6 +10,10 @@ import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationDao import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryLocationVisitDao import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonDao import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryPersonEncounterDao +import de.rki.coronawarnapp.contactdiary.storage.dao.ContactDiaryCoronaTestDao +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryCoronaTestEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.asTestResultEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.canBeAddedToJournal import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationEntity import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryLocationVisitEntity @@ -18,6 +22,8 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEnco import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounterEntity import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEncounterSortedList import de.rki.coronawarnapp.contactdiary.storage.entity.toContactDiaryPersonEntity +import de.rki.coronawarnapp.coronatest.qrcode.CoronaTestGUID +import de.rki.coronawarnapp.coronatest.type.CoronaTest import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import org.joda.time.LocalDate @@ -47,6 +53,10 @@ class DefaultContactDiaryRepository @Inject constructor( contactDiaryDatabase.personEncounterDao() } + private val contactDiaryCoronaTestDao: ContactDiaryCoronaTestDao by lazy { + contactDiaryDatabase.coronaTestDao() + } + // Location override val locations: Flow<List<ContactDiaryLocation>> by lazy { contactDiaryLocationDao @@ -243,6 +253,16 @@ class DefaultContactDiaryRepository @Inject constructor( contactDiaryPersonEncounterDao.deleteAll() } + override val testResults: Flow<List<ContactDiaryCoronaTestEntity>> by lazy { + contactDiaryCoronaTestDao.allTests() + } + + override suspend fun updateTests(tests: Map<CoronaTestGUID, CoronaTest>) { + tests.filter { it.value.canBeAddedToJournal() } + .map { it.asTestResultEntity() } + .forEach { contactDiaryCoronaTestDao.insertTest(it) } + } + private suspend fun executeWhenIdNotDefault(id: Long, action: (suspend () -> Unit) = { }) { if (id != 0L) { action() diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt index 740b2545e..8bf90d76a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/CoronaTestRepository.kt @@ -1,6 +1,7 @@ package de.rki.coronawarnapp.coronatest import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.coronatest.errors.CoronaTestNotFoundException import de.rki.coronawarnapp.coronatest.errors.DuplicateCoronaTestException import de.rki.coronawarnapp.coronatest.errors.UnknownTestTypeException @@ -39,6 +40,7 @@ class CoronaTestRepository @Inject constructor( private val storage: CoronaTestStorage, private val processors: Set<@JvmSuppressWildcards CoronaTestProcessor>, private val legacyMigration: PCRTestMigration, + private val contactDiaryRepository: ContactDiaryRepository ) { private val internalData: HotDataFlow<Map<CoronaTestGUID, CoronaTest>> = HotDataFlow( @@ -62,6 +64,7 @@ class CoronaTestRepository @Inject constructor( Timber.tag(TAG).v("CoronaTest data changed: %s", it) storage.coronaTests = it.values.toSet() legacyMigration.finishMigration() + contactDiaryRepository.updateTests(it) } .catch { it.reportProblem(TAG, "Failed to snapshot CoronaTest data to storage.") diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt index 02353dcc1..dea5510d6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTest.kt @@ -19,6 +19,7 @@ interface CoronaTest { val isViewed: Boolean val isPositive: Boolean + val isNegative: Boolean val isPending: Boolean @@ -35,9 +36,6 @@ interface CoronaTest { // TODO why do we need this PER test val isAdvancedConsentGiven: Boolean - // TODO Why do we need to store this? - val isJournalEntryCreated: Boolean - val isResultAvailableNotificationSent: Boolean enum class Type(val raw: String) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt index 78c38a1d8..f9ef2816d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRCoronaTest.kt @@ -26,9 +26,6 @@ data class PCRCoronaTest( @SerializedName("isAdvancedConsentGiven") override val isAdvancedConsentGiven: Boolean = false, - @SerializedName("isJournalEntryCreated") - override val isJournalEntryCreated: Boolean = false, - @SerializedName("isResultAvailableNotificationSent") override val isResultAvailableNotificationSent: Boolean = false, @@ -54,6 +51,9 @@ data class PCRCoronaTest( override val isPositive: Boolean get() = testResult == CoronaTestResult.PCR_POSITIVE + override val isNegative: Boolean + get() = testResult == CoronaTestResult.PCR_NEGATIVE + override val isPending: Boolean get() = testResult == CoronaTestResult.PCR_OR_RAT_PENDING diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt index 6e353f9b9..2af0abee2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RACoronaTest.kt @@ -34,9 +34,6 @@ data class RACoronaTest( @SerializedName("isAdvancedConsentGiven") override val isAdvancedConsentGiven: Boolean = false, - @SerializedName("isJournalEntryCreated") - override val isJournalEntryCreated: Boolean = false, - @SerializedName("isResultAvailableNotificationSent") override val isResultAvailableNotificationSent: Boolean = false, @@ -92,6 +89,9 @@ data class RACoronaTest( override val isPositive: Boolean get() = testResult == RAT_POSITIVE + override val isNegative: Boolean + get() = testResult == RAT_NEGATIVE + override val isPending: Boolean get() = setOf(PCR_OR_RAT_PENDING, RAT_PENDING).contains(testResult) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt index c3e2537a4..100c26a50 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/storage/CoronaTestStorageTest.kt @@ -47,7 +47,6 @@ class CoronaTestStorageTest : BaseTest() { isSubmitted = true, isViewed = true, isAdvancedConsentGiven = true, - isJournalEntryCreated = false, isResultAvailableNotificationSent = false, testResult = CoronaTestResult.PCR_POSITIVE, testResultReceivedAt = Instant.ofEpochMilli(2000), @@ -60,7 +59,6 @@ class CoronaTestStorageTest : BaseTest() { isSubmitted = true, isViewed = true, isAdvancedConsentGiven = true, - isJournalEntryCreated = false, isResultAvailableNotificationSent = false, testResult = CoronaTestResult.RAT_POSITIVE, testResultReceivedAt = Instant.ofEpochMilli(2000), @@ -109,7 +107,6 @@ class CoronaTestStorageTest : BaseTest() { "isSubmitted": true, "isViewed": true, "isAdvancedConsentGiven": true, - "isJournalEntryCreated": false, "isResultAvailableNotificationSent": false, "testResultReceivedAt": 2000, "testResult": 2, @@ -148,7 +145,6 @@ class CoronaTestStorageTest : BaseTest() { "isSubmitted": true, "isViewed": true, "isAdvancedConsentGiven": true, - "isJournalEntryCreated": false, "isResultAvailableNotificationSent": false, "testResultReceivedAt": 2000, "lastUpdatedAt": 2001, diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt index 3dbba9d53..6f8799d44 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/util/DataResetTest.kt @@ -24,8 +24,8 @@ import de.rki.coronawarnapp.storage.TracingSettings import de.rki.coronawarnapp.submission.SubmissionRepository import de.rki.coronawarnapp.submission.SubmissionSettings import de.rki.coronawarnapp.ui.presencetracing.TraceLocationPreferences -import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository import de.rki.coronawarnapp.vaccination.core.VaccinationPreferences +import de.rki.coronawarnapp.vaccination.core.repository.VaccinationRepository import de.rki.coronawarnapp.vaccination.core.repository.ValueSetsRepository import io.mockk.MockKAnnotations import io.mockk.coVerify -- GitLab