From 4574a8b09b7f86ef0f03840a16f0b34255bbc83f Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 4 Sep 2025 06:05:50 -0500 Subject: [PATCH] Add active tracking functionality to gmaps (#2953) --- .../com/geeksville/mesh/ui/map/MapView.kt | 87 ++++++++++++++++++- .../ui/map/components/MapControlsOverlay.kt | 20 +++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt index bd426fd5e..763d3357a 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/MapView.kt @@ -37,6 +37,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.TripOrigin import androidx.compose.material3.Card import androidx.compose.material3.ExperimentalMaterial3Api @@ -50,6 +51,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberFloatingToolbarState import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key @@ -94,6 +96,11 @@ import com.geeksville.mesh.util.mpsToKmph import com.geeksville.mesh.util.mpsToMph import com.geeksville.mesh.util.toString import com.geeksville.mesh.waypoint +import com.google.android.gms.location.LocationCallback +import com.google.android.gms.location.LocationRequest +import com.google.android.gms.location.LocationResult +import com.google.android.gms.location.LocationServices +import com.google.android.gms.location.Priority import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.gms.maps.model.BitmapDescriptor import com.google.android.gms.maps.model.BitmapDescriptorFactory @@ -188,6 +195,9 @@ fun MapView( var hasLocationPermission by remember { mutableStateOf(false) } val displayUnits by mapViewModel.displayUnits.collectAsStateWithLifecycle() + // Location tracking state + var isLocationTrackingEnabled by remember { mutableStateOf(false) } + LocationPermissionsHandler { isGranted -> hasLocationPermission = isGranted } val filePickerLauncher = @@ -220,6 +230,53 @@ fun MapView( } ?: CameraPosition.fromLatLngZoom(defaultLatLng, 7f) } + // Location tracking functionality + val fusedLocationClient = remember { LocationServices.getFusedLocationProviderClient(context) } + val locationCallback = remember { + object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + if (isLocationTrackingEnabled) { + locationResult.lastLocation?.let { location -> + val latLng = LatLng(location.latitude, location.longitude) + coroutineScope.launch { + try { + cameraPositionState.animate( + CameraUpdateFactory.newLatLngZoom(latLng, cameraPositionState.position.zoom), + ) + } catch (e: IllegalStateException) { + debug("Error animating camera to location: ${e.message}") + } + } + } + } + } + } + } + + // Start/stop location tracking based on state + LaunchedEffect(isLocationTrackingEnabled, hasLocationPermission) { + if (isLocationTrackingEnabled && hasLocationPermission) { + val locationRequest = + LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L) + .setMinUpdateIntervalMillis(2000L) + .build() + + try { + fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null) + debug("Started location tracking") + } catch (e: SecurityException) { + debug("Location permission not available: ${e.message}") + isLocationTrackingEnabled = false + } + } else { + fusedLocationClient.removeLocationUpdates(locationCallback) + debug("Stopped location tracking") + } + } + + // Clean up location tracking on disposal + DisposableEffect(Unit) { onDispose { fusedLocationClient.removeLocationUpdates(locationCallback) } } + val floatingToolbarState = rememberFloatingToolbarState() val exitAlwaysScrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = End, state = floatingToolbarState) @@ -324,7 +381,7 @@ fun MapView( zoomControlsEnabled = true, mapToolbarEnabled = true, compassEnabled = true, - myLocationButtonEnabled = hasLocationPermission, + myLocationButtonEnabled = false, // Disabled - we use custom location button rotationGesturesEnabled = true, scrollGesturesEnabled = true, tiltGesturesEnabled = true, @@ -557,6 +614,34 @@ fun MapView( }, showFilterButton = focusedNodeNum == null, scrollBehavior = exitAlwaysScrollBehavior, + hasLocationPermission = hasLocationPermission, + isLocationTrackingEnabled = isLocationTrackingEnabled, + onToggleLocationTracking = { + if (hasLocationPermission) { + if (!isLocationTrackingEnabled) { + // When enabling tracking, get current location and center on it + try { + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + location?.let { + val latLng = LatLng(it.latitude, it.longitude) + coroutineScope.launch { + try { + cameraPositionState.animate( + CameraUpdateFactory.newLatLngZoom(latLng, 16f), + ) + } catch (e: IllegalStateException) { + debug("Error centering camera on location: ${e.message}") + } + } + } + } + } catch (e: SecurityException) { + debug("Location permission not available: ${e.message}") + } + } + isLocationTrackingEnabled = !isLocationTrackingEnabled + } + }, ) } if (showLayersBottomSheet) { diff --git a/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt b/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt index 1aa939342..ec811d753 100644 --- a/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt +++ b/app/src/google/java/com/geeksville/mesh/ui/map/components/MapControlsOverlay.kt @@ -19,8 +19,10 @@ package com.geeksville.mesh.ui.map.components import androidx.compose.foundation.layout.Box import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.LocationDisabled import androidx.compose.material.icons.outlined.Layers import androidx.compose.material.icons.outlined.Map +import androidx.compose.material.icons.outlined.MyLocation import androidx.compose.material.icons.outlined.Tune import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarScrollBehavior @@ -46,6 +48,10 @@ fun MapControlsOverlay( onManageCustomTileProvidersClicked: () -> Unit, // New parameter showFilterButton: Boolean, scrollBehavior: FloatingToolbarScrollBehavior, + // Location tracking parameters + hasLocationPermission: Boolean = false, + isLocationTrackingEnabled: Boolean = false, + onToggleLocationTracking: () -> Unit = {}, ) { VerticalFloatingToolbar( modifier = modifier, @@ -88,6 +94,20 @@ fun MapControlsOverlay( contentDescription = stringResource(id = R.string.manage_map_layers), onClick = onManageLayersClicked, ) + + // Location tracking button + if (hasLocationPermission) { + MapButton( + icon = + if (isLocationTrackingEnabled) { + Icons.Default.LocationDisabled + } else { + Icons.Outlined.MyLocation + }, + contentDescription = stringResource(id = R.string.toggle_my_position), + onClick = onToggleLocationTracking, + ) + } }, ) }