EN: (UI) Improve display of reported exposures

This commit is contained in:
Marvin W 2020-12-12 00:02:15 +01:00
parent 369c3d7557
commit 9b91bf63c6
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
8 changed files with 101 additions and 39 deletions

View File

@ -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 { "<tt>$it</tt>" })
}.joinToString("<br>").takeIf { it.isNotEmpty() }
data class NTuple4<T1, T2, T3, T4>(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("<br>").let { getString(R.string.pref_exposure_app_last_report_summary_encounters_prefix, merged.size) + "<br>$it<br><i>" + getString(R.string.pref_exposure_app_last_report_summary_encounters_suffix) + "</i>" }
}
Triple(reportTitle, "$diagnosisKeysLine<br>$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 { "<small><tt>$it</tt></small>" })
}.joinToString("<br>").takeIf { it.isNotEmpty() }
apiUsage.isVisible = apiUsageSummary != null
apiUsage.summary = HtmlCompat.fromHtml(apiUsageSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim()
}

View File

@ -12,5 +12,5 @@
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M13 14H11V9H13M13 18H11V16H13M1 21H23L12 2L1 21Z" />
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" />
</vector>

View File

@ -15,7 +15,13 @@
<string name="pref_exposure_collected_rpis_title">Gesammelte IDs</string>
<string name="pref_exposure_collected_rpis_summary"><xliff:g example="63">%1$d</xliff:g> IDs in den letzten 60 Minuten</string>
<string name="pref_exposure_advertising_id_title">Aktuell verwendete ID</string>
<string name="pref_exposure_app_last_report_title">Letzter Bericht (<xliff:g example="vor 2 Stunden">%1$s</xliff:g>)</string>
<string name="prefcat_exposure_app_report_title">Gemeldete Begegnungen</string>
<string name="pref_exposure_app_report_updated_title">Aktualisiert: <xliff:g example="Today, 14:02">%1$s</xliff:g></string>
<string name="pref_exposure_app_report_entry_time_short">Kürzer als 5 Minutem</string>
<string name="pref_exposure_app_report_entry_time_about">Etwa <xliff:g example="13">%1$d</xliff:g> Minuten</string>
<string name="pref_exposure_app_report_entry_distance_close">nahe Begegnung</string>
<string name="pref_exposure_app_report_entry_distance_far">entfernte Begegnung</string>
<string name="pref_exposure_app_report_entry_combined"><xliff:g example="About 12 minutes">%1$s</xliff:g>, <xliff:g example="distant exposure">%2$s</xliff:g></string>
<string name="pref_exposure_app_last_report_summary_diagnosis_keys"><xliff:g example="121031">%1$d</xliff:g> Diagnoseschlüssel verarbeitet.</string>
<string name="pref_exposure_app_last_report_summary_encounters_no">Keine Risiko-Begegnung erfasst.</string>
<string name="pref_exposure_app_last_report_summary_encounters_prefix"><xliff:g example="3">%1$d</xliff:g> Risiko-Begegnungen:</string>

View File

@ -25,7 +25,13 @@
<string name="pref_exposure_collected_rpis_title">Collected IDs</string>
<string name="pref_exposure_collected_rpis_summary"><xliff:g example="63">%1$d</xliff:g> IDs in last hour</string>
<string name="pref_exposure_advertising_id_title">Currently broadcasted ID</string>
<string name="pref_exposure_app_last_report_title">Last report (<xliff:g example="3 hours ago">%1$s</xliff:g>)</string>
<string name="prefcat_exposure_app_report_title">Reported exposures</string>
<string name="pref_exposure_app_report_updated_title">Updated: <xliff:g example="Today, 14:02">%1$s</xliff:g></string>
<string name="pref_exposure_app_report_entry_time_short">Less than 5 minutes</string>
<string name="pref_exposure_app_report_entry_time_about">About <xliff:g example="13">%1$d</xliff:g> minutes</string>
<string name="pref_exposure_app_report_entry_distance_close">nearby exposure</string>
<string name="pref_exposure_app_report_entry_distance_far">distant exposure</string>
<string name="pref_exposure_app_report_entry_combined"><xliff:g example="About 12 minutes">%1$s</xliff:g>, <xliff:g example="distant exposure">%2$s</xliff:g></string>
<string name="pref_exposure_app_last_report_summary_diagnosis_keys">Processed <xliff:g example="121031">%1$d</xliff:g> diagnosis keys.</string>
<string name="pref_exposure_app_last_report_summary_encounters_no">No exposure encounters reported.</string>
<string name="pref_exposure_app_last_report_summary_encounters_prefix">Reported <xliff:g example="3">%1$d</xliff:g> exposure encounters:</string>

View File

@ -61,7 +61,7 @@
tools:summary="@string/pref_exposure_collected_rpis_summary" />
<Preference
android:key="pref_exposure_advertising_id"
android:selectable="false"
android:enabled="false"
android:title="@string/pref_exposure_advertising_id_title"
tools:summary="9a799d68-925f-4c0c-a73c-b418f22a1250" />
</PreferenceCategory>

View File

@ -5,6 +5,7 @@
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
@ -12,17 +13,25 @@
android:key="pref_exposure_app_open"
android:title="@string/open_app" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<org.microg.gms.ui.TextPreference
android:key="pref_exposure_app_report"
android:selectable="false"
tools:summary="@string/pref_exposure_app_last_report_summary_encounters_no"
tools:title="@string/pref_exposure_app_last_report_title" />
<PreferenceCategory
android:key="prefcat_exposure_app_report"
android:title="@string/prefcat_exposure_app_report_title">
<Preference
android:enabled="false"
android:key="pref_exposure_app_report_none"
android:order="0"
android:title="@string/list_no_item_none" />
<Preference
android:enabled="false"
android:key="pref_exposure_app_report_updated"
android:order="100"
android:summary="@string/pref_exposure_app_last_report_summary_diagnosis_keys"
android:title="@string/pref_exposure_app_report_updated_title" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:enabled="false"
android:key="pref_exposure_app_api_usage"
android:selectable="false"
android:title="@string/pref_exposure_app_api_usage_title"
tools:summary="@string/pref_exposure_app_api_usage_summary_line" />
</PreferenceCategory>

View File

@ -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)

View File

@ -42,9 +42,9 @@ fun List<MeasuredExposure>.merge(): List<MergedExposure> {
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<MergedSubExposure>) {
data class MergedExposure internal constructor(val key: TemporaryExposureKey, val timestamp: Long, val txPower: Int, @CalibrationConfidence val confidence: Int, val subs: List<MergedSubExposure>) {
@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 {