From 11a86d9169677e39578d52727597c7a680f16580 Mon Sep 17 00:00:00 2001 From: Marvin W Date: Fri, 8 Jan 2021 15:43:05 +0100 Subject: [PATCH] EN: Display historgram of collected IDs using hourly heat map --- play-services-nearby-core-ui/build.gradle | 2 - .../src/main/AndroidManifest.xml | 2 - .../gms/nearby/core/ui/BarChartPreference.kt | 56 ------- .../gms/nearby/core/ui/DotChartPreference.kt | 39 +++++ .../microg/gms/nearby/core/ui/DotChartView.kt | 157 ++++++++++++++++++ .../ui/ExposureNotificationsRpisFragment.kt | 41 +---- .../main/res/layout/preference_bar_chart.xml | 18 -- .../main/res/layout/preference_dot_chart.xml | 12 ++ ...references_exposure_notifications_rpis.xml | 9 +- .../exposurenotification/ExposureDatabase.kt | 21 +-- .../exposurenotification/MeasuredExposure.kt | 2 + 11 files changed, 225 insertions(+), 134 deletions(-) delete mode 100644 play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/BarChartPreference.kt create mode 100644 play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartPreference.kt create mode 100644 play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt delete mode 100644 play-services-nearby-core-ui/src/main/res/layout/preference_bar_chart.xml create mode 100644 play-services-nearby-core-ui/src/main/res/layout/preference_dot_chart.xml diff --git a/play-services-nearby-core-ui/build.gradle b/play-services-nearby-core-ui/build.gradle index 38c65af0..6ad704ad 100644 --- a/play-services-nearby-core-ui/build.gradle +++ b/play-services-nearby-core-ui/build.gradle @@ -14,8 +14,6 @@ dependencies { implementation project(':play-services-nearby-core') implementation project(':play-services-base-core-ui') - implementation "com.diogobernardino:williamchart:3.7.1" - // AndroidX UI implementation "androidx.multidex:multidex:$multidexVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" diff --git a/play-services-nearby-core-ui/src/main/AndroidManifest.xml b/play-services-nearby-core-ui/src/main/AndroidManifest.xml index d7ac93e7..fd0cbb27 100644 --- a/play-services-nearby-core-ui/src/main/AndroidManifest.xml +++ b/play-services-nearby-core-ui/src/main/AndroidManifest.xml @@ -7,8 +7,6 @@ xmlns:tools="http://schemas.android.com/tools" package="org.microg.gms.nearby.core.ui"> - - diff --git a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/BarChartPreference.kt b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/BarChartPreference.kt deleted file mode 100644 index c887c223..00000000 --- a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/BarChartPreference.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020, microG Project Team - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.microg.gms.nearby.core.ui - -import android.content.Context -import android.util.AttributeSet -import androidx.preference.Preference -import androidx.preference.PreferenceViewHolder -import com.db.williamchart.data.Scale -import com.db.williamchart.view.BarChartView - -class BarChartPreference : Preference { - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) - constructor(context: Context?) : super(context) - - init { - layoutResource = R.layout.preference_bar_chart - } - - private lateinit var chart: BarChartView - var labelsFormatter: (Float) -> String = { it.toString() } - set(value) { - field = value - if (this::chart.isInitialized) { - chart.labelsFormatter = value - } - } - var scale: Scale? = null - set(value) { - field = value - if (value != null && this::chart.isInitialized) { - chart.scale = value - } - } - var data: LinkedHashMap = linkedMapOf() - set(value) { - field = value - if (this::chart.isInitialized) { - chart.animate(data) - } - } - - override fun onBindViewHolder(holder: PreferenceViewHolder) { - super.onBindViewHolder(holder) - chart = holder.itemView as? BarChartView ?: holder.findViewById(R.id.bar_chart) as BarChartView - chart.labelsFormatter = labelsFormatter - scale?.let { chart.scale = it } - chart.animate(data) - } - -} diff --git a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartPreference.kt b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartPreference.kt new file mode 100644 index 00000000..2bdb12e3 --- /dev/null +++ b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartPreference.kt @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.core.ui + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import org.microg.gms.nearby.exposurenotification.ExposureScanSummary + +class DotChartPreference : Preference { + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?) : super(context) + + init { + layoutResource = R.layout.preference_dot_chart + } + + private lateinit var chart: DotChartView + var data: Set = emptySet() + set(value) { + field = value + if (this::chart.isInitialized) { + chart.data = data + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + chart = holder.itemView as? DotChartView ?: holder.findViewById(R.id.dot_chart) as DotChartView + chart.data = data + } + +} diff --git a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt new file mode 100644 index 00000000..dbcd4f95 --- /dev/null +++ b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/DotChartView.kt @@ -0,0 +1,157 @@ +/* + * SPDX-FileCopyrightText: 2020, microG Project Team + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.microg.gms.nearby.core.ui + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.content.Context +import android.content.res.TypedArray +import android.graphics.* +import android.provider.Settings +import android.text.TextUtils +import android.util.AttributeSet +import android.util.Log +import android.util.TypedValue +import android.view.View +import org.microg.gms.nearby.exposurenotification.ExposureScanSummary +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.max + + +class DotChartView : View { + @TargetApi(21) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) + constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context?) : super(context) + + var data: Set? = null + @SuppressLint("SimpleDateFormat") + set(value) { + field = value + val displayData = hashMapOf>>() + val now = System.currentTimeMillis() + val min = now - 14 * 24 * 60 * 60 * 1000L + val date = Date(min) + val format = Settings.System.getString(context.contentResolver, Settings.System.DATE_FORMAT); + val dateFormat = if (TextUtils.isEmpty(format)) { + android.text.format.DateFormat.getMediumDateFormat(context) + } else { + SimpleDateFormat(format) + } + val lowest = dateFormat.parse(dateFormat.format(date))?.time ?: date.time + for (day in 0 until 15) { + date.time = now - (14 - day) * 24 * 60 * 60 * 1000L + displayData[day] = dateFormat.format(date) to hashMapOf() + } + if (value != null) { + for (summary in value) { + val off = summary.time - lowest + if (off < 0) continue + val totalHours = (off / 1000 / 60 / 60).toInt() + val day = totalHours / 24 + val hour = totalHours % 24 + displayData[day]?.second?.set(hour, (displayData[day]?.second?.get(hour) ?: 0) + summary.rpis) + } + } + for (hour in 0..((min-lowest)/1000/60/60).toInt()) { + displayData[0]?.second?.set(hour, displayData[0]?.second?.get(hour) ?: -1) + } + for (hour in ((min-lowest)/1000/60/60).toInt() until 24) { + displayData[14]?.second?.set(hour, displayData[14]?.second?.get(hour) ?: -1) + } + this.displayData = displayData + invalidate() + } + + private var displayData: Map>> = emptyMap() + private val paint = Paint() + private val tempRect = Rect() + private val tempRectF = RectF() + + private fun fetchAccentColor(): Int { + val typedValue = TypedValue() + val a: TypedArray = context.obtainStyledAttributes(typedValue.data, intArrayOf(androidx.appcompat.R.attr.colorAccent)) + val color = a.getColor(0, 0) + a.recycle() + return color + } + + override fun onDraw(canvas: Canvas) { + if (data == null) data = emptySet() + paint.textSize = 10 * resources.displayMetrics.scaledDensity + paint.isAntiAlias = true + paint.strokeWidth = 2f + var maxTextWidth = 0 + var maxTextHeight = 0 + for (dateString in displayData.values.map { it.first }) { + paint.getTextBounds(dateString, 0, dateString.length, tempRect) + maxTextWidth = max(maxTextWidth, tempRect.width()) + maxTextHeight = max(maxTextHeight, tempRect.height()) + } + + val legendLeft = maxTextWidth + 4 * resources.displayMetrics.scaledDensity + val legendBottom = maxTextHeight + 4 * resources.displayMetrics.scaledDensity + + val distHeight = (height - 28 - paddingTop - paddingBottom - legendBottom).toDouble() + val distWidth = (width - 46 - paddingLeft - paddingRight - legendLeft).toDouble() + val perHeight = distHeight / 15.0 + val perWidth = distWidth / 24.0 + + paint.textAlign = Paint.Align.RIGHT + val maxValue = displayData.values.mapNotNull { it.second.values.maxOrNull() }.maxOrNull() ?: 0 + val accentColor = fetchAccentColor() + val accentRed = Color.red(accentColor) + val accentGreen = Color.green(accentColor) + val accentBlue = Color.blue(accentColor) + for (day in 0 until 15) { + val (dateString, hours) = displayData[day] ?: "" to emptyMap() + val top = day * (perHeight + 2) + paddingTop + if (day % 2 == 0) { + paint.setARGB(255, 100, 100, 100) + canvas.drawText(dateString, (paddingLeft + legendLeft - 4 * resources.displayMetrics.scaledDensity), (top + perHeight / 2.0 + maxTextHeight / 2.0).toFloat(), paint) + } + for (hour in 0 until 24) { + val value = hours[hour] ?: 0 // TODO: Actually allow null to display offline state as soon as we properly record it + val left = hour * (perWidth + 2) + paddingLeft + legendLeft + tempRectF.set(left.toFloat() + 2f, top.toFloat() + 2f, (left + perWidth).toFloat() - 2f, (top + perHeight).toFloat() - 2f) + when { + value == null -> { + paint.style = Paint.Style.FILL_AND_STROKE + paint.setARGB(30, 100, 100, 100) + canvas.drawRoundRect(tempRectF, 2f, 2f, paint) + paint.style = Paint.Style.FILL + } + maxValue == 0 -> { + paint.setARGB(50, accentRed, accentGreen, accentBlue) + paint.style = Paint.Style.STROKE + canvas.drawRoundRect(tempRectF, 2f, 2f, paint) + paint.style = Paint.Style.FILL + } + value >= 0 -> { + val alpha = ((value.toDouble() / maxValue.toDouble()) * 255).toInt() + paint.setARGB(max(50, alpha), accentRed, accentGreen, accentBlue) + paint.style = Paint.Style.STROKE + canvas.drawRoundRect(tempRectF, 2f, 2f, paint) + paint.style = Paint.Style.FILL + paint.setARGB(alpha, accentRed, accentGreen, accentBlue) + canvas.drawRoundRect(tempRectF, 2f, 2f, paint) + } + } + } + } + val legendTop = 15 * (perHeight + 2) + paddingTop + maxTextHeight + 4 * resources.displayMetrics.scaledDensity + paint.textAlign = Paint.Align.CENTER + paint.setARGB(255, 100, 100, 100) + for (hour in 0 until 24) { + if (hour % 3 == 0) { + val left = hour * (perWidth + 2) + paddingLeft + legendLeft + perWidth / 2.0 + canvas.drawText("${hour}:00", left.toFloat(), legendTop.toFloat(), paint) + } + } + } +} diff --git a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsRpisFragment.kt b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsRpisFragment.kt index d643a1c3..339bc88a 100644 --- a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsRpisFragment.kt +++ b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsRpisFragment.kt @@ -7,22 +7,17 @@ package org.microg.gms.nearby.core.ui import android.annotation.TargetApi import android.os.Bundle -import android.text.format.DateFormat import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat -import com.db.williamchart.data.Scale import org.microg.gms.nearby.exposurenotification.ExposureDatabase -import java.util.* -import kotlin.math.roundToInt -import kotlin.math.roundToLong @TargetApi(21) class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() { private lateinit var histogramCategory: PreferenceCategory - private lateinit var histogram: BarChartPreference + private lateinit var histogram: DotChartPreference private lateinit var deleteAll: Preference private lateinit var exportDb: Preference @@ -63,37 +58,11 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() { fun updateChart() { lifecycleScope.launchWhenResumed { - val (totalRpiCount, rpiHistogram) = ExposureDatabase.with(requireContext()) { database -> - val map = linkedMapOf() - val lowestDate = (System.currentTimeMillis() / 24 / 60 / 60 / 1000 - 13).toDouble().roundToLong() * 24 * 60 * 60 * 1000 - for (i in 0..13) { - val date = Calendar.getInstance().apply { this.time = Date(lowestDate + i * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH) - val str = when (i) { - 0, 13 -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), lowestDate + i * 24 * 60 * 60 * 1000).toString() - else -> IntArray(date).joinToString("").replace("0", "\u200B") - } - map[str] = 0f - } - val refDateLow = Calendar.getInstance().apply { this.time = Date(lowestDate) }.get(Calendar.DAY_OF_MONTH) - val refDateHigh = Calendar.getInstance().apply { this.time = Date(lowestDate + 13 * 24 * 60 * 60 * 1000) }.get(Calendar.DAY_OF_MONTH) - for (entry in database.rpiHistogram) { - val time = Date(entry.key * 24 * 60 * 60 * 1000) - if (time.time < lowestDate) continue // Ignore old data - val date = Calendar.getInstance().apply { this.time = time }.get(Calendar.DAY_OF_MONTH) - val str = when (date) { - refDateLow, refDateHigh -> DateFormat.format(DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMd"), entry.key * 24 * 60 * 60 * 1000).toString() - else -> IntArray(date).joinToString("").replace("0", "\u200B") - } - map[str] = entry.value.toFloat() - } - val totalRpiCount = database.totalRpiCount - totalRpiCount to map - } - deleteAll.isEnabled = totalRpiCount != 0L + val rpiHourHistogram = ExposureDatabase.with(requireContext()) { database -> database.rpiHourHistogram } + val totalRpiCount = rpiHourHistogram.map { it.rpis }.sum() + deleteAll.isEnabled = totalRpiCount > 0 histogramCategory.title = getString(R.string.prefcat_exposure_rpis_histogram_title, totalRpiCount) - histogram.labelsFormatter = { it.roundToInt().toString() } - histogram.scale = Scale(0f, rpiHistogram.values.max()?.coerceAtLeast(0.1f) ?: 0.1f) - histogram.data = rpiHistogram + histogram.data = rpiHourHistogram } } } diff --git a/play-services-nearby-core-ui/src/main/res/layout/preference_bar_chart.xml b/play-services-nearby-core-ui/src/main/res/layout/preference_bar_chart.xml deleted file mode 100644 index 51b45d33..00000000 --- a/play-services-nearby-core-ui/src/main/res/layout/preference_bar_chart.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - diff --git a/play-services-nearby-core-ui/src/main/res/layout/preference_dot_chart.xml b/play-services-nearby-core-ui/src/main/res/layout/preference_dot_chart.xml new file mode 100644 index 00000000..574a335e --- /dev/null +++ b/play-services-nearby-core-ui/src/main/res/layout/preference_dot_chart.xml @@ -0,0 +1,12 @@ + + + + diff --git a/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_rpis.xml b/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_rpis.xml index 37168c0e..66658dec 100644 --- a/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_rpis.xml +++ b/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_rpis.xml @@ -9,15 +9,16 @@ - + android:selectable="false" + tools:layout="@layout/preference_dot_chart" /> + android:summary="@string/pref_exposure_rpi_export_summary" + android:title="@string/pref_exposure_rpi_export_title" /> + val rpiHourHistogram: Set get() = readableDatabase.run { - rawQuery("SELECT round(timestamp/(24*60*60*1000)), COUNT(*) FROM $TABLE_ADVERTISEMENTS WHERE timestamp > ? GROUP BY round(timestamp/(24*60*60*1000)) ORDER BY timestamp ASC;", arrayOf((Date().time - (14 * 24 * 60 * 60 * 1000)).toString())).use { cursor -> - val map = linkedMapOf() + rawQuery("SELECT round(timestamp/(60*60*1000))*60*60*1000, COUNT(*), COUNT(*) FROM $TABLE_ADVERTISEMENTS WHERE timestamp > ? GROUP BY round(timestamp/(60*60*1000)) ORDER BY timestamp ASC;", arrayOf((System.currentTimeMillis() - (14 * 24 * 60 * 60 * 1000L)).toString())).use { cursor -> + val set = hashSetOf() while (cursor.moveToNext()) { - map[cursor.getLong(0)] = cursor.getLong(1) - } - map - } - } - - val totalRpiCount: Long - get() = readableDatabase.run { - rawQuery("SELECT COUNT(*) FROM $TABLE_ADVERTISEMENTS WHERE timestamp > ?;", arrayOf((Date().time - (14 * 24 * 60 * 60 * 1000)).toString())).use { cursor -> - if (cursor.moveToNext()) { - cursor.getLong(0) - } else { - 0L + set.add(ExposureScanSummary(cursor.getLong(0), cursor.getInt(1), cursor.getInt(2))) } + set } } diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt index 456738d4..4bb7f2de 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/MeasuredExposure.kt @@ -9,6 +9,8 @@ import android.util.Log import com.google.android.gms.nearby.exposurenotification.* import java.util.concurrent.TimeUnit +data class ExposureScanSummary(val time: Long, val rpis: Int, val records: Int) + data class PlainExposure(val rpi: ByteArray, val aem: ByteArray, val timestamp: Long, val duration: Long, val rssi: Int) data class MeasuredExposure(val timestamp: Long, val duration: Long, val rssi: Int, val txPower: Int, @CalibrationConfidence val confidence: Int, val key: TemporaryExposureKey) {