Verified Commit 5f70d943 authored by Marvin W.'s avatar Marvin W. 🐿
Browse files

Add initial Exposure Notification API implementation

parent af28a78b
......@@ -14,6 +14,7 @@ buildscript {
ext.annotationVersion = '1.1.0'
ext.appcompatVersion = '1.1.0'
ext.coreVersion = '1.3.0'
ext.fragmentVersion = '1.2.5'
ext.lifecycleVersion = '2.2.0'
ext.mediarouterVersion = '1.1.0'
......
......@@ -19,3 +19,7 @@ wire {
compileKotlin {
kotlinOptions.jvmTarget = 1.8
}
compileTestKotlin {
kotlinOptions.jvmTarget = 1.8
}
......@@ -26,6 +26,7 @@ configurations {
dependencies {
implementation "com.squareup.wire:wire-runtime:$wireVersion"
implementation "de.hdodenhof:circleimageview:1.3.0"
implementation "com.diogobernardino:williamchart:3.7.1"
implementation "org.conscrypt:conscrypt-android:2.1.0"
// TODO: Switch to upstream once raw requests are merged
// https://github.com/vitalidze/chromecast-java-api-v2/pull/99
......@@ -40,6 +41,7 @@ dependencies {
implementation project(':firebase-dynamic-links-api')
implementation project(':play-services-base-core')
implementation project(':play-services-location-core')
implementation project(':play-services-nearby-core')
implementation project(':play-services-core-proto')
implementation project(':play-services-core:microg-ui-tools') // deprecated
implementation project(':play-services-api')
......
......@@ -107,6 +107,8 @@
android:name="android.permission.UPDATE_APP_OPS_STATS"
tools:ignore="ProtectedPermissions" />
<uses-sdk tools:overrideLibrary="com.db.williamchart" />
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
......@@ -419,6 +421,15 @@
<!-- microG custom UI -->
<activity
android:name="org.microg.gms.ui.ExposureNotificationsConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.DayNight.Dialog.Alert.NoActionBar">
<intent-filter>
<action android:name="org.microg.gms.nearby.exposurenotification.CONFIRM" />
</intent-filter>
</activity>
<!-- microG Settings shown in Launcher -->
<activity
android:name="org.microg.gms.ui.SettingsActivity"
......@@ -427,11 +438,11 @@
android:roundIcon="@mipmap/ic_microg_settings">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<action android:name="com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
......
package org.microg.gms.ui;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
......@@ -11,6 +13,8 @@ import androidx.navigation.ui.NavigationUI;
import com.google.android.gms.R;
import org.microg.gms.nearby.exposurenotification.Constants;
public class SettingsActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
......@@ -21,6 +25,12 @@ public class SettingsActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (Constants.ACTION_EXPOSURE_NOTIFICATION_SETTINGS.equals(intent.getAction()) && intent.getData() == null) {
intent.setData(Uri.parse("x-gms-settings://exposure-notifications"));
}
setContentView(R.layout.settings_root_activity);
appBarConfiguration = new AppBarConfiguration.Builder(getNavController().getGraph()).build();
......
package org.microg.gms.ui;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
......@@ -10,6 +11,7 @@ import com.google.android.gms.R;
import org.microg.gms.checkin.CheckinPrefs;
import org.microg.gms.gcm.GcmDatabase;
import org.microg.gms.gcm.GcmPrefs;
import org.microg.gms.nearby.exposurenotification.ExposurePreferences;
import org.microg.gms.snet.SafetyNetPrefs;
import org.microg.tools.ui.ResourceSettingsFragment;
......@@ -20,6 +22,7 @@ public class SettingsFragment extends ResourceSettingsFragment {
public static final String PREF_SNET = "pref_snet";
public static final String PREF_UNIFIEDNLP = "pref_unifiednlp";
public static final String PREF_CHECKIN = "pref_checkin";
public static final String PREF_EXPOSURE = "pref_exposure";
public SettingsFragment() {
preferencesResource = R.xml.preferences_start;
......@@ -86,6 +89,20 @@ public class SettingsFragment extends ResourceSettingsFragment {
NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.openUnifiedNlpSettings);
return true;
});
if (Build.VERSION.SDK_INT >= 21) {
findPreference(PREF_EXPOSURE).setVisible(true);
if (new ExposurePreferences(getContext()).getScannerEnabled()) {
findPreference(PREF_EXPOSURE).setSummary(getString(R.string.service_status_enabled_short));
} else {
findPreference(PREF_EXPOSURE).setSummary(R.string.service_status_disabled_short);
}
findPreference(PREF_EXPOSURE).setOnPreferenceClickListener(preference -> {
NavHostFragment.findNavController(SettingsFragment.this).navigate(R.id.openExposureNotificationSettings);
return true;
});
} else {
findPreference(PREF_EXPOSURE).setVisible(false);
}
boolean checkinEnabled = CheckinPrefs.get(getContext()).isEnabled();
findPreference(PREF_CHECKIN).setSummary(checkinEnabled ? R.string.service_status_enabled_short : R.string.service_status_disabled_short);
......
......@@ -6,15 +6,21 @@
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.widget.ImageView
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
class AppIconPreference(context: Context) : Preference(context) {
override fun onBindViewHolder(holder: PreferenceViewHolder?) {
class AppIconPreference : Preference {
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context) : super(context)
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
val icon = holder?.findViewById(android.R.id.icon)
val icon = holder.findViewById(android.R.id.icon)
if (icon is ImageView) {
icon.adjustViewBounds = true
icon.scaleType = ImageView.ScaleType.CENTER_INSIDE
......
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder
import com.db.williamchart.data.Scale
import com.db.williamchart.view.BarChartView
import com.google.android.gms.R
class BarChartPreference : Preference {
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context?) : super(context)
init {
layoutResource = R.layout.preference_bar_chart
}
private lateinit var chart: BarChartView
var labelsFormatter: (Float) -> String = { it.toString() }
set(value) {
field = value
if (this::chart.isInitialized) {
chart.labelsFormatter = value
}
}
var scale: Scale? = null
set(value) {
field = value
if (value != null && this::chart.isInitialized) {
chart.scale = value
}
}
var data: LinkedHashMap<String, Float> = linkedMapOf()
set(value) {
field = value
if (this::chart.isInitialized) {
chart.animate(data)
}
}
override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
chart = holder.itemView as? BarChartView ?: holder.findViewById(R.id.bar_chart) as BarChartView
chart.labelsFormatter = labelsFormatter
scale?.let { chart.scale = it }
chart.animate(data)
}
}
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.R
import com.google.android.gms.databinding.ExposureNotificationsAppFragmentBinding
import com.google.android.gms.databinding.ExposureNotificationsFragmentBinding
import org.microg.gms.nearby.exposurenotification.ExposurePreferences
class ExposureNotificationsAppFragment : Fragment(R.layout.exposure_notifications_app_fragment) {
private lateinit var binding: ExposureNotificationsAppFragmentBinding
val packageName: String?
get() = arguments?.getString("package")
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = ExposureNotificationsAppFragmentBinding.inflate(inflater, container, false)
binding.callbacks = object : ExposureNotificationsAppFragmentCallbacks {
override fun onAppClicked() {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri: Uri = Uri.fromParts("package", packageName, null)
intent.data = uri
context!!.startActivity(intent)
}
}
childFragmentManager.findFragmentById(R.id.sub_preferences)?.arguments = arguments
return binding.root
}
override fun onResume() {
super.onResume()
lifecycleScope.launchWhenResumed {
val pm = requireContext().packageManager
val applicationInfo = pm.getApplicationInfoIfExists(packageName)
binding.appName = applicationInfo?.loadLabel(pm)?.toString() ?: packageName
binding.appIcon = applicationInfo?.loadIcon(pm)
?: AppCompatResources.getDrawable(requireContext(), android.R.mipmap.sym_def_app_icon)
}
}
}
interface ExposureNotificationsAppFragmentCallbacks {
fun onAppClicked()
}
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.content.Intent
import android.os.Bundle
import android.text.format.DateUtils
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.gms.R
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
class ExposureNotificationsAppPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var open: Preference
private lateinit var checks: 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
checks = preferenceScreen.findPreference("pref_exposure_app_checks") ?: checks
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()
}
fun updateContent() {
packageName?.let { packageName ->
val database = ExposureDatabase(requireContext())
var str = getString(R.string.pref_exposure_app_checks_summary, database.countMethodCalls(packageName, "provideDiagnosisKeys"))
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))
}
checks.summary = str
database.close()
}
}
}
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.os.Bundle
import android.os.ResultReceiver
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.R
import com.google.android.gms.nearby.exposurenotification.ExposureNotificationStatusCodes.*
import org.microg.gms.nearby.exposurenotification.*
class ExposureNotificationsConfirmActivity : AppCompatActivity() {
private var resultCode: Int = FAILED
private val resultData: Bundle = Bundle()
private val receiver: ResultReceiver?
get() = intent.getParcelableExtra(KEY_CONFIRM_RECEIVER)
private val action: String?
get() = intent.getStringExtra(KEY_CONFIRM_ACTION)
private val targetPackageName: String?
get() = intent.getStringExtra(KEY_CONFIRM_PACKAGE)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.exposure_notifications_confirm_activity)
val applicationInfo = packageManager.getApplicationInfoIfExists(targetPackageName)
when (action) {
CONFIRM_ACTION_START -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_start_title)
findViewById<TextView>(android.R.id.summary).text = getString(R.string.exposure_confirm_start_summary, applicationInfo?.loadLabel(packageManager)
?: targetPackageName)
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_start_button)
}
CONFIRM_ACTION_STOP -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_stop_title)
findViewById<TextView>(android.R.id.summary).text = getString(R.string.exposure_confirm_stop_summary)
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_stop_button)
}
CONFIRM_ACTION_KEYS -> {
findViewById<TextView>(android.R.id.title).text = getString(R.string.exposure_confirm_keys_title, applicationInfo?.loadLabel(packageManager)
?: targetPackageName)
findViewById<TextView>(android.R.id.summary).text = getString(R.string.exposure_confirm_keys_summary)
findViewById<Button>(android.R.id.button1).text = getString(R.string.exposure_confirm_keys_button)
}
else -> {
resultCode = INTERNAL_ERROR
finish()
}
}
findViewById<Button>(android.R.id.button1).setOnClickListener {
resultCode = SUCCESS
finish()
}
findViewById<Button>(android.R.id.button2).setOnClickListener {
resultCode = FAILED_REJECTED_OPT_IN
finish()
}
}
override fun onStop() {
super.onStop()
receiver?.send(resultCode, resultData)
}
}
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.R
import com.google.android.gms.databinding.ExposureNotificationsFragmentBinding
import org.microg.gms.nearby.exposurenotification.ExposurePreferences
class ExposureNotificationsFragment : Fragment(R.layout.exposure_notifications_fragment) {
private lateinit var binding: ExposureNotificationsFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = ExposureNotificationsFragmentBinding.inflate(inflater, container, false)
binding.switchBarCallback = object : PreferenceSwitchBarCallback {
override fun onChecked(newStatus: Boolean) {
ExposurePreferences(requireContext()).scannerEnabled = newStatus
binding.scannerEnabled = newStatus
}
}
return binding.root
}
override fun onResume() {
super.onResume()
lifecycleScope.launchWhenResumed {
binding.scannerEnabled = ExposurePreferences(requireContext()).scannerEnabled
}
}
}
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package org.microg.gms.ui
import android.os.Bundle
import android.os.Handler
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.google.android.gms.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.microg.gms.nearby.exposurenotification.ExposureDatabase
import org.microg.gms.nearby.exposurenotification.ExposurePreferences
class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() {
private lateinit var exposureEnableInfo: Preference
private lateinit var exposureApps: PreferenceCategory
private lateinit var exposureAppsNone: Preference
private lateinit var collectedRpis: Preference
private lateinit var advertisingId: Preference
private lateinit var database: ExposureDatabase
private val handler = Handler()
private val updateStatusRunnable = Runnable { updateStatus() }
private val updateContentRunnable = Runnable { updateContent() }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
database = ExposureDatabase(requireContext())
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_exposure_notifications)
}
override fun onBindPreferences() {
exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo
exposureApps = preferenceScreen.findPreference("prefcat_exposure_apps") ?: exposureApps
exposureAppsNone = preferenceScreen.findPreference("pref_exposure_apps_none") ?: exposureAppsNone
collectedRpis = preferenceScreen.findPreference("pref_exposure_collected_rpis") ?: collectedRpis
advertisingId = preferenceScreen.findPreference("pref_exposure_advertising_id") ?: advertisingId
collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(R.id.openExposureRpis)
true
}
}
override fun onResume() {
super.onResume()
updateStatus()
updateContent()
}
override fun onPause() {
super.onPause()
database.close()
handler.removeCallbacks(updateStatusRunnable)
handler.removeCallbacks(updateContentRunnable)
}
private fun updateStatus() {
lifecycleScope.launchWhenResumed {
handler.postDelayed(updateStatusRunnable, UPDATE_STATUS_INTERVAL)
val preferences = ExposurePreferences(requireContext())
exposureEnableInfo.isVisible = !preferences.scannerEnabled
advertisingId.isVisible = preferences.advertiserEnabled
}
}
private fun updateContent() {
lifecycleScope.launchWhenResumed {
handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL)
val context = requireContext()
val (apps, lastHourKeys, currentId) = withContext(Dispatchers.IO) {
val apps = database.appList.map { packageName ->
context.packageManager.getApplicationInfoIfExists(packageName)
}.filterNotNull().mapIndexed { idx, applicationInfo ->
val pref = AppIconPreference(context)
pref.order = idx
pref.title = applicationInfo.loadLabel(context.packageManager)
pref.icon = applicationInfo.loadIcon(context.packageManager)
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
findNavController().navigate(R.id.openExposureAppDetails, bundleOf(
"package" to applicationInfo.packageName
))
true
}
pref.key = "pref_exposure_app_" + applicationInfo.packageName
pref
}
val lastHourKeys = database.hourRpiCount
val currentId = database.currentRpiId
database.close()
Triple(apps, lastHourKeys, currentId)
}
collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys)
advertisingId.summary = currentId.toString()
exposureApps.removeAll()
if (apps.isEmpty()) {
exposureApps.addPreference(exposureAppsNone)
} else {