/* * SPDX-FileCopyrightText: 2020, microG Project Team * SPDX-License-Identifier: Apache-2.0 */ 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 import org.microg.gms.nearby.exposurenotification.merge class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() { private lateinit var open: 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") override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_exposure_notifications_app) } override fun onBindPreferences() { open = preferenceScreen.findPreference("pref_exposure_app_open") ?: open 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 { packageName?.let { context?.packageManager?.getLaunchIntentForPackage(it)?.let { intent -> intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context?.startActivity(intent) } } } catch (ignored: Exception) { } true } } override fun onResume() { super.onResume() updateContent() } 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 { 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 NTuple4(null, null, null, methodUsageHistogram) val lastCheckTime = database.lastMethodCall(packageName, "provideDiagnosisKeys") ?: return@with NTuple4(null, null, null, methodUsageHistogram) val config = database.loadConfiguration(packageName, token) ?: 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) } reportedExposures.removeAll() reportedExposures.addPreference(reportedExposuresNone) if (mergedExposures.isNullOrEmpty()) { reportedExposuresNone.isVisible = true } else { reportedExposuresNone.isVisible = false 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() } } } }