Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 11a86d91 authored by Marvin W.'s avatar Marvin W. 🐿️
Browse files

EN: Display historgram of collected IDs using hourly heat map

parent 6e176cce
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -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"
+0 −2
Original line number Diff line number Diff line
@@ -7,8 +7,6 @@
    xmlns:tools="http://schemas.android.com/tools"
    package="org.microg.gms.nearby.core.ui">

    <uses-sdk tools:overrideLibrary="com.db.williamchart" />

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

+39 −0
Original line number Diff line number Diff line
@@ -9,48 +9,31 @@ 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
import org.microg.gms.nearby.exposurenotification.ExposureScanSummary

class BarChartPreference : Preference {
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_bar_chart
        layoutResource = R.layout.preference_dot_chart
    }

    private lateinit var chart: BarChartView
    var labelsFormatter: (Float) -> String = { it.toString() }
    private lateinit var chart: DotChartView
    var data: Set<ExposureScanSummary> = emptySet()
        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<String, Float> = linkedMapOf()
        set(value) {
            field = value
            if (this::chart.isInitialized) {
                chart.animate(data)
                chart.data = 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)
        chart = holder.itemView as? DotChartView ?: holder.findViewById(R.id.dot_chart) as DotChartView
        chart.data = data
    }

}
+157 −0
Original line number Diff line number Diff line
/*
 * 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<ExposureScanSummary>? = null
        @SuppressLint("SimpleDateFormat")
        set(value) {
            field = value
            val displayData = hashMapOf<Int, Pair<String, MutableMap<Int, Int>>>()
            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<Int, Pair<String, Map<Int, Int>>> = 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)
            }
        }
    }
}
+5 −36
Original line number Diff line number Diff line
@@ -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<String, Float>()
                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
        }
    }
}
Loading