diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt index 3d7bc4e5fbd13ea12bd820192049a5576089e59d..4038ad7448406bca7ff8339defe6b6089175da6f 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/contactdiary/ContactDiaryOverviewFragmentTest.kt @@ -16,6 +16,7 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.DiaryOverviewItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.DayOverviewItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.contact.ContactItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.risk.storage.RiskLevelStorage import de.rki.coronawarnapp.task.TaskController import de.rki.coronawarnapp.util.TimeStamper @@ -47,6 +48,7 @@ class ContactDiaryOverviewFragmentTest : BaseUITest() { @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var exporter: ContactDiaryExporter + @MockK lateinit var checkInRepository: CheckInRepository private lateinit var viewModel: ContactDiaryOverviewViewModel @@ -63,6 +65,7 @@ class ContactDiaryOverviewFragmentTest : BaseUITest() { contactDiaryRepository = contactDiaryRepository, riskLevelStorage = riskLevelStorage, timeStamper = timeStamper, + checkInRepository = checkInRepository, exporter = exporter ) ) 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 49c71ca142afd1334e8f68d8f1e24e26d6de6996..eddad5048b6c161ac5d076c2d3ace5972b35a58e 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 @@ -85,12 +85,14 @@ object DiaryData { val HIGH_RISK_EVENT = RiskEventItem.Event( name = HIGH_RISK_EVENT_LOCATION.name, + description = "2", bulledPointColor = R.color.colorBulletPointHighRisk, riskInfoAddition = R.string.contact_diary_trace_location_risk_high ) val LOW_RISK_EVENT = RiskEventItem.Event( name = LOW_RISK_EVENT_LOCATION.name, + description = "1", bulledPointColor = R.color.colorBulletPointLowRisk, riskInfoAddition = R.string.contact_diary_trace_location_risk_low ) 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 3409ad4b6bea470d8213773a93070efb7081697a..47e84e294291653f8f1e52e4653201ab300b28d7 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 @@ -19,6 +19,8 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.contact.Contact import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskenf.RiskEnfItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskEventItem import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk @@ -29,10 +31,10 @@ import de.rki.coronawarnapp.task.common.DefaultTaskRequest import de.rki.coronawarnapp.util.TimeAndDateExtensions.toLocalDateUtc import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.DispatcherProvider +import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.util.ui.SingleLiveEvent import de.rki.coronawarnapp.util.viewmodel.CWAViewModel import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import org.joda.time.LocalDate @@ -44,6 +46,7 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( contactDiaryRepository: ContactDiaryRepository, riskLevelStorage: RiskLevelStorage, timeStamper: TimeStamper, + checkInRepository: CheckInRepository, private val exporter: ContactDiaryExporter ) : CWAViewModel(dispatcherProvider = dispatcherProvider) { @@ -57,23 +60,25 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( private val riskLevelPerDateFlow = riskLevelStorage.ewDayRiskStates private val traceLocationCheckInRiskFlow = riskLevelStorage.traceLocationCheckInRiskStates + private val allCheckInsFlow = checkInRepository.allCheckIns val listItems = combine( flowOf(dates), locationVisitsFlow, personEncountersFlow, riskLevelPerDateFlow, - traceLocationCheckInRiskFlow - ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList -> + traceLocationCheckInRiskFlow, + allCheckInsFlow + ) { dateList, locationVisists, personEncounters, riskLevelPerDateList, traceLocationCheckInRiskList, checkInList -> mutableListOf<DiaryOverviewItem>().apply { add(OverviewSubHeaderItem) addAll( - createListItemList( - dateList, + dateList.createListItemList( locationVisists, personEncounters, riskLevelPerDateList, - traceLocationCheckInRiskList + traceLocationCheckInRiskList, + checkInList ) ) }.toList() @@ -88,12 +93,12 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( ) } - private fun createListItemList( - dateList: List<LocalDate>, + private fun List<LocalDate>.createListItemList( visits: List<ContactDiaryLocationVisit>, encounters: List<ContactDiaryPersonEncounter>, riskLevelPerDateList: List<ExposureWindowDayRisk>, - traceLocationCheckInRiskList: List<TraceLocationCheckInRisk> + traceLocationCheckInRiskList: List<TraceLocationCheckInRisk>, + checkInList: List<CheckIn> ): List<DiaryOverviewItem> { Timber.v( "createListItemList(" + @@ -101,14 +106,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( "visits=%s, " + "encounters=%s, " + "riskLevelPerDateList=%s, " + - "traceLocationCheckInRiskList=%s", - dateList, + "traceLocationCheckInRiskList=%s," + + "checkInList=%s", + this, visits, encounters, riskLevelPerDateList, - traceLocationCheckInRiskList + traceLocationCheckInRiskList, + checkInList ) - return dateList.map { date -> + return map { date -> val visitsForDate = visits.filter { it.date == date } val encountersForDate = encounters.filter { it.date == date } @@ -125,16 +132,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( .firstOrNull { riskLevelPerDate -> riskLevelPerDate.localDateUtc == date } ?.toRisk(coreItemData.isNotEmpty()) - val riskEventItem = visitsForDate - .map { - it to traceLocationCheckInRisksForDate.find { - checkInRisk -> - checkInRisk.checkInId == it.checkInID + val riskEventItem = traceLocationCheckInRisksForDate + .mapNotNull { + val locationVisit = visitsForDate.find { visit -> visit.checkInID == it.checkInId } + val checkIn = checkInList.find { checkIn -> checkIn.id == it.checkInId } + + return@mapNotNull when (locationVisit != null && checkIn != null) { + true -> RiskEventDataHolder(it, locationVisit, checkIn) + else -> null } - } - .toMap() - .filter { it.value != null } - .toRiskEventItem() + }.toRiskEventItem() DayOverviewItem( date = date, @@ -187,10 +194,16 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( type = ContactItem.Type.LOCATION ) - private fun Map<ContactDiaryLocationVisit, TraceLocationCheckInRisk?>.toRiskEventItem(): RiskEventItem? { + private data class RiskEventDataHolder( + val traceLocationCheckInRisk: TraceLocationCheckInRisk, + val locationVisit: ContactDiaryLocationVisit, + val checkIn: CheckIn + ) + + private fun List<RiskEventDataHolder>.toRiskEventItem(): RiskEventItem? { if (isEmpty()) return null - val isHighRisk = values.any { it?.riskState == RiskState.INCREASED_RISK } + val isHighRisk = any { it.traceLocationCheckInRisk.riskState == RiskState.INCREASED_RISK } val body: Int = R.string.contact_diary_trace_location_risk_body val drawableID: Int @@ -207,15 +220,13 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( } } - val events = mapNotNull { entry -> - if (entry.value == null) return null - - val name = entry.key.contactDiaryLocation.locationName + val events = map { data -> + val name = data.locationVisit.contactDiaryLocation.locationName val bulletPointColor: Int var riskInfoAddition: Int? - when (entry.value?.riskState == RiskState.INCREASED_RISK) { + when (data.traceLocationCheckInRisk.riskState == RiskState.INCREASED_RISK) { true -> { bulletPointColor = R.color.colorBulletPointHighRisk riskInfoAddition = R.string.contact_diary_trace_location_risk_high @@ -228,8 +239,11 @@ class ContactDiaryOverviewViewModel @AssistedInject constructor( if (size < 2) riskInfoAddition = null + val description = data.checkIn.description + RiskEventItem.Event( name = name, + description = description, bulledPointColor = bulletPointColor, riskInfoAddition = riskInfoAddition ) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt index 157b2cbc6f48a67dd8f1fbe6a04425069781f9e1..ed4eed0a1ccf2dbc33ed196ad76e01336025e92f 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventAdapter.kt @@ -1,8 +1,9 @@ package de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent import android.view.ViewGroup +import androidx.recyclerview.widget.SortedList +import androidx.recyclerview.widget.SortedListAdapterCallback import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.contactdiary.util.clearAndAddAll import de.rki.coronawarnapp.databinding.ContactDiaryOverviewDayListItemRiskEventListItemBinding import de.rki.coronawarnapp.ui.lists.BaseAdapter import de.rki.coronawarnapp.util.ContextExtensions.getColorCompat @@ -10,18 +11,38 @@ import de.rki.coronawarnapp.util.lists.BindableVH class RiskEventAdapter : BaseAdapter<RiskEventAdapter.RiskEventListItemVH>() { - private val events: MutableList<RiskEventItem.Event> = mutableListOf() + private val events: SortedList<RiskEventItem.Event> = SortedList( + RiskEventItem.Event::class.java, + SortedList.BatchedCallback( + object : SortedListAdapterCallback<RiskEventItem.Event>( + this + ) { + override fun compare(o1: RiskEventItem.Event, o2: RiskEventItem.Event): Int = + o1.description.compareTo(o2.description) + + override fun areContentsTheSame(oldItem: RiskEventItem.Event?, newItem: RiskEventItem.Event?): Boolean = + oldItem == newItem + + override fun areItemsTheSame(item1: RiskEventItem.Event?, item2: RiskEventItem.Event?): Boolean = + item1 == item2 + } + ) + ) override fun onCreateBaseVH(parent: ViewGroup, viewType: Int): RiskEventListItemVH = RiskEventListItemVH(parent) override fun onBindBaseVH(holder: RiskEventListItemVH, position: Int, payloads: MutableList<Any>) = holder.bind(events[position], payloads) - override fun getItemCount(): Int = events.size + override fun getItemCount(): Int = events.size() fun setItems(events: List<RiskEventItem.Event>) { - this.events.clearAndAddAll(events) - notifyDataSetChanged() + this.events.apply { + beginBatchedUpdates() + clear() + addAll(events) + endBatchedUpdates() + } } inner class RiskEventListItemVH(parent: ViewGroup) : diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt index faa1f95d67bca1761c3f8a3204de02da3e9b3292..ea836cc33184e784385173a4f9154ab22e4b9212 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/contactdiary/ui/overview/adapter/day/riskevent/RiskEventItem.kt @@ -17,6 +17,7 @@ data class RiskEventItem( data class Event( val name: String, + val description: String, @ColorRes val bulledPointColor: Int, @StringRes val riskInfoAddition: Int? = null ) diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt index bd06b47d299e49aeb0b611eac9e98b24534209ce..0ad2d082233fc461ff2d7e8ec0e1db8dfa0dfb65 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/contactdiary/ui/overview/ContactDiaryOverviewViewModelTest.kt @@ -15,6 +15,8 @@ import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.day.riskevent.RiskE import de.rki.coronawarnapp.contactdiary.ui.overview.adapter.subheader.OverviewSubHeaderItem import de.rki.coronawarnapp.contactdiary.util.ContactDiaryData import de.rki.coronawarnapp.contactdiary.util.mockStringsForContactDiaryExporterTests +import de.rki.coronawarnapp.eventregistration.checkins.CheckIn +import de.rki.coronawarnapp.eventregistration.checkins.CheckInRepository import de.rki.coronawarnapp.risk.RiskState import de.rki.coronawarnapp.risk.TraceLocationCheckInRisk import de.rki.coronawarnapp.risk.result.ExposureWindowDayRisk @@ -29,6 +31,7 @@ import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.runs import io.mockk.verify import kotlinx.coroutines.flow.flowOf @@ -51,9 +54,10 @@ open class ContactDiaryOverviewViewModelTest { @MockK lateinit var riskLevelStorage: RiskLevelStorage @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var context: Context + @MockK lateinit var checkInRepository: CheckInRepository private val testDispatcherProvider = TestDispatcherProvider() - private val date = LocalDate.now() + private val date = LocalDate.parse("2021-04-07") private val dateMillis = date.toDateTimeAtStartOfDay(DateTimeZone.UTC).millis @BeforeEach @@ -65,9 +69,10 @@ open class ContactDiaryOverviewViewModelTest { every { contactDiaryRepository.personEncounters } returns flowOf(emptyList()) every { riskLevelStorage.ewDayRiskStates } returns flowOf(emptyList()) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(emptyList()) + every { checkInRepository.allCheckIns } returns flowOf(emptyList()) mockStringsForContactDiaryExporterTests(context) - every { timeStamper.nowUTC } returns Instant.now() + every { timeStamper.nowUTC } returns Instant.ofEpochMilli(dateMillis) } private val person = DefaultContactDiaryPerson(123, "Romeo") @@ -113,6 +118,16 @@ open class ContactDiaryOverviewViewModelTest { override val riskState: RiskState = RiskState.INCREASED_RISK } + private val checkInLow = mockk<CheckIn>().apply { + every { id } returns traceLocationCheckInRiskLow.checkInId + every { description } returns "I can make orange rhyme with banana... Bornana" + } + + private val checkInHigh = mockk<CheckIn>().apply { + every { id } returns traceLocationCheckInRiskHigh.checkInId + every { description } returns "I'm the bad guy cause I caused the high risk" + } + private val aggregatedRiskPerDateResultLowRisk = ExposureWindowDayRisk( dateMillisSinceEpoch = dateMillis, riskLevel = RiskCalculationParametersOuterClass.NormalizedTimeToRiskLevelMapping.RiskLevel.LOW, @@ -139,8 +154,9 @@ open class ContactDiaryOverviewViewModelTest { dispatcherProvider = testDispatcherProvider, contactDiaryRepository = contactDiaryRepository, riskLevelStorage = riskLevelStorage, - timeStamper, - ContactDiaryExporter( + timeStamper = timeStamper, + checkInRepository = checkInRepository, + exporter = ContactDiaryExporter( context, timeStamper, testDispatcherProvider @@ -345,6 +361,7 @@ open class ContactDiaryOverviewViewModelTest { every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultLowRisk)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh)) every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh)) var item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -360,9 +377,14 @@ open class ContactDiaryOverviewViewModelTest { riskEventItem!!.validate(highRisk = true) } - every { riskLevelStorage.ewDayRiskStates } returns flowOf(listOf(aggregatedRiskPerDateResultHighRiskDueToLowRiskEncounter)) + every { riskLevelStorage.ewDayRiskStates } returns flowOf( + listOf( + aggregatedRiskPerDateResultHighRiskDueToLowRiskEncounter + ) + ) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow)) every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow)) item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -392,6 +414,7 @@ open class ContactDiaryOverviewViewModelTest { fun `low risk event by attending event with low risk`() { every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventLowRiskVisit)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskLow)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInLow)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -404,6 +427,7 @@ open class ContactDiaryOverviewViewModelTest { name shouldBe locationEventLowRisk.locationName riskInfoAddition shouldBe null bulledPointColor shouldBe R.color.colorBulletPointLowRisk + description shouldBe checkInLow.description } } } @@ -412,6 +436,7 @@ open class ContactDiaryOverviewViewModelTest { fun `high risk event by attending event with high risk`() { every { contactDiaryRepository.locationVisits } returns flowOf(listOf(locationEventHighRiskVisit)) every { riskLevelStorage.traceLocationCheckInRiskStates } returns flowOf(listOf(traceLocationCheckInRiskHigh)) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh)) val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date @@ -424,6 +449,7 @@ open class ContactDiaryOverviewViewModelTest { name shouldBe locationEventHighRisk.locationName riskInfoAddition shouldBe null bulledPointColor shouldBe R.color.colorBulletPointHighRisk + description shouldBe checkInHigh.description } } } @@ -443,6 +469,8 @@ open class ContactDiaryOverviewViewModelTest { ) ) + every { checkInRepository.allCheckIns } returns flowOf(listOf(checkInHigh, checkInLow)) + val item = createInstance().listItems.getOrAwaitValue().first { it is DayOverviewItem && it.date == date } as DayOverviewItem