Unverified Commit adef88c7 authored by stopcovid@lunabee.com's avatar stopcovid@lunabee.com
Browse files

Update to 4.1.0

- Redesign news and key figures on Home
- Don't blur last contact date coming from ROBERT server
- Enhance key figure charts
parent b7e54571
package com.lunabeestudio.stopcovid.coreui.fastitem
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.lunabeestudio.stopcovid.coreui.R
import com.lunabeestudio.stopcovid.coreui.databinding.ItemHorizontalRecyclerviewBinding
import com.mikepenz.fastadapter.GenericItem
import com.mikepenz.fastadapter.adapters.GenericFastItemAdapter
import com.mikepenz.fastadapter.binding.AbstractBindingItem
class HorizontalRecyclerViewItem : AbstractBindingItem<ItemHorizontalRecyclerviewBinding>() {
override val type: Int = R.id.item_horizontal_recyclerview
var horizontalItems: List<GenericItem> = listOf()
var viewPool: RecyclerView.RecycledViewPool? = null
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): ItemHorizontalRecyclerviewBinding {
return ItemHorizontalRecyclerviewBinding.inflate(inflater, parent, false).apply {
recyclerview.setRecycledViewPool(viewPool)
}
}
override fun bindView(binding: ItemHorizontalRecyclerviewBinding, payloads: List<Any>) {
super.bindView(binding, payloads)
// Avoid clipping recycler when refresh
var fastAdapter = binding.recyclerview.adapter as? GenericFastItemAdapter
if (fastAdapter == null) {
fastAdapter = GenericFastItemAdapter()
binding.recyclerview.adapter = fastAdapter
}
fastAdapter.setNewList(horizontalItems)
}
}
fun horizontalRecyclerViewItem(
block: (HorizontalRecyclerViewItem.() -> Unit)
): HorizontalRecyclerViewItem = HorizontalRecyclerViewItem().apply(block)
......@@ -22,6 +22,7 @@ class SpaceItem : BaseItem<SpaceItem.ViewHolder>(
) {
@DimenRes
var spaceRes: Int = DEFAULT_HEIGHT
var orientation: Orientation = Orientation.VERTICAL
override fun bindView(holder: ViewHolder, payloads: List<Any>) {
super.bindView(holder, payloads)
......@@ -29,10 +30,15 @@ class SpaceItem : BaseItem<SpaceItem.ViewHolder>(
if (spaceRes == DEFAULT_HEIGHT) {
updateLayoutParams {
height = 0
width = 0
}
} else {
updateLayoutParams {
height = context.resources.getDimensionPixelSize(spaceRes)
if (orientation == Orientation.VERTICAL) {
height = context.resources.getDimensionPixelSize(spaceRes)
} else {
width = context.resources.getDimensionPixelSize(spaceRes)
}
}
}
}
......@@ -45,6 +51,10 @@ class SpaceItem : BaseItem<SpaceItem.ViewHolder>(
companion object {
const val DEFAULT_HEIGHT: Int = -1
}
enum class Orientation {
VERTICAL, HORIZONTAL
}
}
fun spaceItem(block: (SpaceItem.() -> Unit)): SpaceItem = SpaceItem()
......
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
\ No newline at end of file
......@@ -37,6 +37,7 @@
<dimen name="indicator_font_size">24sp</dimen>
<dimen name="title_big_font_size">24sp</dimen>
<dimen name="key_figure_font_size">50sp</dimen>
<dimen name="home_key_figure_font_size">36sp</dimen>
<dimen name="title_font_size">18sp</dimen>
<dimen name="nav_title_font_size">24sp</dimen>
<dimen name="title_small_font_size">12sp</dimen>
......
......@@ -19,4 +19,5 @@
<item name="item_card_with_actions" type="id" />
<!--Attempt to fix crash with notification on App upgrade-->
<item name="ic_notification_bar" type="drawable" />
<item name="item_horizontal_recyclerview" type="id" />
</resources>
\ No newline at end of file
......@@ -97,4 +97,10 @@
<item name="android:textSize">@dimen/health_chip_font_size</item>
<item name="android:fontFamily">@font/marianne_bold</item>
</style>
<style name="TextAppearance.StopCovid.Home.KeyFigure" parent="TextAppearance.MaterialComponents.Headline5">
<item name="fontFamily">@font/marianne_extrabold</item>
<item name="android:textStyle">bold</item>
<item name="android:textSize">@dimen/home_key_figure_font_size</item>
</style>
</resources>
......@@ -627,9 +627,11 @@ class RobertManagerImpl(
val newStatusList = listOfNotNull(currentRobertRiskStatus(), currentCleaRiskStatus())
val newAtRiskStatus: AtRiskStatus = newStatusList.maxByOrNull { it.riskLevel }!!
newAtRiskStatus.ntpLastContactS = newAtRiskStatus.ntpLastContactS?.let { ntpLastContactS ->
(ntpLastContactS + Random.nextLong(-RobertConstant.LAST_CONTACT_DELTA_S, RobertConstant.LAST_CONTACT_DELTA_S))
.coerceAtMost(System.currentTimeMillis().unixTimeMsToNtpTimeS() - RobertConstant.LAST_CONTACT_DELTA_S)
if (newAtRiskStatus == currentCleaRiskStatus() && newAtRiskStatus != prevAtRiskStatus) {
newAtRiskStatus.ntpLastContactS = newAtRiskStatus.ntpLastContactS?.let { ntpLastContactS ->
(ntpLastContactS + Random.nextLong(-RobertConstant.LAST_CONTACT_DELTA_S, RobertConstant.LAST_CONTACT_DELTA_S))
.coerceAtMost(System.currentTimeMillis().unixTimeMsToNtpTimeS() - RobertConstant.LAST_CONTACT_DELTA_S)
}
}
// Edge case: robert status might failed because user has been unregistered after 18 days of failing status
......@@ -638,7 +640,7 @@ class RobertManagerImpl(
val isCleaStatusOk = wStatusResult is RobertResultData.Success
val isRiskRaised = newAtRiskStatus.riskLevel > prevAtRiskStatus?.riskLevel ?: 0f
return if (isRobertStatusOk && isCleaStatusOk || isRiskRaised) {
return if ((isRobertStatusOk && isCleaStatusOk) || isRiskRaised) {
keystoreRepository.atRiskLastRefresh = System.currentTimeMillis()
keystoreRepository.atRiskStatus = newAtRiskStatus
if (!isImmune) {
......
......@@ -46,8 +46,8 @@ android {
applicationId "fr.gouv.android.stopcovid"
minSdkVersion 21
targetSdkVersion 31
versionCode 434
versionName "4.0.3"
versionCode 438
versionName "4.1.0"
vectorDrawables.useSupportLibrary = true
......
......@@ -2,11 +2,11 @@
"config":[
{
"name":"lastUpdate",
"value":"13 Dec 2021"
"value":"23 Dec 2021"
},
{
"name":"version",
"value":111
"value":115
},
{
"name":"versionCalibrationBle",
......@@ -148,6 +148,13 @@
72
]
},
{
"name":"app.wallet.dccKids",
"value":{
"age":11,
"s":["🐼","🐰","🐻","🦁","🐱","🐯","🐨"]
}
},
{
"name":"app.walletPubKeys",
"value":[
......@@ -198,7 +205,7 @@
"vacc22DosesNbDays":153,
"vaccJan11DosesNbDays":31,
"recNbDays":153,
"displayElgDays":60
"displayElgDays":26
}
},
{
......
......@@ -11,7 +11,7 @@
},
{
"section": "How to interpret",
"description": "Since December 8 2020, all test results, RT-PCR or TAg (antigen test), enter into the production of SI-DEP indicators (incidence rate, positivity rate and screening rate).\n\nThe epidemiological approach of Santé publique France favors indicators (incidence rate, positivity, screening) focused on people. The current calculation methods applied by Santé publique France are as follows:\n\n- number of people tested: calculated over a given period (7 days for example), it corresponds to the number of people having had at least one test during this period and who have never tested positive in the previous 60 days;\n\n- number of people who tested positive: a person who tests positive either for the first time or more than 60 days after a previous positive test will be counted as a new case.\n\nIn general, the indicators should be interpreted with caution and in their entirety.\n\nFor more information on the analysis of the health situation, please consult the weekly epidemiological point of Santé publique France (SpF) available on santepubliquefrance.fr",
"description": "Since December 8 2020, all test results, RT-PCR or TAg (antigen test), enter into the production of SI-DEP indicators (incidence rate, positivity rate and screening rate).\n\nThe epidemiological approach of Santé publique France favors indicators (incidence rate, positivity, screening) focused on people. The current calculation methods applied by Santé publique France are as follows:\n\n- number of people tested: calculated over a given period (7 days for example), it corresponds to the number of people having had at least one test during this period and who have never tested positive in the previous 60 days;\n\n- number of people who tested positive: a person who tests positive either for the first time or more than 60 days after a previous positive test will be counted as a new case.\n\nThe Vaccination indicators are expressed by place of residence of vaccinated people (replacing the place of vaccination).\n\nIn general, the indicators should be interpreted with caution and in their entirety.\n\nFor more information on the analysis of the health situation, please consult the weekly epidemiological point of Santé publique France (SpF) available on santepubliquefrance.fr",
"links": [
{
"label": "Santé publique France",
......
......@@ -11,7 +11,7 @@
},
{
"section": "Comment interpréter",
"description": "Depuis le 8 décembre 2020, tous les résultats de tests, RT-PCR ou TAg (test antigénique), entrent dans la production des indicateurs SI-DEP (taux d’incidence, taux de positivité et taux de dépistage). \n\nL’approche épidémiologique de Santé Publique France privilégie des indicateurs (taux d’incidence, de positivité, de dépistage) centrés sur les personnes. Les méthodes de calcul actuelles appliquées par Santé publique France sont les suivantes :\n\n- nombre de personnes testées : calculé sur une période donnée (7 jours par exemple), il correspond au nombre de personnes ayant eu au moins un test pendant cette période et qui n‘ont jamais été testées positives dans les 60 jours précédents ;\n\n- nombre de personnes testées positives : une personne qui présente un test positif soit pour la première fois, soit plus de 60 jours après un précédent test positif sera compté comme un nouveau cas.\n\nDe manière générale, les indicateurs doivent être interprétés avec précaution et dans leur globalité.\n\nPour plus d’information sur l’analyse de la situation sanitaire, veuillez consulter le point épidémiologique hebdomadaire de Santé publique France (SpF) disponible sur santepubliquefrance.fr",
"description": "Depuis le 8 décembre 2020, tous les résultats de tests, RT-PCR ou TAg (test antigénique), entrent dans la production des indicateurs SI-DEP (taux d’incidence, taux de positivité et taux de dépistage). \n\nL’approche épidémiologique de Santé Publique France privilégie des indicateurs (taux d’incidence, de positivité, de dépistage) centrés sur les personnes. Les méthodes de calcul actuelles appliquées par Santé publique France sont les suivantes :\n\n- nombre de personnes testées : calculé sur une période donnée (7 jours par exemple), il correspond au nombre de personnes ayant eu au moins un test pendant cette période et qui n‘ont jamais été testées positives dans les 60 jours précédents ;\n\n- nombre de personnes testées positives : une personne qui présente un test positif soit pour la première fois, soit plus de 60 jours après un précédent test positif sera compté comme un nouveau cas.\n\nLes indicateurs sur la vaccination sont exprimés par lieu de résidence des personnes vaccinées (en remplacement du lieu de vaccination).\n\nDe manière générale, les indicateurs doivent être interprétés avec précaution et dans leur globalité.\n\nPour plus d’information sur l’analyse de la situation sanitaire, veuillez consulter le point épidémiologique hebdomadaire de Santé publique France (SpF) disponible sur santepubliquefrance.fr",
"links": [
{
"label": "Santé publique France",
......
......@@ -131,8 +131,9 @@ object Constants {
const val WIDGET_MARGIN_SIZE: Float = 6f
const val ZOOM_MIN_THRESHOLD: Float = 1.25f
const val SIGNIFICANT_DIGIT_MAX: Int = 3
const val PAGER_FIRST_TAB_THRESHOLD = 31
const val PAGER_SECOND_TAB_THRESHOLD = 91
const val SECOND_IN_ONE_DAY = 86400L
const val PAGER_FIRST_TAB_THRESHOLD = 31 * SECOND_IN_ONE_DAY
const val PAGER_SECOND_TAB_THRESHOLD = 91 * SECOND_IN_ONE_DAY
}
object QrCode {
......
......@@ -33,7 +33,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDeepLinkBuilder
import androidx.navigation.NavDeepLinkRequest
import androidx.navigation.NavDestination
import androidx.navigation.fragment.DialogFragmentNavigator
......
......@@ -212,22 +212,28 @@ private fun KeyFigure.generateLineData(figureNumber: Int, context: Context, stri
fun Pair<KeyFigure, KeyFigure>.generateCombinedData(context: Context, strings: LocalizedStrings, minDate: Long): CombinedData {
return CombinedData().apply {
val keyFigure1 = first.clearSeries(second)
val keyFigure2 = second.clearSeries(first)
val lineData = LineData()
val barData = BarData()
when (first.chartType) {
KeyFigureChartType.LINES -> lineData.addDataSet(first.generateLineData(0, context, strings, minDate))
KeyFigureChartType.BARS -> barData.addDataSet(first.generateBarData(0, context, strings, minDate))
when (keyFigure1.chartType) {
KeyFigureChartType.LINES -> lineData.addDataSet(keyFigure1.generateLineData(0, context, strings, minDate))
KeyFigureChartType.BARS -> barData.addDataSet(keyFigure1.generateBarData(0, context, strings, minDate))
}
when (second.chartType) {
KeyFigureChartType.LINES -> lineData.addDataSet(second.generateLineData(1, context, strings, minDate))
KeyFigureChartType.BARS -> barData.addDataSet(second.generateBarData(1, context, strings, minDate))
when (keyFigure2.chartType) {
KeyFigureChartType.LINES -> lineData.addDataSet(keyFigure2.generateLineData(1, context, strings, minDate))
KeyFigureChartType.BARS -> barData.addDataSet(keyFigure2.generateBarData(1, context, strings, minDate))
}
if (barData.dataSets.isNotEmpty()) {
barData.apply {
val entriesCount = barData.entryCount
val xValueDiff = xMax - xMin
val spacing = 0.05f
val entriesCount = barData.entryCount / barData.dataSetCount
barWidth = xValueDiff / (entriesCount) - (spacing * xValueDiff / (entriesCount + 1))
barWidth = xValueDiff / entriesCount
if (barData.dataSetCount > 1) {
groupBars(xMin, 0f, 0f)
}
}
}
setData(lineData)
......@@ -235,6 +241,26 @@ fun Pair<KeyFigure, KeyFigure>.generateCombinedData(context: Context, strings: L
}
}
private fun KeyFigure.clearSeries(keyFigure2: KeyFigure): KeyFigure {
val newSeries = keyFigure2.series?.let { serie2 -> this.series?.filter { entry -> serie2.any { it.date == entry.date } } }
return KeyFigure(
this.category,
this.labelKey,
this.valueGlobalToDisplay,
this.valueGlobal,
false,
false,
this.extractDateS,
null,
false,
null,
this.chartType,
newSeries,
null,
this.magnitude
)
}
private fun KeyFigure.getLegend(strings: LocalizedStrings): String {
val unit = strings[this.unitStringKey]?.let { "($it)" } ?: ""
return "${strings[this.labelStringKey]} $unit"
......
......@@ -17,6 +17,7 @@ import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.lunabeestudio.stopcovid.R
import com.lunabeestudio.stopcovid.coreui.extension.safeEmojiSpanify
import com.lunabeestudio.stopcovid.coreui.extension.setTextOrHide
import com.lunabeestudio.stopcovid.coreui.fastitem.BaseItem
class BigTitleItem : BaseItem<BigTitleItem.ViewHolder>(
......@@ -25,16 +26,21 @@ class BigTitleItem : BaseItem<BigTitleItem.ViewHolder>(
var text: String? = null
var gravity: Int = Gravity.NO_GRAVITY
var importantForAccessibility: Int = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES
var linkText: String? = null
var onClickLink: View.OnClickListener? = null
override fun bindView(holder: ViewHolder, payloads: List<Any>) {
super.bindView(holder, payloads)
holder.itemView.importantForAccessibility = importantForAccessibility
holder.textView.text = text.safeEmojiSpanify()
holder.textView.gravity = gravity
holder.linkTextView.setTextOrHide(linkText)
holder.linkTextView.setOnClickListener(onClickLink)
}
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
val textView: TextView = v.findViewById(R.id.textView)
val linkTextView: TextView = v.findViewById(R.id.endTextView)
}
}
......
......@@ -20,8 +20,9 @@ class CompareFigureChartItem : AbstractBindingItem<ItemKeyFigureChartCardBinding
var shareContentDescription: String? = null
var onShareCard: ((binding: ItemKeyFigureChartCardBinding) -> Unit)? = null
var onClickListener: View.OnClickListener? = null
var chartData: CombinedData? = null
var areMagnitudeTheSame: Boolean? = null
var chartData: (() -> ChartCompareFiguresData?)? = null
var isChartAnimated: Boolean = true
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): ItemKeyFigureChartCardBinding {
return ItemKeyFigureChartCardBinding.inflate(inflater, parent, false).apply {
......@@ -39,20 +40,24 @@ class CompareFigureChartItem : AbstractBindingItem<ItemKeyFigureChartCardBinding
shareButton.contentDescription = shareContentDescription
shareButton.setOnClickListener { onShareCard?.invoke(binding) }
val datas = chartData?.invoke()
keyFigureCombinedChart.apply {
data = chartData
areMagnitudeTheSame?.let { setupStyle(!it) }
animateX(Constants.Chart.X_ANIMATION_DURATION_MILLIS)
data = datas?.combinedData
datas?.areMagnitudeTheSame?.let { setupStyle(!it) }
if (isChartAnimated) {
animateX(Constants.Chart.X_ANIMATION_DURATION_MILLIS)
}
}
// Set legend
chartSerie1LegendTextView.text = chartData?.dataSets?.get(0)?.label
chartSerie2LegendTextView.text = chartData?.dataSets?.get(1)?.label
chartData?.dataSets?.get(0)?.color?.let {
chartSerie1LegendTextView.text = datas?.combinedData?.dataSets?.get(0)?.label
chartSerie2LegendTextView.text = datas?.combinedData?.dataSets?.get(1)?.label
datas?.combinedData?.dataSets?.get(0)?.color?.let {
chartSerie1LegendTextView.setTextColor(it)
TextViewCompat.setCompoundDrawableTintList(chartSerie1LegendTextView, ColorStateList.valueOf(it))
}
chartData?.dataSets?.get(1)?.color?.let {
datas?.combinedData?.dataSets?.get(1)?.color?.let {
chartSerie2LegendTextView.setTextColor(it)
TextViewCompat.setCompoundDrawableTintList(chartSerie2LegendTextView, ColorStateList.valueOf(it))
}
......@@ -77,6 +82,11 @@ class CompareFigureChartItem : AbstractBindingItem<ItemKeyFigureChartCardBinding
binding.keyFigureCombinedChart.xAxis.resetAxisMinimum()
binding.keyFigureCombinedChart.xAxis.resetAxisMaximum()
}
data class ChartCompareFiguresData(
val combinedData: CombinedData?,
val areMagnitudeTheSame: Boolean
)
}
fun compareFigureCardChartItem(block: (CompareFigureChartItem.() -> Unit)): CompareFigureChartItem = CompareFigureChartItem().apply(
......
package com.lunabeestudio.stopcovid.fastitem
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.lunabeestudio.stopcovid.R
import com.lunabeestudio.stopcovid.coreui.extension.setTextOrHide
import com.lunabeestudio.stopcovid.databinding.ItemHighlightedNumberCardBinding
import com.mikepenz.fastadapter.binding.AbstractBindingItem
class HighlightedNumberCardItem : AbstractBindingItem<ItemHighlightedNumberCardBinding>() {
var updatedAt: String? = null
var value: String? = null
var label: String? = null
var color: Int? = null
var onClickListener: View.OnClickListener? = null
override val type: Int = R.id.item_highlighted_number_card
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): ItemHighlightedNumberCardBinding {
return ItemHighlightedNumberCardBinding.inflate(inflater, parent, false)
}
override fun bindView(binding: ItemHighlightedNumberCardBinding, payloads: List<Any>) {
super.bindView(binding, payloads)
binding.headerTextView.setTextOrHide(label)
binding.subheaderTextView.setTextOrHide(updatedAt)
binding.figureTextView.setTextOrHide(value)
color?.let { color ->
binding.headerTextView.setTextColor(color)
binding.headerTextView.compoundDrawablesRelative.forEach {
it?.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
}
}
binding.root.setOnClickListener(onClickListener)
}
override fun unbindView(binding: ItemHighlightedNumberCardBinding) {
super.unbindView(binding)
binding.headerTextView.setTextColor(Color.BLACK)
}
}
fun highlightedNumberCardItem(block: (HighlightedNumberCardItem.() -> Unit)): HighlightedNumberCardItem = HighlightedNumberCardItem().apply(
block
)
package com.lunabeestudio.stopcovid.fastitem
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.ColorRes
import com.lunabeestudio.stopcovid.R
import com.lunabeestudio.stopcovid.coreui.extension.fetchSystemColor
import com.lunabeestudio.stopcovid.coreui.extension.setTextOrHide
import com.lunabeestudio.stopcovid.databinding.ItemHomeScreenFigureCardBinding
import com.mikepenz.fastadapter.binding.AbstractBindingItem
class HomeScreenFigureCardItem : AbstractBindingItem<ItemHomeScreenFigureCardBinding>() {
override val type: Int = R.id.item_home_screen_figure_card
var regionText: String? = null
var valueText: String? = null
var figureText: String? = null
var onClick: View.OnClickListener? = null
@ColorRes
var colorBackground: Int? = null
override fun createBinding(inflater: LayoutInflater, parent: ViewGroup?): ItemHomeScreenFigureCardBinding {
return ItemHomeScreenFigureCardBinding.inflate(inflater, parent, false)
}
override fun bindView(binding: ItemHomeScreenFigureCardBinding, payloads: List<Any>) {
super.bindView(binding, payloads)
binding.apply {
root.setOnClickListener(onClick)
colorBackground?.let {
binding.constraintLayout.setBackgroundColor(it)
}
regionTextView.setTextOrHide(regionText)
figureValueTextView.setTextOrHide(valueText)
bottomActionTextView.setTextOrHide(figureText)
}
}
override fun unbindView(binding: ItemHomeScreenFigureCardBinding) {
super.unbindView(binding)
binding.apply {
root.setOnClickListener(null)
regionTextView.text = null
figureValueTextView.text = null
bottomActionTextView.text = null
val defaultColor = R.attr.colorPrimary.fetchSystemColor(root.context)
binding.constraintLayout.setBackgroundColor(defaultColor)
}
}
}
fun homeScreeFigureCardItem(
block: (HomeScreenFigureCardItem.() -> Unit)
): HomeScreenFigureCardItem = HomeScreenFigureCardItem().apply(block)
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment