Skip to content
Snippets Groups Projects
Unverified Commit 3582286d authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Merge pull request #2328 from corona-warn-app/feature/4741-contact-journal-extension-main

Contact journal extension (main-feature-branch) (EXPOSUREAPP-4741)
parents 47f39c11 88d322b2
No related branches found
No related tags found
No related merge requests found
Showing
with 835 additions and 90 deletions
{
"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
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()
}
}
}
......@@ -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
}
}
......@@ -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)
......
......@@ -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")
)
}
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
)
}
}
......@@ -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)
}
}
}
......
......@@ -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>()
......
......@@ -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"
......
......@@ -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>
......@@ -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> =
......
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> =
......
......@@ -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> =
......
......@@ -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> =
......
......@@ -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
......
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
)
}
......@@ -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
......
......@@ -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
)
}
......@@ -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()
}
......
......@@ -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
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment