From 9b91bf63c61a4f90d48fe12c2cfb963e6acb963d Mon Sep 17 00:00:00 2001 From: Marvin W Date: Sat, 12 Dec 2020 00:02:15 +0100 Subject: [PATCH] EN: (UI) Improve display of reported exposures --- ...sureNotificationsAppPreferencesFragment.kt | 90 ++++++++++++++----- .../src/main/res/drawable/ic_alert.xml | 2 +- .../src/main/res/values-de/strings.xml | 8 +- .../src/main/res/values/strings.xml | 8 +- .../preferences_exposure_notifications.xml | 2 +- ...preferences_exposure_notifications_app.xml | 23 +++-- .../exposurenotification/ExposureDatabase.kt | 1 - .../exposurenotification/MeasuredExposure.kt | 6 +- 8 files changed, 101 insertions(+), 39 deletions(-) diff --git a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsAppPreferencesFragment.kt b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsAppPreferencesFragment.kt index cbe9e888..d1ae47f5 100644 --- a/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsAppPreferencesFragment.kt +++ b/play-services-nearby-core-ui/src/main/kotlin/org/microg/gms/nearby/core/ui/ExposureNotificationsAppPreferencesFragment.kt @@ -8,10 +8,14 @@ package org.microg.gms.nearby.core.ui import android.content.Intent import android.os.Bundle import android.text.format.DateUtils +import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.text.HtmlCompat import androidx.lifecycle.lifecycleScope import androidx.preference.Preference +import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceViewHolder import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import org.json.JSONObject import org.microg.gms.nearby.exposurenotification.ExposureDatabase @@ -19,7 +23,9 @@ import org.microg.gms.nearby.exposurenotification.merge class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() { private lateinit var open: Preference - private lateinit var report: Preference + private lateinit var reportedExposures: PreferenceCategory + private lateinit var reportedExposuresNone: Preference + private lateinit var reportedExposuresUpdated: Preference private lateinit var apiUsage: Preference private val packageName: String? get() = arguments?.getString("package") @@ -30,7 +36,11 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() { override fun onBindPreferences() { open = preferenceScreen.findPreference("pref_exposure_app_open") ?: open - report = preferenceScreen.findPreference("pref_exposure_app_report") ?: report + reportedExposures = preferenceScreen.findPreference("prefcat_exposure_app_report") ?: reportedExposures + reportedExposuresNone = preferenceScreen.findPreference("pref_exposure_app_report_none") + ?: reportedExposuresNone + reportedExposuresUpdated = preferenceScreen.findPreference("pref_exposure_app_report_updated") + ?: reportedExposuresUpdated apiUsage = preferenceScreen.findPreference("pref_exposure_app_api_usage") ?: apiUsage open.onPreferenceClickListener = Preference.OnPreferenceClickListener { try { @@ -54,35 +64,67 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() { private fun ExposureConfiguration?.orDefault() = this ?: ExposureConfiguration.ExposureConfigurationBuilder().build() + private fun formatRelativeDateTimeString(time: Long): CharSequence? = + DateUtils.getRelativeDateTimeString( + requireContext(), + time, + DateUtils.DAY_IN_MILLIS, + DateUtils.DAY_IN_MILLIS * 2, + 0 + ) + fun updateContent() { packageName?.let { packageName -> lifecycleScope.launchWhenResumed { - val (reportTitle, reportSummary, apiUsageSummary) = ExposureDatabase.with(requireContext()) { database -> - val apiUsageSummary = database.methodUsageHistogram(packageName).map { - getString(R.string.pref_exposure_app_api_usage_summary_line, it.second, it.first.let { "$it" }) - }.joinToString("
").takeIf { it.isNotEmpty() } + data class NTuple4(val t1: T1, val t2: T2, val t3: T3, val t4: T4) + val (mergedExposures, keysInvolved, lastCheckTime, methodUsageHistogram) = ExposureDatabase.with(requireContext()) { database -> + val methodUsageHistogram = database.methodUsageHistogram(packageName) + val token = database.lastMethodCallArgs(packageName, "provideDiagnosisKeys")?.let { JSONObject(it).getString("request_token") } - ?: return@with Triple(null, null, apiUsageSummary) + ?: return@with NTuple4(null, null, null, methodUsageHistogram) val lastCheckTime = database.lastMethodCall(packageName, "provideDiagnosisKeys") - ?: return@with Triple(null, null, apiUsageSummary) + ?: return@with NTuple4(null, null, null, methodUsageHistogram) val config = database.loadConfiguration(packageName, token) - ?: return@with Triple(null, null, apiUsageSummary) - val merged = database.findAllMeasuredExposures(config.first).merge().sortedBy { it.timestamp } - val reportTitle = getString(R.string.pref_exposure_app_last_report_title, DateUtils.getRelativeTimeSpanString(lastCheckTime, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS)) - val diagnosisKeysLine = getString(R.string.pref_exposure_app_last_report_summary_diagnosis_keys, database.countDiagnosisKeysInvolved(config.first)) - val encountersLine = if (merged.isEmpty()) { - getString(R.string.pref_exposure_app_last_report_summary_encounters_no) - } else { - merged.map { - val riskScore = it.getRiskScore(config.second.orDefault()) - "· " + getString(R.string.pref_exposure_app_last_report_summary_encounters_line, DateUtils.formatDateRange(requireContext(), it.timestamp, it.timestamp + it.durationInMinutes * 60 * 1000L, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE), riskScore) - }.joinToString("
").let { getString(R.string.pref_exposure_app_last_report_summary_encounters_prefix, merged.size) + "
$it
" + getString(R.string.pref_exposure_app_last_report_summary_encounters_suffix) + "" } - } - Triple(reportTitle, "$diagnosisKeysLine
$encountersLine", apiUsageSummary) + ?: return@with NTuple4(null, null, null, methodUsageHistogram) + val mergedExposures = database.findAllMeasuredExposures(config.first).merge().sortedBy { it.timestamp } + val keysInvolved = database.countDiagnosisKeysInvolved(config.first) + NTuple4(mergedExposures, keysInvolved, lastCheckTime, methodUsageHistogram) } - report.isVisible = reportSummary != null - report.title = reportTitle - report.summary = HtmlCompat.fromHtml(reportSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim() + + reportedExposures.removeAll() + if (mergedExposures.isNullOrEmpty()) { + reportedExposures.addPreference(reportedExposuresNone) + } else { + for (exposure in mergedExposures) { + val minAttenuation = exposure.subs.map { it.attenuation }.minOrNull() ?: exposure.attenuation + val nearby = exposure.attenuation < 63 || minAttenuation < 55 + val distanceString = if (nearby) getString(R.string.pref_exposure_app_report_entry_distance_close) else getString(R.string.pref_exposure_app_report_entry_distance_far) + val durationString = if (exposure.durationInMinutes < 5) getString(R.string.pref_exposure_app_report_entry_time_short) else getString(R.string.pref_exposure_app_report_entry_time_about, exposure.durationInMinutes) + val preference = object : Preference(requireContext()) { + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + val titleView = holder!!.findViewById(android.R.id.title) as? TextView + val titleViewTextColor = titleView?.textColors + super.onBindViewHolder(holder) + if (titleViewTextColor != null) titleView.setTextColor(titleViewTextColor) + } + } + preference.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_alert) + preference.title = DateUtils.formatDateRange(requireContext(), exposure.timestamp, exposure.timestamp + exposure.durationInMinutes * 60 * 1000L, DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE) + preference.summary = getString(R.string.pref_exposure_app_report_entry_combined, durationString, distanceString) + preference.isSelectable = false + reportedExposures.addPreference(preference) + } + } + + reportedExposuresUpdated.isVisible = lastCheckTime != null + reportedExposuresUpdated.title = if (lastCheckTime != null) getString(R.string.pref_exposure_app_report_updated_title, DateUtils.getRelativeDateTimeString(requireContext(), lastCheckTime, DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS * 2, 0)) else null + reportedExposuresUpdated.summary = getString(R.string.pref_exposure_app_last_report_summary_diagnosis_keys, keysInvolved?.toInt() + ?: 0) + reportedExposures.addPreference(reportedExposuresUpdated) + + val apiUsageSummary = methodUsageHistogram.map { + getString(R.string.pref_exposure_app_api_usage_summary_line, it.second, it.first.let { "$it" }) + }.joinToString("
").takeIf { it.isNotEmpty() } apiUsage.isVisible = apiUsageSummary != null apiUsage.summary = HtmlCompat.fromHtml(apiUsageSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim() } diff --git a/play-services-nearby-core-ui/src/main/res/drawable/ic_alert.xml b/play-services-nearby-core-ui/src/main/res/drawable/ic_alert.xml index 81bf78b7..f17af6bf 100644 --- a/play-services-nearby-core-ui/src/main/res/drawable/ic_alert.xml +++ b/play-services-nearby-core-ui/src/main/res/drawable/ic_alert.xml @@ -12,5 +12,5 @@ android:viewportHeight="24"> + android:pathData="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z" /> diff --git a/play-services-nearby-core-ui/src/main/res/values-de/strings.xml b/play-services-nearby-core-ui/src/main/res/values-de/strings.xml index bcd5d252..b02ce44f 100644 --- a/play-services-nearby-core-ui/src/main/res/values-de/strings.xml +++ b/play-services-nearby-core-ui/src/main/res/values-de/strings.xml @@ -15,7 +15,13 @@ Gesammelte IDs %1$d IDs in den letzten 60 Minuten Aktuell verwendete ID - Letzter Bericht (%1$s) + Gemeldete Begegnungen + Aktualisiert: %1$s + Kürzer als 5 Minutem + Etwa %1$d Minuten + nahe Begegnung + entfernte Begegnung + %1$s, %2$s %1$d Diagnoseschlüssel verarbeitet. Keine Risiko-Begegnung erfasst. %1$d Risiko-Begegnungen: diff --git a/play-services-nearby-core-ui/src/main/res/values/strings.xml b/play-services-nearby-core-ui/src/main/res/values/strings.xml index 9029d772..6b5dfaa2 100644 --- a/play-services-nearby-core-ui/src/main/res/values/strings.xml +++ b/play-services-nearby-core-ui/src/main/res/values/strings.xml @@ -25,7 +25,13 @@ Collected IDs %1$d IDs in last hour Currently broadcasted ID - Last report (%1$s) + Reported exposures + Updated: %1$s + Less than 5 minutes + About %1$d minutes + nearby exposure + distant exposure + %1$s, %2$s Processed %1$d diagnosis keys. No exposure encounters reported. Reported %1$d exposure encounters: diff --git a/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications.xml b/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications.xml index 2e1ddc25..e2d68374 100644 --- a/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications.xml +++ b/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications.xml @@ -61,7 +61,7 @@ tools:summary="@string/pref_exposure_collected_rpis_summary" /> diff --git a/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_app.xml b/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_app.xml index d8d030b7..50c3b5b2 100644 --- a/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_app.xml +++ b/play-services-nearby-core-ui/src/main/res/xml/preferences_exposure_notifications_app.xml @@ -5,6 +5,7 @@ --> - - + + + diff --git a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt index 15bbc529..1b4874f3 100644 --- a/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt +++ b/play-services-nearby-core/src/main/kotlin/org/microg/gms/nearby/exposurenotification/ExposureDatabase.kt @@ -907,7 +907,6 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit val (dbMigrateFile, dbMigrateWalFile) = prepareDatabaseMigration(context) val database = ExposureDatabase(context.applicationContext) try { - Log.d(TAG, "Created instance ${database.hashCode()} of database for ${context.javaClass.simpleName}") completeInstance(database) finishDatabaseMigration(database, dbMigrateFile, dbMigrateWalFile) newInstance.complete(database) 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 77c5c277..456738d4 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 @@ -42,9 +42,9 @@ fun List.merge(): List { return result } -internal data class MergedSubExposure(val attenuation: Int, val duration: Long) +data class MergedSubExposure(val attenuation: Int, val duration: Long) -data class MergedExposure internal constructor(val key: TemporaryExposureKey, val timestamp: Long, val txPower: Int, @CalibrationConfidence val confidence: Int, internal val subs: List) { +data class MergedExposure internal constructor(val key: TemporaryExposureKey, val timestamp: Long, val txPower: Int, @CalibrationConfidence val confidence: Int, val subs: List) { @RiskLevel val transmissionRiskLevel: Int get() = key.transmissionRiskLevel @@ -56,7 +56,7 @@ data class MergedExposure internal constructor(val key: TemporaryExposureKey, va get() = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestamp) val attenuation - get() = (subs.map { it.attenuation * it.duration }.sum().toDouble() / subs.map { it.duration }.sum().toDouble()).toInt() + get() = if (subs.map { it.duration }.sum() == 0L) subs[0].attenuation else (subs.map { it.attenuation * it.duration }.sum().toDouble() / subs.map { it.duration }.sum().toDouble()).toInt() fun getAttenuationRiskScore(configuration: ExposureConfiguration): Int { return when {