mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-06-26 06:25:24 -04:00
feat(permissions): runtime-permission + adapter-state recovery UX; remove Accompanist (#5851)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -68,8 +67,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
@@ -130,7 +127,9 @@ import org.meshtastic.core.ui.icon.Layers
|
||||
import org.meshtastic.core.ui.icon.Lens
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.PinDrop
|
||||
import org.meshtastic.core.ui.util.PermissionStatus
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.core.ui.util.rememberLocationPermissionState
|
||||
import org.meshtastic.core.ui.util.showToast
|
||||
import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
@@ -208,7 +207,6 @@ private fun cacheManagerCallback(onTaskComplete: () -> Unit, onTaskFailed: (Int)
|
||||
* @param mapViewModel The [MapViewModel] providing data and state for the map.
|
||||
* @param navigateToNodeDetails Callback to navigate to the details screen of a selected node.
|
||||
*/
|
||||
@OptIn(ExperimentalPermissionsApi::class) // Added for Accompanist
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@Composable
|
||||
fun MapView(
|
||||
@@ -246,9 +244,8 @@ fun MapView(
|
||||
val unknownText = stringResource(Res.string.unknown)
|
||||
val nowText = stringResource(Res.string.now)
|
||||
|
||||
// Accompanist permissions state for location
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
// Location permission state (native; recomputed on resume).
|
||||
val locationPermission = rememberLocationPermissionState()
|
||||
var triggerLocationToggleAfterPermission by remember { mutableStateOf(false) }
|
||||
|
||||
fun loadOnlineTileSourceBase(): ITileSource {
|
||||
@@ -309,8 +306,8 @@ fun MapView(
|
||||
}
|
||||
|
||||
// Effect to toggle MyLocation after permission is granted
|
||||
LaunchedEffect(locationPermissionsState.allPermissionsGranted) {
|
||||
if (locationPermissionsState.allPermissionsGranted && triggerLocationToggleAfterPermission) {
|
||||
LaunchedEffect(locationPermission.isGranted) {
|
||||
if (locationPermission.isGranted && triggerLocationToggleAfterPermission) {
|
||||
map.toggleMyLocation()
|
||||
triggerLocationToggleAfterPermission = false
|
||||
}
|
||||
@@ -637,11 +634,17 @@ fun MapView(
|
||||
},
|
||||
isLocationTrackingEnabled = myLocationOverlay != null,
|
||||
onToggleLocationTracking = {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
map.toggleMyLocation()
|
||||
} else {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
when {
|
||||
locationPermission.isGranted -> map.toggleMyLocation()
|
||||
|
||||
// Permanently denied: the system won't prompt again, so send the user to settings.
|
||||
locationPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
||||
locationPermission.openAppSettings()
|
||||
|
||||
else -> {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermission.request()
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
package org.meshtastic.app.map
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -57,8 +56,6 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.google.android.gms.location.LocationCallback
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationResult
|
||||
@@ -131,8 +128,10 @@ import org.meshtastic.core.ui.icon.Map
|
||||
import org.meshtastic.core.ui.icon.MeshtasticIcons
|
||||
import org.meshtastic.core.ui.icon.TripOrigin
|
||||
import org.meshtastic.core.ui.theme.TracerouteColors
|
||||
import org.meshtastic.core.ui.util.PermissionStatus
|
||||
import org.meshtastic.core.ui.util.formatAgo
|
||||
import org.meshtastic.core.ui.util.formatPositionTime
|
||||
import org.meshtastic.core.ui.util.rememberLocationPermissionState
|
||||
import org.meshtastic.feature.map.BaseMapViewModel.MapFilterState
|
||||
import org.meshtastic.feature.map.LastHeardFilter
|
||||
import org.meshtastic.feature.map.component.MapButton
|
||||
@@ -177,7 +176,7 @@ private const val TRACEROUTE_OFFSET_METERS = 100.0
|
||||
private const val TRACEROUTE_BOUNDS_PADDING_PX = 120
|
||||
|
||||
@Suppress("CyclomaticComplexMethod", "LongMethod")
|
||||
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class)
|
||||
@OptIn(MapsComposeExperimentalApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MapView(
|
||||
modifier: Modifier = Modifier,
|
||||
@@ -190,16 +189,15 @@ fun MapView(
|
||||
val mapLayers by mapViewModel.mapLayers.collectAsStateWithLifecycle()
|
||||
|
||||
// --- Location permissions ---
|
||||
val locationPermissionsState =
|
||||
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
|
||||
val locationPermission = rememberLocationPermissionState()
|
||||
var triggerLocationToggleAfterPermission by remember { mutableStateOf(false) }
|
||||
|
||||
// --- Location tracking ---
|
||||
var isLocationTrackingEnabled by remember { mutableStateOf(false) }
|
||||
var followPhoneBearing by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(locationPermissionsState.allPermissionsGranted) {
|
||||
if (locationPermissionsState.allPermissionsGranted && triggerLocationToggleAfterPermission) {
|
||||
LaunchedEffect(locationPermission.isGranted) {
|
||||
if (locationPermission.isGranted && triggerLocationToggleAfterPermission) {
|
||||
isLocationTrackingEnabled = true
|
||||
triggerLocationToggleAfterPermission = false
|
||||
}
|
||||
@@ -280,8 +278,8 @@ fun MapView(
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isLocationTrackingEnabled, locationPermissionsState.allPermissionsGranted) {
|
||||
if (isLocationTrackingEnabled && locationPermissionsState.allPermissionsGranted) {
|
||||
LaunchedEffect(isLocationTrackingEnabled, locationPermission.isGranted) {
|
||||
if (isLocationTrackingEnabled && locationPermission.isGranted) {
|
||||
val locationRequest =
|
||||
LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L)
|
||||
.setMinUpdateIntervalMillis(2000L)
|
||||
@@ -529,7 +527,7 @@ fun MapView(
|
||||
properties =
|
||||
MapProperties(
|
||||
mapType = effectiveGoogleMapType,
|
||||
isMyLocationEnabled = isLocationTrackingEnabled && locationPermissionsState.allPermissionsGranted,
|
||||
isMyLocationEnabled = isLocationTrackingEnabled && locationPermission.isGranted,
|
||||
),
|
||||
onMapLongClick = { latLng ->
|
||||
if (isMainMode && isConnected) {
|
||||
@@ -695,14 +693,22 @@ fun MapView(
|
||||
},
|
||||
isLocationTrackingEnabled = isLocationTrackingEnabled,
|
||||
onToggleLocationTracking = {
|
||||
if (locationPermissionsState.allPermissionsGranted) {
|
||||
isLocationTrackingEnabled = !isLocationTrackingEnabled
|
||||
if (!isLocationTrackingEnabled) {
|
||||
followPhoneBearing = false
|
||||
when {
|
||||
locationPermission.isGranted -> {
|
||||
isLocationTrackingEnabled = !isLocationTrackingEnabled
|
||||
if (!isLocationTrackingEnabled) {
|
||||
followPhoneBearing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Permanently denied: the system won't prompt again, so send the user to settings to recover.
|
||||
locationPermission.status == PermissionStatus.PERMANENTLY_DENIED ->
|
||||
locationPermission.openAppSettings()
|
||||
|
||||
else -> {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermission.request()
|
||||
}
|
||||
} else {
|
||||
triggerLocationToggleAfterPermission = true
|
||||
locationPermissionsState.launchMultiplePermissionRequest()
|
||||
}
|
||||
},
|
||||
bearing = cameraPositionState.position.bearing,
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
Android 17 (API 37) Local Network Protection: targetSdk=37 apps are blocked
|
||||
from local-network access by default. Required for both NSD/mDNS device
|
||||
discovery on the Connections screen and the built-in TAK Server's localhost
|
||||
loopback binding. Requested at runtime via rememberRequestLocalNetworkPermission.
|
||||
loopback binding. Requested at runtime via rememberLocalNetworkPermissionState.
|
||||
See: https://developer.android.com/privacy-and-security/local-network-permission
|
||||
-->
|
||||
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
|
||||
|
||||
Reference in New Issue
Block a user