/* * SPDX-FileCopyrightText: 2020, microG Project Team * SPDX-License-Identifier: Apache-2.0 */ package org.microg.gms.nearby.core.ui import android.bluetooth.BluetoothAdapter import android.content.Context.LOCATION_SERVICE import android.content.Intent import android.content.pm.PackageManager import android.location.LocationManager import android.os.Build import android.os.Bundle import android.os.Handler import android.provider.Settings import androidx.core.content.ContextCompat import androidx.core.location.LocationManagerCompat 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 org.microg.gms.nearby.exposurenotification.* import org.microg.gms.ui.AppIconPreference import org.microg.gms.ui.getApplicationInfoIfExists import org.microg.gms.ui.navigate class ExposureNotificationsPreferencesFragment : PreferenceFragmentCompat() { private lateinit var exposureEnableInfo: Preference private lateinit var exposureBluetoothOff: Preference private lateinit var exposureLocationOff: Preference private lateinit var exposureNearbyNotGranted: Preference private lateinit var exposureBluetoothUnsupported: Preference private lateinit var exposureBluetoothNoAdvertisement: Preference private lateinit var exposureApps: PreferenceCategory private lateinit var exposureAppsNone: Preference private lateinit var collectedRpis: Preference private lateinit var advertisingId: Preference private var turningBluetoothOn: Boolean = false private val handler = Handler() private val updateStatusRunnable = Runnable { updateStatus() } private val updateContentRunnable = Runnable { updateContent() } private var permissionRequestCode = 33 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences_exposure_notifications) } override fun onBindPreferences() { exposureEnableInfo = preferenceScreen.findPreference("pref_exposure_enable_info") ?: exposureEnableInfo exposureBluetoothOff = preferenceScreen.findPreference("pref_exposure_error_bluetooth_off") ?: exposureBluetoothOff exposureLocationOff = preferenceScreen.findPreference("pref_exposure_error_location_off") ?: exposureLocationOff exposureNearbyNotGranted = preferenceScreen.findPreference("pref_exposure_error_nearby_not_granted") ?: exposureNearbyNotGranted exposureBluetoothUnsupported = preferenceScreen.findPreference("pref_exposure_error_bluetooth_unsupported") ?: exposureBluetoothUnsupported exposureBluetoothNoAdvertisement = preferenceScreen.findPreference("pref_exposure_error_bluetooth_no_advertise") ?: exposureBluetoothNoAdvertisement 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 exposureLocationOff.onPreferenceClickListener = Preference.OnPreferenceClickListener { val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) startActivity(intent) true } exposureBluetoothOff.onPreferenceClickListener = Preference.OnPreferenceClickListener { lifecycleScope.launchWhenStarted { turningBluetoothOn = true it.isVisible = false val adapter = BluetoothAdapter.getDefaultAdapter() if (adapter != null && !adapter.enableAsync(requireContext())) { turningBluetoothOn = false val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) startActivityForResult(intent, 144) } else { turningBluetoothOn = false updateStatus() } } true } exposureNearbyNotGranted.onPreferenceClickListener = Preference.OnPreferenceClickListener { val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") requestPermissions(nearbyPermissions, ++permissionRequestCode) true } collectedRpis.onPreferenceClickListener = Preference.OnPreferenceClickListener { findNavController().navigate(requireContext(), R.id.openExposureRpis) true } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == this.permissionRequestCode) { updateStatus() // Tell the NotifyService that it should update the notification val intent = Intent(NOTIFICATION_UPDATE_ACTION) requireContext().sendBroadcast(intent) } } override fun onResume() { super.onResume() updateStatus() updateContent() } override fun onPause() { super.onPause() handler.removeCallbacks(updateStatusRunnable) handler.removeCallbacks(updateContentRunnable) } private fun updateStatus() { val appContext = requireContext().applicationContext lifecycleScope.launchWhenResumed { handler.postDelayed(updateStatusRunnable, UPDATE_STATUS_INTERVAL) val enabled = getExposureNotificationsServiceInfo(appContext).configuration.enabled exposureEnableInfo.isVisible = !enabled val bluetoothSupported = ScannerService.isSupported(appContext) val advertisingSupported = if (bluetoothSupported == true) AdvertiserService.isSupported(appContext) else bluetoothSupported val nearbyPermissions = arrayOf("android.permission.BLUETOOTH_ADVERTISE", "android.permission.BLUETOOTH_SCAN") val nearbyPermissionsGranted = Build.VERSION.SDK_INT >= 31 || nearbyPermissions.all { ContextCompat.checkSelfPermission(appContext, it) == PackageManager.PERMISSION_GRANTED } exposureNearbyNotGranted.isVisible = enabled && !nearbyPermissionsGranted exposureLocationOff.isVisible = enabled && bluetoothSupported != false && !LocationManagerCompat.isLocationEnabled(appContext.getSystemService(LOCATION_SERVICE) as LocationManager) exposureBluetoothOff.isVisible = enabled && bluetoothSupported == null && !turningBluetoothOn exposureBluetoothUnsupported.isVisible = enabled && bluetoothSupported == false exposureBluetoothNoAdvertisement.isVisible = enabled && bluetoothSupported == true && advertisingSupported != true advertisingId.isVisible = enabled && advertisingSupported == true } } private fun updateContent() { val context = requireContext() lifecycleScope.launchWhenResumed { handler.postDelayed(updateContentRunnable, UPDATE_CONTENT_INTERVAL) val (apps, lastHourKeys, currentId) = ExposureDatabase.with(context) { database -> 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(requireContext(), 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 Triple(apps, lastHourKeys, currentId) } collectedRpis.summary = getString(R.string.pref_exposure_collected_rpis_summary, lastHourKeys) if (currentId != null) { advertisingId.isVisible = true advertisingId.summary = currentId.toString() } else { advertisingId.isVisible = false } exposureApps.removeAll() if (apps.isEmpty()) { exposureApps.addPreference(exposureAppsNone) } else { for (app in apps) { exposureApps.addPreference(app) } } } } companion object { private const val UPDATE_STATUS_INTERVAL = 1000L private const val UPDATE_CONTENT_INTERVAL = 60000L } }