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 {