fix: catch SecurityException when location permission revoked while backgrounded#662
Conversation
…ackgrounded Fixes COLUMBA-5R. When a user grants location permission, backgrounds the app, then revokes the permission from Android Settings, the ViewModel retains stale hasLocationPermission=true state. On some OEMs (observed on Realme 8s 5G / Android 13) the process isn't killed on revocation, so MapLibre's LocationManager.getLastKnownLocation() throws a SecurityException. Three changes: - Re-check actual Android permission in ON_RESUME lifecycle handler to sync ViewModel state with reality after returning from background - Wrap both location component activation paths in try-catch as a TOCTOU safety net (permission can be revoked between our check and MapLibre's internal LocationManager call) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR fixes a fatal The fix introduces three complementary layers of defence:
Key observations:
Confidence Score: 4/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant User
participant Android as Android Settings
participant Lifecycle as LifecycleObserver (ON_RESUME)
participant LPM as LocationPermissionManager
participant LC as MapLibre LocationComponent
participant VM as MapViewModel
participant View as MapView
User->>Android: Revoke location permission
User->>Lifecycle: Return to app (ON_RESUME fires)
Lifecycle->>LPM: hasPermission(context)
LPM-->>Lifecycle: false
Lifecycle->>LC: isLocationComponentEnabled = false
Lifecycle->>VM: onPermissionResult(false)
Lifecycle->>View: view.onResume() (safe — component already disabled)
note over LC,VM: LaunchedEffect(hasLocationPermission) fires on state change — no-op since false
note over LC: If onMapStyleLoaded or LaunchedEffect fires with stale true,<br/>SecurityException is caught → onPermissionResult(false)
Last reviewed commit: bf724bc |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
- Add viewModel.onPermissionResult(false) in onMapStyleLoaded catch block to sync ViewModel state when SecurityException is caught - Reorder ON_RESUME handler to check permission BEFORE view.onResume() to prevent MapLibre from resuming the location engine with revoked permission - Clean up platformLocationListener when permission revoked on resume to prevent stale GPS queries - Disable MapLibre location component before calling view.onResume() when permission has been revoked Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| platformLocationListener?.let { | ||
| LocationCompat.removeLocationUpdates(context, it) | ||
| } | ||
| platformLocationListener = null |
There was a problem hiding this comment.
GMS location updates not removed on permission revocation
The ON_RESUME handler removes the platformLocationListener for the non-GMS path, but when useGms = true the FusedLocationProviderClient locationCallback registered in startLocationUpdates() is created as a local variable and returned as null — its reference is never stored, so there is no way to call fusedLocationClient.removeLocationUpdates(locationCallback) here.
FusedLocationProviderClient typically handles revoked permissions gracefully without throwing a SecurityException (so this is unlikely to cause a crash), but the subscription will linger until the next process start or until the user re-grants permission and a new call to startLocationUpdates is made. To close this gap, consider refactoring startLocationUpdates to return a sealed/union type that holds either a LocationListener (non-GMS) or a Pair<FusedLocationProviderClient, LocationCallback> (GMS) so both paths can be cleaned up uniformly:
// Rough sketch — store alongside platformLocationListener
var gmsLocationCallback: LocationCallback? by remember { mutableStateOf(null) }
// In ON_RESUME revocation block:
gmsLocationCallback?.let { fusedLocationClient?.removeLocationUpdates(it) }
gmsLocationCallback = nullPrompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt
Line: 704-707
Comment:
**GMS location updates not removed on permission revocation**
The `ON_RESUME` handler removes the `platformLocationListener` for the non-GMS path, but when `useGms = true` the `FusedLocationProviderClient` `locationCallback` registered in `startLocationUpdates()` is created as a local variable and returned as `null` — its reference is never stored, so there is no way to call `fusedLocationClient.removeLocationUpdates(locationCallback)` here.
`FusedLocationProviderClient` typically handles revoked permissions gracefully without throwing a `SecurityException` (so this is unlikely to cause a crash), but the subscription will linger until the next process start or until the user re-grants permission and a new call to `startLocationUpdates` is made. To close this gap, consider refactoring `startLocationUpdates` to return a sealed/union type that holds either a `LocationListener` (non-GMS) or a `Pair<FusedLocationProviderClient, LocationCallback>` (GMS) so both paths can be cleaned up uniformly:
```kotlin
// Rough sketch — store alongside platformLocationListener
var gmsLocationCallback: LocationCallback? by remember { mutableStateOf(null) }
// In ON_RESUME revocation block:
gmsLocationCallback?.let { fusedLocationClient?.removeLocationUpdates(it) }
gmsLocationCallback = null
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
Fixes COLUMBA-5R — fatal
SecurityExceptioncrash on the Map screen.hasLocationPermission=true(stale), but MapLibre callsLocationManager.getLastKnownLocation()which throwsSecurityException.onMapStyleLoadedand theLaunchedEffectlocation component activation paths now catchSecurityExceptionas a TOCTOU guardObserved on Realme 8s 5G / Android 13 — some OEMs don't kill the app process on permission revocation.
Test plan
MapViewModelTestpasses (permission state management)🤖 Generated with Claude Code