EN: Display more details about app usage, add feature for deleting RPI storage

This commit is contained in:
Marvin W 2020-10-17 22:43:55 +02:00
parent 15fb118bbd
commit b67a11f4e6
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
10 changed files with 135 additions and 54 deletions

View File

@ -57,4 +57,8 @@ android {
sourceCompatibility = 1.8
targetCompatibility = 1.8
}
kotlinOptions {
jvmTarget = 1.8
}
}

View File

@ -8,16 +8,18 @@ package org.microg.gms.nearby.core.ui
import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
import androidx.core.text.HtmlCompat
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import org.json.JSONObject
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
import java.util.concurrent.TimeUnit
import org.microg.gms.nearby.exposurenotification.merge
class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var open: Preference
private lateinit var checks: Preference
private lateinit var report: Preference
private lateinit var apiUsage: Preference
private val packageName: String?
get() = arguments?.getString("package")
@ -27,7 +29,8 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
override fun onBindPreferences() {
open = preferenceScreen.findPreference("pref_exposure_app_open") ?: open
checks = preferenceScreen.findPreference("pref_exposure_app_checks") ?: checks
report = preferenceScreen.findPreference("pref_exposure_app_report") ?: report
apiUsage = preferenceScreen.findPreference("pref_exposure_app_api_usage") ?: apiUsage
open.onPreferenceClickListener = Preference.OnPreferenceClickListener {
try {
packageName?.let {
@ -50,27 +53,34 @@ class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
fun updateContent() {
packageName?.let { packageName ->
lifecycleScope.launchWhenResumed {
checks.summary = ExposureDatabase.with(requireContext()) { database ->
var str = getString(R.string.pref_exposure_app_checks_summary, database.countMethodCalls(packageName, "provideDiagnosisKeys"))
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() }
val token = database.lastMethodCallArgs(packageName, "provideDiagnosisKeys")?.let { JSONObject(it).getString("request_token") }
?: return@with Triple(null, null, apiUsageSummary)
val lastCheckTime = database.lastMethodCall(packageName, "provideDiagnosisKeys")
if (lastCheckTime != null && lastCheckTime != 0L) {
str += "\n" + getString(R.string.pref_exposure_app_last_check_summary, DateUtils.getRelativeDateTimeString(context, lastCheckTime, DateUtils.MINUTE_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, DateUtils.FORMAT_SHOW_TIME))
?: return@with Triple(null, null, apiUsageSummary)
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 {
database.findAllMeasuredExposures(config.first).merge().map {
val riskScore = it.getRiskScore(config.second)
"· " + 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>" }
}
val lastExposureSummaryTime = database.lastMethodCall(packageName, "getExposureSummary")
val lastExposureSummary = database.lastMethodCallArgs(packageName, "getExposureSummary")
if (lastExposureSummaryTime != null && lastExposureSummary != null && System.currentTimeMillis() - lastExposureSummaryTime <= TimeUnit.DAYS.toMillis(1)) {
try {
val json = JSONObject(lastExposureSummary)
val matchedKeys = json.optInt("response_matched_keys")
val daysSince = json.optInt("response_days_since", -1)
if (matchedKeys > 0 && daysSince >= 0) {
str += "\n" + resources.getQuantityString(R.plurals.pref_exposure_app_last_report_summary, matchedKeys, matchedKeys, daysSince)
}
} catch (ignored: Exception) {
}
}
str
Triple(reportTitle, "$diagnosisKeysLine<br>$encountersLine", apiUsageSummary)
}
report.isVisible = reportSummary != null
report.title = reportTitle
report.summary = HtmlCompat.fromHtml(reportSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim()
apiUsage.isVisible = apiUsageSummary != null
apiUsage.summary = HtmlCompat.fromHtml(apiUsageSummary.orEmpty(), HtmlCompat.FROM_HTML_MODE_COMPACT).trim()
}
}
}

View File

@ -6,9 +6,15 @@
package org.microg.gms.nearby.core.ui
import android.annotation.TargetApi
import android.content.DialogInterface
import android.os.Bundle
import android.text.format.DateFormat
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.db.williamchart.data.Scale
@ -23,6 +29,7 @@ import kotlin.math.roundToLong
class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
private lateinit var histogramCategory: PreferenceCategory
private lateinit var histogram: BarChartPreference
private lateinit var deleteAll: Preference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications_rpis)
@ -31,6 +38,22 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
override fun onBindPreferences() {
histogramCategory = preferenceScreen.findPreference("prefcat_exposure_rpi_histogram") ?: histogramCategory
histogram = preferenceScreen.findPreference("pref_exposure_rpi_histogram") ?: histogram
deleteAll = preferenceScreen.findPreference("pref_exposure_rpi_delete_all") ?: deleteAll
deleteAll.onPreferenceClickListener = Preference.OnPreferenceClickListener {
AlertDialog.Builder(requireContext())
.setTitle(R.string.pref_exposure_rpi_delete_all_title)
.setView(R.layout.exposure_notification_confirm_delete)
.setPositiveButton(R.string.pref_exposure_rpi_delete_all_warning_confirm_button) { _, _ ->
lifecycleScope.launchWhenStarted {
ExposureDatabase.with(requireContext()) { it.deleteAllCollectedAdvertisements() }
updateChart()
}
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.create()
.show()
true
}
}
override fun onResume() {
@ -66,6 +89,7 @@ class ExposureNotificationsRpisFragment : PreferenceFragmentCompat() {
val totalRpiCount = database.totalRpiCount
totalRpiCount to map
}
deleteAll.isEnabled = totalRpiCount != 0L
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)

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ SPDX-FileCopyrightText: 2020, microG Project Team
~ SPDX-License-Identifier: Apache-2.0
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:padding="?attr/dialogPreferredPadding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/pref_exposure_rpi_delete_all_warning" />
</FrameLayout>

View File

@ -11,10 +11,18 @@
<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_checks_summary"><xliff:g example="5">%1$d</xliff:g> Prüfungen während der letzen 14 Tage</string>
<string name="pref_exposure_app_last_check_summary">Letzte Prüfung: <xliff:g example="3 hours ago">%1$s</xliff:g></string>
<string name="pref_exposure_app_last_report_title">Letzter Bericht (<xliff:g example="vor 2 Stunden">%1$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>
<string name="pref_exposure_app_last_report_summary_encounters_line"><xliff:g example="Gestern, 12:00 - 12:30">%1$s</xliff:g>, Risiko-Level <xliff:g example="99">%2$d</xliff:g></string>
<string name="pref_exposure_app_last_report_summary_encounters_suffix">Hinweis: Der Risiko-Level wird durch die App bestimmt. Hohe Werte können ein niedriges Risiko bedeuten oder andersherum.</string>
<string name="pref_exposure_app_api_usage_title">Nutzung der API in den letzten 14 Tagen</string>
<string name="pref_exposure_app_api_usage_summary_line"><xliff:g example="12">%1$d</xliff:g> Aufrufe von <xliff:g example="provideDiagnosisKeys">%2$s</xliff:g></string>
<string name="prefcat_exposure_rpis_histogram_title"><xliff:g example="230">%1$d</xliff:g> gesammelte IDs</string>
<string name="pref_exposure_rpi_delete_all_title">Alle gesammelten IDs löschen</string>
<string name="pref_exposure_rpi_delete_all_warning">Nach dem Löschen der gesammelten IDs kannst du nicht mehr informiert werden, falls einer deiner Kontakte der letzten 14 Tage positiv getested wurde.</string>
<string name="pref_exposure_rpi_delete_all_warning_confirm_button">Trotzdem löschen</string>
<string name="pref_exposure_info_summary">"Die Exposure Notifications API ermöglicht es Apps, dich zu warnen, falls du Kontakt zu einer positiv getesteten Person hattest.
Das Datum, die Zeitdauer und die Signalstärke, die dem Kontakt zugeordnet sind, werden mit der zugehörigen App geteilt."</string>

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2017 microG Project Team
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<plurals name="pref_exposure_app_last_report_summary">
<item quantity="one">Last report: <xliff:g example="1">%1$d</xliff:g> match, <xliff:g example="3">%2$d</xliff:g> days ago</item>
<item quantity="other">Last report: <xliff:g example="2">%1$d</xliff:g> matches, latest <xliff:g example="3">%2$d</xliff:g> days ago</item>
</plurals>
</resources>

View File

@ -21,10 +21,18 @@
<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_checks_summary"><xliff:g example="5">%1$d</xliff:g> checks in past 14 days</string>
<string name="pref_exposure_app_last_check_summary">Last check: <xliff:g example="3 hours ago">%1$s</xliff:g></string>
<string name="pref_exposure_app_last_report_title">Last report (<xliff:g example="3 hours ago">%1$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>
<string name="pref_exposure_app_last_report_summary_encounters_line"><xliff:g example="Yesterday, 12:00 - 14:00">%1$s</xliff:g>, risk score <xliff:g example="99">%2$d</xliff:g></string>
<string name="pref_exposure_app_last_report_summary_encounters_suffix">Note: The risk score is defined by the app. High numbers can refer to low risk or vice-versa.</string>
<string name="pref_exposure_app_api_usage_title">API usage in the last 14 days</string>
<string name="pref_exposure_app_api_usage_summary_line"><xliff:g example="12">%1$d</xliff:g> calls to <xliff:g example="provideDiagnosisKeys">%2$s</xliff:g></string>
<string name="prefcat_exposure_rpis_histogram_title"><xliff:g example="230">%1$d</xliff:g> IDs collected</string>
<string name="pref_exposure_rpi_delete_all_title">Delete all collected IDs</string>
<string name="pref_exposure_rpi_delete_all_warning">Deleting collected IDs will make it impossible to notify you in case any of your contacts of the last 14 days is diagnosed.</string>
<string name="pref_exposure_rpi_delete_all_warning_confirm_button">Delete anyways</string>
<string name="pref_exposure_info_summary">"Exposure Notifications API allows apps to notify you if you were exposed to someone who reported to be diagnosed positive.
The date, duration, and signal strength associated with an exposure will be shared with the corresponding app."</string>

View File

@ -14,8 +14,16 @@
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:key="pref_exposure_app_checks"
android:key="pref_exposure_app_report"
android:selectable="false"
tools:summary="7 checks in past 14 days\nLast check: 3 hours ago\nLast report: 2 matches, latest 3 days ago" />
tools:summary="@string/pref_exposure_app_last_report_summary_encounters_no"
tools:title="@string/pref_exposure_app_last_report_title" />
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
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>
</PreferenceScreen>

View File

@ -15,7 +15,6 @@
</PreferenceCategory>
<PreferenceCategory android:layout="@layout/preference_category_no_label">
<Preference
android:enabled="false"
android:key="pref_exposure_rpi_delete_all"
android:title="@string/pref_exposure_rpi_delete_all_title" />
</PreferenceCategory>

View File

@ -19,6 +19,7 @@ import com.google.android.gms.nearby.exposurenotification.ExposureConfiguration
import com.google.android.gms.nearby.exposurenotification.TemporaryExposureKey
import kotlinx.coroutines.*
import okio.ByteString
import org.json.JSONObject
import java.io.File
import java.lang.Runnable
import java.nio.ByteBuffer
@ -162,9 +163,6 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
update(TABLE_TEK_CHECK_SINGLE, ContentValues().apply {
put("matched", 0)
}, null, null)
update(TABLE_TEK_CHECK_FILE, ContentValues().apply {
put("matched", 0)
}, null, null)
}
fun noteAppAction(packageName: String, method: String, args: String? = null, timestamp: Long = Date().time) = writableDatabase.run {
@ -658,6 +656,34 @@ class ExposureDatabase private constructor(private val context: Context) : SQLit
}
}
fun countDiagnosisKeysInvolved(tid: Long): Long = readableDatabase.run {
val fromFile = rawQuery("SELECT SUM($TABLE_TEK_CHECK_FILE.keys) AS keys FROM $TABLE_TEK_CHECK_FILE_TOKEN JOIN $TABLE_TEK_CHECK_FILE ON $TABLE_TEK_CHECK_FILE_TOKEN.tcfid = $TABLE_TEK_CHECK_FILE.tcfid WHERE $TABLE_TEK_CHECK_FILE_TOKEN.tid = $tid;", null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
0
}
}
val single = rawQuery("SELECT COUNT(*) as keys FROM $TABLE_TEK_CHECK_SINGLE_TOKEN WHERE $TABLE_TEK_CHECK_SINGLE_TOKEN.tid = $tid;", null).use { cursor ->
if (cursor.moveToNext()) {
cursor.getLong(0)
} else {
0
}
}
return fromFile + single
}
fun methodUsageHistogram(packageName: String): List<Pair<String, Int>> = readableDatabase.run {
val list = arrayListOf<Pair<String, Int>>()
rawQuery("SELECT method, COUNT(*) AS count FROM $TABLE_APP_LOG WHERE package = ? GROUP BY method;", arrayOf(packageName)).use { cursor ->
while (cursor.moveToNext()) {
list.add(cursor.getString(0) to cursor.getInt(1))
}
}
list.sortedByDescending { it.second }
}
private fun ensureTemporaryExposureKey(): TemporaryExposureKey = writableDatabase.let { database ->
database.beginTransactionNonExclusive()
try {