Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Add Local Incidence Card to Statistics (EXPOSUREAPP-7446) #3596

Merged
merged 35 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
8787d51
Plumbing for the local incidence cards including layouts and item vie…
kolyaopahle Jul 1, 2021
0735f42
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
kolyaopahle Jul 1, 2021
bd6ca38
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
jurajkusnier Jul 5, 2021
0b7023a
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 7, 2021
ccf5e9f
added support for addition and removal of local statistics homescreen…
kolyaopahle Jul 11, 2021
bea26f2
Merge remote-tracking branch 'origin/feature/7446-local-incidence-car…
kolyaopahle Jul 11, 2021
838a70d
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
kolyaopahle Jul 11, 2021
d396a82
Linting
kolyaopahle Jul 11, 2021
2cbe535
Fixed unittests
kolyaopahle Jul 11, 2021
f346806
Removed unused layout tag
kolyaopahle Jul 11, 2021
97ef5a3
linting
kolyaopahle Jul 12, 2021
26c6341
Disabled local stats add card after 5 local stats are displayed
kolyaopahle Jul 12, 2021
c84e60c
Storing SelectedDistrict instead of District allows us to match the u…
kolyaopahle Jul 12, 2021
cb28845
Using the district id as stable id for local incidence cards
kolyaopahle Jul 12, 2021
b03a0d1
linting
kolyaopahle Jul 12, 2021
4144b19
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 12, 2021
7c55478
Hopefully this fixes the recycler view crashes
kolyaopahle Jul 12, 2021
fe88766
Merge remote-tracking branch 'origin/feature/7446-local-incidence-car…
kolyaopahle Jul 12, 2021
01525d5
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
jurajkusnier Jul 12, 2021
a3eb888
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
jurajkusnier Jul 13, 2021
daada49
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 13, 2021
b30055f
First round of addressing comments
kolyaopahle Jul 13, 2021
1495c8e
Fixed card recycling
kolyaopahle Jul 13, 2021
12e051f
Merge remote-tracking branch 'origin/feature/7446-local-incidence-car…
kolyaopahle Jul 13, 2021
20a5400
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 13, 2021
2740acb
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 13, 2021
c0a90fe
Refactored the Statistics Cards into different global and local subtypes
kolyaopahle Jul 13, 2021
aa1fe8e
Merge remote-tracking branch 'origin/feature/7446-local-incidence-car…
kolyaopahle Jul 13, 2021
8e4d728
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
kolyaopahle Jul 13, 2021
5567ca2
NRW is now also allowed to procure their local statistics
kolyaopahle Jul 13, 2021
15d580c
Merge remote-tracking branch 'origin/feature/7446-local-incidence-car…
kolyaopahle Jul 13, 2021
95b0e24
small xml formatting changes
kolyaopahle Jul 13, 2021
17d1fe8
Local Stats trend semantic is now inferred by trend
kolyaopahle Jul 13, 2021
3caf433
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 13, 2021
9a59ed5
Merge branch 'release/2.6.x' into feature/7446-local-incidence-card
harambasicluka Jul 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ class Districts @Inject constructor(
@AppContext private val context: Context,
@BaseGson private val gson: Gson
) {

suspend fun loadDistricts(): List<District> {
fun loadDistricts(): List<District> {
return try {
val rawDistricts = context.assets.open(ASSET_NAME).bufferedReader().use { it.readText() }
gson.fromJson(rawDistricts)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package de.rki.coronawarnapp.statistics

import de.rki.coronawarnapp.server.protocols.internal.stats.KeyFigureCardOuterClass.KeyFigure
import de.rki.coronawarnapp.server.protocols.internal.stats.LocalStatisticsOuterClass
import de.rki.coronawarnapp.statistics.local.storage.SelectedDistrict
import org.joda.time.Instant
import timber.log.Timber

Expand All @@ -22,6 +22,20 @@ data class StatisticsData(
}
}

data class LocalStatisticsData(
val items: List<LocalIncidenceStats> = emptyList()
) {
val isDataAvailable: Boolean = items.isNotEmpty()

override fun toString(): String {
return "StatisticsData(cards=${
items.map {
it.cardType.name + " " + it.updatedAt
}
})"
}
}

sealed class GenericStatsItem

data class AddStatsItem(val isEnabled: Boolean) : GenericStatsItem()
Expand Down Expand Up @@ -91,7 +105,7 @@ data class IncidenceStats(
data class LocalIncidenceStats(
override val updatedAt: Instant,
override val keyFigures: List<KeyFigure>,
val federalState: LocalStatisticsOuterClass.FederalStateData.FederalState
val selectedDistrict: SelectedDistrict?,
) : StatsItem(cardType = Type.LOCAL_INCIDENCE) {

val sevenDayIncidence: KeyFigure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,10 @@ enum class FederalStateToPackageId(val packageId: Int) {
SN(6),
ST(6),
SH(4),
TH(6)
TH(6);
kolyaopahle marked this conversation as resolved.
Show resolved Hide resolved

companion object {
fun getForName(name: String): FederalStateToPackageId? =
values().firstOrNull { it.name == name }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,38 @@ import dagger.Reusable
import de.rki.coronawarnapp.server.protocols.internal.stats.KeyFigureCardOuterClass
import de.rki.coronawarnapp.server.protocols.internal.stats.LocalStatisticsOuterClass
import de.rki.coronawarnapp.statistics.LocalIncidenceStats
import de.rki.coronawarnapp.statistics.StatisticsData
import de.rki.coronawarnapp.statistics.LocalStatisticsData
import de.rki.coronawarnapp.statistics.local.storage.LocalStatisticsConfigStorage
import org.joda.time.Instant
import timber.log.Timber
import javax.inject.Inject

@Reusable
class LocalStatisticsParser @Inject constructor() {

fun parse(rawData: ByteArray): StatisticsData {
class LocalStatisticsParser @Inject constructor(
private val localStatisticsConfigStorage: LocalStatisticsConfigStorage,
) {
fun parse(rawData: ByteArray): LocalStatisticsData {
val parsed = LocalStatisticsOuterClass.LocalStatistics.parseFrom(rawData)

val mappedFederalStates = parsed.federalStateDataList.mapNotNull { rawState ->
try {
val updatedAt = Instant.ofEpochSecond(rawState.updatedAt)
val stateIncidenceKeyFigure = rawState.sevenDayIncidence.toKeyFigure()

LocalIncidenceStats(
updatedAt = updatedAt,
keyFigures = listOf(stateIncidenceKeyFigure),
federalState = rawState.federalState
).also {
Timber.tag(TAG).v("Parsed %s", it.toString().replace("\n", ", "))
it.requireValidity()
}
} catch (e: Exception) {
Timber.tag(TAG).e("Failed to parse raw federal state: %s", rawState)
null
}
}

val mappedAdministrativeUnit = parsed.administrativeUnitDataList.mapNotNull { rawState ->
try {
val updatedAt = Instant.ofEpochSecond(rawState.updatedAt)
val administrativeUnitIncidenceKeyFigure = rawState.sevenDayIncidence.toKeyFigure()

val federalStateId = rawState.administrativeUnitShortId.toString().dropLast(3).toInt()
val leftPaddedShortId = rawState.administrativeUnitShortId
.toString()
.padStart(5, '0')

val districtId = "110$leftPaddedShortId".toInt()

val selectedDistrict = localStatisticsConfigStorage.activeDistricts.value.firstOrNull {
it.district.districtId == districtId
}

LocalIncidenceStats(
updatedAt = updatedAt,
keyFigures = listOf(administrativeUnitIncidenceKeyFigure),
federalState = LocalStatisticsOuterClass.FederalStateData.FederalState.forNumber(federalStateId)
selectedDistrict = selectedDistrict
).also {
Timber.tag(TAG).v("Parsed %s", it.toString().replace("\n", ", "))
it.requireValidity()
Expand All @@ -55,9 +46,7 @@ class LocalStatisticsParser @Inject constructor() {
}
}

val mappedItems = mappedFederalStates + mappedAdministrativeUnit

return StatisticsData(items = mappedItems).also {
return LocalStatisticsData(items = mappedAdministrativeUnit).also {
Timber.tag(TAG).d("Parsed local statistics data, %d cards.", it.items.size)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package de.rki.coronawarnapp.statistics.local.source

import de.rki.coronawarnapp.statistics.StatisticsData
import de.rki.coronawarnapp.statistics.LocalStatisticsData
import de.rki.coronawarnapp.statistics.local.FederalStateToPackageId
import de.rki.coronawarnapp.statistics.local.storage.LocalStatisticsConfigStorage
import de.rki.coronawarnapp.util.coroutine.AppScope
Expand All @@ -9,6 +9,8 @@ import de.rki.coronawarnapp.util.flow.HotDataFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import org.joda.time.Duration
import timber.log.Timber
import javax.inject.Inject
Expand Down Expand Up @@ -41,23 +43,35 @@ class LocalStatisticsProvider @Inject constructor(
}
}

val current: Flow<List<StatisticsData>> = localStatisticsData.data
val current: Flow<LocalStatisticsData> = localStatisticsData.data.map {
kolyaopahle marked this conversation as resolved.
Show resolved Hide resolved
val groupedStats = it.reduceOrNull { acc, localStatisticsData ->
LocalStatisticsData(acc.items + localStatisticsData.items)
} ?: LocalStatisticsData()

groupedStats.copy(
items = groupedStats.items.filter {
localStatisticsConfigStorage.activeDistricts.value.any { selected ->
selected.district.districtId == it.selectedDistrict?.district?.districtId
}
}.sortedBy { selected -> selected.selectedDistrict?.addedAt }.reversed()
)
}
kolyaopahle marked this conversation as resolved.
Show resolved Hide resolved

private fun fetchCacheFirst(): List<StatisticsData> {
private suspend fun fetchCacheFirst(): List<LocalStatisticsData> {
Timber.tag(TAG).d("fromCache()")

val targetedStates = localStatisticsConfigStorage.activeStates.value
val targetedStates = localStatisticsConfigStorage.activeStates.first()

val cacheResults = targetedStates.map { fromCache(it) }

if (cacheResults.contains(null)) {
triggerUpdate()
}

return cacheResults.map { it ?: StatisticsData() }
return cacheResults.map { it ?: LocalStatisticsData() }
}

private fun fromCache(forState: FederalStateToPackageId): StatisticsData? = try {
private fun fromCache(forState: FederalStateToPackageId): LocalStatisticsData? = try {
Timber.tag(TAG).d("fromCache(%s)", forState)

localStatisticsCache.load(forState)?.let { localStatisticsParser.parse(it) }?.also {
Expand All @@ -68,15 +82,15 @@ class LocalStatisticsProvider @Inject constructor(
null
}

private suspend fun fromServer(): List<StatisticsData> {
private suspend fun fromServer(): List<LocalStatisticsData> {
Timber.tag(TAG).d("fromServer()")

val targetedStates = localStatisticsConfigStorage.activeStates.value
val targetedStates = localStatisticsConfigStorage.activeStates.first()

return targetedStates.map { fromServer(it) }
}

private suspend fun fromServer(forState: FederalStateToPackageId): StatisticsData {
private suspend fun fromServer(forState: FederalStateToPackageId): LocalStatisticsData {
Timber.tag(TAG).d("fromServer(%s)", forState)

val rawData = server.getRawLocalStatistics(forState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import de.rki.coronawarnapp.util.coroutine.AppScope
import de.rki.coronawarnapp.util.device.ForegroundState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
Expand All @@ -24,7 +23,7 @@ class LocalStatisticsRetrievalScheduler @Inject constructor(

private val updateStatsTrigger = combine(
foregroundState.isInForeground,
localStatisticsConfigStorage.activeStates.flow
localStatisticsConfigStorage.activeStates
) { isInForeground, activeStates ->
val statsChanged = !lastActiveStates.containsAll(activeStates)
lastActiveStates.clear()
Expand All @@ -36,7 +35,7 @@ class LocalStatisticsRetrievalScheduler @Inject constructor(
statsChanged
)
isInForeground || statsChanged
}.distinctUntilChanged()
}

fun setup() {
Timber.tag(TAG).i("setup()")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
package de.rki.coronawarnapp.statistics.local.storage

import android.content.Context
import com.google.gson.Gson
import de.rki.coronawarnapp.statistics.local.FederalStateToPackageId
import de.rki.coronawarnapp.util.di.AppContext
import de.rki.coronawarnapp.util.preferences.FlowPreference
import de.rki.coronawarnapp.util.preferences.createFlowPreference
import de.rki.coronawarnapp.util.serialization.BaseGson
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class LocalStatisticsConfigStorage @Inject constructor(
@AppContext val context: Context
@AppContext val context: Context,
@BaseGson private val gson: Gson,
) {
private val prefs by lazy {
context.getSharedPreferences("statistics_local_config", Context.MODE_PRIVATE)
}

val activeStates = prefs.createFlowPreference(PKEY_ACTIVE_STATES, emptyList<FederalStateToPackageId>())
val activeDistricts = prefs.createFlowPreference(
key = PKEY_ACTIVE_DISTRICTS,
reader = FlowPreference.gsonReader(gson, emptySet<SelectedDistrict>()),
writer = FlowPreference.gsonWriter(gson)
)

val activeStates = activeDistricts.flow
.map {
it.map { district -> district.district.federalStateShortName }
.mapNotNull { stateId -> FederalStateToPackageId.getForName(stateId) }
kolyaopahle marked this conversation as resolved.
Show resolved Hide resolved
.distinct()
}
kolyaopahle marked this conversation as resolved.
Show resolved Hide resolved

companion object {
private const val PKEY_ACTIVE_STATES = "statistics.local.states"
private const val PKEY_ACTIVE_DISTRICTS = "statistics.local.districts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.rki.coronawarnapp.statistics.local.storage

import de.rki.coronawarnapp.datadonation.analytics.common.Districts
import org.joda.time.Instant

data class SelectedDistrict(
val district: Districts.District,
val addedAt: Instant,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import de.rki.coronawarnapp.statistics.AppliedVaccinationRatesStats
import de.rki.coronawarnapp.statistics.IncidenceStats
import de.rki.coronawarnapp.statistics.InfectionStats
import de.rki.coronawarnapp.statistics.KeySubmissionsStats
import de.rki.coronawarnapp.statistics.LocalIncidenceStats
import de.rki.coronawarnapp.statistics.PersonsVaccinatedCompletelyStats
import de.rki.coronawarnapp.statistics.PersonsVaccinatedOnceStats
import de.rki.coronawarnapp.statistics.SevenDayRValue
Expand All @@ -17,6 +18,7 @@ import de.rki.coronawarnapp.statistics.ui.homecards.cards.AppliedVaccinationRate
import de.rki.coronawarnapp.statistics.ui.homecards.cards.IncidenceCard
import de.rki.coronawarnapp.statistics.ui.homecards.cards.InfectionsCard
import de.rki.coronawarnapp.statistics.ui.homecards.cards.KeySubmissionsCard
import de.rki.coronawarnapp.statistics.ui.homecards.cards.LocalIncidenceCard
import de.rki.coronawarnapp.statistics.ui.homecards.cards.PersonsVaccinatedCompletelyCard
import de.rki.coronawarnapp.statistics.ui.homecards.cards.PersonsVaccinatedOnceCard
import de.rki.coronawarnapp.statistics.ui.homecards.cards.SevenDayRValueCard
Expand All @@ -41,6 +43,7 @@ class StatisticsCardAdapter :
DataBinderMod<StatisticsCardItem, ItemVH<StatisticsCardItem, ViewBinding>>(data),
TypedVHCreatorMod({ data[it].stats is InfectionStats }) { InfectionsCard(it) },
TypedVHCreatorMod({ data[it].stats is IncidenceStats }) { IncidenceCard(it) },
TypedVHCreatorMod({ data[it].stats is LocalIncidenceStats }) { LocalIncidenceCard(it) },
TypedVHCreatorMod({ data[it].stats is KeySubmissionsStats }) { KeySubmissionsCard(it) },
TypedVHCreatorMod({ data[it].stats is SevenDayRValue }) { SevenDayRValueCard(it) },
TypedVHCreatorMod({ data[it].stats is PersonsVaccinatedOnceStats }) { PersonsVaccinatedOnceCard(it) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.recyclerview.widget.PagerSnapHelper
import de.rki.coronawarnapp.R
import de.rki.coronawarnapp.databinding.HomeStatisticsScrollcontainerBinding
import de.rki.coronawarnapp.statistics.GenericStatsItem
import de.rki.coronawarnapp.statistics.LocalIncidenceStats
import de.rki.coronawarnapp.statistics.StatisticsData
import de.rki.coronawarnapp.statistics.ui.homecards.cards.StatisticsCardItem
import de.rki.coronawarnapp.ui.main.home.HomeAdapter
Expand Down Expand Up @@ -59,7 +60,7 @@ class StatisticsHomeCard(
savedStateKey = "stats:${item.stableId}"

item.data.items.map {
StatisticsCardItem(it, item.onClickListener)
StatisticsCardItem(it, item.onClickListener, item.onRemoveListener)
}.let {
statisticsCardAdapter.update(it)
}
Expand All @@ -81,7 +82,8 @@ class StatisticsHomeCard(

data class Item(
val data: StatisticsData,
val onClickListener: (GenericStatsItem) -> Unit
val onClickListener: (GenericStatsItem) -> Unit,
val onRemoveListener: (LocalIncidenceStats) -> Unit = {},
) : HomeItem {
override val stableId: Long = Item::class.java.name.hashCode().toLong()

Expand Down
Loading