diff --git a/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/2.json b/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/2.json new file mode 100644 index 0000000000000000000000000000000000000000..7fac999cccd2e6a75fb830f388529d813b2b4de6 --- /dev/null +++ b/Corona-Warn-App/schemas/de.rki.coronawarnapp.contactdiary.storage.ContactDiaryDatabase/2.json @@ -0,0 +1,232 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "d702472d6dd506b73ff6a7b340686c9a", + "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)", + "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 + } + ], + "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, 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 + } + ], + "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" + ] + } + ] + } + ], + "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, 'd702472d6dd506b73ff6a7b340686c9a')" + ] + } +} \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..bd23cf8461818998bacbad0daf0f931762ab63a4 --- /dev/null +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/contactdiary/storage/ContactDiaryDatabaseMigrationTest.kt @@ -0,0 +1,196 @@ +package de.rki.coronawarnapp.contactdiary.storage + +import android.database.sqlite.SQLiteException +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.contactdiary.storage.entity.ContactDiaryLocationEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitWrapper +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEncounterEntity +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEncounterWrapper +import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity +import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration1To2 +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.joda.time.LocalDate +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import testhelpers.BaseTest +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class ContactDiaryDatabaseMigrationTest : BaseTest() { + private val DB_NAME = "contactdiary_migration_test.db" + + @get:Rule + val helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + ContactDiaryDatabase::class.java.canonicalName, + FrameworkSQLiteOpenHelperFactory() + ) + + /** + * Test migration to add new optional attributes + */ + @Test + fun migrate1To2() { + helper.createDatabase(DB_NAME, 1).apply { + execSQL( + """ + INSERT INTO "locations" ( + "locationId", + "locationName" + ) VALUES ( + '1', + 'Location1' + ); + """.trimIndent() + ) + execSQL( + """ + INSERT INTO "persons" ( + "personId", + "fullName" + ) VALUES ( + '100', + 'Person100' + ); + """.trimIndent() + ) + execSQL( + """ + INSERT INTO "locationvisits" ( + "id", + "date", + "fkLocationId" + ) VALUES ( + '2', + '2020-04-20', + '1' + ); + """.trimIndent() + ) + + execSQL( + """ + INSERT INTO "personencounters" ( + "id", + "date", + "fkPersonId" + ) VALUES ( + '3', + '2020-12-31', + '100' + ); + """.trimIndent() + ) + + close() + } + + // Run migration + helper.runMigrationsAndValidate( + DB_NAME, + 2, + true, + ContactDiaryDatabaseMigration1To2 + ) + + val daoDb = ContactDiaryDatabase.Factory( + ctx = ApplicationProvider.getApplicationContext() + ).create(databaseName = DB_NAME) + + val location = ContactDiaryLocationEntity( + locationId = 1, + locationName = "Location1", + phoneNumber = null, + emailAddress = null + ) + runBlocking { daoDb.locationDao().allEntries().first() }.single() shouldBe location + + val person = ContactDiaryPersonEntity( + personId = 100, + fullName = "Person100", + phoneNumber = null, + emailAddress = null + ) + runBlocking { daoDb.personDao().allEntries().first() }.single() shouldBe person + + runBlocking { + daoDb.locationVisitDao().allEntries().first() + }.single() shouldBe ContactDiaryLocationVisitWrapper( + contactDiaryLocationEntity = location, + contactDiaryLocationVisitEntity = ContactDiaryLocationVisitEntity( + id = 2, + date = LocalDate.parse("2020-04-20"), + fkLocationId = 1, + duration = null, + circumstances = null + ) + ) + + runBlocking { + daoDb.personEncounterDao().allEntries().first() + }.single() shouldBe ContactDiaryPersonEncounterWrapper( + contactDiaryPersonEntity = person, + contactDiaryPersonEncounterEntity = ContactDiaryPersonEncounterEntity( + id = 3, + date = LocalDate.parse("2020-12-31"), + fkPersonId = 100, + withMask = null, + wasOutside = null, + durationClassification = null, + circumstances = null + ) + ) + } + + @Test + fun migrate1To2_failure_throws() { + helper.createDatabase(DB_NAME, 1).apply { + execSQL("DROP TABLE IF EXISTS locations") + execSQL("DROP TABLE IF EXISTS locationvisits") + execSQL("DROP TABLE IF EXISTS persons") + execSQL("DROP TABLE IF EXISTS personencounters") + // Has incompatible existing column phoneNumber of wrong type + execSQL("CREATE TABLE IF NOT EXISTS `locations` (`locationId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `locationName` TEXT NOT NULL, `phoneNumber` INTEGER )") + execSQL("CREATE TABLE IF NOT EXISTS `persons` (`personId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `fullName` TEXT NOT NULL)") + execSQL("CREATE TABLE IF NOT EXISTS `locationvisits` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` TEXT NOT NULL, `fkLocationId` INTEGER NOT NULL, FOREIGN KEY(`fkLocationId`) REFERENCES `locations`(`locationId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)") + execSQL("CREATE TABLE IF NOT EXISTS `personencounters` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` TEXT NOT NULL, `fkPersonId` INTEGER NOT NULL, FOREIGN KEY(`fkPersonId`) REFERENCES `persons`(`personId`) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)") + close() + } + + shouldThrow<SQLiteException> { + // Run migration + helper.runMigrationsAndValidate( + DB_NAME, + 2, + true, + ContactDiaryDatabaseMigration1To2 + ) + } + } + + @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. + ContactDiaryDatabase.Factory( + ctx = ApplicationProvider.getApplicationContext() + ).create(databaseName = DB_NAME).apply { + openHelper.writableDatabase + close() + } + } +} 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 11fb352a5b7f982f910ade41c89c384dd121783c..6853d1150cb63ba33f903ccf2f61a0cc1a580c98 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 @@ -3,6 +3,7 @@ package de.rki.coronawarnapp.contactdiary.storage 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.ContactDiaryLocationEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitEntity import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationVisitWrapper @@ -13,6 +14,7 @@ import io.kotest.matchers.shouldBe import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +import org.joda.time.Duration import org.joda.time.LocalDate import org.junit.After import org.junit.Test @@ -24,18 +26,40 @@ class ContactDiaryDatabaseTest : BaseTest() { // TestData private val date = LocalDate.now() - private val person = ContactDiaryPersonEntity(personId = 1, fullName = "Peter") - private val location = ContactDiaryLocationEntity(locationId = 2, locationName = "Rewe Wiesloch") - private val personEncounter = ContactDiaryPersonEncounterEntity(id = 3, date = date, fkPersonId = person.personId) - private val locationVisit = ContactDiaryLocationVisitEntity(id = 4, date = date, fkLocationId = location.locationId) + private val person = ContactDiaryPersonEntity( + personId = 1, + fullName = "Peter", + emailAddress = "person-emailAddress", + phoneNumber = "person-phoneNumber" + ) + private val location = ContactDiaryLocationEntity( + locationId = 2, + locationName = "Rewe Wiesloch", + emailAddress = "location-emailAddress", + phoneNumber = "location-phoneNumber" + ) + private val personEncounter = ContactDiaryPersonEncounterEntity( + id = 3, + date = date, + fkPersonId = person.personId, + durationClassification = ContactDiaryPersonEncounter.DurationClassification.MORE_THAN_15_MINUTES, + withMask = true, + wasOutside = false, + circumstances = "You could see the smile under his mask." + ) + private val locationVisit = ContactDiaryLocationVisitEntity( + id = 4, + date = date, + fkLocationId = location.locationId, + duration = Duration.standardMinutes(99), + circumstances = "I had to buy snacks." + ) // DB - private val contactDiaryDatabase: ContactDiaryDatabase = - Room.inMemoryDatabaseBuilder( - ApplicationProvider.getApplicationContext(), - ContactDiaryDatabase::class.java - ) - .build() + private val contactDiaryDatabase: ContactDiaryDatabase = Room.inMemoryDatabaseBuilder( + ApplicationProvider.getApplicationContext(), + ContactDiaryDatabase::class.java + ).build() private val personDao = contactDiaryDatabase.personDao() private val locationDao = contactDiaryDatabase.locationDao() @@ -97,14 +121,38 @@ class ContactDiaryDatabaseTest : BaseTest() { fun getCorrectEntityForDate() = runBlocking { val yesterday = LocalDate.now().minusDays(1) val tomorrow = LocalDate.now().plusDays(1) - val personEncounterYesterday = - ContactDiaryPersonEncounterEntity(id = 5, date = yesterday, fkPersonId = person.personId) - val personEncounterTomorrow = - ContactDiaryPersonEncounterEntity(id = 6, date = tomorrow, fkPersonId = person.personId) - val locationVisitYesterday = - ContactDiaryLocationVisitEntity(id = 7, date = yesterday, fkLocationId = location.locationId) - val locationVisitTomorrow = - ContactDiaryLocationVisitEntity(id = 8, date = tomorrow, fkLocationId = location.locationId) + val personEncounterYesterday = ContactDiaryPersonEncounterEntity( + id = 5, + date = yesterday, + fkPersonId = person.personId, + durationClassification = ContactDiaryPersonEncounter.DurationClassification.LESS_THAN_15_MINUTES, + withMask = false, + wasOutside = false, + circumstances = "encounter-yesterday" + ) + val personEncounterTomorrow = ContactDiaryPersonEncounterEntity( + id = 6, + date = tomorrow, + fkPersonId = person.personId, + durationClassification = ContactDiaryPersonEncounter.DurationClassification.MORE_THAN_15_MINUTES, + withMask = true, + wasOutside = true, + circumstances = "encounter-today" + ) + val locationVisitYesterday = ContactDiaryLocationVisitEntity( + id = 7, + date = yesterday, + fkLocationId = location.locationId, + duration = Duration.standardMinutes(42), + circumstances = "visit-yesterday" + ) + val locationVisitTomorrow = ContactDiaryLocationVisitEntity( + id = 8, + date = tomorrow, + fkLocationId = location.locationId, + duration = Duration.standardMinutes(1), + circumstances = "visit-today" + ) val encounterList = listOf(personEncounter, personEncounterYesterday, personEncounterTomorrow) val visitList = listOf(locationVisit, locationVisitYesterday, locationVisitTomorrow) val personEncounterFlow = personEncounterDao.allEntries().map { it.toContactDiaryPersonEncounterEntityList() } @@ -119,12 +167,61 @@ class ContactDiaryDatabaseTest : BaseTest() { personEncounterFlow.first() shouldBe encounterList locationVisitFlow.first() shouldBe visitList - personEncounterDao.entitiesForDate(yesterday).first().toContactDiaryPersonEncounterEntityList() shouldBe listOf(personEncounterYesterday) - personEncounterDao.entitiesForDate(date).first().toContactDiaryPersonEncounterEntityList() shouldBe listOf(personEncounter) - personEncounterDao.entitiesForDate(tomorrow).first().toContactDiaryPersonEncounterEntityList() shouldBe listOf(personEncounterTomorrow) + personEncounterDao.entitiesForDate(yesterday).first().toContactDiaryPersonEncounterEntityList() shouldBe listOf( + personEncounterYesterday + ) + personEncounterDao.entitiesForDate(date).first().toContactDiaryPersonEncounterEntityList() shouldBe listOf( + personEncounter + ) + personEncounterDao.entitiesForDate(tomorrow).first().toContactDiaryPersonEncounterEntityList() shouldBe listOf( + personEncounterTomorrow + ) + + locationVisitDao.entitiesForDate(yesterday).first().toContactDiaryLocationVisitEntityList() shouldBe listOf( + locationVisitYesterday + ) + locationVisitDao.entitiesForDate(date).first().toContactDiaryLocationVisitEntityList() shouldBe listOf( + locationVisit + ) + locationVisitDao.entitiesForDate(tomorrow).first().toContactDiaryLocationVisitEntityList() shouldBe listOf( + locationVisitTomorrow + ) + } + + @Test + fun updatingLocationVisits(): Unit = runBlocking { + val locationVisitFlow = locationVisitDao.allEntries().map { it.toContactDiaryLocationVisitEntityList() } + + locationDao.insert(location) + locationVisitDao.insert(listOf(locationVisit)) + + locationVisitFlow.first().single() shouldBe locationVisit + + val updatedLocation = locationVisit.copy( + duration = Duration.millis(123L), + circumstances = "Suspicious" + ) + locationVisitDao.update(updatedLocation) + + locationVisitFlow.first().single() shouldBe updatedLocation + } - locationVisitDao.entitiesForDate(yesterday).first().toContactDiaryLocationVisitEntityList() shouldBe listOf(locationVisitYesterday) - locationVisitDao.entitiesForDate(date).first().toContactDiaryLocationVisitEntityList() shouldBe listOf(locationVisit) - locationVisitDao.entitiesForDate(tomorrow).first().toContactDiaryLocationVisitEntityList() shouldBe listOf(locationVisitTomorrow) + @Test + fun updatingPersonEncounters(): Unit = runBlocking { + val personEncounterFlow = personEncounterDao.allEntries().map { it.toContactDiaryPersonEncounterEntityList() } + + personDao.insert(person) + personEncounterDao.insert(personEncounter) + + personEncounterFlow.first().single() shouldBe personEncounter + + val updatedEncounter = personEncounter.copy( + withMask = true, + wasOutside = false, + durationClassification = ContactDiaryPersonEncounter.DurationClassification.MORE_THAN_15_MINUTES, + circumstances = "He lend me a coffee cup but the handle broke and it dropped onto my laptop." + ) + personEncounterDao.update(updatedEncounter) + personEncounterFlow.first().single() shouldBe updatedEncounter } } diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryDayFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryDayFragmentTest.kt index 2202440649b5b02ff4377e738bb7e04853500402..578c99ae6d7b47aa319708cc0b2f5b4c0a59a74c 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryDayFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryDayFragmentTest.kt @@ -7,17 +7,16 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import dagger.Module import dagger.android.ContributesAndroidInjector import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation -import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.contactdiary.ui.day.ContactDiaryDayFragment import de.rki.coronawarnapp.contactdiary.ui.day.ContactDiaryDayFragmentArgs import de.rki.coronawarnapp.contactdiary.ui.day.ContactDiaryDayViewModel import de.rki.coronawarnapp.contactdiary.ui.day.tabs.location.ContactDiaryLocationListFragment import de.rki.coronawarnapp.contactdiary.ui.day.tabs.location.ContactDiaryLocationListViewModel +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.location.DiaryLocationListItem import de.rki.coronawarnapp.contactdiary.ui.day.tabs.person.ContactDiaryPersonListFragment import de.rki.coronawarnapp.contactdiary.ui.day.tabs.person.ContactDiaryPersonListViewModel -import de.rki.coronawarnapp.contactdiary.util.SelectableItem +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.person.DiaryPersonListItem import de.rki.coronawarnapp.contactdiary.util.toFormattedDay import de.rki.coronawarnapp.ui.contactdiary.DiaryData.LOCATIONS import de.rki.coronawarnapp.ui.contactdiary.DiaryData.PERSONS @@ -33,13 +32,13 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import testhelpers.BaseUITest -import testhelpers.takeScreenshot import testhelpers.Screenshot import testhelpers.SystemUIDemoModeRule import testhelpers.TestDispatcherProvider import testhelpers.launchFragment2 import testhelpers.launchFragmentInContainer2 import testhelpers.selectTabAtPosition +import testhelpers.takeScreenshot import tools.fastlane.screengrab.locale.LocaleTestRule import tools.fastlane.screengrab.locale.LocaleUtil import java.util.Locale @@ -103,8 +102,8 @@ class ContactDiaryDayFragmentTest : BaseUITest() { } private fun captureScreen( - persons: List<SelectableItem<ContactDiaryPerson>>, - locations: List<SelectableItem<ContactDiaryLocation>>, + persons: List<DiaryPersonListItem>, + locations: List<DiaryLocationListItem>, suffix: String ) { every { personListViewModel.uiList } returns MutableLiveData(persons) diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt index c6b8e97ffd73055f414903dfca305524848b04c4..55b95459ee73e9658f413de65d6ebbf0c1b5f1bd 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/DiaryData.kt @@ -4,10 +4,14 @@ import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocation +import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryPerson +import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.location.DiaryLocationListItem +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.person.DiaryPersonListItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.ListItem -import de.rki.coronawarnapp.contactdiary.util.SelectableItem -import de.rki.coronawarnapp.util.ui.toLazyString +import org.joda.time.Duration +import org.joda.time.LocalDate object DiaryData { @@ -15,24 +19,42 @@ object DiaryData { ListItem.Data( R.drawable.ic_contact_diary_person_item, "Max Mustermann", + null, + listOf( + R.string.contact_diary_person_encounter_duration_below_15_min, + R.string.contact_diary_person_encounter_mask_with, + R.string.contact_diary_person_encounter_environment_inside + ), + "Notizen notizen", ListItem.Type.PERSON ), ListItem.Data( R.drawable.ic_contact_diary_person_item, "Erika Mustermann", + null, + listOf( + R.string.contact_diary_person_encounter_environment_inside + ), + "Notizen notizen", ListItem.Type.PERSON ), ListItem.Data( R.drawable.ic_contact_diary_location, "Fitnessstudio", + Duration.millis(1800000), + null, + "Notizen notizen", ListItem.Type.LOCATION ), ListItem.Data( R.drawable.ic_contact_diary_location, "Supermarket", + null, + null, + null, ListItem.Type.LOCATION ) ) @@ -49,69 +71,95 @@ object DiaryData { R.drawable.ic_low_risk_alert ) - val LOCATIONS: List<SelectableItem<ContactDiaryLocation>> = listOf( - SelectableItem( - selected = true, + val LOCATIONS: List<DiaryLocationListItem> = listOf( + DiaryLocationListItem( item = DefaultContactDiaryLocation(locationName = "Sport"), - contentDescription = "".toLazyString(), - onClickDescription = "".toLazyString(), - clickLabel = R.string.accessibility_location, - onClickLabel = R.string.accessibility_location + visit = DefaultContactDiaryLocationVisit( + contactDiaryLocation = DefaultContactDiaryLocation(locationName = ""), + date = LocalDate.now() + ), + onItemClick = {}, + onDurationChanged = { _, _ -> }, + onCircumstancesChanged = { _, _ -> }, + onCircumStanceInfoClicked = {} ), - SelectableItem( - selected = true, + DiaryLocationListItem( item = DefaultContactDiaryLocation(locationName = "Büro"), - contentDescription = "".toLazyString(), - onClickDescription = "".toLazyString(), - clickLabel = R.string.accessibility_location, - onClickLabel = R.string.accessibility_location + visit = DefaultContactDiaryLocationVisit( + contactDiaryLocation = DefaultContactDiaryLocation(locationName = ""), + date = LocalDate.now() + ), + onItemClick = {}, + onDurationChanged = { _, _ -> }, + onCircumstancesChanged = { _, _ -> }, + onCircumStanceInfoClicked = {} ), - SelectableItem( - selected = false, + DiaryLocationListItem( item = DefaultContactDiaryLocation(locationName = "Supermarkt"), - contentDescription = "".toLazyString(), - onClickDescription = "".toLazyString(), - clickLabel = R.string.accessibility_location, - onClickLabel = R.string.accessibility_location + visit = null, + onItemClick = {}, + onDurationChanged = { _, _ -> }, + onCircumstancesChanged = { _, _ -> }, + onCircumStanceInfoClicked = {} ) ) - val PERSONS: List<SelectableItem<ContactDiaryPerson>> = listOf( - SelectableItem( - selected = true, + val PERSONS: List<DiaryPersonListItem> = listOf( + DiaryPersonListItem( item = DefaultContactDiaryPerson(fullName = "Erika Mustermann"), - contentDescription = "".toLazyString(), - onClickDescription = "".toLazyString(), - clickLabel = R.string.accessibility_person, - onClickLabel = R.string.accessibility_person + personEncounter = DefaultContactDiaryPersonEncounter( + contactDiaryPerson = DefaultContactDiaryPerson(fullName = ""), + date = LocalDate.now() + ), + onItemClick = {}, + onDurationChanged = { _, _ -> }, + onCircumstancesChanged = { _, _ -> }, + onWithMaskChanged = { _, _ -> }, + onWasOutsideChanged = { _, _ -> }, + onCircumstanceInfoClicked = {} ), - SelectableItem( - selected = true, + DiaryPersonListItem( item = DefaultContactDiaryPerson(fullName = "Max Mustermann"), - contentDescription = "".toLazyString(), - onClickDescription = "".toLazyString(), - clickLabel = R.string.accessibility_person, - onClickLabel = R.string.accessibility_person + personEncounter = DefaultContactDiaryPersonEncounter( + contactDiaryPerson = DefaultContactDiaryPerson(fullName = ""), + date = LocalDate.now() + ), + onItemClick = {}, + onDurationChanged = { _, _ -> }, + onCircumstancesChanged = { _, _ -> }, + onWithMaskChanged = { _, _ -> }, + onWasOutsideChanged = { _, _ -> }, + onCircumstanceInfoClicked = {} ), - SelectableItem( - selected = false, + DiaryPersonListItem( item = DefaultContactDiaryPerson(fullName = "John Doe"), - contentDescription = "".toLazyString(), - onClickDescription = "".toLazyString(), - clickLabel = R.string.accessibility_person, - onClickLabel = R.string.accessibility_person + personEncounter = null, + onItemClick = {}, + onDurationChanged = { _, _ -> }, + onCircumstancesChanged = { _, _ -> }, + onWithMaskChanged = { _, _ -> }, + onWasOutsideChanged = { _, _ -> }, + onCircumstanceInfoClicked = {} ) ) val LOCATIONS_EDIT_LIST: List<ContactDiaryLocation> = listOf( DefaultContactDiaryLocation(locationName = "Sport"), - DefaultContactDiaryLocation(locationName = "Büro"), + DefaultContactDiaryLocation( + locationName = "Büro", + phoneNumber = "+49153397029", + emailAddress = "office@work.com" + ), DefaultContactDiaryLocation(locationName = "Supermarkt") ) val PERSONS_EDIT_LIST: List<ContactDiaryPerson> = listOf( - DefaultContactDiaryPerson(fullName = "Max Mustermann"), - DefaultContactDiaryPerson(fullName = "Erika Mustermann"), + DefaultContactDiaryPerson( + fullName = "Max Mustermann", + phoneNumber = "+49151237865", + emailAddress = "max.musterman@me.com" + ), + DefaultContactDiaryPerson(fullName = "Erika Mustermann", emailAddress = "erika.mustermann@me.com"), DefaultContactDiaryPerson(fullName = "John Doe") ) } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryCommentInfoTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryCommentInfoTestFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..39a057c7646d9d23ee459f866004d5bb91a517f6 --- /dev/null +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryCommentInfoTestFragment.kt @@ -0,0 +1,31 @@ +package de.rki.coronawarnapp.test.contactdiary.ui + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.ContactDiaryCommentInfoFragmentBinding +import de.rki.coronawarnapp.test.menu.ui.TestMenuItem +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy + +class ContactDiaryCommentInfoTestFragment : Fragment(R.layout.contact_diary_comment_info_fragment) { + + private val binding: ContactDiaryCommentInfoFragmentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbar.setNavigationOnClickListener { + popBackStack() + } + } + + companion object { + val MENU_ITEM = TestMenuItem( + title = "Contact Diary Comment Info", + description = "Contact diary comment info screen", + targetId = R.id.test_contact_diary_comment_fragment + ) + } +} diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt index 5d067c9d65353d79255bb7171e7ba427332443de..7ff3c8f2039548359ae0d95a8412e986c6c077f9 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/contactdiary/ui/ContactDiaryTestFragment.kt @@ -5,6 +5,8 @@ import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.ui.durationpicker.ContactDiaryDurationPickerFragment +import de.rki.coronawarnapp.contactdiary.ui.durationpicker.toContactDiaryFormat import de.rki.coronawarnapp.databinding.FragmentTestContactDiaryBinding import de.rki.coronawarnapp.test.menu.ui.TestMenuItem import de.rki.coronawarnapp.util.di.AutoInject @@ -12,10 +14,14 @@ import de.rki.coronawarnapp.util.ui.observe2 import de.rki.coronawarnapp.util.ui.viewBindingLazy import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider import de.rki.coronawarnapp.util.viewmodel.cwaViewModels +import org.joda.time.Duration import javax.inject.Inject @SuppressLint("SetTextI18n") -class ContactDiaryTestFragment : Fragment(R.layout.fragment_test_contact_diary), AutoInject { +class ContactDiaryTestFragment : + Fragment(R.layout.fragment_test_contact_diary), + AutoInject, + ContactDiaryDurationPickerFragment.OnChangeListener { @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory private val vm: ContactDiaryTestFragmentViewModel by cwaViewModels { viewModelFactory } @@ -42,6 +48,31 @@ class ContactDiaryTestFragment : Fragment(R.layout.fragment_test_contact_diary), normalPersonEncountersButton.setOnClickListener { vm.createPersonEncounters(false) } locationVisitsCleanButton.setOnClickListener { vm.clearLocationVisits() } personEncountersCleanButton.setOnClickListener { vm.clearPersonEncounters() } + durationValue.setOnClickListener { + val args = Bundle() + args.putString( + ContactDiaryDurationPickerFragment.DURATION_ARGUMENT_KEY, + binding.durationValue.text.toString() + ) + + val durationPicker = ContactDiaryDurationPickerFragment() + durationPicker.arguments = args + durationPicker.setTargetFragment(this@ContactDiaryTestFragment, 0) + durationPicker.show(parentFragmentManager, "ContactDiaryDurationPickerFragment") + } + } + } + + override fun onChange(duration: Duration) { + with(binding.durationValue) { + text = duration.toContactDiaryFormat() + if (duration.millis == 0L) { + setBackgroundResource(R.drawable.contact_diary_duration_background_default) + setTextAppearance(R.style.bodyNeutral) + } else { + setBackgroundResource(R.drawable.contact_diary_duration_background_selected) + setTextAppearance(R.style.body1) + } } } diff --git a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt index 6d09d4e6bc5d38de5e6556b177aa116a3a37b415..7fdfb6b6f7bf074f727bd22a550a72c5b12adb80 100644 --- a/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt +++ b/Corona-Warn-App/src/deviceForTesters/java/de/rki/coronawarnapp/test/menu/ui/TestMenuFragmentViewModel.kt @@ -5,6 +5,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import de.rki.coronawarnapp.miscinfo.MiscInfoFragment import de.rki.coronawarnapp.test.appconfig.ui.AppConfigTestFragment +import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryCommentInfoTestFragment import de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryTestFragment import de.rki.coronawarnapp.test.crash.ui.SettingsCrashReportFragment import de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment @@ -34,7 +35,8 @@ class TestMenuFragmentViewModel @AssistedInject constructor() : CWAViewModel() { ContactDiaryTestFragment.MENU_ITEM, PlaygroundFragment.MENU_ITEM, DataDonationTestFragment.MENU_ITEM, - DeltaonboardingFragment.MENU_ITEM + DeltaonboardingFragment.MENU_ITEM, + ContactDiaryCommentInfoTestFragment.MENU_ITEM ).let { MutableLiveData(it) } } val showTestScreenEvent = SingleLiveEvent<TestMenuItem>() diff --git a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml index 248f44fa2b398f89f1a4a9b01e3de69116927664..8069c616d6f6db8c8de3515ac1167695900cbbbd 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/layout/fragment_test_contact_diary.xml @@ -14,6 +14,39 @@ android:orientation="vertical" android:paddingBottom="32dp"> + <androidx.constraintlayout.widget.ConstraintLayout + style="@style/Card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:backgroundTint="@color/colorContactDiaryListItem" + android:layout_margin="@dimen/spacing_tiny"> + + <TextView + android:id="@+id/duration_picker" + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Open duration picker dialog" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/duration_value" + style="@style/bodyNeutral" + android:layout_width="70dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/spacing_tiny" + android:paddingTop="11dp" + android:paddingBottom="11dp" + android:paddingLeft="13dp" + android:paddingRight="13dp" + android:text="@string/duration_dialog_default_value" + android:background="@drawable/contact_diary_duration_background_default" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/duration_picker"/> + + </androidx.constraintlayout.widget.ConstraintLayout> + <androidx.constraintlayout.widget.ConstraintLayout style="@style/Card" android:layout_width="match_parent" diff --git a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml index 3fd5c94a377c5b2af530a939ceecc65a54017cc8..5a10dea8abfb6f61fb5adcdded0d4b41c082e6b3 100644 --- a/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml +++ b/Corona-Warn-App/src/deviceForTesters/res/navigation/test_nav_graph.xml @@ -43,6 +43,9 @@ <action android:id="@+id/action_test_menu_fragment_to_dataDonationFragment" app:destination="@id/test_datadonation_fragment" /> + <action + android:id="@+id/action_test_menu_fragment_to_test_contact_diary_person_comment_fragment" + app:destination="@id/test_contact_diary_comment_fragment" /> <action android:id="@+id/action_test_menu_fragment_to_deltaonboardingFragment" app:destination="@id/test_deltaonboarding_fragment" /> @@ -112,17 +115,23 @@ <fragment android:id="@+id/playgroundFragment" android:name="de.rki.coronawarnapp.test.playground.ui.PlaygroundFragment" - tools:layout="@layout/fragment_test_playground" - android:label="PlaygroundFragment" /> + android:label="PlaygroundFragment" + tools:layout="@layout/fragment_test_playground" /> <fragment android:id="@+id/test_datadonation_fragment" - tools:layout="@layout/fragment_test_datadonation" android:name="de.rki.coronawarnapp.test.datadonation.ui.DataDonationTestFragment" - android:label="DataDonationFragment" /> + android:label="DataDonationFragment" + tools:layout="@layout/fragment_test_datadonation" /> <fragment android:id="@+id/test_deltaonboarding_fragment" android:name="de.rki.coronawarnapp.test.deltaonboarding.ui.DeltaonboardingFragment" android:label="DeltaonboardingFragment" tools:layout="@layout/fragment_test_deltaonboarding" /> + <fragment + android:id="@+id/test_contact_diary_comment_fragment" + android:name="de.rki.coronawarnapp.test.contactdiary.ui.ContactDiaryCommentInfoTestFragment" + android:label="CommentInfoTestFragment" + tools:layout="@layout/contact_diary_comment_info_fragment" /> + </navigation> diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt index d4192af041ce8789f72bb20a020d7e0d645126e0..1221b89bb7badc8431fbc5b73e148145799686f2 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocation.kt @@ -6,6 +6,8 @@ import java.util.Locale interface ContactDiaryLocation : HasStableId { val locationId: Long var locationName: String + val phoneNumber: String? + val emailAddress: String? } fun List<ContactDiaryLocation>.sortByNameAndIdASC(): List<ContactDiaryLocation> = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt index 2d477bcee18248a08aa8ecdfd9d4937f7e635ed2..d5eb4b2852a4ef1a1a26d59d765e762303f8e58b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryLocationVisit.kt @@ -1,5 +1,6 @@ package de.rki.coronawarnapp.contactdiary.model +import org.joda.time.Duration import org.joda.time.LocalDate import java.util.Locale @@ -7,6 +8,12 @@ interface ContactDiaryLocationVisit { val id: Long val date: LocalDate val contactDiaryLocation: ContactDiaryLocation + + /* + Value in miliseconds + */ + val duration: Duration? + val circumstances: String? } fun List<ContactDiaryLocationVisit>.sortByNameAndIdASC(): List<ContactDiaryLocationVisit> = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt index de391152bcfba7267c0d095124025137b2fc3214..0340fa66e10e5d3f7565b5ad4c69ada52a749039 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPerson.kt @@ -6,6 +6,8 @@ import java.util.Locale interface ContactDiaryPerson : HasStableId { val personId: Long var fullName: String + val phoneNumber: String? + val emailAddress: String? } fun List<ContactDiaryPerson>.sortByNameAndIdASC(): List<ContactDiaryPerson> = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt index 47975a267757d5a18018d5a21c46d088bf32da3f..be1c173f981f2730cf80736a11a8749c569f25e7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/ContactDiaryPersonEncounter.kt @@ -7,6 +7,17 @@ interface ContactDiaryPersonEncounter { val id: Long val date: LocalDate val contactDiaryPerson: ContactDiaryPerson + val durationClassification: DurationClassification? + val withMask: Boolean? + val wasOutside: Boolean? + val circumstances: String? + + enum class DurationClassification( + val key: String + ) { + LESS_THAN_15_MINUTES("LessThan15Minutes"), + MORE_THAN_15_MINUTES("MoreThan15Minutes") + } } fun List<ContactDiaryPersonEncounter>.sortByNameAndIdASC(): List<ContactDiaryPersonEncounter> = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt index 78ff94aa01a3af64f4036d54b4e8daeb14cc1321..3c20aa3cbdcd9dd56371687bacc227c80a8d7c4b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocation.kt @@ -2,7 +2,9 @@ package de.rki.coronawarnapp.contactdiary.model data class DefaultContactDiaryLocation( override val locationId: Long = 0L, - override var locationName: String + override var locationName: String, + override val phoneNumber: String? = null, + override val emailAddress: String? = null ) : ContactDiaryLocation { override val stableId: Long get() = locationId diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocationVisit.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocationVisit.kt index 0c2124a3c625123c3bac258c68db6e82e92e6702..e9c16848e2c7a7ffdf8a82d54f901e3779477e45 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocationVisit.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryLocationVisit.kt @@ -1,9 +1,23 @@ package de.rki.coronawarnapp.contactdiary.model +import org.joda.time.Duration import org.joda.time.LocalDate data class DefaultContactDiaryLocationVisit( override val id: Long = 0L, override val date: LocalDate, - override val contactDiaryLocation: ContactDiaryLocation + override val contactDiaryLocation: ContactDiaryLocation, + override val duration: Duration? = null, + override val circumstances: String? = null ) : ContactDiaryLocationVisit + +fun ContactDiaryLocationVisit.toEditableVariant(): DefaultContactDiaryLocationVisit { + if (this is DefaultContactDiaryLocationVisit) return this + return DefaultContactDiaryLocationVisit( + id = id, + date = date, + contactDiaryLocation = contactDiaryLocation, + duration = duration, + circumstances = circumstances + ) +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPerson.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPerson.kt index 911bd939bfc14b4fbdebc634d6ccdaf11bf68d7e..8827126fc9d1850d950573ffe1c4d274c38eee48 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPerson.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPerson.kt @@ -2,7 +2,9 @@ package de.rki.coronawarnapp.contactdiary.model data class DefaultContactDiaryPerson( override val personId: Long = 0L, - override var fullName: String + override var fullName: String, + override val phoneNumber: String? = null, + override val emailAddress: String? = null ) : ContactDiaryPerson { override val stableId: Long get() = personId diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPersonEncounter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPersonEncounter.kt index bdd303ef066b478fc64b0063e8800bab7a098285..409c766f41af39e645090e8182f5585a8aa22d6c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPersonEncounter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/model/DefaultContactDiaryPersonEncounter.kt @@ -5,5 +5,23 @@ import org.joda.time.LocalDate data class DefaultContactDiaryPersonEncounter( override val id: Long = 0L, override val date: LocalDate, - override val contactDiaryPerson: ContactDiaryPerson + override val contactDiaryPerson: ContactDiaryPerson, + override val durationClassification: ContactDiaryPersonEncounter.DurationClassification? = null, + override val withMask: Boolean? = null, + override val wasOutside: Boolean? = null, + override val circumstances: String? = null ) : ContactDiaryPersonEncounter + +fun ContactDiaryPersonEncounter.toEditableVariant(): DefaultContactDiaryPersonEncounter { + if (this is DefaultContactDiaryPersonEncounter) return this + + return DefaultContactDiaryPersonEncounter( + id = id, + date = date, + contactDiaryPerson = contactDiaryPerson, + durationClassification = durationClassification, + withMask = withMask, + wasOutside = wasOutside, + circumstances = circumstances + ) +} 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 b80474dca2a053ec526af698686352a6904cb838..bb7eb9c19a64048d56d2f992a978bdb80011065a 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 @@ -13,6 +13,8 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationEnti 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.internal.converters.ContactDiaryRoomConverters +import de.rki.coronawarnapp.contactdiary.storage.internal.migrations.ContactDiaryDatabaseMigration1To2 import de.rki.coronawarnapp.util.database.CommonConverters import de.rki.coronawarnapp.util.di.AppContext import javax.inject.Inject @@ -24,10 +26,10 @@ import javax.inject.Inject ContactDiaryPersonEntity::class, ContactDiaryPersonEncounterEntity::class ], - version = 1, + version = 2, exportSchema = true ) -@TypeConverters(CommonConverters::class) +@TypeConverters(CommonConverters::class, ContactDiaryRoomConverters::class) abstract class ContactDiaryDatabase : RoomDatabase() { abstract fun locationDao(): ContactDiaryLocationDao @@ -36,9 +38,9 @@ abstract class ContactDiaryDatabase : RoomDatabase() { abstract fun personEncounterDao(): ContactDiaryPersonEncounterDao class Factory @Inject constructor(@AppContext private val ctx: Context) { - fun create(): ContactDiaryDatabase = Room - .databaseBuilder(ctx, ContactDiaryDatabase::class.java, CONTACT_DIARY_DATABASE_NAME) - .fallbackToDestructiveMigration() + fun create(databaseName: String = CONTACT_DIARY_DATABASE_NAME): ContactDiaryDatabase = Room + .databaseBuilder(ctx, ContactDiaryDatabase::class.java, databaseName) + .addMigrations(ContactDiaryDatabaseMigration1To2) .build() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt index ed36e6276d4349e459baa7180fe60f778c7fd069..c7e21989fd781d408136627899c6b122adea96bc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationEntity.kt @@ -11,11 +11,18 @@ import kotlinx.parcelize.Parcelize @Entity(tableName = "locations") data class ContactDiaryLocationEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "locationId") override val locationId: Long = 0L, - @ColumnInfo(name = "locationName") override var locationName: String + @ColumnInfo(name = "locationName") override var locationName: String, + override val phoneNumber: String?, + override val emailAddress: String? ) : ContactDiaryLocation, Parcelable { override val stableId: Long get() = locationId } fun ContactDiaryLocation.toContactDiaryLocationEntity(): ContactDiaryLocationEntity = - ContactDiaryLocationEntity(this.locationId, this.locationName) + ContactDiaryLocationEntity( + locationId = this.locationId, + locationName = this.locationName, + phoneNumber = this.phoneNumber, + emailAddress = this.emailAddress + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitEntity.kt index 3bfdd7541c082e0559f490ae39a3fda5bef1ff6b..4dfe111b24f432c8555818aeab22edd926bba60b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitEntity.kt @@ -6,6 +6,8 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit +import de.rki.coronawarnapp.util.trimToLength +import org.joda.time.Duration import org.joda.time.LocalDate @Entity( @@ -25,8 +27,16 @@ import org.joda.time.LocalDate data class ContactDiaryLocationVisitEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0L, @ColumnInfo(name = "date") val date: LocalDate, - @ColumnInfo(name = "fkLocationId") val fkLocationId: Long + @ColumnInfo(name = "fkLocationId") val fkLocationId: Long, + @ColumnInfo(name = "duration") val duration: Duration?, + @ColumnInfo(name = "circumstances") val circumstances: String? ) fun ContactDiaryLocationVisit.toContactDiaryLocationVisitEntity(): ContactDiaryLocationVisitEntity = - ContactDiaryLocationVisitEntity(id = this.id, date = this.date, fkLocationId = this.contactDiaryLocation.locationId) + ContactDiaryLocationVisitEntity( + id = this.id, + date = this.date, + fkLocationId = this.contactDiaryLocation.locationId, + duration = this.duration, + circumstances = this.circumstances?.trimToLength(250) + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitWrapper.kt index 333659e2cdb0f3e8a116457c1bea579581906dcc..040de1b5c260a36759ae000e8196454d4e49b09e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitWrapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryLocationVisitWrapper.kt @@ -6,7 +6,7 @@ import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.model.sortByNameAndIdASC -class ContactDiaryLocationVisitWrapper( +data class ContactDiaryLocationVisitWrapper( @Embedded val contactDiaryLocationVisitEntity: ContactDiaryLocationVisitEntity, @Relation(parentColumn = "fkLocationId", entityColumn = "locationId") val contactDiaryLocationEntity: ContactDiaryLocationEntity @@ -16,7 +16,9 @@ fun ContactDiaryLocationVisitWrapper.toContactDiaryLocationVisit(): ContactDiary DefaultContactDiaryLocationVisit( id = this.contactDiaryLocationVisitEntity.id, date = this.contactDiaryLocationVisitEntity.date, - contactDiaryLocation = this.contactDiaryLocationEntity + contactDiaryLocation = this.contactDiaryLocationEntity, + duration = contactDiaryLocationVisitEntity.duration, + circumstances = contactDiaryLocationVisitEntity.circumstances ) fun List<ContactDiaryLocationVisitWrapper>.toContactDiaryLocationVisitSortedList(): List<ContactDiaryLocationVisit> = diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterEntity.kt index a98ba8661d9f28757e2adee4ce7cdd28f6655e89..a8df756f9fd1911c740bf273d0de5ed40c67afcb 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterEntity.kt @@ -6,6 +6,8 @@ import androidx.room.ForeignKey import androidx.room.Index import androidx.room.PrimaryKey import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter.DurationClassification +import de.rki.coronawarnapp.util.trimToLength import org.joda.time.LocalDate @Entity( @@ -25,8 +27,20 @@ import org.joda.time.LocalDate data class ContactDiaryPersonEncounterEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Long = 0L, @ColumnInfo(name = "date") val date: LocalDate, - @ColumnInfo(name = "fkPersonId") val fkPersonId: Long + @ColumnInfo(name = "fkPersonId") val fkPersonId: Long, + @ColumnInfo(name = "durationClassification") val durationClassification: DurationClassification?, + @ColumnInfo(name = "withMask") val withMask: Boolean?, + @ColumnInfo(name = "wasOutside") val wasOutside: Boolean?, + @ColumnInfo(name = "circumstances") val circumstances: String? ) fun ContactDiaryPersonEncounter.toContactDiaryPersonEncounterEntity(): ContactDiaryPersonEncounterEntity = - ContactDiaryPersonEncounterEntity(id = this.id, date = this.date, fkPersonId = this.contactDiaryPerson.personId) + ContactDiaryPersonEncounterEntity( + id = this.id, + date = this.date, + fkPersonId = this.contactDiaryPerson.personId, + durationClassification = this.durationClassification, + withMask = this.withMask, + wasOutside = this.wasOutside, + circumstances = this.circumstances?.trimToLength(250) + ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterWrapper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterWrapper.kt index 4371d4e3aeda27e4e557112f7a36b7e0fc736f7a..5d34506b2b1f552bcc1778ac742ff68c2a5f1cc3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterWrapper.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEncounterWrapper.kt @@ -16,7 +16,11 @@ fun ContactDiaryPersonEncounterWrapper.toContactDiaryPersonEncounter(): ContactD DefaultContactDiaryPersonEncounter( id = this.contactDiaryPersonEncounterEntity.id, date = this.contactDiaryPersonEncounterEntity.date, - contactDiaryPerson = contactDiaryPersonEntity + contactDiaryPerson = this.contactDiaryPersonEntity, + durationClassification = this.contactDiaryPersonEncounterEntity.durationClassification, + withMask = this.contactDiaryPersonEncounterEntity.withMask, + wasOutside = this.contactDiaryPersonEncounterEntity.wasOutside, + circumstances = this.contactDiaryPersonEncounterEntity.circumstances ) fun List<ContactDiaryPersonEncounterWrapper>.toContactDiaryPersonEncounterSortedList(): diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEntity.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEntity.kt index 0d1d1738d28c0b648327e6a18e09212eb752de06..f9a10824c76675487d7466cfb996a0a3131b517b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEntity.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/entity/ContactDiaryPersonEntity.kt @@ -11,11 +11,18 @@ import kotlinx.parcelize.Parcelize @Entity(tableName = "persons") data class ContactDiaryPersonEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "personId") override val personId: Long = 0L, - @ColumnInfo(name = "fullName") override var fullName: String + @ColumnInfo(name = "fullName") override var fullName: String, + @ColumnInfo(name = "phoneNumber") override val phoneNumber: String?, + @ColumnInfo(name = "emailAddress") override val emailAddress: String? ) : ContactDiaryPerson, Parcelable { override val stableId: Long get() = personId } fun ContactDiaryPerson.toContactDiaryPersonEntity(): ContactDiaryPersonEntity = - ContactDiaryPersonEntity(this.personId, this.fullName) + ContactDiaryPersonEntity( + personId = this.personId, + fullName = this.fullName, + phoneNumber = this.phoneNumber, + emailAddress = this.emailAddress + ) 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 new file mode 100644 index 0000000000000000000000000000000000000000..95821990d0dd7fd91dcccb6da51a8012793083d1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConverters.kt @@ -0,0 +1,28 @@ +package de.rki.coronawarnapp.contactdiary.storage.internal.converters + +import androidx.room.TypeConverter +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import org.joda.time.Duration + +class ContactDiaryRoomConverters { + @TypeConverter + fun toContactDurationClassification(value: String?): ContactDiaryPersonEncounter.DurationClassification? { + if (value == null) return null + return ContactDiaryPersonEncounter.DurationClassification.values().singleOrNull { it.key == value } + } + + @TypeConverter + fun fromContactDurationClassification(value: ContactDiaryPersonEncounter.DurationClassification?): String? { + return value?.key + } + + @TypeConverter + fun toJodaDuration(millis: Long?): Duration? { + return millis?.let { Duration.millis(it) } + } + + @TypeConverter + fun fromJodaDuration(duration: Duration?): Long? { + return duration?.millis + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration1To2.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration1To2.kt new file mode 100644 index 0000000000000000000000000000000000000000..ca5ad82ca3e31d1e445a847a528826e3feefc448 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/storage/internal/migrations/ContactDiaryDatabaseMigration1To2.kt @@ -0,0 +1,77 @@ +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 1 to schema version 2. + * We are adding additional columns for new optional attributes + * Person: PhoneNumber, Email + * Location: PhoneNumber, Email + * PersonEncounter: DurationType, Mask?, Outside?, Comment + * LocationVisit: Duration, Comment + */ +object ContactDiaryDatabaseMigration1To2 : 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, "ContactDiary database migration failed.") + throw e + } + } + + private fun performMigration(database: SupportSQLiteDatabase) = with(database) { + Timber.d("Running MIGRATION_1_2") + + migratePersonsTable() + migrateLocationsTable() + migratePersonEncounterTable() + migrateLocationVisitTable() + } + + private val migratePersonsTable: SupportSQLiteDatabase.() -> Unit = { + Timber.d("Table 'persons': Add column 'phoneNumber'") + execSQL("ALTER TABLE `persons` ADD COLUMN `phoneNumber` TEXT") + + Timber.d("Table 'emailAddress': Add column 'phoneNumber'") + execSQL("ALTER TABLE `persons` ADD COLUMN `emailAddress` TEXT") + } + + private val migrateLocationsTable: SupportSQLiteDatabase.() -> Unit = { + Timber.d("Table 'locations': Add column 'phoneNumber'") + execSQL("ALTER TABLE `locations` ADD COLUMN `phoneNumber` TEXT") + + Timber.d("Table 'locations': Add column 'emailAddress'") + execSQL("ALTER TABLE `locations` ADD COLUMN `emailAddress` TEXT") + } + + private val migratePersonEncounterTable: SupportSQLiteDatabase.() -> Unit = { + Timber.d("Table 'personencounters': Add column 'durationClassification'") + execSQL("ALTER TABLE `personencounters` ADD COLUMN `durationClassification` TEXT") + + Timber.d("Table 'personencounters': Add column 'circumstances'") + execSQL("ALTER TABLE `personencounters` ADD COLUMN `circumstances` TEXT") + + Timber.d("Table 'personencounters': Add column 'withMask'") + execSQL("ALTER TABLE `personencounters` ADD COLUMN `withMask` INTEGER") + + Timber.d("Table 'personencounters': Add column 'wasOutside'") + execSQL("ALTER TABLE `personencounters` ADD COLUMN `wasOutside` INTEGER") + } + + private val migrateLocationVisitTable: SupportSQLiteDatabase.() -> Unit = { + Timber.d("Table 'locationvisits': Add column 'duration'") + execSQL("ALTER TABLE `locationvisits` ADD COLUMN `duration` INTEGER") + + Timber.d("Table 'locationvisits': Add column 'circumstances'") + execSQL("ALTER TABLE `locationvisits` ADD COLUMN `circumstances` TEXT") + } +} 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 286e5d9ed7842d6dfcfd6f01962d328ba10d9224..d2685422c75f1161e171c2fc4a5316c7ffd85542 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 @@ -22,6 +22,7 @@ interface ContactDiaryRepository { val locationVisits: Flow<List<ContactDiaryLocationVisit>> fun locationVisitsForDate(date: LocalDate): Flow<List<ContactDiaryLocationVisit>> suspend fun addLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit) + suspend fun updateLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit) suspend fun deleteLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit) suspend fun deleteLocationVisits(contactDiaryLocationVisits: List<ContactDiaryLocationVisit>) suspend fun deleteAllLocationVisits() @@ -38,6 +39,7 @@ interface ContactDiaryRepository { val personEncounters: Flow<List<ContactDiaryPersonEncounter>> fun personEncountersForDate(date: LocalDate): Flow<List<ContactDiaryPersonEncounter>> suspend fun addPersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter) + suspend fun updatePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter) suspend fun deletePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter) suspend fun deletePersonEncounters(contactDiaryPersonEncounters: List<ContactDiaryPersonEncounter>) suspend fun deleteAllPersonEncounters() 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 6cdec5daf2882792cdb9449f6bc5d250d99c0634..b4a1e1efa058075f393ef3fd6c18b11110ea7bbf 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 @@ -108,6 +108,13 @@ class DefaultContactDiaryRepository @Inject constructor( contactDiaryLocationVisitDao.insert(contactDiaryLocationVisitEntity) } + override suspend fun updateLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit) { + executeWhenIdNotDefault(contactDiaryLocationVisit.id) { + val contactDiaryLocationVisitEntity = contactDiaryLocationVisit.toContactDiaryLocationVisitEntity() + contactDiaryLocationVisitDao.update(contactDiaryLocationVisitEntity) + } + } + override suspend fun deleteLocationVisit(contactDiaryLocationVisit: ContactDiaryLocationVisit) { Timber.d("Deleting location visit $contactDiaryLocationVisit") executeWhenIdNotDefault(contactDiaryLocationVisit.id) { @@ -195,6 +202,13 @@ class DefaultContactDiaryRepository @Inject constructor( contactDiaryPersonEncounterDao.insert(contactDiaryPersonEncounterEntity) } + override suspend fun updatePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter) { + executeWhenIdNotDefault(contactDiaryPersonEncounter.id) { + val contactDiaryPersonEncounterEntity = contactDiaryPersonEncounter.toContactDiaryPersonEncounterEntity() + contactDiaryPersonEncounterDao.update(contactDiaryPersonEncounterEntity) + } + } + override suspend fun deletePersonEncounter(contactDiaryPersonEncounter: ContactDiaryPersonEncounter) { Timber.d("Deleting person encounter $contactDiaryPersonEncounter") executeWhenIdNotDefault(contactDiaryPersonEncounter.id) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/ContactDiaryUIModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/ContactDiaryUIModule.kt index 0947e119ce9ba4f567f88b54f69493a846dd466e..69968ec42f1554ea3a008148f4041fbae07663a7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/ContactDiaryUIModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/ContactDiaryUIModule.kt @@ -13,10 +13,10 @@ import de.rki.coronawarnapp.contactdiary.ui.onboarding.ContactDiaryOnboardingFra import de.rki.coronawarnapp.contactdiary.ui.onboarding.ContactDiaryOnboardingFragmentModule import de.rki.coronawarnapp.contactdiary.ui.overview.ContactDiaryOverviewFragment import de.rki.coronawarnapp.contactdiary.ui.overview.ContactDiaryOverviewFragmentModule -import de.rki.coronawarnapp.contactdiary.ui.sheets.location.ContactDiaryLocationBottomSheetDialogFragment -import de.rki.coronawarnapp.contactdiary.ui.sheets.location.ContactDiaryLocationBottomSheetDialogModule -import de.rki.coronawarnapp.contactdiary.ui.sheets.person.ContactDiaryPersonBottomSheetDialogFragment -import de.rki.coronawarnapp.contactdiary.ui.sheets.person.ContactDiaryPersonBottomSheetDialogModule +import de.rki.coronawarnapp.contactdiary.ui.location.ContactDiaryAddLocationFragment +import de.rki.coronawarnapp.contactdiary.ui.location.ContactDiaryAddLocationFragmentModule +import de.rki.coronawarnapp.contactdiary.ui.person.ContactDiaryAddPersonFragment +import de.rki.coronawarnapp.contactdiary.ui.person.ContactDiaryAddPersonModule @Module(includes = [ContactDiaryEditModule::class]) abstract class ContactDiaryUIModule { @@ -29,11 +29,11 @@ abstract class ContactDiaryUIModule { @ContributesAndroidInjector(modules = [ContactDiaryLocationListModule::class]) abstract fun contactDiaryLocationListFragment(): ContactDiaryLocationListFragment - @ContributesAndroidInjector(modules = [ContactDiaryPersonBottomSheetDialogModule::class]) - abstract fun contactDiaryPersonBottomSheetDialogFragment(): ContactDiaryPersonBottomSheetDialogFragment + @ContributesAndroidInjector(modules = [ContactDiaryAddPersonModule::class]) + abstract fun contactDiaryAddPersonFragment(): ContactDiaryAddPersonFragment - @ContributesAndroidInjector(modules = [ContactDiaryLocationBottomSheetDialogModule::class]) - abstract fun contactDiaryLocationBottomSheetDialogFragment(): ContactDiaryLocationBottomSheetDialogFragment + @ContributesAndroidInjector(modules = [ContactDiaryAddLocationFragmentModule::class]) + abstract fun contactDiaryAddLocationFragment(): ContactDiaryAddLocationFragment @ContributesAndroidInjector(modules = [ContactDiaryOnboardingFragmentModule::class]) abstract fun contactDiaryOnboardingFragment(): ContactDiaryOnboardingFragment diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayFragment.kt index 61fb40e02c09387e53d8d431055de6a16d30f223..56bc855e2d76bb55ae29113cd674629a019fcfa6 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayFragment.kt @@ -73,15 +73,15 @@ class ContactDiaryDayFragment : Fragment(R.layout.contact_diary_day_fragment), A viewModel.routeToScreen.observe2(this) { when (it) { ContactDiaryDayNavigationEvents.NavigateToOverviewFragment -> popBackStack() - ContactDiaryDayNavigationEvents.NavigateToAddPersonBottomSheet -> doNavigate( + ContactDiaryDayNavigationEvents.NavigateToAddPersonFragment -> doNavigate( ContactDiaryDayFragmentDirections - .actionContactDiaryDayFragmentToContactDiaryPersonBottomSheetDialogFragment( + .actionContactDiaryDayFragmentToContactDiaryAddPersonFragment( addedAt = navArgs.selectedDay ) ) - ContactDiaryDayNavigationEvents.NavigateToAddLocationBottomSheet -> doNavigate( + ContactDiaryDayNavigationEvents.NavigateToAddLocationFragment -> doNavigate( ContactDiaryDayFragmentDirections - .actionContactDiaryDayFragmentToContactDiaryLocationBottomSheetDialogFragment( + .actionContactDiaryDayFragmentToContactDiaryAddLocationFragment( addedAt = navArgs.selectedDay ) ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayNavigationEvents.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayNavigationEvents.kt index 8524c31442a6e990256712d41f3fb09e764f69ce..5dae4103991127874f4711be26e3a3ab84e641c3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayNavigationEvents.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayNavigationEvents.kt @@ -2,6 +2,6 @@ package de.rki.coronawarnapp.contactdiary.ui.day sealed class ContactDiaryDayNavigationEvents { object NavigateToOverviewFragment : ContactDiaryDayNavigationEvents() - object NavigateToAddPersonBottomSheet : ContactDiaryDayNavigationEvents() - object NavigateToAddLocationBottomSheet : ContactDiaryDayNavigationEvents() + object NavigateToAddPersonFragment : ContactDiaryDayNavigationEvents() + object NavigateToAddLocationFragment : ContactDiaryDayNavigationEvents() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayViewModel.kt index 6e6a3244d38da0fabf5bf6bba29afd1bef89acb4..530d026fd3ba426a917600d2b8688e7b82001a33 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/ContactDiaryDayViewModel.kt @@ -39,11 +39,9 @@ class ContactDiaryDayViewModel @AssistedInject constructor( fun onCreateButtonClicked(activeTab: ContactDiaryDayTab) { when (activeTab) { ContactDiaryDayTab.LocationTab -> - routeToScreen - .postValue(ContactDiaryDayNavigationEvents.NavigateToAddLocationBottomSheet) + routeToScreen.postValue(ContactDiaryDayNavigationEvents.NavigateToAddLocationFragment) ContactDiaryDayTab.PersonTab -> - routeToScreen - .postValue(ContactDiaryDayNavigationEvents.NavigateToAddPersonBottomSheet) + routeToScreen.postValue(ContactDiaryDayNavigationEvents.NavigateToAddPersonFragment) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/ContactDiaryCommentInfoFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/ContactDiaryCommentInfoFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..06b77c17c8ed3726de6e0b502949e004679e0d3f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/ContactDiaryCommentInfoFragment.kt @@ -0,0 +1,22 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.databinding.ContactDiaryCommentInfoFragmentBinding +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy + +class ContactDiaryCommentInfoFragment : Fragment(R.layout.contact_diary_comment_info_fragment) { + + private val binding: ContactDiaryCommentInfoFragmentBinding by viewBindingLazy() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.toolbar.setNavigationOnClickListener { + popBackStack() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt new file mode 100644 index 0000000000000000000000000000000000000000..11db3cab18d1285d54c2c1d70cd85381ec0f67e5 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryCircumstancesTextView.kt @@ -0,0 +1,67 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.common + +import android.content.Context +import android.text.InputType +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.inputmethod.EditorInfo +import android.widget.EditText +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import de.rki.coronawarnapp.R +import timber.log.Timber + +class DiaryCircumstancesTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val input: EditText + private val infoButton: ImageView + + private var afterTextChangedListener: ((String) -> Unit)? = null + + init { + LayoutInflater.from(context).inflate(R.layout.view_diary_circumstances_textview, this, true) + + input = findViewById<EditText>(R.id.input).apply { + setOnFocusChangeListener { v, hasFocus -> + if (hasFocus) { + Timber.v("Focused on %s", v) + } else { + Timber.v("Lost focus on %s", v) + notifyTextChanged(text.toString()) + } + } + imeOptions = EditorInfo.IME_ACTION_DONE + setRawInputType(InputType.TYPE_CLASS_TEXT) + } + infoButton = findViewById(R.id.info_button) + } + + override fun onFinishInflate() { + input.clearFocus() + super.onFinishInflate() + } + + private fun notifyTextChanged(text: String) { + // Prevent Copy&Paste inserting new lines. + afterTextChangedListener?.invoke(text.trim().replace("\n", "")) + } + + fun setInfoButtonClickListener(listener: () -> Unit) { + infoButton.setOnClickListener { listener() } + } + + fun setInputTextChangedListener(listener: ((String) -> Unit)?) { + afterTextChangedListener = listener + } + + fun setInputText(text: String) { + val temp = afterTextChangedListener + afterTextChangedListener = null + input.setText(text) + afterTextChangedListener = temp + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryTabViewHolderExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryTabViewHolderExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..9df0a38f4b04dbf7276fef3cad44447ac6819edb --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/DiaryTabViewHolderExtensions.kt @@ -0,0 +1,14 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.common + +import android.view.View +import com.google.android.material.button.MaterialButtonToggleGroup + +fun MaterialButtonToggleGroup.setOnCheckedChangeListener(listener: (checkedId: Int?) -> Unit) { + clearOnButtonCheckedListeners() + addOnButtonCheckedListener { group, checkedId, isChecked -> + when { + isChecked -> listener.invoke(checkedId) + group.checkedButtonId == View.NO_ID -> listener.invoke(null) + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/ExpandingDiaryListItemView.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/ExpandingDiaryListItemView.kt new file mode 100644 index 0000000000000000000000000000000000000000..83a6be14d2c5b6fe0f3d9f170cf2ac10f62ffc0c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/ExpandingDiaryListItemView.kt @@ -0,0 +1,66 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.common + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.ui.setGone + +class ExpandingDiaryListItemView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + private val titleView: TextView + private val checkboxView: ImageView + private val container: ViewGroup + private val divider: View + val header: ViewGroup + + init { + LayoutInflater.from(context).inflate(R.layout.view_expanding_diary_listitem, this, true) + + titleView = findViewById(R.id.header_title) + checkboxView = findViewById(R.id.header_checkbox) + header = findViewById(R.id.header) + container = findViewById(R.id.container) + divider = findViewById(R.id.divider) + } + + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams?) { + if (TOPLEVEL_IDS.contains(child.id)) { + super.addView(child, index, params) + } else { + container.addView(child, index, params) + } + } + + var title: String + get() = titleView.text.toString() + set(value) { + titleView.text = value + } + + var isExpanded: Boolean + get() = isSelected + set(expanded) { + isSelected = expanded + checkboxView.setImageResource(if (expanded) R.drawable.ic_selected else R.drawable.ic_unselected) + container.setGone(!expanded) + divider.setGone(!expanded) + } + + companion object { + private val TOPLEVEL_IDS = listOf( + R.id.header, + R.id.divider, + R.id.container + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/SelectableDiaryItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/SelectableDiaryItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..57445c07acbbbaa24b4ec64c4d7127e38f47d70a --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/common/SelectableDiaryItem.kt @@ -0,0 +1,17 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.common + +import androidx.annotation.StringRes +import de.rki.coronawarnapp.util.lists.HasStableId +import de.rki.coronawarnapp.util.ui.LazyString + +abstract class SelectableDiaryItem<T : HasStableId> : HasStableId { + abstract val onItemClick: (SelectableDiaryItem<T>) -> Unit + abstract val selected: Boolean + abstract val item: T + abstract val contentDescription: LazyString + abstract val onClickDescription: LazyString + @get:StringRes abstract val clickLabel: Int + @get:StringRes abstract val onClickLabel: Int + override val stableId: Long + get() = item.stableId +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListAdapter.kt index 050dcc52be0bb9b00fd006db11f403e96bb4ce18..c92dce45ff8934c4288cad6e307a092ffea92598 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListAdapter.kt @@ -1,53 +1,16 @@ package de.rki.coronawarnapp.contactdiary.ui.day.tabs.location import android.view.ViewGroup -import android.view.accessibility.AccessibilityEvent -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation import de.rki.coronawarnapp.contactdiary.util.AbstractAdapter -import de.rki.coronawarnapp.contactdiary.util.SelectableItem -import de.rki.coronawarnapp.contactdiary.util.setClickLabel -import de.rki.coronawarnapp.databinding.ContactDiaryLocationListItemBinding -import de.rki.coronawarnapp.ui.lists.BaseAdapter -import de.rki.coronawarnapp.util.lists.BindableVH import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter -import de.rki.coronawarnapp.util.ui.setOnClickListenerThrottled -internal class ContactDiaryLocationListAdapter( - private val onTappedCallback: (item: SelectableItem<ContactDiaryLocation>) -> Unit -) : AbstractAdapter<SelectableItem<ContactDiaryLocation>, ContactDiaryLocationListAdapter.CachedLocationViewHolder>(), - AsyncDiffUtilAdapter<SelectableItem<ContactDiaryLocation>> { +internal class ContactDiaryLocationListAdapter : + AbstractAdapter<DiaryLocationListItem, DiaryLocationViewHolder>(), + AsyncDiffUtilAdapter<DiaryLocationListItem> { - override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): CachedLocationViewHolder = - CachedLocationViewHolder(parent) + override fun onCreateBaseVH(parent: ViewGroup, viewType: Int) = DiaryLocationViewHolder(parent) - override fun onBindBaseVH(holder: CachedLocationViewHolder, position: Int, payloads: MutableList<Any>) { - val item = data[position] - holder.itemView.setOnClickListenerThrottled { - it.contentDescription = item.onClickDescription.get(holder.context) - it.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) - onTappedCallback(item) - } - holder.bind(item) - } - - class CachedLocationViewHolder( - parent: ViewGroup - ) : BaseAdapter.VH(R.layout.contact_diary_location_list_item, parent), - BindableVH<SelectableItem<ContactDiaryLocation>, ContactDiaryLocationListItemBinding> { - override val viewBinding = lazy { ContactDiaryLocationListItemBinding.bind(itemView) } - - override val onBindData: ContactDiaryLocationListItemBinding.( - key: SelectableItem<ContactDiaryLocation>, - payloads: List<Any> - ) -> Unit = { key, _ -> - contactDiaryLocationListItemName.text = key.item.locationName - contactDiaryLocationListItem.contentDescription = key.contentDescription.get(context) - contactDiaryLocationListItem.setClickLabel(context.getString(key.clickLabel)) - when (key.selected) { - true -> contactDiaryLocationListItemIcon.setImageResource(R.drawable.ic_selected) - false -> contactDiaryLocationListItemIcon.setImageResource(R.drawable.ic_unselected) - } - } + override fun onBindBaseVH(holder: DiaryLocationViewHolder, position: Int, payloads: MutableList<Any>) { + holder.bind(data[position]) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListFragment.kt index ee5cedaaad5e8115464aa21674b2a20c850d34da..3321505cd75328547b00da24991918abc0977ee7 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListFragment.kt @@ -33,9 +33,7 @@ class ContactDiaryLocationListFragment : Fragment(R.layout.contact_diary_locatio override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val locationListAdapter = ContactDiaryLocationListAdapter { - viewModel.onLocationSelectionChanged(it) - } + val locationListAdapter = ContactDiaryLocationListAdapter() binding.contactDiaryLocationListRecyclerView.apply { adapter = locationListAdapter diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt index f232f9f94455927fc274822a9e9711c084e7c091..d9155c01dbe5bc9d3c8f597dc5fb101017dc2a41 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/ContactDiaryLocationListViewModel.kt @@ -4,20 +4,19 @@ import androidx.lifecycle.asLiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryLocationVisit +import de.rki.coronawarnapp.contactdiary.model.toEditableVariant import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository -import de.rki.coronawarnapp.contactdiary.util.SelectableItem import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.ui.toResolvingString +import de.rki.coronawarnapp.util.trimToLength import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first +import org.joda.time.Duration import org.joda.time.LocalDate class ContactDiaryLocationListViewModel @AssistedInject constructor( @@ -34,31 +33,29 @@ class ContactDiaryLocationListViewModel @AssistedInject constructor( private val dayElement = contactDiaryRepository.locationVisitsForDate(localDate) private val selectableLocations = contactDiaryRepository.locations - val uiList = selectableLocations.combine(dayElement) { locations, dayElement -> - locations.map { contactDiaryLocation -> - if (dayElement.any { it.contactDiaryLocation.locationId == contactDiaryLocation.locationId }) { - SelectableItem( - true, - contactDiaryLocation, - SELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryLocation.locationName), - UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryLocation.locationName), - DESELECT_ACTION_DESCRIPTION, - SELECT_ACTION_DESCRIPTION - ) - } else { - SelectableItem( - false, - contactDiaryLocation, - UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryLocation.locationName), - SELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryLocation.locationName), - SELECT_ACTION_DESCRIPTION, - DESELECT_ACTION_DESCRIPTION - ) + val uiList = selectableLocations.combine(dayElement) { locations, encounters -> + locations.map { location -> + val visit = encounters.singleOrNull { + it.contactDiaryLocation.locationId == location.locationId } + DiaryLocationListItem( + item = location, + visit = visit, + onItemClick = { onLocationSelectionChanged(it as DiaryLocationListItem) }, + onDurationChanged = { item, duration -> + onDurationChanged(item, duration) + }, + onCircumstancesChanged = { item, circumstances -> + onCircumstancesChanged(item, circumstances) + }, + onCircumStanceInfoClicked = { + // TODO + } + ) } }.asLiveData() - fun onLocationSelectionChanged(item: SelectableItem<ContactDiaryLocation>) = launch(coroutineExceptionHandler) { + private fun onLocationSelectionChanged(item: DiaryLocationListItem) = launch(coroutineExceptionHandler) { if (!item.selected) { contactDiaryRepository.addLocationVisit( DefaultContactDiaryLocationVisit( @@ -74,6 +71,27 @@ class ContactDiaryLocationListViewModel @AssistedInject constructor( } } + private fun onDurationChanged( + item: DiaryLocationListItem, + duration: Duration? + ) { + val visit = item.visit?.toEditableVariant() ?: return + launch { + contactDiaryRepository.updateLocationVisit(visit.copy(duration = duration)) + } + } + + private fun onCircumstancesChanged( + item: DiaryLocationListItem, + circumstances: String + ) { + val visit = item.visit?.toEditableVariant() ?: return + val sanitized = circumstances.trim().trimToLength(250) + launch { + contactDiaryRepository.updateLocationVisit(visit.copy(circumstances = sanitized)) + } + } + @AssistedFactory interface Factory : CWAViewModelFactory<ContactDiaryLocationListViewModel> { fun create(selectedDay: String): ContactDiaryLocationListViewModel @@ -81,7 +99,3 @@ class ContactDiaryLocationListViewModel @AssistedInject constructor( } private val TAG = ContactDiaryLocationListViewModel::class.java.simpleName -private const val SELECTED_CONTENT_DESCRIPTION = R.string.accessibility_location_selected -private const val UNSELECTED_CONTENT_DESCRIPTION = R.string.accessibility_location_unselected -private const val SELECT_ACTION_DESCRIPTION = R.string.accessibility_action_select -private const val DESELECT_ACTION_DESCRIPTION = R.string.accessibility_action_deselect diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationListItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationListItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..df57fbbe68b4263ecdb1a7079c0f634d452dc277 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationListItem.kt @@ -0,0 +1,82 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.location + +import androidx.annotation.StringRes +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocation +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.SelectableDiaryItem +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer +import de.rki.coronawarnapp.util.ui.LazyString +import de.rki.coronawarnapp.util.ui.toResolvingString +import org.joda.time.Duration + +data class DiaryLocationListItem( + override val item: ContactDiaryLocation, + val visit: ContactDiaryLocationVisit?, + override val onItemClick: (SelectableDiaryItem<ContactDiaryLocation>) -> Unit, + val onDurationChanged: (DiaryLocationListItem, Duration?) -> Unit, + val onCircumstancesChanged: (DiaryLocationListItem, String) -> Unit, + val onCircumStanceInfoClicked: () -> Unit +) : SelectableDiaryItem<ContactDiaryLocation>(), HasPayloadDiffer { + override val selected: Boolean + get() = visit != null + + override val contentDescription: LazyString + get() = if (selected) { + SELECTED_CONTENT_DESCRIPTION.toResolvingString(item.locationName) + } else { + UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(item.locationName) + } + override val onClickDescription: LazyString + get() = if (selected) { + UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(item.locationName) + } else { + SELECTED_CONTENT_DESCRIPTION.toResolvingString(item.locationName) + } + override val clickLabel: Int + @StringRes get() = if (selected) { + DESELECT_ACTION_DESCRIPTION + } else { + SELECT_ACTION_DESCRIPTION + } + override val onClickLabel: Int + @StringRes get() = if (selected) { + SELECT_ACTION_DESCRIPTION + } else { + DESELECT_ACTION_DESCRIPTION + } + + override fun diffPayload(old: Any, new: Any): Any? { + old as DiaryLocationListItem + new as DiaryLocationListItem + // null causes a full re-layout to be executed + return when { + old.item != new.item -> null // Major change + old.visit == null && new.visit != null -> null // Container needs to grow + else -> new + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DiaryLocationListItem + + if (item != other.item) return false + if (visit != other.visit) return false + + return true + } + + override fun hashCode(): Int { + var result = item.hashCode() + result = 31 * result + (visit?.hashCode() ?: 0) + return result + } +} + +private const val SELECTED_CONTENT_DESCRIPTION = R.string.accessibility_location_selected +private const val UNSELECTED_CONTENT_DESCRIPTION = R.string.accessibility_location_unselected +private const val SELECT_ACTION_DESCRIPTION = R.string.accessibility_action_select +private const val DESELECT_ACTION_DESCRIPTION = R.string.accessibility_action_deselect diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..720d330f2e5255fb4cda6f29b48eda9f2e00bb99 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/location/DiaryLocationViewHolder.kt @@ -0,0 +1,44 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.location + +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.setClickLabel +import de.rki.coronawarnapp.databinding.ContactDiaryLocationListItemBinding +import de.rki.coronawarnapp.ui.lists.BaseAdapter +import de.rki.coronawarnapp.util.lists.BindableVH +import de.rki.coronawarnapp.util.ui.setOnClickListenerThrottled + +class DiaryLocationViewHolder( + parent: ViewGroup +) : BaseAdapter.VH(R.layout.contact_diary_location_list_item, parent), + BindableVH<DiaryLocationListItem, ContactDiaryLocationListItemBinding> { + + override val viewBinding = lazy { ContactDiaryLocationListItemBinding.bind(itemView) } + + override val onBindData: ContactDiaryLocationListItemBinding.( + item: DiaryLocationListItem, + changes: List<Any> + ) -> Unit = { initial, changes -> + val item = changes.firstOrNull() as? DiaryLocationListItem ?: initial + + mainBox.apply { + header.setOnClickListenerThrottled { + it.contentDescription = item.onClickDescription.get(context) + it.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) + item.onItemClick(item) + } + + title = item.item.locationName + isExpanded = item.selected + contentDescription = item.contentDescription.get(context) + setClickLabel(context.getString(item.clickLabel)) + } + + circumstances.apply { + if (changes.isEmpty()) setInputText(item.visit?.circumstances ?: "") + circumstances.setInputTextChangedListener { item.onCircumstancesChanged(item, it) } + setInfoButtonClickListener { item.onCircumStanceInfoClicked() } + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListAdapter.kt index c5787ef0561d03c23ec19f3bf9053dedbf53c966..af8b594fef960a20a074062b1f6b5d5c7ca6b7df 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListAdapter.kt @@ -1,53 +1,16 @@ package de.rki.coronawarnapp.contactdiary.ui.day.tabs.person import android.view.ViewGroup -import android.view.accessibility.AccessibilityEvent -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson import de.rki.coronawarnapp.contactdiary.util.AbstractAdapter -import de.rki.coronawarnapp.contactdiary.util.SelectableItem -import de.rki.coronawarnapp.contactdiary.util.setClickLabel -import de.rki.coronawarnapp.databinding.ContactDiaryPersonListItemBinding -import de.rki.coronawarnapp.ui.lists.BaseAdapter -import de.rki.coronawarnapp.util.lists.BindableVH import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffUtilAdapter -import de.rki.coronawarnapp.util.ui.setOnClickListenerThrottled -internal class ContactDiaryPersonListAdapter( - private val onTappedCallback: (item: SelectableItem<ContactDiaryPerson>) -> Unit -) : AbstractAdapter<SelectableItem<ContactDiaryPerson>, ContactDiaryPersonListAdapter.CachedPersonViewHolder>(), - AsyncDiffUtilAdapter<SelectableItem<ContactDiaryPerson>> { +internal class ContactDiaryPersonListAdapter : + AbstractAdapter<DiaryPersonListItem, DiaryPersonViewHolder>(), + AsyncDiffUtilAdapter<DiaryPersonListItem> { - override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): CachedPersonViewHolder = - CachedPersonViewHolder(parent) + override fun onCreateBaseVH(parent: ViewGroup, viewType: Int) = DiaryPersonViewHolder(parent) - override fun onBindBaseVH(holder: CachedPersonViewHolder, position: Int, payloads: MutableList<Any>) { - val item = data[position] - holder.itemView.setOnClickListenerThrottled { - it.contentDescription = item.onClickDescription.get(holder.context) - it.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) - onTappedCallback(item) - } - holder.bind(item, payloads) - } - - class CachedPersonViewHolder( - parent: ViewGroup - ) : BaseAdapter.VH(R.layout.contact_diary_person_list_item, parent), - BindableVH<SelectableItem<ContactDiaryPerson>, ContactDiaryPersonListItemBinding> { - override val viewBinding = lazy { ContactDiaryPersonListItemBinding.bind(itemView) } - - override val onBindData: ContactDiaryPersonListItemBinding.( - key: SelectableItem<ContactDiaryPerson>, - payloads: List<Any> - ) -> Unit = { key, _ -> - contactDiaryPersonListItemName.text = key.item.fullName - contactDiaryPersonListItem.contentDescription = key.contentDescription.get(context) - contactDiaryPersonListItem.setClickLabel(context.getString(key.clickLabel)) - when (key.selected) { - true -> contactDiaryPersonListItemIcon.setImageResource(R.drawable.ic_selected) - false -> contactDiaryPersonListItemIcon.setImageResource(R.drawable.ic_unselected) - } - } + override fun onBindBaseVH(holder: DiaryPersonViewHolder, position: Int, payloads: MutableList<Any>) { + holder.bind(data[position], payloads) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListFragment.kt index bab1d0324e3b531dde01514248a80889307bdd84..0d6e9c0525bdb01e4e1908adc223936ceeec79ad 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListFragment.kt @@ -33,17 +33,11 @@ class ContactDiaryPersonListFragment : Fragment(R.layout.contact_diary_person_li override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val personListAdapter = ContactDiaryPersonListAdapter() { - viewModel.onPersonSelectionChanged(it) - } + val personListAdapter = ContactDiaryPersonListAdapter() binding.contactDiaryPersonListRecyclerView.apply { adapter = personListAdapter - addItemDecoration( - MarginRecyclerViewDecoration( - resources.getDimensionPixelSize(R.dimen.spacing_tiny) - ) - ) + addItemDecoration(MarginRecyclerViewDecoration(resources.getDimensionPixelSize(R.dimen.spacing_tiny))) } viewModel.uiList.observe2(this) { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt index 3051b597a387d14d5b2391e2a4ba307f14c05ff9..5df0f9adddb61faa799f9d07a0d7081a962e3f43 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/ContactDiaryPersonListViewModel.kt @@ -1,24 +1,25 @@ package de.rki.coronawarnapp.contactdiary.ui.day.tabs.person +import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter import de.rki.coronawarnapp.contactdiary.model.DefaultContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.model.toEditableVariant import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository -import de.rki.coronawarnapp.contactdiary.util.SelectableItem import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report import de.rki.coronawarnapp.util.coroutine.DispatcherProvider -import de.rki.coronawarnapp.util.ui.toResolvingString +import de.rki.coronawarnapp.util.flow.combine +import de.rki.coronawarnapp.util.trimToLength import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import org.joda.time.LocalDate +import timber.log.Timber class ContactDiaryPersonListViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, @@ -31,34 +32,43 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor( private val localDate = LocalDate.parse(selectedDay) - private val dayElement = contactDiaryRepository.personEncountersForDate(localDate) + private val dayEncounters = contactDiaryRepository.personEncountersForDate(localDate) private val selectablePersons = contactDiaryRepository.people - val uiList = selectablePersons.combine(dayElement) { persons, dayElement -> - persons.map { contactDiaryPerson -> - if (dayElement.any { it.contactDiaryPerson.personId == contactDiaryPerson.personId }) { - SelectableItem( - true, - contactDiaryPerson, - SELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryPerson.fullName), - UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryPerson.fullName), - DESELECT_ACTION_DESCRIPTION, - SELECT_ACTION_DESCRIPTION - ) - } else { - SelectableItem( - false, - contactDiaryPerson, - UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryPerson.fullName), - SELECTED_CONTENT_DESCRIPTION.toResolvingString(contactDiaryPerson.fullName), - SELECT_ACTION_DESCRIPTION, - DESELECT_ACTION_DESCRIPTION - ) + val uiList: LiveData<List<DiaryPersonListItem>> = combine( + selectablePersons, + dayEncounters + ) { persons, encounters -> + persons.map { person -> + val encounter = encounters.singleOrNull { + it.contactDiaryPerson.personId == person.personId } + DiaryPersonListItem( + item = person, + personEncounter = encounter, + onItemClick = { onPersonSelectionChanged(it as DiaryPersonListItem) }, + onDurationChanged = { item, duration -> + onDurationChanged(item, duration) + }, + onWasOutsideChanged = { item, wasOutside -> + onWasOutsideChanged(item, wasOutside) + }, + onWithMaskChanged = { item, withMask -> + onWithmaskChanged(item, withMask) + }, + onCircumstancesChanged = { item, circumstances -> + onCircumstancesChanged(item, circumstances) + }, + onCircumstanceInfoClicked = { + // TODO + } + ) } - }.asLiveData() + }.asLiveData(context = dispatcherProvider.Default) - fun onPersonSelectionChanged(item: SelectableItem<ContactDiaryPerson>) = launch(coroutineExceptionHandler) { + private fun onPersonSelectionChanged( + item: DiaryPersonListItem + ) = launch(coroutineExceptionHandler) { if (!item.selected) { contactDiaryRepository.addPersonEncounter( DefaultContactDiaryPersonEncounter( @@ -67,12 +77,57 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor( ) ) } else { - val visit = dayElement.first() + val visit = dayEncounters.first() .find { it.contactDiaryPerson.personId == item.item.personId } visit?.let { contactDiaryRepository.deletePersonEncounter(it) } } } + private fun onDurationChanged( + item: DiaryPersonListItem, + duration: ContactDiaryPersonEncounter.DurationClassification? + ) { + Timber.d("onDurationChanged(item=%s, duration=%s)", item, duration) + val encounter = item.personEncounter?.toEditableVariant() ?: return + launch { + contactDiaryRepository.updatePersonEncounter(encounter.copy(durationClassification = duration)) + } + } + + private fun onWithmaskChanged( + item: DiaryPersonListItem, + withMask: Boolean? + ) { + Timber.d("onWithmaskChanged(item=%s, withMask=%s)", item, withMask) + val encounter = item.personEncounter?.toEditableVariant() ?: return + launch { + contactDiaryRepository.updatePersonEncounter(encounter.copy(withMask = withMask)) + } + } + + private fun onWasOutsideChanged( + item: DiaryPersonListItem, + wasOutside: Boolean? + ) { + Timber.d("onWasOutsideChanged(item=%s, onWasOutside=%s)", item, wasOutside) + val encounter = item.personEncounter?.toEditableVariant() ?: return + launch { + contactDiaryRepository.updatePersonEncounter(encounter.copy(wasOutside = wasOutside)) + } + } + + private fun onCircumstancesChanged( + item: DiaryPersonListItem, + circumstances: String + ) { + Timber.d("onCircumstancesChanged(item=%s, circumstances=%s)", item, circumstances) + val encounter = item.personEncounter?.toEditableVariant() ?: return + launch { + val sanitized = circumstances.trim().trimToLength(250) + contactDiaryRepository.updatePersonEncounter(encounter.copy(circumstances = sanitized)) + } + } + @AssistedFactory interface Factory : CWAViewModelFactory<ContactDiaryPersonListViewModel> { fun create(selectedDay: String): ContactDiaryPersonListViewModel @@ -80,7 +135,3 @@ class ContactDiaryPersonListViewModel @AssistedInject constructor( } private val TAG = ContactDiaryPersonListViewModel::class.java.simpleName -private const val SELECTED_CONTENT_DESCRIPTION = R.string.accessibility_person_selected -private const val UNSELECTED_CONTENT_DESCRIPTION = R.string.accessibility_person_unselected -private const val SELECT_ACTION_DESCRIPTION = R.string.accessibility_action_select -private const val DESELECT_ACTION_DESCRIPTION = R.string.accessibility_action_deselect diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonListItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonListItem.kt new file mode 100644 index 0000000000000000000000000000000000000000..bcd78c442864838c69f7207f7b4c503626be279f --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonListItem.kt @@ -0,0 +1,85 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.person + +import androidx.annotation.StringRes +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPerson +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.SelectableDiaryItem +import de.rki.coronawarnapp.util.lists.diffutil.HasPayloadDiffer +import de.rki.coronawarnapp.util.ui.LazyString +import de.rki.coronawarnapp.util.ui.toResolvingString + +data class DiaryPersonListItem( + override val item: ContactDiaryPerson, + val personEncounter: ContactDiaryPersonEncounter?, + override val onItemClick: (SelectableDiaryItem<ContactDiaryPerson>) -> Unit, + val onDurationChanged: (DiaryPersonListItem, ContactDiaryPersonEncounter.DurationClassification?) -> Unit, + val onWithMaskChanged: (DiaryPersonListItem, Boolean?) -> Unit, + val onWasOutsideChanged: (DiaryPersonListItem, Boolean?) -> Unit, + val onCircumstancesChanged: (DiaryPersonListItem, String) -> Unit, + val onCircumstanceInfoClicked: () -> Unit +) : SelectableDiaryItem<ContactDiaryPerson>(), HasPayloadDiffer { + + override val selected: Boolean + get() = personEncounter != null + + override val contentDescription: LazyString + get() = if (selected) { + SELECTED_CONTENT_DESCRIPTION.toResolvingString(item.fullName) + } else { + UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(item.fullName) + } + + override val onClickDescription: LazyString + get() = if (selected) { + UNSELECTED_CONTENT_DESCRIPTION.toResolvingString(item.fullName) + } else { + SELECTED_CONTENT_DESCRIPTION.toResolvingString(item.fullName) + } + override val clickLabel: Int + @StringRes get() = if (selected) { + DESELECT_ACTION_DESCRIPTION + } else { + SELECT_ACTION_DESCRIPTION + } + override val onClickLabel: Int + @StringRes get() = if (selected) { + SELECT_ACTION_DESCRIPTION + } else { + DESELECT_ACTION_DESCRIPTION + } + + override fun diffPayload(old: Any, new: Any): Any? { + old as DiaryPersonListItem + new as DiaryPersonListItem + // null causes a full re-layout to be executed + return when { + old.item != new.item -> null // Major change + old.personEncounter == null && new.personEncounter != null -> null // Container needs to grow + else -> new + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DiaryPersonListItem + + if (item != other.item) return false + if (personEncounter != other.personEncounter) return false + + return true + } + + override fun hashCode(): Int { + var result = item.hashCode() + result = 31 * result + (personEncounter?.hashCode() ?: 0) + return result + } +} + +private const val SELECTED_CONTENT_DESCRIPTION = R.string.accessibility_person_selected +private const val UNSELECTED_CONTENT_DESCRIPTION = R.string.accessibility_person_unselected +private const val SELECT_ACTION_DESCRIPTION = R.string.accessibility_action_select +private const val DESELECT_ACTION_DESCRIPTION = R.string.accessibility_action_deselect diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..bae3f5575dfa1c90a1e6ea5afae426efd88012d2 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/day/tabs/person/DiaryPersonViewHolder.kt @@ -0,0 +1,96 @@ +package de.rki.coronawarnapp.contactdiary.ui.day.tabs.person + +import android.view.ViewGroup +import android.view.accessibility.AccessibilityEvent +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter.DurationClassification +import de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.setOnCheckedChangeListener +import de.rki.coronawarnapp.contactdiary.util.setClickLabel +import de.rki.coronawarnapp.databinding.ContactDiaryPersonListItemBinding +import de.rki.coronawarnapp.ui.lists.BaseAdapter +import de.rki.coronawarnapp.util.lists.BindableVH +import de.rki.coronawarnapp.util.ui.setOnClickListenerThrottled + +class DiaryPersonViewHolder( + parent: ViewGroup +) : BaseAdapter.VH(R.layout.contact_diary_person_list_item, parent), + BindableVH<DiaryPersonListItem, ContactDiaryPersonListItemBinding> { + + override val viewBinding = lazy { ContactDiaryPersonListItemBinding.bind(itemView) } + + override val onBindData: ContactDiaryPersonListItemBinding.( + item: DiaryPersonListItem, + changes: List<Any> + ) -> Unit = { initial, changes -> + val item = changes.firstOrNull() as? DiaryPersonListItem ?: initial + + mainBox.apply { + header.setOnClickListenerThrottled { + it.contentDescription = item.onClickDescription.get(context) + it.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION) + item.onItemClick(item) + } + + title = item.item.fullName + isExpanded = item.selected + contentDescription = item.contentDescription.get(context) + setClickLabel(context.getString(item.clickLabel)) + } + + durationGroup.apply { + clearOnButtonCheckedListeners() + when (item.personEncounter?.durationClassification) { + DurationClassification.MORE_THAN_15_MINUTES -> check(R.id.duration_above_15) + DurationClassification.LESS_THAN_15_MINUTES -> check(R.id.duration_below_15) + null -> clearChecked() + } + setOnCheckedChangeListener { checkedId -> + when (checkedId) { + R.id.duration_above_15 -> DurationClassification.MORE_THAN_15_MINUTES + R.id.duration_below_15 -> DurationClassification.LESS_THAN_15_MINUTES + else -> null + }.let { item.onDurationChanged(item, it) } + } + } + + maskGroup.apply { + clearOnButtonCheckedListeners() + when (item.personEncounter?.withMask) { + true -> check(R.id.mask_with) + false -> check(R.id.mask_without) + null -> clearChecked() + } + setOnCheckedChangeListener { checkedId -> + when (checkedId) { + R.id.mask_with -> true + R.id.mask_without -> false + else -> null + }.let { item.onWithMaskChanged(item, it) } + } + } + + environmentGroup.apply { + clearOnButtonCheckedListeners() + when (item.personEncounter?.wasOutside) { + true -> check(R.id.environment_outside) + false -> check(R.id.environment_inside) + null -> clearChecked() + } + setOnCheckedChangeListener { checkedId -> + when (checkedId) { + R.id.environment_outside -> true + R.id.environment_inside -> false + else -> null + }.let { item.onWasOutsideChanged(item, it) } + } + } + + circumstances.apply { + // When data changes, we get that via payload + // To not update the edittext while typing, only the the text input on the first "bind" + if (changes.isEmpty()) setInputText(item.personEncounter?.circumstances ?: "") + circumstances.setInputTextChangedListener { item.onCircumstancesChanged(item, it) } + setInfoButtonClickListener { item.onCircumstanceInfoClicked() } + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/ContactDiaryDurationPickerFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/ContactDiaryDurationPickerFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..6a39ab7edd0af915f3786eea3a32c7630941fc0c --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/ContactDiaryDurationPickerFragment.kt @@ -0,0 +1,73 @@ +package de.rki.coronawarnapp.contactdiary.ui.durationpicker + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import de.rki.coronawarnapp.databinding.ContactDiaryDurationPickerDialogFragmentBinding +import org.joda.time.Duration +import org.joda.time.format.PeriodFormatter +import org.joda.time.format.PeriodFormatterBuilder + +class ContactDiaryDurationPickerFragment : DialogFragment() { + + interface OnChangeListener { + fun onChange(duration: Duration) + } + + val binding: Lazy<ContactDiaryDurationPickerDialogFragmentBinding> = lazy { + ContactDiaryDurationPickerDialogFragmentBinding.inflate( + layoutInflater + ) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return binding.value.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + with(binding.value.hours) { + minValue = 0 + maxValue = hoursArray.size - 1 + displayedValues = hoursArray + } + + with(binding.value.minutes) { + minValue = 0 + maxValue = minutesArray.size - 1 + displayedValues = minutesArray + } + + with(binding.value) { + val duration = requireArguments().getString(DURATION_ARGUMENT_KEY)!!.split(":").toTypedArray() + hours.value = hoursArray.indexOf(duration[0]) + minutes.value = minutesArray.indexOf(duration[1]) + + cancelButton.setOnClickListener { dismiss() } + okButton.setOnClickListener { + (targetFragment as? OnChangeListener)?.onChange(getDuration(hours.value, minutes.value)) + dismiss() + } + } + } + + companion object { + const val DURATION_ARGUMENT_KEY = "duration" + + val minutesArray = arrayOf("00", "15", "30", "45") + val hoursArray = Array(24) { "%02d".format(it) } + + fun getDuration(hours: Int, minutes: Int): Duration { + val durationString = hoursArray[hours] + ":" + minutesArray[minutes] + val formatter: PeriodFormatter = PeriodFormatterBuilder() + .appendHours() + .appendLiteral(":") + .appendMinutes() + .toFormatter() + return formatter.parsePeriod(durationString).toStandardDuration() + } + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtension.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtension.kt new file mode 100644 index 0000000000000000000000000000000000000000..270c455e0cda681f10ebcb4e2d0736acac986481 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/DurationExtension.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.contactdiary.ui.durationpicker + +import org.joda.time.Duration + +fun Duration.toContactDiaryFormat(): String { + val hours = if (standardHours < 10) { + "0$standardHours" + } else { + standardHours.toString() + } + val minutesCleaned = standardMinutes - standardHours * 60 + val minutes = if (minutesCleaned < 10) { + "0$minutesCleaned" + } else { + minutesCleaned.toString() + } + return "$hours:$minutes" +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsFragment.kt index 5a226c1139680e183acad1ee8307828b1a8fc347..d5a83e00c287beb09f8e0bda7f47b37c309854af 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsFragment.kt @@ -7,7 +7,7 @@ import androidx.core.view.isGone import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.ui.edit.ContactDiaryEditLocationsViewModel.NavigationEvent.ShowDeletionConfirmationDialog -import de.rki.coronawarnapp.contactdiary.ui.edit.ContactDiaryEditLocationsViewModel.NavigationEvent.ShowLocationDetailSheet +import de.rki.coronawarnapp.contactdiary.ui.edit.ContactDiaryEditLocationsViewModel.NavigationEvent.ShowLocationDetailFragment import de.rki.coronawarnapp.contactdiary.ui.edit.adapter.LocationEditAdapter import de.rki.coronawarnapp.databinding.ContactDiaryEditLocationsFragmentBinding import de.rki.coronawarnapp.util.DialogHelper @@ -56,10 +56,10 @@ class ContactDiaryEditLocationsFragment : Fragment(R.layout.contact_diary_edit_l when (it) { ShowDeletionConfirmationDialog -> DialogHelper.showDialog(deleteAllLocationsConfirmationDialog) - is ShowLocationDetailSheet -> { + is ShowLocationDetailFragment -> { doNavigate( ContactDiaryEditLocationsFragmentDirections - .actionContactDiaryEditLocationsFragmentToContactDiaryLocationBottomSheetDialogFragment( + .actionContactDiaryEditLocationsFragmentToContactDiaryAddLocationFragment( it.location ) ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt index 76f288d4efe549ed745434ba65f962250eb5ccba..d18e809397cc4ea72290cd873c3c9f7700501e27 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModel.kt @@ -47,7 +47,7 @@ class ContactDiaryEditLocationsViewModel @AssistedInject constructor( } fun onEditLocationClick(location: ContactDiaryLocation) { - navigationEvent.postValue(NavigationEvent.ShowLocationDetailSheet(location.toContactDiaryLocationEntity())) + navigationEvent.postValue(NavigationEvent.ShowLocationDetailFragment(location.toContactDiaryLocationEntity())) } @AssistedFactory @@ -55,7 +55,7 @@ class ContactDiaryEditLocationsViewModel @AssistedInject constructor( sealed class NavigationEvent { object ShowDeletionConfirmationDialog : NavigationEvent() - data class ShowLocationDetailSheet(val location: ContactDiaryLocationEntity) : NavigationEvent() + data class ShowLocationDetailFragment(val location: ContactDiaryLocationEntity) : NavigationEvent() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsFragment.kt index bd46ac7977fcb952ecb6a172ed22f49935327ff5..cc8a3c514ef9df8de6b549da05c9e00ab398a309 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsFragment.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsFragment.kt @@ -7,7 +7,7 @@ import androidx.core.view.isGone import androidx.fragment.app.Fragment import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.ui.edit.ContactDiaryEditPersonsViewModel.NavigationEvent.ShowDeletionConfirmationDialog -import de.rki.coronawarnapp.contactdiary.ui.edit.ContactDiaryEditPersonsViewModel.NavigationEvent.ShowPersonDetailSheet +import de.rki.coronawarnapp.contactdiary.ui.edit.ContactDiaryEditPersonsViewModel.NavigationEvent.ShowPersonDetailFragment import de.rki.coronawarnapp.contactdiary.ui.edit.adapter.PersonEditAdapter import de.rki.coronawarnapp.databinding.ContactDiaryEditPersonsFragmentBinding import de.rki.coronawarnapp.util.DialogHelper @@ -54,10 +54,10 @@ class ContactDiaryEditPersonsFragment : Fragment(R.layout.contact_diary_edit_per when (it) { ShowDeletionConfirmationDialog -> DialogHelper.showDialog(deleteAllPersonsConfirmationDialog) - is ShowPersonDetailSheet -> { + is ShowPersonDetailFragment -> { doNavigate( ContactDiaryEditPersonsFragmentDirections - .actionContactDiaryEditPersonsFragmentToContactDiaryPersonBottomSheetDialogFragment( + .actionContactDiaryEditPersonsFragmentToContactDiaryAddPersonFragment( it.person ) ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt index 052ec29c6a45352a75b540a203b2175f0a942d34..d648222c2cc2c0ad8196031e4295f02499c4ce28 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModel.kt @@ -48,7 +48,7 @@ class ContactDiaryEditPersonsViewModel @AssistedInject constructor( } fun onEditPersonClick(person: ContactDiaryPerson) { - navigationEvent.postValue(NavigationEvent.ShowPersonDetailSheet(person.toContactDiaryPersonEntity())) + navigationEvent.postValue(NavigationEvent.ShowPersonDetailFragment(person.toContactDiaryPersonEntity())) } @AssistedFactory @@ -56,7 +56,7 @@ class ContactDiaryEditPersonsViewModel @AssistedInject constructor( sealed class NavigationEvent { object ShowDeletionConfirmationDialog : NavigationEvent() - data class ShowPersonDetailSheet(val person: ContactDiaryPersonEntity) : NavigationEvent() + data class ShowPersonDetailFragment(val person: ContactDiaryPersonEntity) : NavigationEvent() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..078300ce3c65481721f754a75d063ac56cddaa27 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationFragment.kt @@ -0,0 +1,120 @@ +package de.rki.coronawarnapp.contactdiary.ui.location + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.focusAndShowKeyboard +import de.rki.coronawarnapp.contactdiary.util.hideKeyboard +import de.rki.coronawarnapp.databinding.ContactDiaryAddLocationFragmentBinding +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted +import javax.inject.Inject + +class ContactDiaryAddLocationFragment : Fragment(R.layout.contact_diary_add_location_fragment), AutoInject { + + private val binding: ContactDiaryAddLocationFragmentBinding by viewBindingLazy() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: ContactDiaryAddLocationViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as ContactDiaryAddLocationViewModel.Factory + factory.create(navArgs.addedAt) + } + ) + + private val navArgs: ContactDiaryAddLocationFragmentArgs by navArgs() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val location = navArgs.selectedLocation + if (location != null) { + binding.apply { + contactDiaryAddLocationNameInputEditText.setText(location.locationName) + contactDiaryAddLocationPhoneInputEditText.setText(location.phoneNumber) + contactDiaryAddLocationEmailInputEditText.setText(location.emailAddress) + contactDiaryAddLocationDeleteButton.visibility = View.VISIBLE + contactDiaryAddLocationDeleteButton.setOnClickListener { + DialogHelper.showDialog(deleteLocationConfirmationDialog) + } + contactDiaryAddLocationSaveButton.setOnClickListener { + it.hideKeyboard() + viewModel.updateLocation( + location, + phoneNumber = binding.contactDiaryAddLocationPhoneInputEditText.text.toString().trim(), + emailAddress = binding.contactDiaryAddLocationEmailInputEditText.text.toString().trim() + ) + } + } + viewModel.locationChanged(location.locationName) + } else { + binding.apply { + contactDiaryAddLocationDeleteButton.visibility = View.GONE + contactDiaryAddLocationSaveButton.setOnClickListener { + it.hideKeyboard() + viewModel.addLocation( + phoneNumber = binding.contactDiaryAddLocationPhoneInputEditText.text.toString().trim(), + emailAddress = binding.contactDiaryAddLocationEmailInputEditText.text.toString().trim() + ) + } + } + } + + binding.apply { + contactDiaryAddLocationNameInputEditText.focusAndShowKeyboard() + + contactDiaryAddLocationCloseButton.setOnClickListener { + it.hideKeyboard() + viewModel.closePressed() + } + contactDiaryAddLocationNameInputEditText.doAfterTextChanged { + viewModel.locationChanged(it.toString()) + } + + contactDiaryAddLocationEmailInputEditText.setOnEditorActionListener { _, actionId, _ -> + return@setOnEditorActionListener when (actionId) { + EditorInfo.IME_ACTION_DONE -> { + if (viewModel.isValid.value == true) { + binding.contactDiaryAddLocationSaveButton.performClick() + } + false + } + else -> true + } + } + } + + viewModel.shouldClose.observe2(this) { + popBackStack() + } + + viewModel.isValid.observe2(this) { + binding.contactDiaryAddLocationSaveButton.isEnabled = it + } + } + + private val deleteLocationConfirmationDialog by lazy { + DialogHelper.DialogInstance( + requireActivity(), + R.string.contact_diary_delete_location_title, + R.string.contact_diary_delete_locations_message, + R.string.contact_diary_delete_button_positive, + R.string.contact_diary_delete_button_negative, + positiveButtonFunction = { + navArgs.selectedLocation?.let { + viewModel.deleteLocation(it) + } + } + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationFragmentModule.kt similarity index 50% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationFragmentModule.kt index f1a84587d419662e38ede9d162cd8290c4589057..e73bc057f18868ae3b69361c333ddc3145b55e2b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationFragmentModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.contactdiary.ui.sheets.person +package de.rki.coronawarnapp.contactdiary.ui.location import dagger.Binds import dagger.Module @@ -8,11 +8,11 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey @Module -abstract class ContactDiaryPersonBottomSheetDialogModule { +abstract class ContactDiaryAddLocationFragmentModule { @Binds @IntoMap - @CWAViewModelKey(ContactDiaryPersonBottomSheetDialogViewModel::class) - abstract fun contactDiaryPersonBottomSheetDialogFragment( - factory: ContactDiaryPersonBottomSheetDialogViewModel.Factory + @CWAViewModelKey(ContactDiaryAddLocationViewModel::class) + abstract fun contactDiaryAddLocationFragment( + factory: ContactDiaryAddLocationViewModel.Factory ): CWAViewModelFactory<out CWAViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt similarity index 64% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogViewModel.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt index 14263b7dc44788935daf863f5432db8251f22fef..3f0b7c7ab96dfecadf57e861e7a475b3ad9f09d9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/location/ContactDiaryAddLocationViewModel.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.contactdiary.ui.sheets.location +package de.rki.coronawarnapp.contactdiary.ui.location import androidx.lifecycle.asLiveData import dagger.assisted.Assisted @@ -10,7 +10,6 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryLocationEnti import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.contactdiary.util.formatContactDiaryNameField import de.rki.coronawarnapp.ui.SingleLiveEvent import de.rki.coronawarnapp.util.coroutine.DispatcherProvider import de.rki.coronawarnapp.util.viewmodel.CWAViewModel @@ -21,7 +20,7 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import org.joda.time.LocalDate -class ContactDiaryLocationBottomSheetDialogViewModel @AssistedInject constructor( +class ContactDiaryAddLocationViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, @Assisted private val addedAt: String?, private val contactDiaryRepository: ContactDiaryRepository @@ -31,25 +30,24 @@ class ContactDiaryLocationBottomSheetDialogViewModel @AssistedInject constructor ex.report(ExceptionCategory.INTERNAL, TAG) } - private val text = MutableStateFlow("") - - val isValid = text.map { - it.isNotEmpty() && it.length <= MAX_LOCATION_NAME_LENGTH - }.asLiveData() - val shouldClose = SingleLiveEvent<Unit>() - private val formattedName: String - get() = text.value.formatContactDiaryNameField(MAX_LOCATION_NAME_LENGTH) + private val locationName = MutableStateFlow("") + + val isValid = locationName + .map { it.isNotEmpty() } + .asLiveData() - fun textChanged(locationName: String) { - text.value = locationName + fun locationChanged(value: String) { + locationName.value = value.trim() } - fun addLocation() = launch(coroutineExceptionHandler) { + fun addLocation(phoneNumber: String, emailAddress: String) = launch(coroutineExceptionHandler) { val location = contactDiaryRepository.addLocation( DefaultContactDiaryLocation( - locationName = formattedName + locationName = locationName.value, + phoneNumber = phoneNumber, + emailAddress = emailAddress ) ) @@ -61,19 +59,21 @@ class ContactDiaryLocationBottomSheetDialogViewModel @AssistedInject constructor ) ) } - shouldClose.postValue(null) } - fun updateLocation(location: ContactDiaryLocationEntity) = launch(coroutineExceptionHandler) { - contactDiaryRepository.updateLocation( - DefaultContactDiaryLocation( - location.locationId, - locationName = formattedName + fun updateLocation(location: ContactDiaryLocationEntity, phoneNumber: String, emailAddress: String) = + launch(coroutineExceptionHandler) { + contactDiaryRepository.updateLocation( + DefaultContactDiaryLocation( + location.locationId, + locationName = locationName.value, + phoneNumber = phoneNumber, + emailAddress = emailAddress + ) ) - ) - shouldClose.postValue(null) - } + shouldClose.postValue(null) + } fun deleteLocation(location: ContactDiaryLocationEntity) = launch(coroutineExceptionHandler) { contactDiaryRepository.locationVisits.firstOrNull()?.forEach { @@ -89,12 +89,11 @@ class ContactDiaryLocationBottomSheetDialogViewModel @AssistedInject constructor } companion object { - private const val MAX_LOCATION_NAME_LENGTH = 250 - private val TAG = ContactDiaryLocationBottomSheetDialogViewModel::class.java.simpleName + private val TAG = ContactDiaryAddLocationViewModel::class.java.simpleName } @AssistedFactory - interface Factory : CWAViewModelFactory<ContactDiaryLocationBottomSheetDialogViewModel> { - fun create(addedAt: String?): ContactDiaryLocationBottomSheetDialogViewModel + interface Factory : CWAViewModelFactory<ContactDiaryAddLocationViewModel> { + fun create(addedAt: String?): ContactDiaryAddLocationViewModel } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt index f295f4cd5b72cefd2c6eacaa46349984291424cb..f2c304ca1f3daa15c2c6b0c0bf004a4fa9677fc3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModel.kt @@ -9,6 +9,8 @@ import dagger.assisted.AssistedInject import de.rki.coronawarnapp.R import de.rki.coronawarnapp.contactdiary.model.ContactDiaryLocationVisit import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter.DurationClassification.LESS_THAN_15_MINUTES +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter.DurationClassification.MORE_THAN_15_MINUTES import de.rki.coronawarnapp.contactdiary.retention.ContactDiaryCleanTask import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.ListItem @@ -117,7 +119,10 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( .map { personEncounter -> ListItem.Data( R.drawable.ic_contact_diary_person_item, - personEncounter.contactDiaryPerson.fullName, + name = personEncounter.contactDiaryPerson.fullName, + duration = null, + attributes = getPersonAttributes(personEncounter), + circumstances = personEncounter.circumstances, ListItem.Type.PERSON ) } @@ -133,6 +138,9 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( ListItem.Data( R.drawable.ic_contact_diary_location_item, locationVisit.contactDiaryLocation.locationName, + duration = locationVisit.duration, + attributes = null, + circumstances = locationVisit.circumstances, ListItem.Type.LOCATION ) } @@ -146,6 +154,24 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( routeToScreen.postValue(ContactDiaryOverviewNavigationEvents.NavigateToContactDiaryDayFragment(listItem.date)) } + private fun getPersonAttributes(personEncounter: ContactDiaryPersonEncounter): List<Int> = + mutableListOf<Int>().apply { + when (personEncounter.durationClassification) { + LESS_THAN_15_MINUTES -> add(R.string.contact_diary_person_encounter_duration_below_15_min) + MORE_THAN_15_MINUTES -> add(R.string.contact_diary_person_encounter_duration_above_15_min) + } + + when (personEncounter.withMask) { + true -> add(R.string.contact_diary_person_encounter_mask_with) + false -> add(R.string.contact_diary_person_encounter_mask_without) + } + + when (personEncounter.wasOutside) { + true -> add(R.string.contact_diary_person_encounter_environment_outside) + false -> add(R.string.contact_diary_person_encounter_environment_inside) + } + } + fun onExportPress(ctx: Context) { Timber.d("Exporting person and location entries") launch { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ContactDiaryOverviewNestedAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ContactDiaryOverviewNestedAdapter.kt index e9047030c4fae8d17c1bda662c86adfe6271e080..1846b08686dc16a092bc0aa886930f7213631f14 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ContactDiaryOverviewNestedAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ContactDiaryOverviewNestedAdapter.kt @@ -6,6 +6,7 @@ import de.rki.coronawarnapp.contactdiary.util.clearAndAddAll import de.rki.coronawarnapp.databinding.ContactDiaryOverviewNestedListItemBinding import de.rki.coronawarnapp.ui.lists.BaseAdapter import de.rki.coronawarnapp.util.lists.BindableVH +import org.joda.time.Duration class ContactDiaryOverviewNestedAdapter : BaseAdapter<ContactDiaryOverviewNestedAdapter.NestedItemViewHolder>() { @@ -27,19 +28,28 @@ class ContactDiaryOverviewNestedAdapter : BaseAdapter<ContactDiaryOverviewNested inner class NestedItemViewHolder(parent: ViewGroup) : BaseAdapter.VH(R.layout.contact_diary_overview_nested_list_item, parent), BindableVH<ListItem.Data, ContactDiaryOverviewNestedListItemBinding> { - override val viewBinding: - Lazy<ContactDiaryOverviewNestedListItemBinding> = - lazy { ContactDiaryOverviewNestedListItemBinding.bind(itemView) } - - override val onBindData: - ContactDiaryOverviewNestedListItemBinding.(item: ListItem.Data, payloads: List<Any>) -> Unit = - { key, _ -> - contactDiaryOverviewElementImage.setImageResource(key.drawableId) - contactDiaryOverviewElementName.text = key.text - contactDiaryOverviewElementName.contentDescription = when (key.type) { - ListItem.Type.LOCATION -> context.getString(R.string.accessibility_location, key.text) - ListItem.Type.PERSON -> context.getString(R.string.accessibility_person, key.text) - } - } + override val viewBinding: Lazy<ContactDiaryOverviewNestedListItemBinding> = + lazy { ContactDiaryOverviewNestedListItemBinding.bind(itemView) } + + override val onBindData: ContactDiaryOverviewNestedListItemBinding.( + item: ListItem.Data, + payloads: List<Any> + ) -> Unit = { key, _ -> + contactDiaryOverviewElementImage.setImageResource(key.drawableId) + contactDiaryOverviewElementName.text = key.name + contactDiaryOverviewElementName.contentDescription = when (key.type) { + ListItem.Type.LOCATION -> context.getString(R.string.accessibility_location, key.name) + ListItem.Type.PERSON -> context.getString(R.string.accessibility_person, key.name) + } + contactDiaryOverviewElementAttributes.text = + getAttributes(key.duration, key.attributes, key.circumstances) + } + + private fun getAttributes(duration: Duration?, resources: List<Int>?, circumstances: String?): String = + mutableListOf<String>().apply { + duration?.run { add(toStandardHours().toString()) } + resources?.run { forEach { add(context.getString(it)) } } + circumstances?.run { add(this) } + }.joinToString() } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ListItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ListItem.kt index f45c8704107408158b164ae2f94edb6ba52f9176..525d988f570055861e91369b66af1d1258063993 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ListItem.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/ListItem.kt @@ -2,6 +2,7 @@ package de.rki.coronawarnapp.contactdiary.ui.overview.adapter import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import org.joda.time.Duration import org.joda.time.LocalDate data class ListItem( @@ -12,7 +13,10 @@ data class ListItem( data class Data( @DrawableRes val drawableId: Int, - val text: String, + val name: String, + val duration: Duration?, + val attributes: List<Int>?, + val circumstances: String?, val type: Type ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..0a897ddac8ad4a258e06da3eaa3bedae8d93a840 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonFragment.kt @@ -0,0 +1,120 @@ +package de.rki.coronawarnapp.contactdiary.ui.person + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo.IME_ACTION_DONE +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.contactdiary.util.focusAndShowKeyboard +import de.rki.coronawarnapp.contactdiary.util.hideKeyboard +import de.rki.coronawarnapp.databinding.ContactDiaryAddPersonFragmentBinding +import de.rki.coronawarnapp.util.DialogHelper +import de.rki.coronawarnapp.util.di.AutoInject +import de.rki.coronawarnapp.util.ui.observe2 +import de.rki.coronawarnapp.util.ui.popBackStack +import de.rki.coronawarnapp.util.ui.viewBindingLazy +import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider +import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted +import javax.inject.Inject + +class ContactDiaryAddPersonFragment : + Fragment(R.layout.contact_diary_add_person_fragment), + AutoInject { + + private val binding: ContactDiaryAddPersonFragmentBinding by viewBindingLazy() + + @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory + private val viewModel: ContactDiaryAddPersonViewModel by cwaViewModelsAssisted( + factoryProducer = { viewModelFactory }, + constructorCall = { factory, _ -> + factory as ContactDiaryAddPersonViewModel.Factory + factory.create(navArgs.addedAt) + } + ) + + private val navArgs: ContactDiaryAddPersonFragmentArgs by navArgs() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val person = navArgs.selectedPerson + if (person != null) { + binding.apply { + contactDiaryPersonNameEditText.setText(person.fullName) + contactDiaryPersonPhoneNumberEditText.setText(person.phoneNumber) + contactDiaryPersonEmailEditText.setText(person.emailAddress) + contactDiaryPersonDeleteButton.visibility = View.VISIBLE + contactDiaryPersonDeleteButton.setOnClickListener { + DialogHelper.showDialog(deletePersonConfirmationDialog) + } + contactDiaryPersonSaveButton.setOnClickListener { + it.hideKeyboard() + viewModel.updatePerson( + person, + phoneNumber = binding.contactDiaryPersonPhoneNumberEditText.text.toString().trim(), + emailAddress = binding.contactDiaryPersonEmailEditText.text.toString().trim() + ) + } + } + viewModel.nameChanged(person.fullName) + } else { + binding.contactDiaryPersonDeleteButton.visibility = View.GONE + binding.contactDiaryPersonSaveButton.setOnClickListener { + it.hideKeyboard() + viewModel.addPerson( + phoneNumber = binding.contactDiaryPersonPhoneNumberEditText.text.toString().trim(), + emailAddress = binding.contactDiaryPersonEmailEditText.text.toString().trim() + ) + } + } + + binding.apply { + contactDiaryPersonNameEditText.focusAndShowKeyboard() + + contactDiaryPersonCloseButton.setOnClickListener { + it.hideKeyboard() + viewModel.closePressed() + } + contactDiaryPersonNameEditText.doAfterTextChanged { + viewModel.nameChanged(it.toString()) + } + + contactDiaryPersonEmailEditText.setOnEditorActionListener { _, actionId, _ -> + return@setOnEditorActionListener when (actionId) { + IME_ACTION_DONE -> { + if (viewModel.isNameValid.value == true) { + binding.contactDiaryPersonSaveButton.performClick() + } + false + } + else -> true + } + } + } + + viewModel.shouldClose.observe2(this) { + popBackStack() + } + + viewModel.isNameValid.observe2(this) { isValid -> + binding.contactDiaryPersonSaveButton.isEnabled = isValid + } + } + + private val deletePersonConfirmationDialog by lazy { + DialogHelper.DialogInstance( + requireActivity(), + R.string.contact_diary_delete_person_title, + R.string.contact_diary_delete_persons_message, + R.string.contact_diary_delete_button_positive, + R.string.contact_diary_delete_button_negative, + positiveButtonFunction = { + navArgs.selectedPerson?.let { + viewModel.deletePerson(it) + } + } + ) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonModule.kt similarity index 50% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogModule.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonModule.kt index 5a3123d10b890a0d4fbede8091a4295847d3ce46..9693086615ccf75f9410d51478eb2b0cb08f8359 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogModule.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonModule.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.contactdiary.ui.sheets.location +package de.rki.coronawarnapp.contactdiary.ui.person import dagger.Binds import dagger.Module @@ -8,11 +8,11 @@ import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactory import de.rki.coronawarnapp.util.viewmodel.CWAViewModelKey @Module -abstract class ContactDiaryLocationBottomSheetDialogModule { +abstract class ContactDiaryAddPersonModule { @Binds @IntoMap - @CWAViewModelKey(ContactDiaryLocationBottomSheetDialogViewModel::class) - abstract fun contactDiaryLocationBottomSheetDialogFragment( - factory: ContactDiaryLocationBottomSheetDialogViewModel.Factory + @CWAViewModelKey(ContactDiaryAddPersonViewModel::class) + abstract fun contactDiaryAddPersonFragment( + factory: ContactDiaryAddPersonViewModel.Factory ): CWAViewModelFactory<out CWAViewModel> } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt similarity index 63% rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogViewModel.kt rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt index 6f96510f35871ea2425d763025523792d04e9087..f00889cfb4be8cc61242db89f5a8bc90f1fb271a 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogViewModel.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/person/ContactDiaryAddPersonViewModel.kt @@ -1,4 +1,4 @@ -package de.rki.coronawarnapp.contactdiary.ui.sheets.person +package de.rki.coronawarnapp.contactdiary.ui.person import androidx.lifecycle.asLiveData import dagger.assisted.Assisted @@ -10,9 +10,8 @@ import de.rki.coronawarnapp.contactdiary.storage.entity.ContactDiaryPersonEntity import de.rki.coronawarnapp.contactdiary.storage.repo.ContactDiaryRepository import de.rki.coronawarnapp.exception.ExceptionCategory import de.rki.coronawarnapp.exception.reporting.report -import de.rki.coronawarnapp.contactdiary.util.formatContactDiaryNameField -import de.rki.coronawarnapp.ui.SingleLiveEvent 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.CoroutineExceptionHandler @@ -21,35 +20,33 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import org.joda.time.LocalDate -class ContactDiaryPersonBottomSheetDialogViewModel @AssistedInject constructor( +class ContactDiaryAddPersonViewModel @AssistedInject constructor( dispatcherProvider: DispatcherProvider, @Assisted private val addedAt: String?, private val contactDiaryRepository: ContactDiaryRepository ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { private val coroutineExceptionHandler = CoroutineExceptionHandler { _, ex -> - shouldClose.postValue(null) ex.report(ExceptionCategory.INTERNAL, TAG) } - private val text = MutableStateFlow("") - - val isValid = text.map { - it.isNotEmpty() && it.length <= MAX_PERSON_NAME_LENGTH - }.asLiveData() - val shouldClose = SingleLiveEvent<Unit>() - private val formattedName: String - get() = text.value.formatContactDiaryNameField(MAX_PERSON_NAME_LENGTH) + private val name = MutableStateFlow("") - fun textChanged(locationName: String) { - text.value = locationName + val isNameValid = name + .map { it.isNotEmpty() } + .asLiveData() + + fun nameChanged(value: String) { + name.value = value.trim() } - fun addPerson() = launch(coroutineExceptionHandler) { + fun addPerson(phoneNumber: String, emailAddress: String) = launch(coroutineExceptionHandler) { val person = contactDiaryRepository.addPerson( DefaultContactDiaryPerson( - fullName = formattedName + fullName = name.value, + phoneNumber = phoneNumber, + emailAddress = emailAddress ) ) @@ -61,19 +58,22 @@ class ContactDiaryPersonBottomSheetDialogViewModel @AssistedInject constructor( ) ) } - shouldClose.postValue(null) } - fun updatePerson(person: ContactDiaryPersonEntity) = launch(coroutineExceptionHandler) { - contactDiaryRepository.updatePerson( - DefaultContactDiaryPerson( - person.personId, - fullName = formattedName + fun updatePerson(person: ContactDiaryPersonEntity, phoneNumber: String, emailAddress: String) = + launch(coroutineExceptionHandler) { + contactDiaryRepository.updatePerson( + DefaultContactDiaryPerson( + person.personId, + fullName = name.value, + phoneNumber = phoneNumber, + emailAddress = emailAddress + + ) ) - ) - shouldClose.postValue(null) - } + shouldClose.postValue(null) + } fun deletePerson(person: ContactDiaryPersonEntity) = launch(coroutineExceptionHandler) { contactDiaryRepository.personEncounters.firstOrNull()?.forEach { @@ -89,12 +89,11 @@ class ContactDiaryPersonBottomSheetDialogViewModel @AssistedInject constructor( } companion object { - private const val MAX_PERSON_NAME_LENGTH = 250 - private val TAG = ContactDiaryPersonBottomSheetDialogViewModel::class.java.simpleName + private val TAG = ContactDiaryAddPersonViewModel::class.java.simpleName } @AssistedFactory - interface Factory : CWAViewModelFactory<ContactDiaryPersonBottomSheetDialogViewModel> { - fun create(addedAt: String?): ContactDiaryPersonBottomSheetDialogViewModel + interface Factory : CWAViewModelFactory<ContactDiaryAddPersonViewModel> { + fun create(addedAt: String?): ContactDiaryAddPersonViewModel } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogFragment.kt deleted file mode 100644 index af7f6db44b2bfd9cf382e2e0c4ad8eb93563a37b..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/location/ContactDiaryLocationBottomSheetDialogFragment.kt +++ /dev/null @@ -1,112 +0,0 @@ -package de.rki.coronawarnapp.contactdiary.ui.sheets.location - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo -import androidx.core.widget.doAfterTextChanged -import androidx.navigation.fragment.navArgs -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.util.focusAndShowKeyboard -import de.rki.coronawarnapp.databinding.ContactDiaryLocationBottomSheetFragmentBinding -import de.rki.coronawarnapp.util.DialogHelper -import de.rki.coronawarnapp.util.di.AutoInject -import de.rki.coronawarnapp.util.ui.observe2 -import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider -import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted -import javax.inject.Inject - -class ContactDiaryLocationBottomSheetDialogFragment : BottomSheetDialogFragment(), AutoInject { - private var _binding: ContactDiaryLocationBottomSheetFragmentBinding? = null - private val binding get() = _binding!! - - @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val viewModel: ContactDiaryLocationBottomSheetDialogViewModel by cwaViewModelsAssisted( - factoryProducer = { viewModelFactory }, - constructorCall = { factory, _ -> - factory as ContactDiaryLocationBottomSheetDialogViewModel.Factory - factory.create(navArgs.addedAt) - } - ) - - private val navArgs: ContactDiaryLocationBottomSheetDialogFragmentArgs by navArgs() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = ContactDiaryLocationBottomSheetFragmentBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val location = navArgs.selectedLocation - if (location != null) { - binding.contactDiaryLocationBottomSheetTextInputEditText.setText(location.locationName) - binding.contactDiaryLocationBottomSheetDeleteButton.visibility = View.VISIBLE - binding.contactDiaryLocationBottomSheetDeleteButton.setOnClickListener { - DialogHelper.showDialog(deleteLocationConfirmationDialog) - } - binding.contactDiaryLocationBottomSheetSaveButton.setOnClickListener { - viewModel.updateLocation(location) - } - } else { - binding.contactDiaryLocationBottomSheetDeleteButton.visibility = View.GONE - binding.contactDiaryLocationBottomSheetSaveButton.setOnClickListener { - viewModel.addLocation() - } - } - - binding.contactDiaryLocationBottomSheetCloseButton.setOnClickListener { - viewModel.closePressed() - } - - binding.contactDiaryLocationBottomSheetTextInputEditText.doAfterTextChanged { - viewModel.textChanged(it.toString()) - } - - binding.contactDiaryLocationBottomSheetTextInputEditText.setOnEditorActionListener { _, actionId, _ -> - return@setOnEditorActionListener when (actionId) { - EditorInfo.IME_ACTION_DONE -> { - if (viewModel.isValid.value == true) { - binding.contactDiaryLocationBottomSheetSaveButton.performClick() - } - false - } - else -> true - } - } - - binding.contactDiaryLocationBottomSheetTextInputEditText.focusAndShowKeyboard() - - viewModel.shouldClose.observe2(this) { - dismiss() - } - - viewModel.isValid.observe2(this) { - binding.contactDiaryLocationBottomSheetTextInputLayout.isErrorEnabled = it - binding.contactDiaryLocationBottomSheetSaveButton.isEnabled = it - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private val deleteLocationConfirmationDialog by lazy { - DialogHelper.DialogInstance( - requireActivity(), - R.string.contact_diary_delete_location_title, - R.string.contact_diary_delete_locations_message, - R.string.contact_diary_delete_button_positive, - R.string.contact_diary_delete_button_negative, - positiveButtonFunction = { - navArgs.selectedLocation?.let { - viewModel.deleteLocation(it) - } - } - ) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogFragment.kt deleted file mode 100644 index e0e470712e92be4f54763c1d9e553e3c36beced3..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/sheets/person/ContactDiaryPersonBottomSheetDialogFragment.kt +++ /dev/null @@ -1,112 +0,0 @@ -package de.rki.coronawarnapp.contactdiary.ui.sheets.person - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.EditorInfo.IME_ACTION_DONE -import androidx.core.widget.doAfterTextChanged -import androidx.navigation.fragment.navArgs -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.util.focusAndShowKeyboard -import de.rki.coronawarnapp.databinding.ContactDiaryPersonBottomSheetFragmentBinding -import de.rki.coronawarnapp.util.DialogHelper -import de.rki.coronawarnapp.util.di.AutoInject -import de.rki.coronawarnapp.util.ui.observe2 -import de.rki.coronawarnapp.util.viewmodel.CWAViewModelFactoryProvider -import de.rki.coronawarnapp.util.viewmodel.cwaViewModelsAssisted -import javax.inject.Inject - -class ContactDiaryPersonBottomSheetDialogFragment : BottomSheetDialogFragment(), AutoInject { - private var _binding: ContactDiaryPersonBottomSheetFragmentBinding? = null - private val binding get() = _binding!! - - @Inject lateinit var viewModelFactory: CWAViewModelFactoryProvider.Factory - private val viewModel: ContactDiaryPersonBottomSheetDialogViewModel by cwaViewModelsAssisted( - factoryProducer = { viewModelFactory }, - constructorCall = { factory, _ -> - factory as ContactDiaryPersonBottomSheetDialogViewModel.Factory - factory.create(navArgs.addedAt) - } - ) - - private val navArgs: ContactDiaryPersonBottomSheetDialogFragmentArgs by navArgs() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = ContactDiaryPersonBottomSheetFragmentBinding.inflate(inflater) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val person = navArgs.selectedPerson - if (person != null) { - binding.contactDiaryPersonBottomSheetTextInputEditText.setText(person.fullName) - binding.contactDiaryPersonBottomSheetDeleteButton.visibility = View.VISIBLE - binding.contactDiaryPersonBottomSheetDeleteButton.setOnClickListener { - DialogHelper.showDialog(deletePersonConfirmationDialog) - } - binding.contactDiaryPersonBottomSheetSaveButton.setOnClickListener { - viewModel.updatePerson(person) - } - } else { - binding.contactDiaryPersonBottomSheetDeleteButton.visibility = View.GONE - binding.contactDiaryPersonBottomSheetSaveButton.setOnClickListener { - viewModel.addPerson() - } - } - - binding.contactDiaryPersonBottomSheetCloseButton.setOnClickListener { - viewModel.closePressed() - } - - binding.contactDiaryPersonBottomSheetTextInputEditText.doAfterTextChanged { - viewModel.textChanged(it.toString()) - } - - binding.contactDiaryPersonBottomSheetTextInputEditText.setOnEditorActionListener { _, actionId, _ -> - return@setOnEditorActionListener when (actionId) { - IME_ACTION_DONE -> { - if (viewModel.isValid.value == true) { - binding.contactDiaryPersonBottomSheetSaveButton.performClick() - } - false - } - else -> true - } - } - - binding.contactDiaryPersonBottomSheetTextInputEditText.focusAndShowKeyboard() - - viewModel.shouldClose.observe2(this) { - dismiss() - } - - viewModel.isValid.observe2(this) { isValid -> - binding.contactDiaryPersonBottomSheetTextInputLayout.isErrorEnabled = isValid - binding.contactDiaryPersonBottomSheetSaveButton.isEnabled = isValid - } - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - private val deletePersonConfirmationDialog by lazy { - DialogHelper.DialogInstance( - requireActivity(), - R.string.contact_diary_delete_person_title, - R.string.contact_diary_delete_persons_message, - R.string.contact_diary_delete_button_positive, - R.string.contact_diary_delete_button_negative, - positiveButtonFunction = { - navArgs.selectedPerson?.let { - viewModel.deletePerson(it) - } - } - ) - } -} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/AbstractAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/AbstractAdapter.kt index bb33057d5a087dcf09310442a0049505d0a3a705..9fd0d0a9f6e032e5680babda46a20763570640ea 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/AbstractAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/AbstractAdapter.kt @@ -8,6 +8,11 @@ import de.rki.coronawarnapp.util.lists.diffutil.AsyncDiffer internal abstract class AbstractAdapter<T : HasStableId, U : BaseAdapter.VH> : BaseAdapter<U>(), AsyncDiffUtilAdapter<T> { + + init { + setHasStableIds(true) + } + override val asyncDiffer: AsyncDiffer<T> = AsyncDiffer(this) override fun getItemCount(): Int = data.size diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensions.kt index 0d2a4be3c8201527fb525074141fed432f338f44..f09c9babde3f90577844a98b469c265eb9cd340b 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensions.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensions.kt @@ -3,7 +3,6 @@ package de.rki.coronawarnapp.contactdiary.util import android.content.Context import android.os.Build import android.view.View -import android.view.ViewTreeObserver import android.view.inputmethod.InputMethodManager import androidx.core.view.AccessibilityDelegateCompat import androidx.core.view.ViewCompat @@ -44,16 +43,6 @@ fun LocalDate.toFormattedDayForAccessibility(locale: Locale): String { DateTimeFormat.longDate().withLocale(locale).print(this) } -fun String.formatContactDiaryNameField(maxLength: Int): String { - val newName = if (isNotBlank()) { - trim() - } else { - // allow only spaces as a name - this - } - return newName.take(maxLength) -} - fun View.focusAndShowKeyboard() { /** * This is to be called when the window already has focus. @@ -70,17 +59,13 @@ fun View.focusAndShowKeyboard() { requestFocus() if (hasWindowFocus()) { showTheKeyboardNow() - } else { - viewTreeObserver.addOnWindowFocusChangeListener( - object : ViewTreeObserver.OnWindowFocusChangeListener { - override fun onWindowFocusChanged(hasFocus: Boolean) { - if (hasFocus) { - this@focusAndShowKeyboard.showTheKeyboardNow() - viewTreeObserver.removeOnWindowFocusChangeListener(this) - } - } - } - ) + } +} + +fun View.hideKeyboard() { + post { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(this.windowToken, 0) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/SelectableItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/SelectableItem.kt deleted file mode 100644 index dcf17cd5fb31dbc2712f4fcbf04d828fc4fef7ea..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/util/SelectableItem.kt +++ /dev/null @@ -1,15 +0,0 @@ -package de.rki.coronawarnapp.contactdiary.util - -import androidx.annotation.StringRes -import de.rki.coronawarnapp.util.lists.HasStableId -import de.rki.coronawarnapp.util.ui.LazyString - -data class SelectableItem<T : HasStableId>( - val selected: Boolean, - val item: T, - val contentDescription: LazyString, - val onClickDescription: LazyString, - @StringRes val clickLabel: Int, - @StringRes val onClickLabel: Int, - override val stableId: Long = item.stableId -) : HasStableId diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/StringExtensions.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/StringExtensions.kt new file mode 100644 index 0000000000000000000000000000000000000000..d598bf4300425054865c97e6129ab2946f70aa92 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/StringExtensions.kt @@ -0,0 +1,5 @@ +package de.rki.coronawarnapp.util + +import kotlin.math.min + +fun String.trimToLength(maxLength: Int) = this.substring(0, min(length, maxLength)) diff --git a/Corona-Warn-App/src/main/res/color/button_contact_diary_person.xml b/Corona-Warn-App/src/main/res/color/button_contact_diary_person.xml new file mode 100644 index 0000000000000000000000000000000000000000..271ca0d8ea170d06cba1ecdc1814778960527bcb --- /dev/null +++ b/Corona-Warn-App/src/main/res/color/button_contact_diary_person.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="@color/colorBackground" android:state_checked="false" /> + <item android:color="@color/colorAccentTintButtonPressed" android:state_checked="true" /> +</selector> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml b/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml index b50e718df93fef8b535cb1c85212cc1c07b7957e..f3502eefc53ea2e4584d34ef3c5060cf6909b219 100644 --- a/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml +++ b/Corona-Warn-App/src/main/res/color/button_text_color_emphasized.xml @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="@color/colorTextPrimary2" android:state_enabled="false" /> <!-- disabled --> + <item android:color="@color/colorBackground" android:state_enabled="false" /> <!-- disabled --> <item android:color="@color/colorTextEmphasizedButton" /> <!-- default --> </selector> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/contact_diary_duration_background_default.xml b/Corona-Warn-App/src/main/res/drawable/contact_diary_duration_background_default.xml new file mode 100644 index 0000000000000000000000000000000000000000..1da6f7a32549b63954dbbf71075912fe0ec5c639 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/contact_diary_duration_background_default.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/radius_card" /> + <solid android:color="@color/colorSurface1" /> +</shape> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/drawable/contact_diary_duration_background_selected.xml b/Corona-Warn-App/src/main/res/drawable/contact_diary_duration_background_selected.xml new file mode 100644 index 0000000000000000000000000000000000000000..b230c96594d3925ca38a19c3802cfaeb1fd3ebc9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/drawable/contact_diary_duration_background_selected.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/radius_card" /> + <solid android:color="@color/colorSurface1" /> + <stroke android:width="2dip" android:color="@color/colorAccent" /> +</shape> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..6b605a2815d151fa890c93c30566a5d92a2633e9 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_add_location_fragment.xml @@ -0,0 +1,129 @@ +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/contact_diary_add_location_close_button" + style="@style/buttonIcon" + android:layout_width="@dimen/button_icon" + android:layout_height="@dimen/button_icon" + android:layout_marginStart="@dimen/spacing_tiny" + android:layout_marginTop="@dimen/spacing_tiny" + android:contentDescription="@string/accessibility_close" + android:padding="@dimen/spacing_mega_tiny" + android:src="@drawable/ic_close" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/contact_diary_add_location_title" + style="@style/headline6" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:text="@string/contact_diary_add_location_title" + app:layout_constraintBottom_toBottomOf="@id/contact_diary_add_location_close_button" + app:layout_constraintEnd_toStartOf="@id/contact_diary_add_location_delete_button" + app:layout_constraintStart_toEndOf="@id/contact_diary_add_location_close_button" + app:layout_constraintTop_toTopOf="@id/contact_diary_add_location_close_button" /> + + <ImageView + android:id="@+id/contact_diary_add_location_delete_button" + style="@style/buttonIcon" + android:layout_width="@dimen/button_icon" + android:layout_height="@dimen/button_icon" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_tiny" + android:contentDescription="@string/contact_diary_delete_icon_content_description" + android:padding="@dimen/button_icon_padding" + android:src="@drawable/ic_baseline_delete" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/contact_diary_add_location_name_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" + android:hint="@string/contact_diary_add_location_text_input_hint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/contact_diary_add_location_close_button"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/contact_diary_add_location_name_input_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textCapWords" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/contact_diary_add_location_phone_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" + android:hint="@string/contact_diary_add_text_input_phone_hint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/contact_diary_add_location_name_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/contact_diary_add_location_phone_input_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="phone" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/contact_diary_add_location_email_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" + android:hint="@string/contact_diary_add_text_input_email_hint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/contact_diary_add_location_phone_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/contact_diary_add_location_email_input_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:inputType="textEmailAddress" /> + + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/contact_diary_add_location_save_button" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" + android:layout_marginBottom="@dimen/spacing_small" + android:text="@string/contact_diary_add_location_save_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.166" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_diary_add_location_email_input_layout" + app:layout_constraintVertical_bias="1.0" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> + diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..90420e38aacae58b2a691c99c11305ba18e98e40 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_add_person_fragment.xml @@ -0,0 +1,129 @@ +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/contact_diary_person_close_button" + style="@style/buttonIcon" + android:layout_width="@dimen/button_icon" + android:layout_height="@dimen/button_icon" + android:layout_marginStart="@dimen/spacing_tiny" + android:layout_marginTop="@dimen/spacing_tiny" + android:contentDescription="@string/accessibility_close" + android:padding="@dimen/spacing_mega_tiny" + android:src="@drawable/ic_close" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/contact_diary_person_title" + style="@style/headline6" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/spacing_normal" + android:text="@string/contact_diary_add_person_title" + app:layout_constraintBottom_toBottomOf="@+id/contact_diary_person_close_button" + app:layout_constraintStart_toEndOf="@+id/contact_diary_person_close_button" + app:layout_constraintTop_toTopOf="@+id/contact_diary_person_close_button" /> + + <ImageView + android:id="@+id/contact_diary_person_delete_button" + style="@style/buttonIcon" + android:layout_width="@dimen/button_icon" + android:layout_height="@dimen/button_icon" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginEnd="@dimen/spacing_tiny" + android:contentDescription="@string/contact_diary_delete_icon_content_description" + android:padding="@dimen/button_icon_padding" + android:src="@drawable/ic_baseline_delete" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/contact_diary_person_name_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" + android:hint="@string/contact_diary_add_person_text_input_name_hint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_diary_person_close_button"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/contact_diary_person_name_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="textCapWords" + app:backgroundTint="@color/colorContactDiaryListItem" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/contact_diary_person_phone_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" + android:hint="@string/contact_diary_add_text_input_phone_hint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_diary_person_name_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/contact_diary_person_phone_number_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionNext" + android:inputType="phone" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/contact_diary_person_email_input_layout" + style="@style/TextInputLayoutTheme" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_tiny" + android:layout_marginBottom="@dimen/spacing_small" + android:hint="@string/contact_diary_add_text_input_email_hint" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/contact_diary_person_phone_input_layout"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/contact_diary_person_email_edit_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:imeOptions="actionDone" + android:inputType="textEmailAddress" /> + + </com.google.android.material.textfield.TextInputLayout> + + <Button + android:id="@+id/contact_diary_person_save_button" + style="@style/buttonPrimary" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_normal" + android:layout_marginTop="@dimen/spacing_small" + android:layout_marginBottom="@dimen/spacing_small" + android:text="@string/contact_diary_add_person_save_button" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.333" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/contact_diary_person_email_input_layout" + app:layout_constraintVertical_bias="1.0" /> + </androidx.constraintlayout.widget.ConstraintLayout> +</ScrollView> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_comment_info_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_comment_info_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..b2d7b3114fcaeacdf31acd056688e643154c1a15 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_comment_info_fragment.xml @@ -0,0 +1,58 @@ +<?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"> + + <Toolbar + android:id="@+id/toolbar" + style="@style/CWAToolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:navigationIcon="@drawable/ic_close" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + style="@style/headline4" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginTop="12dp" + android:layout_marginEnd="24dp" + android:text="@string/contact_diary_comment_info_screen_title" /> + + <TextView + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginTop="28dp" + android:layout_marginEnd="24dp" + android:letterSpacing="0.015" + android:text="@string/contact_diary_comment_info_screen_description" + android:textStyle="bold" /> + + <TextView + style="@style/body1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginTop="14dp" + android:layout_marginEnd="24dp" + android:text="@string/contact_diary_comment_info_screen_body" /> + + </LinearLayout> + + </ScrollView> + +</LinearLayout> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_duration_picker_dialog_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_duration_picker_dialog_fragment.xml new file mode 100644 index 0000000000000000000000000000000000000000..4a5b66cef9fd8e37cb7d063f7782ff6e8a6ae9d8 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_duration_picker_dialog_fragment.xml @@ -0,0 +1,89 @@ +<?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:id="@+id/duration_container" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/title" + style="@style/headline6" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="24dp" + android:layout_marginTop="@dimen/spacing_small" + android:padding="@dimen/spacing_mega_tiny" + android:text="@string/duration_dialog_title" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Dauer" /> + + <NumberPicker + android:id="@+id/hours" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="67dp" + android:theme="@style/DefaultNumberPickerTheme" + app:layout_constraintBottom_toBottomOf="@+id/divider" + app:layout_constraintEnd_toStartOf="@+id/divider" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/divider" /> + + <TextView + android:id="@+id/divider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="6dp" + android:layout_marginEnd="6dp" + android:text=":" + app:layout_constraintBottom_toBottomOf="@+id/minutes" + app:layout_constraintEnd_toStartOf="@+id/minutes" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/hours" + app:layout_constraintTop_toTopOf="@+id/minutes" + tools:ignore="HardcodedText" + tools:text=":" /> + + <NumberPicker + android:id="@+id/minutes" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="18dp" + android:layout_marginEnd="67dp" + android:theme="@style/DefaultNumberPickerTheme" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/divider" + app:layout_constraintTop_toBottomOf="@+id/title" /> + + <Button + android:id="@+id/cancel_button" + style="@style/buttonLight" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="29dp" + android:layout_marginBottom="@dimen/spacing_small" + android:text="@string/duration_dialog_cancel_button" + android:textColor="@color/colorTextTint" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/ok_button" + app:layout_constraintTop_toBottomOf="@+id/minutes" /> + + <Button + android:id="@+id/ok_button" + style="@style/buttonLight" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="29dp" + android:layout_marginEnd="14dp" + android:layout_marginBottom="@dimen/spacing_small" + android:text="@string/duration_dialog_ok_button" + android:textColor="@color/colorTextTint" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/minutes" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_location_bottom_sheet_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_location_bottom_sheet_fragment.xml deleted file mode 100644 index 8fb6fceb7729465237cdc28fce7a9effabc43eae..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_location_bottom_sheet_fragment.xml +++ /dev/null @@ -1,82 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<layout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/contact_diary_location_bottom_sheet_close_button" - style="@style/buttonIcon" - android:layout_width="@dimen/button_icon" - android:layout_height="@dimen/button_icon" - android:layout_marginStart="@dimen/spacing_tiny" - android:layout_marginTop="@dimen/spacing_small" - android:padding="@dimen/spacing_mega_tiny" - android:contentDescription="@string/accessibility_close" - android:src="@drawable/ic_close" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/contact_diary_location_bottom_sheet_title" - style="@style/headline6" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_normal" - android:text="@string/contact_diary_location_bottom_sheet_title" - app:layout_constraintBottom_toBottomOf="@id/contact_diary_location_bottom_sheet_close_button" - app:layout_constraintEnd_toStartOf="@id/contact_diary_location_bottom_sheet_delete_button" - app:layout_constraintStart_toEndOf="@id/contact_diary_location_bottom_sheet_close_button" - app:layout_constraintTop_toTopOf="@id/contact_diary_location_bottom_sheet_close_button" /> - - <ImageView - android:id="@+id/contact_diary_location_bottom_sheet_delete_button" - style="@style/buttonIcon" - android:layout_width="@dimen/button_icon" - android:layout_height="@dimen/button_icon" - android:layout_marginTop="@dimen/spacing_small" - android:layout_marginEnd="@dimen/spacing_tiny" - android:contentDescription="@string/contact_diary_delete_icon_content_description" - android:padding="@dimen/button_icon_padding" - android:src="@drawable/ic_baseline_delete" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/contact_diary_location_bottom_sheet_text_input_layout" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_normal" - android:layout_marginTop="@dimen/spacing_small" - android:hint="@string/contact_diary_location_bottom_sheet_text_input_hint" - app:counterEnabled="true" - app:counterMaxLength="250" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/contact_diary_location_bottom_sheet_close_button"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/contact_diary_location_bottom_sheet_text_input_edit_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:imeOptions="actionDone" - android:inputType="textCapWords" /> - - </com.google.android.material.textfield.TextInputLayout> - - <android.widget.Button - android:id="@+id/contact_diary_location_bottom_sheet_save_button" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_normal" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginBottom="@dimen/spacing_small" - android:text="@string/contact_diary_location_bottom_sheet_save_button" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/contact_diary_location_bottom_sheet_text_input_layout" /> - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_item.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_item.xml index 4a07d8eb7bfea1df946364dab5ace744ed508b88..26c08436cf8452e4495b48674ee599bddecbd333 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_item.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_location_list_item.xml @@ -1,37 +1,46 @@ <?xml version="1.0" encoding="utf-8"?> -<layout xmlns:android="http://schemas.android.com/apk/res/android" +<de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.ExpandingDiaryListItemView 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:id="@+id/main_box" + style="@style/ContactDiaryExpandableListItem" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/contact_diary_location_list_item" - style="@style/contactDiaryCardRipple" - android:layout_width="match_parent" + <FrameLayout + android:id="@+id/duration_container" + android:layout_width="0dp" android:layout_height="wrap_content" - android:minHeight="@dimen/spacing_huge"> - - <ImageView - android:id="@+id/contact_diary_location_list_item_icon" - android:layout_width="@dimen/spacing_medium" - android:layout_height="@dimen/spacing_medium" - android:layout_marginStart="@dimen/spacing_small" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:srcCompat="@drawable/ic_selected" /> + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + <TextView + style="@style/subtitleBoldSixteen" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|start" + android:text="@string/contact_diary_location_visit_duration_label" /> <TextView - android:id="@+id/contact_diary_location_list_item_name" - style="@style/subtitle" - android:layout_width="0dp" + android:id="@+id/duration_input" + android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_small" - android:layout_marginVertical="@dimen/spacing_small" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/contact_diary_location_list_item_icon" - app:layout_constraintTop_toTopOf="parent" - tools:text="@tools:sample/cities" /> - </androidx.constraintlayout.widget.ConstraintLayout> + android:layout_gravity="center_vertical|end" + android:padding="8dp" + android:text="00:00" /> + </FrameLayout> + + <de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.DiaryCircumstancesTextView + android:id="@+id/circumstances" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/duration_container" /> -</layout> +</de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.ExpandingDiaryListItemView> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml index e05f8779d5a8df94f0d1c4ff8dd521df19083fe7..cce9651223d844aed1d06834edd727bd6deb04bb 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_overview_nested_list_item.xml @@ -2,54 +2,54 @@ <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:id="@+id/contact_diary_overview_element_nested_container" + android:id="@+id/contact_diary_overview_element_nested_body" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginVertical="@dimen/spacing_tiny" android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/contact_diary_overview_element_nested_body" - android:layout_width="match_parent" + <ImageView + android:id="@+id/contact_diary_overview_element_image" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_marginStart="@dimen/spacing_small" + android:importantForAccessibility="no" + android:scaleType="centerInside" + android:src="@drawable/ic_contact_diary_person_item" + app:layout_constraintBottom_toBottomOf="@+id/contact_diary_overview_element_name" + app:layout_constraintEnd_toStartOf="@id/contact_diary_overview_element_name" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/contact_diary_overview_element_name" /> + + <TextView + android:id="@+id/contact_diary_overview_element_name" + style="@style/subtitle" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginVertical="@dimen/spacing_tiny" + android:layout_marginStart="@dimen/spacing_small" + android:layout_marginEnd="@dimen/spacing_small" + android:ellipsize="end" android:focusable="true" - app:layout_constraintBottom_toBottomOf="parent" + android:maxLines="3" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintStart_toEndOf="@+id/contact_diary_overview_element_image" + app:layout_constraintTop_toTopOf="parent" + tools:text="Andrea Steinhauer" /> - <ImageView - android:id="@+id/contact_diary_overview_element_image" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_marginStart="@dimen/spacing_small" - android:importantForAccessibility="no" - android:scaleType="centerInside" - android:src="@drawable/ic_contact_diary_person_item" - app:layout_constraintBaseline_toBaselineOf="@id/contact_diary_overview_element_name" - app:layout_constraintEnd_toStartOf="@id/contact_diary_overview_element_name" - app:layout_constraintStart_toStartOf="parent" /> + <TextView + android:id="@+id/contact_diary_overview_element_attributes" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:maxLines="3" + android:text="unter 15 Min, im Freien" + app:layout_constraintEnd_toEndOf="@+id/contact_diary_overview_element_name" + app:layout_constraintStart_toStartOf="@+id/contact_diary_overview_element_name" + app:layout_constraintTop_toBottomOf="@+id/contact_diary_overview_element_name" /> - <TextView - android:id="@+id/contact_diary_overview_element_name" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_small" - android:layout_marginEnd="@dimen/spacing_small" - android:ellipsize="end" - android:focusable="true" - android:maxLines="3" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/contact_diary_overview_element_image" - app:layout_constraintTop_toTopOf="parent" - tools:text="Andrea Steinhauer" /> +</androidx.constraintlayout.widget.ConstraintLayout> - </androidx.constraintlayout.widget.ConstraintLayout> -</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_person_bottom_sheet_fragment.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_person_bottom_sheet_fragment.xml deleted file mode 100644 index 202230fd6d7fbc51ea072e18361baa9ece99ebdf..0000000000000000000000000000000000000000 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_person_bottom_sheet_fragment.xml +++ /dev/null @@ -1,81 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<layout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto"> - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="match_parent" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/contact_diary_person_bottom_sheet_close_button" - style="@style/buttonIcon" - android:layout_width="@dimen/button_icon" - android:layout_height="@dimen/button_icon" - android:layout_marginStart="@dimen/spacing_tiny" - android:layout_marginTop="@dimen/spacing_small" - android:contentDescription="@string/accessibility_close" - android:src="@drawable/ic_close" - android:padding="@dimen/spacing_mega_tiny" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <TextView - android:id="@+id/contact_diary_person_bottom_sheet_title" - style="@style/headline6" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacing_normal" - android:text="@string/contact_diary_person_bottom_sheet_title" - app:layout_constraintBottom_toBottomOf="@+id/contact_diary_person_bottom_sheet_close_button" - app:layout_constraintStart_toEndOf="@+id/contact_diary_person_bottom_sheet_close_button" - app:layout_constraintTop_toTopOf="@+id/contact_diary_person_bottom_sheet_close_button" /> - - <ImageView - android:id="@+id/contact_diary_person_bottom_sheet_delete_button" - style="@style/buttonIcon" - android:layout_width="@dimen/button_icon" - android:layout_height="@dimen/button_icon" - android:layout_marginTop="@dimen/spacing_small" - android:layout_marginEnd="@dimen/spacing_tiny" - android:contentDescription="@string/contact_diary_delete_icon_content_description" - android:padding="@dimen/button_icon_padding" - android:src="@drawable/ic_baseline_delete" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <com.google.android.material.textfield.TextInputLayout - android:id="@+id/contact_diary_person_bottom_sheet_text_input_layout" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_normal" - android:layout_marginTop="@dimen/spacing_small" - android:hint="@string/contact_diary_person_bottom_sheet_text_input_hint" - app:counterEnabled="true" - app:counterMaxLength="250" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/contact_diary_person_bottom_sheet_close_button"> - - <com.google.android.material.textfield.TextInputEditText - android:id="@+id/contact_diary_person_bottom_sheet_text_input_edit_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:imeOptions="actionDone" - android:inputType="textCapWords" /> - - </com.google.android.material.textfield.TextInputLayout> - - <android.widget.Button - android:id="@+id/contact_diary_person_bottom_sheet_save_button" - style="@style/buttonPrimary" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_normal" - android:layout_marginTop="@dimen/spacing_normal" - android:layout_marginBottom="@dimen/spacing_small" - android:text="@string/contact_diary_person_bottom_sheet_save_button" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/contact_diary_person_bottom_sheet_text_input_layout" /> - </androidx.constraintlayout.widget.ConstraintLayout> -</layout> diff --git a/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_item.xml b/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_item.xml index 1b4da486f70f01340c52b402921f1ec591b890ff..df108259f04fafd619afddac188c2238fd161e07 100644 --- a/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_item.xml +++ b/Corona-Warn-App/src/main/res/layout/contact_diary_person_list_item.xml @@ -1,37 +1,123 @@ <?xml version="1.0" encoding="utf-8"?> -<layout xmlns:android="http://schemas.android.com/apk/res/android" +<de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.ExpandingDiaryListItemView 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:id="@+id/main_box" + style="@style/ContactDiaryExpandableListItem" + android:layout_width="match_parent" + android:layout_height="wrap_content"> - <androidx.constraintlayout.widget.ConstraintLayout - android:id="@+id/contact_diary_person_list_item" - style="@style/contactDiaryCardRipple" + <com.google.android.material.button.MaterialButtonToggleGroup + android:id="@+id/duration_group" + style="?attr/materialButtonToggleGroupStyle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="16dp" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:singleSelection="true"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/duration_below_15" + android:layout_width="match_parent" + app:backgroundTint="@color/colorBackground" + style="@style/contactDiaryPersonButton" + android:layout_height="match_parent" + android:layout_weight="1" + android:paddingHorizontal="5dp" + android:text="@string/contact_diary_person_encounter_duration_below_15_min" /> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="@color/cwaGrayHighlight" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/duration_above_15" + style="@style/contactDiaryPersonButton" + app:backgroundTint="@color/colorBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:text="@string/contact_diary_person_encounter_duration_above_15_min" /> + + </com.google.android.material.button.MaterialButtonToggleGroup> + + <com.google.android.material.button.MaterialButtonToggleGroup + android:id="@+id/mask_group" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/duration_group" + app:singleSelection="true"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/mask_with" + style="@style/contactDiaryPersonButton" + android:layout_width="match_parent" + app:backgroundTint="@color/colorBackground" + android:layout_height="match_parent" + android:layout_weight="1" + android:text="@string/contact_diary_person_encounter_mask_with" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/mask_without" + style="@style/contactDiaryPersonButton" + app:backgroundTint="@color/colorBackground" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_weight="1" + android:text="@string/contact_diary_person_encounter_mask_without" /> + + </com.google.android.material.button.MaterialButtonToggleGroup> + + <com.google.android.material.button.MaterialButtonToggleGroup + android:id="@+id/environment_group" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:orientation="horizontal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/mask_group" + app:singleSelection="true"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/environment_outside" + style="@style/contactDiaryPersonButton" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:backgroundTint="@color/colorBackground" + android:layout_weight="1" + android:text="@string/contact_diary_person_encounter_environment_outside" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/environment_inside" + style="@style/contactDiaryPersonButton" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:backgroundTint="@color/colorBackground" + android:layout_weight="1" + android:text="@string/contact_diary_person_encounter_environment_inside" /> + + </com.google.android.material.button.MaterialButtonToggleGroup> + + <de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.DiaryCircumstancesTextView + android:id="@+id/circumstances" android:layout_width="match_parent" android:layout_height="wrap_content" - android:minHeight="@dimen/spacing_huge"> - - <ImageView - android:id="@+id/contact_diary_person_list_item_icon" - android:layout_width="@dimen/spacing_medium" - android:layout_height="@dimen/spacing_medium" - android:layout_marginStart="@dimen/spacing_small" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:srcCompat="@drawable/ic_selected" /> - - <TextView - android:id="@+id/contact_diary_person_list_item_name" - style="@style/subtitle" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_marginHorizontal="@dimen/spacing_small" - android:layout_marginVertical="@dimen/spacing_small" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toEndOf="@+id/contact_diary_person_list_item_icon" - app:layout_constraintTop_toTopOf="parent" - tools:text="@tools:sample/full_names" /> - </androidx.constraintlayout.widget.ConstraintLayout> - -</layout> + android:layout_margin="16dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/environment_group" /> + +</de.rki.coronawarnapp.contactdiary.ui.day.tabs.common.ExpandingDiaryListItemView> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml b/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml new file mode 100644 index 0000000000000000000000000000000000000000..17a22866f6708fd479dc41c16d4257a5908205ce --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/view_diary_circumstances_textview.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge 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" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/input_layout" + style="@style/ContactDiaryCircumstancesTextInputLayout" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:hintEnabled="false" + app:layout_constraintEnd_toStartOf="@id/info_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/environment_group"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/input" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/contact_diary_person_encounter_circumstances_hint" + android:inputType="textMultiLine" + android:maxLength="250" + android:padding="@dimen/spacing_tiny" + tools:text="This was a triumph. I'm making a note here; 'Huge success'. It's hard to overstate my satisfaction. We do what we must, because we can." /> + + </com.google.android.material.textfield.TextInputLayout> + + <ImageButton + android:id="@+id/info_button" + style="@style/ContactDiaryInfoButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/environment_group" /> + +</merge> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/layout/view_expanding_diary_listitem.xml b/Corona-Warn-App/src/main/res/layout/view_expanding_diary_listitem.xml new file mode 100644 index 0000000000000000000000000000000000000000..612acf6411c1ea4ea37aee00bbeb35895078d954 --- /dev/null +++ b/Corona-Warn-App/src/main/res/layout/view_expanding_diary_listitem.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge 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/contactDiaryCard" + android:layout_width="match_parent" + android:layout_height="wrap_content" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:minHeight="@dimen/spacing_huge"> + + <ImageView + android:id="@+id/header_checkbox" + android:layout_width="@dimen/spacing_medium" + android:layout_height="@dimen/spacing_medium" + android:layout_marginStart="@dimen/spacing_small" + android:clickable="false" + android:focusable="false" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_unselected" + tools:srcCompat="@drawable/ic_selected" /> + + <TextView + android:id="@+id/header_title" + style="@style/subtitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/spacing_small" + android:layout_marginVertical="@dimen/spacing_small" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/header_checkbox" + app:layout_constraintTop_toTopOf="parent" + tools:text="Primary list item title" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <View + android:id="@+id/divider" + android:layout_width="match_parent" + android:layout_height="1dp" + android:background="?android:attr/listDivider" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/header" + tools:visibility="visible" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="@dimen/spacing_huge" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/divider" + tools:visibility="visible" /> + +</merge> \ No newline at end of file diff --git a/Corona-Warn-App/src/main/res/navigation/contact_diary_nav_graph.xml b/Corona-Warn-App/src/main/res/navigation/contact_diary_nav_graph.xml index 97572c5b032ecd86fc0757b48b245b2fd8103007..c51fa6233504c24597917c39694102d63bf3b5c6 100644 --- a/Corona-Warn-App/src/main/res/navigation/contact_diary_nav_graph.xml +++ b/Corona-Warn-App/src/main/res/navigation/contact_diary_nav_graph.xml @@ -14,11 +14,11 @@ android:defaultValue="2020-03-25" app:argType="string" /> <action - android:id="@+id/action_contactDiaryDayFragment_to_contactDiaryPersonBottomSheetDialogFragment" - app:destination="@id/contactDiaryPersonBottomSheetDialogFragment" /> + android:id="@+id/action_contactDiaryDayFragment_to_contactDiaryAddPersonFragment" + app:destination="@id/contactDiaryAddPersonFragment" /> <action - android:id="@+id/action_contactDiaryDayFragment_to_contactDiaryLocationBottomSheetDialogFragment" - app:destination="@id/contactDiaryLocationBottomSheetDialogFragment" /> + android:id="@+id/action_contactDiaryDayFragment_to_contactDiaryAddLocationFragment" + app:destination="@id/contactDiaryAddLocationFragment" /> <deepLink app:uri="coronawarnapp://contact-journal/day/{selectedDay}" /> </fragment> <fragment @@ -29,6 +29,9 @@ <argument android:name="selectedDay" app:argType="string" /> + <action + android:id="@+id/action_contactDiaryPersonListFragment_to_contactDiaryPersonCommentInfoFragment" + app:destination="@id/contactDiaryCommentInfoFragment" /> </fragment> <fragment android:id="@+id/contactDiaryPlaceListFragment" @@ -39,11 +42,11 @@ android:name="selectedDay" app:argType="string" /> </fragment> - <dialog - android:id="@+id/contactDiaryPersonBottomSheetDialogFragment" - android:name="de.rki.coronawarnapp.contactdiary.ui.sheets.person.ContactDiaryPersonBottomSheetDialogFragment" - android:label="ContactDiaryPersonBottomSheetDialogFragment" - tools:layout="@layout/contact_diary_person_bottom_sheet_fragment"> + <fragment + android:id="@+id/contactDiaryAddPersonFragment" + android:name="de.rki.coronawarnapp.contactdiary.ui.person.ContactDiaryAddPersonFragment" + android:label="ContactDiaryAddPersonFragment" + tools:layout="@layout/contact_diary_add_person_fragment"> <argument android:name="selectedPerson" android:defaultValue="@null" @@ -54,12 +57,12 @@ android:defaultValue="@null" app:argType="string" app:nullable="true" /> - </dialog> - <dialog - android:id="@+id/contactDiaryLocationBottomSheetDialogFragment" - android:name="de.rki.coronawarnapp.contactdiary.ui.sheets.location.ContactDiaryLocationBottomSheetDialogFragment" - android:label="ContactDiaryLocationBottomSheetDialogFragment" - tools:layout="@layout/contact_diary_location_bottom_sheet_fragment"> + </fragment> + <fragment + android:id="@+id/contactDiaryAddLocationFragment" + android:name="de.rki.coronawarnapp.contactdiary.ui.location.ContactDiaryAddLocationFragment" + android:label="ContactDiaryAddLocationFragment" + tools:layout="@layout/contact_diary_add_location_fragment"> <argument android:name="selectedLocation" android:defaultValue="@null" @@ -70,7 +73,7 @@ android:defaultValue="@null" app:argType="string" app:nullable="true" /> - </dialog> + </fragment> <fragment android:id="@+id/contactDiaryOnboardingFragment" android:name="de.rki.coronawarnapp.contactdiary.ui.onboarding.ContactDiaryOnboardingFragment" @@ -88,9 +91,10 @@ android:name="showBottomNav" android:defaultValue="true" app:argType="boolean" /> - <deepLink app:uri="coronawarnapp://contact-journal/oboarding/?goToDay={goToDay}" + <deepLink app:popUpTo="@id/contact_diary_nav_graph" - app:popUpToInclusive="true" /> + app:popUpToInclusive="true" + app:uri="coronawarnapp://contact-journal/oboarding/?goToDay={goToDay}" /> </fragment> <fragment android:id="@+id/contactDiaryInformationPrivacyFragment" @@ -121,8 +125,8 @@ android:label="ContactDiaryEditLocationsFragment" tools:layout="@layout/contact_diary_edit_locations_fragment"> <action - android:id="@+id/action_contactDiaryEditLocationsFragment_to_contactDiaryLocationBottomSheetDialogFragment" - app:destination="@id/contactDiaryLocationBottomSheetDialogFragment" /> + android:id="@+id/action_contactDiaryEditLocationsFragment_to_contactDiaryAddLocationFragment" + app:destination="@id/contactDiaryAddLocationFragment" /> </fragment> <fragment android:id="@+id/contactDiaryEditPersonsFragment" @@ -130,8 +134,13 @@ android:label="ContactDiaryEditPersonsFragment" tools:layout="@layout/contact_diary_edit_persons_fragment"> <action - android:id="@+id/action_contactDiaryEditPersonsFragment_to_contactDiaryPersonBottomSheetDialogFragment" - app:destination="@id/contactDiaryPersonBottomSheetDialogFragment" /> + android:id="@+id/action_contactDiaryEditPersonsFragment_to_contactDiaryAddPersonFragment" + app:destination="@id/contactDiaryAddPersonFragment" /> </fragment> + <fragment + android:id="@+id/contactDiaryCommentInfoFragment" + android:name="de.rki.coronawarnapp.contactdiary.ui.day.tabs.ContactDiaryCommentInfoFragment" + android:label="CommentInfoFragment" + tools:layout="@layout/contact_diary_comment_info_fragment" /> </navigation> diff --git a/Corona-Warn-App/src/main/res/values-bg/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-bg/contact_diary_strings.xml index f86871acd91c094facd263febcf699f21ab59e07..7b9a7397daa1aebab0d499105fe5c82e02457ee4 100644 --- a/Corona-Warn-App/src/main/res/values-bg/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-bg/contact_diary_strings.xml @@ -10,12 +10,12 @@ <string name="contact_diary_location_list_no_items_subtitle">"Създайте мÑÑто и го добавете към Ð’Ð°ÑˆÐ¸Ñ Ð´Ð½ÐµÐ²Ð½Ð¸Ðº на контактите."</string> <string name="contact_diary_person_list_no_items_title">"Ð’Ñе още нÑма въведени лица"</string> <string name="contact_diary_person_list_no_items_subtitle">"Създайте лице и го добавете към Ð’Ð°ÑˆÐ¸Ñ Ð´Ð½ÐµÐ²Ð½Ð¸Ðº на контактите."</string> - <string name="contact_diary_person_bottom_sheet_title">"Лице"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"Име"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Запазване"</string> - <string name="contact_diary_location_bottom_sheet_title">"ÐœÑÑто"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"ОпиÑание"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Запазване"</string> + <string name="contact_diary_add_person_title">"Лице"</string> + <string name="contact_diary_add_person_text_input_name_hint">"Име"</string> + <string name="contact_diary_add_person_save_button">"Запазване"</string> + <string name="contact_diary_add_location_title">"ÐœÑÑто"</string> + <string name="contact_diary_add_location_text_input_hint">"ОпиÑание"</string> + <string name="contact_diary_add_location_save_button">"Запазване"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Дневник на контактите"</string> diff --git a/Corona-Warn-App/src/main/res/values-de/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-de/contact_diary_strings.xml index 9e707f3c8350e7bbfaf5cee0213fa486f818a9ea..2bf855a09b9124fc7a354ccbe4f0528bca6edf11 100644 --- a/Corona-Warn-App/src/main/res/values-de/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/contact_diary_strings.xml @@ -11,12 +11,14 @@ <string name="contact_diary_location_list_no_items_subtitle">"Legen Sie einen Ort an und fügen Sie ihn in Ihrem Kontakt-Tagebuch hinzu."</string> <string name="contact_diary_person_list_no_items_title">"Noch keine Personen vorhanden"</string> <string name="contact_diary_person_list_no_items_subtitle">"Legen Sie eine Person an und fügen Sie sie in Ihrem Kontakt-Tagebuch hinzu."</string> - <string name="contact_diary_person_bottom_sheet_title">"Person"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"Name"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Speichern"</string> - <string name="contact_diary_location_bottom_sheet_title">"Ort"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"Bezeichnung"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Speichern"</string> + <string name="contact_diary_add_person_title">"Person"</string> + <string name="contact_diary_add_person_text_input_name_hint">"Vorname, Nachname"</string> + <string name="contact_diary_add_text_input_phone_hint">"Telefon"</string> + <string name="contact_diary_add_text_input_email_hint">"E-Mail"</string> + <string name="contact_diary_add_person_save_button">"Speichern"</string> + <string name="contact_diary_add_location_title">"Ort"</string> + <string name="contact_diary_add_location_text_input_hint">"Bezeichnung"</string> + <string name="contact_diary_add_location_save_button">"Speichern"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Kontakt-Tagebuch"</string> @@ -112,6 +114,13 @@ <!-- XHED: Title for the contact diary dialog to delete delete a single person --> <string name="contact_diary_delete_person_title">"Wollen Sie wirklich diese Person entfernen?"</string> + <!-- XHED: Title for the contact diary comment info screen --> + <string name="contact_diary_comment_info_screen_title">"Notiz"</string> + <!-- XTXT: Description for contact diary comment info screen --> + <string name="contact_diary_comment_info_screen_description">"Hier können Sie Notizen machen, die Ihnen helfen, das Risiko einer Infektion besser einzuschätzen."</string> + <!-- XTXT: Body for contact diary comment info screen --> + <string name="contact_diary_comment_info_screen_body">"Notieren Sie Umstände, die zu einem erhöhten Risiko führen könnten, z.B. räumliche Nähe zu anderen Personen oder die Ausübung einer Tätigkeit, die sich auf die Luftqualität auswirkt (Beispiele: „saßen nah beieinander“, „es wurde Sport getrieben“, „wir haben gesungen“).\n\nSollten Sie sich tatsächlich anstecken, können diese Notizen auch dem Gesundheitsamt bei der Nachverfolgung möglicher Infektionsketten helfen."</string> + <!-- XTXT: location (description for screen readers) --> <string name="accessibility_location">"Ort %s"</string> <!-- XTXT: person (description for screen readers) --> diff --git a/Corona-Warn-App/src/main/res/values-de/strings.xml b/Corona-Warn-App/src/main/res/values-de/strings.xml index 21ad6bdfe6638df5d6ae9b85f666dea8c8513f84..8d848ddb9485be9b63290b219540ca5ba3fb5679 100644 --- a/Corona-Warn-App/src/main/res/values-de/strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/strings.xml @@ -1882,4 +1882,16 @@ <!-- XTXT: Analytics voluntary user input, district: UNSPECIFIED --> <string name="analytics_userinput_district_unspecified">keine Angabe</string> + <!-- #################################### + Duration dialog + ###################################### --> + + <!-- XHED: Duration dialog title --> + <string name="duration_dialog_title">Dauer</string> + <!-- XBUT: Duration dialog cancel button --> + <string name="duration_dialog_cancel_button">Cancel</string> + <!-- XBUT: Duration dialog ok button --> + <string name="duration_dialog_ok_button">OK</string> + <!-- NOTR --> + <string name="duration_dialog_default_value">00:00</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml index 747776becd9b4a779b9510a16968b289c25d8536..4590f73acfddedd5dcddbf5dbe74a2b8e330aa8f 100644 --- a/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-en/contact_diary_strings.xml @@ -10,12 +10,12 @@ <string name="contact_diary_location_list_no_items_subtitle">"Create a place and add it to your contact journal."</string> <string name="contact_diary_person_list_no_items_title">"No people defined yet"</string> <string name="contact_diary_person_list_no_items_subtitle">"Create a person and add them to your contact journal."</string> - <string name="contact_diary_person_bottom_sheet_title">"Person"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"Name"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Save"</string> - <string name="contact_diary_location_bottom_sheet_title">"Place"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"Description"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Save"</string> + <string name="contact_diary_add_person_title">"Person"</string> + <string name="contact_diary_add_person_text_input_name_hint">"Name"</string> + <string name="contact_diary_add_person_save_button">"Save"</string> + <string name="contact_diary_add_location_title">"Place"</string> + <string name="contact_diary_add_location_text_input_hint">"Description"</string> + <string name="contact_diary_add_location_save_button">"Save"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Contact Journal"</string> diff --git a/Corona-Warn-App/src/main/res/values-pl/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-pl/contact_diary_strings.xml index 0f24b87a458c8948a1a47a330d631f630ef5c603..38df500cf588e0f9c76fc2d46671d4d9ac3affb0 100644 --- a/Corona-Warn-App/src/main/res/values-pl/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-pl/contact_diary_strings.xml @@ -10,12 +10,12 @@ <string name="contact_diary_location_list_no_items_subtitle">"Utwórz miejsce i dodaj je do dziennika kontaktów."</string> <string name="contact_diary_person_list_no_items_title">"Nie zdefiniowano jeszcze żadnych osób"</string> <string name="contact_diary_person_list_no_items_subtitle">"Utwórz osobÄ™ i dodaj jÄ… do dziennika kontaktów."</string> - <string name="contact_diary_person_bottom_sheet_title">"Osoba"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"ImiÄ™ i nazwisko"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Zapisz"</string> - <string name="contact_diary_location_bottom_sheet_title">"Miejsce"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"Opis"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Zapisz"</string> + <string name="contact_diary_add_person_title">"Osoba"</string> + <string name="contact_diary_add_person_text_input_name_hint">"ImiÄ™ i nazwisko"</string> + <string name="contact_diary_add_person_save_button">"Zapisz"</string> + <string name="contact_diary_add_location_title">"Miejsce"</string> + <string name="contact_diary_add_location_text_input_hint">"Opis"</string> + <string name="contact_diary_add_location_save_button">"Zapisz"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Dziennik kontaktów"</string> diff --git a/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml index c2c2a61be302e0fe1c1b6a793a59993996803360..90539612e53b315e2e8674688e51d65ae34521da 100644 --- a/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-ro/contact_diary_strings.xml @@ -10,12 +10,12 @@ <string name="contact_diary_location_list_no_items_subtitle">"CreaÈ›i un loc È™i adăugaÈ›i-l la jurnalul dvs. de contacte."</string> <string name="contact_diary_person_list_no_items_title">"Nicio persoană definită deocamdată"</string> <string name="contact_diary_person_list_no_items_subtitle">"CreaÈ›i o persoană È™i adăugaÈ›i-o la jurnalul dvs. de contacte."</string> - <string name="contact_diary_person_bottom_sheet_title">"Persoană"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"Nume"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Salvare"</string> - <string name="contact_diary_location_bottom_sheet_title">"Loc"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"Descriere"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Salvare"</string> + <string name="contact_diary_add_person_title">"Persoană"</string> + <string name="contact_diary_add_person_text_input_name_hint">"Nume"</string> + <string name="contact_diary_add_person_save_button">"Salvare"</string> + <string name="contact_diary_add_location_title">"Loc"</string> + <string name="contact_diary_add_location_text_input_hint">"Descriere"</string> + <string name="contact_diary_add_location_save_button">"Salvare"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Jurnal de contacte"</string> diff --git a/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml index 15528c32aeadeb66ebca8f4789036e4292b23234..c8b833111fc2d158b4b355975f9dde90202d6968 100644 --- a/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values-tr/contact_diary_strings.xml @@ -10,12 +10,12 @@ <string name="contact_diary_location_list_no_items_subtitle">"Bir yer oluÅŸturun ve temas güncenize ekleyin."</string> <string name="contact_diary_person_list_no_items_title">"Henüz hiçbir kiÅŸi tanımlanmadı"</string> <string name="contact_diary_person_list_no_items_subtitle">"Bir kiÅŸi oluÅŸturun ve temas güncenize ekleyin."</string> - <string name="contact_diary_person_bottom_sheet_title">"KiÅŸi"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"Ad"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Kaydet"</string> - <string name="contact_diary_location_bottom_sheet_title">"Yer"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"Tanım"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Kaydet"</string> + <string name="contact_diary_add_person_title">"KiÅŸi"</string> + <string name="contact_diary_add_person_text_input_name_hint">"Ad"</string> + <string name="contact_diary_add_person_save_button">"Kaydet"</string> + <string name="contact_diary_add_location_title">"Yer"</string> + <string name="contact_diary_add_location_text_input_hint">"Tanım"</string> + <string name="contact_diary_add_location_save_button">"Kaydet"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Temas Güncesi"</string> diff --git a/Corona-Warn-App/src/main/res/values/contact_diary_strings.xml b/Corona-Warn-App/src/main/res/values/contact_diary_strings.xml index 88e5af74820651c19482cf9617dcd98fe99d7cee..b9c30d5c6f88ffd376e71d628ed4a36df1405e99 100644 --- a/Corona-Warn-App/src/main/res/values/contact_diary_strings.xml +++ b/Corona-Warn-App/src/main/res/values/contact_diary_strings.xml @@ -11,12 +11,14 @@ <string name="contact_diary_location_list_no_items_subtitle">"Create a place and add it to your contact journal."</string> <string name="contact_diary_person_list_no_items_title">"No people defined yet"</string> <string name="contact_diary_person_list_no_items_subtitle">"Create a person and add them to your contact journal."</string> - <string name="contact_diary_person_bottom_sheet_title">"Person"</string> - <string name="contact_diary_person_bottom_sheet_text_input_hint">"Name"</string> - <string name="contact_diary_person_bottom_sheet_save_button">"Save"</string> - <string name="contact_diary_location_bottom_sheet_title">"Place"</string> - <string name="contact_diary_location_bottom_sheet_text_input_hint">"Description"</string> - <string name="contact_diary_location_bottom_sheet_save_button">"Save"</string> + <string name="contact_diary_add_person_title">"Person"</string> + <string name="contact_diary_add_person_text_input_name_hint">"Name"</string> + <string name="contact_diary_add_text_input_phone_hint" /> + <string name="contact_diary_add_text_input_email_hint"/> + <string name="contact_diary_add_person_save_button">"Save"</string> + <string name="contact_diary_add_location_title">"Place"</string> + <string name="contact_diary_add_location_text_input_hint">"Description"</string> + <string name="contact_diary_add_location_save_button">"Save"</string> <!-- XHED: Title for the contact diary card displayed in the homescreen --> <string name="contact_diary_homescreen_card_header">"Contact Journal"</string> @@ -119,6 +121,13 @@ <!-- XTXT: Intro text for the contact journal email export part two--> <string name="contact_diary_export_intro_two" translatable="false">"Die nachfolgende Liste dient dem zuständigen Gesundheitsamt zur Kontaktnachverfolgung gem. § 25 IfSG."</string> + <!-- XHED: Title for the contact diary comment info screen --> + <string name="contact_diary_comment_info_screen_title">"Notiz"</string> + <!-- XTXT: Description for contact diary comment info screen --> + <string name="contact_diary_comment_info_screen_description">"Hier können Sie Notizen machen, die Ihnen helfen, das Risiko einer Infektion besser einzuschätzen."</string> + <!-- XTXT: Body for contact diary comment info screen --> + <string name="contact_diary_comment_info_screen_body">"Notieren Sie Umstände, die zu einem erhöhten Risiko führen könnten, z.B. räumliche Nähe zu anderen Personen oder die Ausübung einer Tätigkeit, die sich auf die Luftqualität auswirkt (Beispiele: „saßen nah beieinander“, „es wurde Sport getrieben“, „wir haben gesungen“).\n\nSollten Sie sich tatsächlich anstecken, können diese Notizen auch dem Gesundheitsamt bei der Nachverfolgung möglicher Infektionsketten helfen."</string> + <!-- XTXT: location (description for screen readers) --> <string name="accessibility_location">"Place %s"</string> <!-- XTXT: person (description for screen readers) --> diff --git a/Corona-Warn-App/src/main/res/values/strings.xml b/Corona-Warn-App/src/main/res/values/strings.xml index 1971d65abbab3b0d66f688e25ac1e108cb42febe..de77c976edb37749ce346df5532e89ab9d307def 100644 --- a/Corona-Warn-App/src/main/res/values/strings.xml +++ b/Corona-Warn-App/src/main/res/values/strings.xml @@ -1896,4 +1896,17 @@ <string name="analytics_userinput_district_title">"Your District (County)"</string> <!-- XTXT: Analytics voluntary user input, district: UNSPECIFIED --> <string name="analytics_userinput_district_unspecified">"No answer"</string> + + <!-- #################################### + Duration dialog + ###################################### --> + + <!-- XHED: Duration dialog title --> + <string name="duration_dialog_title">Dauer</string> + <!-- XBUT: Duration dialog cancel button --> + <string name="duration_dialog_cancel_button">Cancel</string> + <!-- XBUT: Duration dialog ok button --> + <string name="duration_dialog_ok_button">OK</string> + <!-- NOTR --> + <string name="duration_dialog_default_value">00:00</string> </resources> diff --git a/Corona-Warn-App/src/main/res/values/styles.xml b/Corona-Warn-App/src/main/res/values/styles.xml index e2ad52d5cd4a351f797ca5b9a8ade871a1d5fbf8..03958da9288e050a5cd1727203a4c2683433120a 100644 --- a/Corona-Warn-App/src/main/res/values/styles.xml +++ b/Corona-Warn-App/src/main/res/values/styles.xml @@ -12,7 +12,6 @@ <item name="windowActionBar">false</item> <item name="windowNoTitle">true</item> <item name="dialogTheme">@style/DialogTheme</item> - <item name="bottomSheetDialogTheme">@style/BottomSheetDialogTheme</item> </style> <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> @@ -69,11 +68,6 @@ <item name="buttonBarNegativeButtonStyle">@style/DialogButtonTheme</item> </style> - <style name="BottomSheetDialogTheme" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog"> - <item name="colorPrimary">@color/colorBrandSecondary</item> - <item name="colorPrimaryDark">@color/colorStableDark</item> - </style> - <style name="DialogButtonTheme" parent="Widget.MaterialComponents.Button.TextButton"> <item name="android:textColor">@color/colorAccentTintButton</item> </style> @@ -110,6 +104,17 @@ <item name="android:textColor">@color/button_text_color_emphasized</item> </style> + <style name="contactDiaryPersonButton" parent="Widget.MaterialComponents.Button.OutlinedButton"> + <item name="strokeColor">@color/button_contact_diary_person</item> + <item name="strokeWidth">2dp</item> + <item name="android:letterSpacing">0</item> + <item name="textAllCaps">false</item> + <item name="android:fontFamily">sans-serif-medium</item> + <item name="android:textStyle">normal</item> + <item name="android:textColor">#000000</item> + + </style> + <style name="buttonReset" parent="button"> <item name="android:backgroundTint">@color/button_red</item> <item name="android:textColor">@color/button_text_color_emphasized</item> @@ -287,6 +292,10 @@ <item name="android:textColor">@color/colorTextSixteen</item> </style> + <style name="bodyNeutral" parent="@style/TextAppearance.MaterialComponents.Body1"> + <item name="android:textColor">@color/colorTextSemanticNeutral</item> + </style> + <!-- #################################### Icons ###################################### --> @@ -400,6 +409,35 @@ <item name="android:background">@drawable/contact_diary_card_ripple</item> </style> + <style name="ContactDiaryExpandableListItem"> + <item name="android:background">@drawable/contact_diary_card_ripple</item> + </style> + + <style name="ContactDiaryCircumstancesTextInputLayout" parent="Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"> + <item name="boxBackgroundColor">@color/colorBackground</item> + <item name="android:textColorHint">@color/colorTextPrimary2</item> + <item name="android:textSize">@dimen/font_small</item> + <item name="hintTextColor">@color/colorTextPrimary2</item> + <item name="boxCornerRadiusBottomEnd">@dimen/spacing_mega_tiny</item> + <item name="boxCornerRadiusBottomStart">@dimen/spacing_mega_tiny</item> + <item name="boxCornerRadiusTopEnd">@dimen/spacing_mega_tiny</item> + <item name="boxCornerRadiusTopStart">@dimen/spacing_mega_tiny</item> + <item name="boxStrokeWidth">0dp</item> + <item name="boxStrokeWidthFocused">0dp</item> + </style> + + <style name="ContactDiaryInfoButton" parent="buttonIcon"> + <item name="android:height">@dimen/button_icon</item> + <item name="android:width">@dimen/button_icon</item> + <item name="android:src">@drawable/ic_info</item> + <item name="android:paddingStart">12dp</item> + <item name="android:paddingEnd">12dp</item> + <item name="android:paddingTop">7dp</item> + <item name="android:paddingBottom">7dp</item> + <item name="android:contentDescription">@string/statistics_info_button</item> + <item name="android:tint">@color/colorAccent</item> + </style> + <style name="StatisticsCardInfoButton" parent="buttonIcon"> <item name="android:height">@dimen/button_icon</item> <item name="android:width">@dimen/button_icon</item> @@ -429,4 +467,22 @@ <item name="android:textColor">@color/colorTextPrimary1</item> </style> + <style name="TextInputLayoutTheme" parent="Widget.MaterialComponents.TextInputLayout.FilledBox"> + <item name="boxBackgroundColor">@color/colorContactDiaryListItem</item> + <item name="android:textColorHint">@color/colorTextPrimary2</item> + <item name="hintTextColor">@color/colorTextPrimary2</item> + <item name="boxCornerRadiusBottomEnd">@dimen/spacing_mega_tiny</item> + <item name="boxCornerRadiusBottomStart">@dimen/spacing_mega_tiny</item> + <item name="boxCornerRadiusTopEnd">@dimen/spacing_mega_tiny</item> + <item name="boxCornerRadiusTopStart">@dimen/spacing_mega_tiny</item> + <item name="boxStrokeWidth">0dp</item> + <item name="boxStrokeWidthFocused">0dp</item> + <item name="counterTextAppearance">@color/colorTransparent</item> + <item name="android:maxLength">5</item> + </style> + + <style name="DefaultNumberPickerTheme" parent="AppTheme"> + <item name="colorControlNormal">@color/colorAccent</item> + </style> + </resources> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConvertersTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConvertersTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..18b89cc2e5267c46da57f423372b620876fc89ee --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/storage/internal/converters/ContactDiaryRoomConvertersTest.kt @@ -0,0 +1,24 @@ +package de.rki.coronawarnapp.contactdiary.storage.internal.converters + +import de.rki.coronawarnapp.contactdiary.model.ContactDiaryPersonEncounter.DurationClassification +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class ContactDiaryRoomConvertersTest : BaseTest() { + + @Test + fun `test person encounter duration classification`() { + ContactDiaryRoomConverters().apply { + toContactDurationClassification(null) shouldBe null + toContactDurationClassification( + DurationClassification.MORE_THAN_15_MINUTES.key + ) shouldBe DurationClassification.MORE_THAN_15_MINUTES + + fromContactDurationClassification(null) shouldBe null + fromContactDurationClassification( + DurationClassification.LESS_THAN_15_MINUTES + ) shouldBe DurationClassification.LESS_THAN_15_MINUTES.key + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/ContactDiaryDurationPickerFragmentTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/ContactDiaryDurationPickerFragmentTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..3506e3065244d200abf8c8d573f9b267fdc83a38 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/durationpicker/ContactDiaryDurationPickerFragmentTest.kt @@ -0,0 +1,43 @@ +package de.rki.coronawarnapp.contactdiary.ui.durationpicker + +import io.kotest.matchers.shouldBe +import io.mockk.MockKAnnotations +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ContactDiaryDurationPickerFragmentTest { + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun `check hours array`() { + ContactDiaryDurationPickerFragment.hoursArray.count() shouldBe 24 + for (i in 0..9) { + ContactDiaryDurationPickerFragment.hoursArray[i] shouldBe "0$i" + } + for (i in 10..23) { + ContactDiaryDurationPickerFragment.hoursArray[i] shouldBe "$i" + } + } + + @Test + fun `check minutes array`() { + ContactDiaryDurationPickerFragment.minutesArray.count() shouldBe 4 + ContactDiaryDurationPickerFragment.minutesArray[0] shouldBe "00" + for (i in 1..3) { + ContactDiaryDurationPickerFragment.minutesArray[i] shouldBe "${i * 15}" + } + } + + @Test + fun `check duration`() { + ContactDiaryDurationPickerFragment.getDuration(0, 0).toContactDiaryFormat() shouldBe "00:00" + ContactDiaryDurationPickerFragment.getDuration(1, 0).toContactDiaryFormat() shouldBe "01:00" + ContactDiaryDurationPickerFragment.getDuration(23, 3).toContactDiaryFormat() shouldBe "23:45" + ContactDiaryDurationPickerFragment.getDuration(9, 2).toContactDiaryFormat() shouldBe "09:30" + ContactDiaryDurationPickerFragment.getDuration(10, 1).toContactDiaryFormat() shouldBe "10:15" + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt index a9022f4fe16940addcb635c13383dec6c14083e1..6dd141a3353bcc2b643258d518b28e8eb5afcecc 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditLocationsViewModelTest.kt @@ -26,6 +26,8 @@ class ContactDiaryEditLocationsViewModelTest { private val location = object : ContactDiaryLocation { override val locationId = 1L override var locationName = "Supermarket" + override val phoneNumber: String? = null + override val emailAddress: String? = null override val stableId = 1L } private val locationList = listOf(location) @@ -64,7 +66,7 @@ class ContactDiaryEditLocationsViewModelTest { viewModel.navigationEvent.observeForever { } viewModel.onEditLocationClick(location) viewModel.navigationEvent.value shouldBe - ContactDiaryEditLocationsViewModel.NavigationEvent.ShowLocationDetailSheet(location.toContactDiaryLocationEntity()) + ContactDiaryEditLocationsViewModel.NavigationEvent.ShowLocationDetailFragment(location.toContactDiaryLocationEntity()) } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt index f1a8aa1e22d2f65e329e9dbfade18366738ab302..892d74d8be0c919fcea222d3f520c2a54e1904d0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/edit/ContactDiaryEditPersonsViewModelTest.kt @@ -26,6 +26,8 @@ class ContactDiaryEditPersonsViewModelTest { private val person = object : ContactDiaryPerson { override val personId = 1L override var fullName = "Julia" + override val phoneNumber: String? = null + override val emailAddress: String? = null override val stableId = 1L } @@ -65,7 +67,7 @@ class ContactDiaryEditPersonsViewModelTest { viewModel.navigationEvent.observeForever { } viewModel.onEditPersonClick(person) viewModel.navigationEvent.value shouldBe - ContactDiaryEditPersonsViewModel.NavigationEvent.ShowPersonDetailSheet(person.toContactDiaryPersonEntity()) + ContactDiaryEditPersonsViewModel.NavigationEvent.ShowPersonDetailFragment(person.toContactDiaryPersonEntity()) } @Test diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt index c380d34f94e962168f623b5baab946790b7aab78..3723e0471f0809ac63495b7d877ab5c2048b5363 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/util/ContactDiaryExtensionsTest.kt @@ -8,14 +8,6 @@ import org.junit.jupiter.api.Test class ContactDiaryExtensionsTest { - @Test - fun testFormatContactDiaryNameField() { - Assert.assertEquals(" Granny ".formatContactDiaryNameField(5), "Grann") - Assert.assertEquals(" Grann y".formatContactDiaryNameField(5), "Grann") - Assert.assertEquals("Granny ".formatContactDiaryNameField(5), "Grann") - Assert.assertEquals(" ".formatContactDiaryNameField(2), " ") - } - @Test fun `upper and lowercase mix sorting for names`() { val testList = listOf( diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt index b3b262d2539f79fb470db185316c559f8c44c34a..7ab9baac100d97da81b068a3cd71d9d625e2f50e 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/datadonation/analytics/modules/clientmetadata/ClientMetadataDonorTest.kt @@ -76,9 +76,11 @@ class ClientMetadataDonorTest : BaseTest() { val parentBuilder = PpaData.PPADataAndroid.newBuilder() runBlockingTest2 { - val contribution = createInstance().beginDonation(object : DonorModule.Request { - override val currentConfig: ConfigData = mockk() - }) + val contribution = createInstance().beginDonation( + object : DonorModule.Request { + override val currentConfig: ConfigData = mockk() + } + ) contribution.injectData(parentBuilder) contribution.finishDonation(true) } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoFragmentTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoFragmentTest.kt index 03ad1e074b0739a12faf6f28f5e6657fe59efed4..4e8a58260c50bdb7856b437d526c9aa1feefbaf0 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoFragmentTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoFragmentTest.kt @@ -57,7 +57,6 @@ class NewReleaseInfoFragmentTest : BaseTest() { @Test fun `ensure DEFAULT aka FRENCH new release info arrays are of equal length`() = loadAndCompareStringArrayResources() - private fun loadAndCompareStringArrayResources() { val titles = context.resources.getStringArray(R.array.new_release_title) val bodies = context.resources.getStringArray(R.array.new_release_body) @@ -68,5 +67,4 @@ class NewReleaseInfoFragmentTest : BaseTest() { bodies.size shouldBe labels.size labels.size shouldBe urls.size } - -} \ No newline at end of file +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt index 094ae35516f1f4e74ca1d9f2d63c3e54e7369fa3..f9ae8c5c3879e6d6417e0ee3c51ecd8fa41c555f 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/release/NewReleaseInfoViewModelTest.kt @@ -41,7 +41,7 @@ class NewReleaseInfoViewModelTest { val item2 = NewReleaseInfoItemText("title2", "body2") val titles = arrayOf(item1.title, item2.title) val bodies = arrayOf(item1.body, item2.body) - viewModel.getItems(titles, bodies, arrayOf("",""), arrayOf("","")) shouldBe listOf(item1, item2) + viewModel.getItems(titles, bodies, arrayOf("", ""), arrayOf("", "")) shouldBe listOf(item1, item2) } @Test @@ -50,7 +50,7 @@ class NewReleaseInfoViewModelTest { val item2 = NewReleaseInfoItemText("title2", "body2") val titles = arrayOf(item1.title) val bodies = arrayOf(item1.body, item2.body) - viewModel.getItems(titles, bodies, arrayOf("",""), arrayOf("","")) shouldBe emptyList() + viewModel.getItems(titles, bodies, arrayOf("", ""), arrayOf("", "")) shouldBe emptyList() } @Test @@ -59,7 +59,7 @@ class NewReleaseInfoViewModelTest { val item2 = NewReleaseInfoItemText("title2", "body2") val titles = arrayOf(item1.title, item2.title) val bodies = arrayOf(item1.body) - viewModel.getItems(titles, bodies, arrayOf("",""), arrayOf("","")) shouldBe emptyList() + viewModel.getItems(titles, bodies, arrayOf("", ""), arrayOf("", "")) shouldBe emptyList() } @Test @@ -68,7 +68,7 @@ class NewReleaseInfoViewModelTest { val item2 = NewReleaseInfoItemText("title2", "body2") val titles = arrayOf(item1.title, item2.title) val bodies = arrayOf(item1.body, item2.body) - viewModel.getItems(titles, bodies, arrayOf(""), arrayOf("","")) shouldBe emptyList() + viewModel.getItems(titles, bodies, arrayOf(""), arrayOf("", "")) shouldBe emptyList() } @Test @@ -77,7 +77,7 @@ class NewReleaseInfoViewModelTest { val item2 = NewReleaseInfoItemText("title2", "body2") val titles = arrayOf(item1.title, item2.title) val bodies = arrayOf(item1.body, item2.body) - viewModel.getItems(titles, bodies, arrayOf("",""), arrayOf("")) shouldBe emptyList() + viewModel.getItems(titles, bodies, arrayOf("", ""), arrayOf("")) shouldBe emptyList() } @Test