refactor: remove BACKGROUND_LOCATION permission

This commit is contained in:
andrekir 2024-08-26 19:31:41 -03:00
parent 7b44fea81c
commit 3cdbb86fa2
6 changed files with 19 additions and 56 deletions

View file

@ -29,7 +29,6 @@
<!-- Permissions required for providing location (from phone GPS) to mesh --> <!-- Permissions required for providing location (from phone GPS) to mesh -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- This permission is required for analytics - and soon the MQTT gateway --> <!-- This permission is required for analytics - and soon the MQTT gateway -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@ -48,6 +47,7 @@
<!-- We run our mesh code as a foreground service - FIXME, find a way to stop doing this --> <!-- We run our mesh code as a foreground service - FIXME, find a way to stop doing this -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- Needed to open our bluetooth connection to our paired device (after reboot) --> <!-- Needed to open our bluetooth connection to our paired device (after reboot) -->
@ -104,7 +104,7 @@
<service <service
android:name="com.geeksville.mesh.service.MeshService" android:name="com.geeksville.mesh.service.MeshService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="connectedDevice" android:foregroundServiceType="connectedDevice|location"
android:exported="true" tools:ignore="ExportedActivity"> android:exported="true" tools:ignore="ExportedActivity">
<intent-filter> <intent-filter>
<action android:name="com.geeksville.mesh.Service" /> <action android:name="com.geeksville.mesh.Service" />

View file

@ -145,21 +145,6 @@ fun Context.getLocationPermissions(): Array<String> {
/** @return true if the user already has location permission */ /** @return true if the user already has location permission */
fun Context.hasLocationPermission() = getLocationPermissions().isEmpty() fun Context.hasLocationPermission() = getLocationPermissions().isEmpty()
/**
* A list of missing background location permissions (or empty if we already have what we need)
*/
fun Context.getBackgroundPermissions(): Array<String> {
val perms = mutableListOf(Manifest.permission.ACCESS_FINE_LOCATION)
if (android.os.Build.VERSION.SDK_INT >= 29) // only added later
perms.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
return getMissingPermissions(perms)
}
/** @return true if the user already has background location permission */
fun Context.hasBackgroundPermission() = getBackgroundPermissions().isEmpty()
/** /**
* Notification permission (or empty if we already have what we need) * Notification permission (or empty if we already have what we need)
*/ */

View file

@ -1,8 +1,10 @@
package com.geeksville.mesh.repository.location package com.geeksville.mesh.repository.location
import android.annotation.SuppressLint import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.app.Application import android.app.Application
import android.location.LocationManager import android.location.LocationManager
import androidx.annotation.RequiresPermission
import androidx.core.location.LocationCompat import androidx.core.location.LocationCompat
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat import androidx.core.location.LocationManagerCompat
@ -10,7 +12,6 @@ import androidx.core.location.LocationRequestCompat
import androidx.core.location.altitude.AltitudeConverterCompat import androidx.core.location.altitude.AltitudeConverterCompat
import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hasBackgroundPermission
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
@ -32,9 +33,8 @@ class LocationRepository @Inject constructor(
private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false) private val _receivingLocationUpdates: MutableStateFlow<Boolean> = MutableStateFlow(false)
val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates val receivingLocationUpdates: StateFlow<Boolean> get() = _receivingLocationUpdates
@SuppressLint("MissingPermission") @RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
private fun LocationManager.requestLocationUpdates() = callbackFlow { private fun LocationManager.requestLocationUpdates() = callbackFlow {
if (!context.hasBackgroundPermission()) close()
val intervalMs = 30 * 1000L // 30 seconds val intervalMs = 30 * 1000L // 30 seconds
val minDistanceM = 0f val minDistanceM = 0f
@ -96,5 +96,6 @@ class LocationRepository @Inject constructor(
/** /**
* Observable flow for location updates * Observable flow for location updates
*/ */
@RequiresPermission(anyOf = [ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION])
fun getLocations() = locationManager.get().requestLocationUpdates() fun getLocations() = locationManager.get().requestLocationUpdates()
} }

View file

@ -1,5 +1,6 @@
package com.geeksville.mesh.service package com.geeksville.mesh.service
import android.annotation.SuppressLint
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -17,7 +18,7 @@ import com.geeksville.mesh.LocalOnlyProtos.LocalConfig
import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig import com.geeksville.mesh.LocalOnlyProtos.LocalModuleConfig
import com.geeksville.mesh.MeshProtos.MeshPacket import com.geeksville.mesh.MeshProtos.MeshPacket
import com.geeksville.mesh.MeshProtos.ToRadio import com.geeksville.mesh.MeshProtos.ToRadio
import com.geeksville.mesh.android.hasBackgroundPermission import com.geeksville.mesh.android.hasLocationPermission
import com.geeksville.mesh.database.MeshLogRepository import com.geeksville.mesh.database.MeshLogRepository
import com.geeksville.mesh.database.PacketRepository import com.geeksville.mesh.database.PacketRepository
import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.database.entity.MeshLog
@ -175,7 +176,8 @@ class MeshService : Service(), Logging {
// If we're already observing updates, don't register again // If we're already observing updates, don't register again
if (locationFlow?.isActive == true) return if (locationFlow?.isActive == true) return
if (hasBackgroundPermission()) { @SuppressLint("MissingPermission")
if (hasLocationPermission()) {
locationFlow = locationRepository.getLocations().onEach { location -> locationFlow = locationRepository.getLocations().onEach { location ->
sendPosition( sendPosition(
position { position {
@ -299,7 +301,7 @@ class MeshService : Service(), Logging {
serviceNotifications.notifyId, serviceNotifications.notifyId,
notification, notification,
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
} else { } else {
0 0
}, },

View file

@ -138,24 +138,16 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private fun initCommonUI() { private fun initCommonUI() {
val requestBackgroundAndCheckLauncher = val requestLocationPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.entries.any { !it.value }) {
debug("User denied background permission")
model.showSnackbar(getString(R.string.why_background_required))
}
}
val requestLocationAndBackgroundLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.entries.all { it.value }) { if (permissions.entries.all { it.value }) {
// Older versions of android only need Location permission model.provideLocation.value = true
if (!requireContext().hasBackgroundPermission()) model.meshService?.startProvideLocation()
requestBackgroundAndCheckLauncher.launch(requireContext().getBackgroundPermissions())
} else { } else {
debug("User denied location permission") debug("User denied location permission")
model.showSnackbar(getString(R.string.why_background_required)) model.showSnackbar(getString(R.string.why_background_required))
} }
bluetoothViewModel.permissionsUpdated()
} }
// init our region spinner // init our region spinner
@ -240,7 +232,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked -> binding.provideLocationCheckbox.setOnCheckedChangeListener { view, isChecked ->
// Don't check the box until the system setting changes // Don't check the box until the system setting changes
view.isChecked = isChecked && requireContext().hasBackgroundPermission() view.isChecked = isChecked && requireContext().hasLocationPermission()
if (view.isPressed) { // We want to ignore changes caused by code (as opposed to the user) if (view.isPressed) { // We want to ignore changes caused by code (as opposed to the user)
debug("User changed location tracking to $isChecked") debug("User changed location tracking to $isChecked")
@ -255,9 +247,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
.setPositiveButton(getString(R.string.accept)) { _, _ -> .setPositiveButton(getString(R.string.accept)) { _, _ ->
// Make sure we have location permission (prerequisite) // Make sure we have location permission (prerequisite)
if (!requireContext().hasLocationPermission()) { if (!requireContext().hasLocationPermission()) {
requestLocationAndBackgroundLauncher.launch(requireContext().getLocationPermissions()) requestLocationPermissionLauncher.launch(requireContext().getLocationPermissions())
} else {
requestBackgroundAndCheckLauncher.launch(requireContext().getBackgroundPermissions())
} }
} }
.show() .show()

View file

@ -182,21 +182,6 @@ fun MapView(
if (permissions.entries.all { it.value }) map.toggleMyLocation() if (permissions.entries.all { it.value }) map.toggleMyLocation()
} }
fun requestPermissionAndToggle() {
// Google rejects releases claiming this requires BACKGROUND_LOCATION prominent
// disclosure. Adding to comply even though it does not use background location.
MaterialAlertDialogBuilder(context)
.setTitle(R.string.background_required)
.setMessage(R.string.why_background_required)
.setNeutralButton(R.string.cancel) { _, _ ->
debug("User denied location permission")
}
.setPositiveButton(R.string.accept) { _, _ ->
requestPermissionAndToggleLauncher.launch(context.getLocationPermissions())
}
.show()
}
val nodes by model.nodeList.collectAsStateWithLifecycle() val nodes by model.nodeList.collectAsStateWithLifecycle()
val waypoints by model.waypoints.collectAsStateWithLifecycle(emptyMap()) val waypoints by model.waypoints.collectAsStateWithLifecycle(emptyMap())
@ -667,7 +652,7 @@ fun MapView(
IconButton( IconButton(
onClick = { onClick = {
if (context.hasLocationPermission()) map.toggleMyLocation() if (context.hasLocationPermission()) map.toggleMyLocation()
else requestPermissionAndToggle() else requestPermissionAndToggleLauncher.launch(context.getLocationPermissions())
}, },
enabled = hasGps, enabled = hasGps,
drawableRes = if (myLocationOverlay == null) R.drawable.ic_twotone_my_location_24 drawableRes = if (myLocationOverlay == null) R.drawable.ic_twotone_my_location_24