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.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceViewHolder
import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import org.json.JSONObject import org.json.JSONObject
import org.microg.gms.nearby.exposurenotification.ExposureDatabase import org.microg.gms.nearby.exposurenotification.ExposureDatabase
@ -19,7 +23,9 @@ import org.microg.gms.nearby.exposurenotification.merge
class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() { class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var open: Preference 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 lateinit var apiUsage: Preference
private val packageName: String? private val packageName: String?
get() = arguments?.getString("package") get() = arguments?.getString("package")
@ -30,7 +36,11 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
override fun onBindPreferences() { override fun onBindPreferences() {
open = preferenceScreen.findPreference("pref_exposure_app_open") ?: open 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 apiUsage = preferenceScreen.findPreference("pref_exposure_app_api_usage") ?: apiUsage
open.onPreferenceClickListener = Preference.OnPreferenceClickListener { open.onPreferenceClickListener = Preference.OnPreferenceClickListener {
try { try {
@ -54,35 +64,67 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
private fun ExposureConfiguration?.orDefault() = this private fun ExposureConfiguration?.orDefault() = this
?: ExposureConfiguration.ExposureConfigurationBuilder().build() ?: 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() { fun updateContent() {
packageName?.let { packageName -> packageName?.let { packageName ->
lifecycleScope.launchWhenResumed { lifecycleScope.launchWhenResumed {
val (reportTitle, reportSummary, apiUsageSummary) = ExposureDatabase.with(requireContext()) { database -> data class NTuple4<T1, T2, T3, T4>(val t1: T1, val t2: T2, val t3: T3, val t4: T4)
val apiUsageSummary = database.methodUsageHistogram(packageName).map { val (mergedExposures, keysInvolved, lastCheckTime, methodUsageHistogram) = ExposureDatabase.with(requireContext()) { database ->
getString(R.string.pref_exposure_app_api_usage_summary_line, it.second, it.first.let { "<tt>$it</tt>" }) val methodUsageHistogram = database.methodUsageHistogram(packageName)
}.joinToString("<br>").takeIf { it.isNotEmpty() }
val token = database.lastMethodCallArgs(packageName, "provideDiagnosisKeys")?.let { JSONObject(it).getString("request_token") } 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") 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) val config = database.loadConfiguration(packageName, token)
?: return@with Triple(null, null, apiUsageSummary) ?: return@with NTuple4(null, null, null, methodUsageHistogram)
val merged = database.findAllMeasuredExposures(config.first).merge().sortedBy { it.timestamp } val mergedExposures = 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 keysInvolved = database.countDiagnosisKeysInvolved(config.first)
val diagnosisKeysLine = getString(R.string.pref_exposure_app_last_report_summary_diagnosis_keys, database.countDiagnosisKeysInvolved(config.first)) NTuple4(mergedExposures, keysInvolved, lastCheckTime, methodUsageHistogram)
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)
} }
report.isVisible = reportSummary != null
report.title = reportTitle reportedExposures.removeAll()
report.summary = HtmlCompat.fromHtml(reportSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim() 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.isVisible = apiUsageSummary != null
apiUsage.summary = HtmlCompat.fromHtml(apiUsageSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim() apiUsage.summary = HtmlCompat.fromHtml(apiUsageSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim()
} }

View File

@ -12,5 +12,5 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#000" 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> </vector>

View File

@ -15,7 +15,13 @@
<string name="pref_exposure_collected_rpis_title">Gesammelte IDs</string> <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_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_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_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_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> <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_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_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_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_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_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> <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" /> tools:summary="@string/pref_exposure_collected_rpis_summary" />
<Preference <Preference
android:key="pref_exposure_advertising_id" android:key="pref_exposure_advertising_id"
android:selectable="false" android:enabled="false"
android:title="@string/pref_exposure_advertising_id_title" android:title="@string/pref_exposure_advertising_id_title"
tools:summary="9a799d68-925f-4c0c-a73c-b418f22a1250" /> tools:summary="9a799d68-925f-4c0c-a73c-b418f22a1250" />
</PreferenceCategory> </PreferenceCategory>

View File

@ -5,6 +5,7 @@
--> -->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <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"> xmlns:tools="http://schemas.android.com/tools">
<PreferenceCategory android:layout="@layout/preference_category_no_label"> <PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference <Preference
@ -12,17 +13,25 @@
android:key="pref_exposure_app_open" android:key="pref_exposure_app_open"
android:title="@string/open_app" /> android:title="@string/open_app" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label"> <PreferenceCategory
<org.microg.gms.ui.TextPreference android:key="prefcat_exposure_app_report"
android:key="pref_exposure_app_report" android:title="@string/prefcat_exposure_app_report_title">
android:selectable="false" <Preference
tools:summary="@string/pref_exposure_app_last_report_summary_encounters_no" android:enabled="false"
tools:title="@string/pref_exposure_app_last_report_title" /> 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>
<PreferenceCategory android:layout="@layout/preference_category_no_label"> <PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference <Preference
android:enabled="false"
android:key="pref_exposure_app_api_usage" android:key="pref_exposure_app_api_usage"
android:selectable="false"
android:title="@string/pref_exposure_app_api_usage_title" android:title="@string/pref_exposure_app_api_usage_title"
tools:summary="@string/pref_exposure_app_api_usage_summary_line" /> tools:summary="@string/pref_exposure_app_api_usage_summary_line" />
</PreferenceCategory> </PreferenceCategory>

View File

@ -907,7 +907,6 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
val (dbMigrateFile, dbMigrateWalFile) = prepareDatabaseMigration(context) val (dbMigrateFile, dbMigrateWalFile) = prepareDatabaseMigration(context)
val database = ExposureDatabase(context.applicationContext) val database = ExposureDatabase(context.applicationContext)
try { try {
Log.d(TAG, "Created instance ${database.hashCode()} of database for ${context.javaClass.simpleName}")
completeInstance(database) completeInstance(database)
finishDatabaseMigration(database, dbMigrateFile, dbMigrateWalFile) finishDatabaseMigration(database, dbMigrateFile, dbMigrateWalFile)
newInstance.complete(database) newInstance.complete(database)

View File

@ -42,9 +42,9 @@ fun List<MeasuredExposure>.merge(): List<MergedExposure> {
return result 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 @RiskLevel
val transmissionRiskLevel: Int val transmissionRiskLevel: Int
get() = key.transmissionRiskLevel get() = key.transmissionRiskLevel
@ -56,7 +56,7 @@ data class MergedExposure internal constructor(val key: TemporaryExposureKey, va
get() = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestamp) get() = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis() - timestamp)
val attenuation 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 { fun getAttenuationRiskScore(configuration: ExposureConfiguration): Int {
return when { return when {