Skip to content

fix: catch SecurityException when location permission revoked while backgrounded#662

Merged
torlando-tech merged 2 commits intomainfrom
fix/map-location-permission-crash-main
Mar 11, 2026
Merged

fix: catch SecurityException when location permission revoked while backgrounded#662
torlando-tech merged 2 commits intomainfrom
fix/map-location-permission-crash-main

Conversation

@torlando-tech
Copy link
Copy Markdown
Owner

Summary

Fixes COLUMBA-5R — fatal SecurityException crash on the Map screen.

  • Root cause: User grants location permission → backgrounds app → revokes permission in Android Settings → returns to app. ViewModel still has hasLocationPermission=true (stale), but MapLibre calls LocationManager.getLastKnownLocation() which throws SecurityException.
  • ON_RESUME re-check: Syncs ViewModel permission state with the actual Android permission when the app resumes from background
  • try-catch safety net: Both onMapStyleLoaded and the LaunchedEffect location component activation paths now catch SecurityException as a TOCTOU guard

Observed on Realme 8s 5G / Android 13 — some OEMs don't kill the app process on permission revocation.

Test plan

  • MapViewModelTest passes (permission state management)
  • Manual: grant location → background → revoke in Settings → return to map → no crash, blue dot disappears

🤖 Generated with Claude Code

…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-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 11, 2026

Greptile Summary

This PR fixes a fatal SecurityException crash on the Map screen that occurred when a user granted location permission, backgrounded the app, revoked permission in Android Settings, and then returned — leaving the ViewModel with a stale hasLocationPermission=true while MapLibre internally called LocationManager.getLastKnownLocation().

The fix introduces three complementary layers of defence:

  • A pre-onResume permission re-check in the DisposableEffect lifecycle observer that disables the MapLibre location component and notifies the ViewModel before view.onResume() can restart the location engine.
  • try/catch blocks around locationComponent.apply { … } in both onMapStyleLoaded and the LaunchedEffect(state.hasLocationPermission, mapLibreMap) handler, each correctly calling viewModel.onPermissionResult(false) on catch.

Key observations:

  • The fix is logically sound for the primary crash path (MapLibre location component / platform LocationManager).
  • The ON_RESUME handler cleans up the non-GMS platformLocationListener, but the FusedLocationProviderClient locationCallback registered in startLocationUpdates() is a local variable with no stored reference, so it cannot be removed in the same revocation path. This is a pre-existing architectural gap that the PR doesn't address (see inline comment).
  • Code formatting changes (ScreenToLatLng and contactMarkers.find multi-line lambda reformatting) are style-only with no logic impact.

Confidence Score: 4/5

  • Safe to merge — the crash fix is correct and all SecurityException catch sites now correctly sync ViewModel state; one minor cleanup gap for the GMS FusedLocationProviderClient callback exists but does not cause crashes.
  • Both SecurityException catch sites call viewModel.onPermissionResult(false) consistently, and the pre-resume permission gate correctly disables the location component before MapLibre can restart it. The only identified gap (GMS LocationCallback not removable) is a pre-existing architectural issue that is unlikely to cause crashes since FusedLocationProviderClient handles revoked permissions gracefully internally.
  • app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt — specifically the ON_RESUME revocation block and the startLocationUpdates helper regarding GMS callback lifecycle.

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt Adds ON_RESUME permission re-check and try/catch SecurityException guards in both onMapStyleLoaded and the hasLocationPermission LaunchedEffect; non-GMS platform location listener is cleaned up on revocation but the FusedLocationProviderClient callback reference is not stored and therefore cannot be removed in the same path.

Sequence Diagram

sequenceDiagram
    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)
Loading

Last reviewed commit: bf724bc

@sentry
Copy link
Copy Markdown
Contributor

sentry bot commented Mar 11, 2026

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>
@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

Comment on lines +704 to +707
platformLocationListener?.let {
LocationCompat.removeLocationUpdates(context, it)
}
platformLocationListener = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = null
Prompt 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.

@torlando-tech torlando-tech merged commit b327cb6 into main Mar 11, 2026
15 checks passed
@torlando-tech torlando-tech deleted the fix/map-location-permission-crash-main branch March 11, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant