Skip to content

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

Merged
torlando-tech merged 2 commits intorelease/v0.9.xfrom
fix/map-location-permission-crash-v0.9.x
Mar 11, 2026
Merged

fix: catch SecurityException when location permission revoked while backgrounded#663
torlando-tech merged 2 commits intorelease/v0.9.xfrom
fix/map-location-permission-crash-v0.9.x

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 (COLUMBA-5R) on the Map screen caused by a stale hasLocationPermission=true ViewModel state when the user backgrounds the app, revokes location permission in Android Settings, then returns — a pattern observed on some OEM devices (Realme/Android 13) that don't kill the app process on revocation. The fix adds an ON_RESUME pre-check that compares the live Android permission against ViewModel state, disables the MapLibre location component before view.onResume() is called when revocation is detected, and wraps both the onMapStyleLoaded and LaunchedEffect activation paths in try/catch SecurityException blocks as TOCTOU guards.

Key observations:

  • The ON_RESUME pre-check correctly disables the location component and updates ViewModel state before view.onResume(), addressing the ordering issue identified in prior review
  • platformLocationListener is now properly nulled and removed in the revocation path (non-GMS)
  • view.onResume() itself is still not wrapped in a try-catch — if MapLibre's internal location engine resumes and the OS revokes permission in the narrow window between the permission check and the view.onResume() call, the same SecurityException crash can still occur; the other two added catch blocks exist precisely to cover this TOCTOU scenario
  • On GMS devices (useGms = true), platformLocationListener is null, so the cleanup block at lines 513–516 is a no-op; the fusedLocationClient callback is never explicitly removed on the permission revocation path

Confidence Score: 3/5

  • The PR substantially reduces the crash surface but a residual TOCTOU window around view.onResume() means the exact crash scenario can still occur in a narrow race, and GMS devices have an incomplete cleanup path.
  • The core fix (pre-check + component disable before view.onResume()) is correct and handles the primary reported scenario. However, view.onResume() itself is the most likely source of the original SecurityException (MapLibre's location engine resumes here) and it has no try-catch, leaving the crash partially unfixed. The GMS fusedLocationClient cleanup gap is a lower-severity resource leak. Two of the three issues raised in prior review are addressed; the third (TOCTOU on view.onResume()) was described in the previous thread but the fix doesn't yet include a try-catch there.
  • app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt — specifically the ON_RESUME block around view.onResume() (line 518) and the GMS cleanup gap (lines 513–516)

Important Files Changed

Filename Overview
app/src/main/java/com/lxmf/messenger/ui/screens/MapScreen.kt Adds SecurityException guards and an ON_RESUME permission re-check to fix the COLUMBA-5R crash; residual TOCTOU on view.onResume() is uncaught and GMS fusedLocationClient cleanup is missing on the revocation path.

Sequence Diagram

sequenceDiagram
    participant User
    participant Android OS
    participant LifecycleObserver
    participant MapLibre
    participant ViewModel

    User->>Android OS: Grant location permission
    Android OS-->>ViewModel: onPermissionResult(true)
    ViewModel-->>MapLibre: activateLocationComponent (try/catch SecurityException)

    User->>Android OS: Background app
    User->>Android OS: Revoke location permission in Settings
    User->>Android OS: Return to app (ON_RESUME)

    Android OS->>LifecycleObserver: ON_RESUME event
    LifecycleObserver->>Android OS: hasPermission() → false
    LifecycleObserver->>MapLibre: locationComponent.isLocationComponentEnabled = false
    LifecycleObserver->>ViewModel: onPermissionResult(false)
    LifecycleObserver->>Android OS: removeLocationUpdates (non-GMS only)
    LifecycleObserver->>MapLibre: view.onResume() ⚠️ no try/catch here

    Note over LifecycleObserver,MapLibre: TOCTOU window — SecurityException<br/>from view.onResume() is still uncaught
Loading

Last reviewed commit: e239543

- 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>
@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!

@torlando-tech
Copy link
Copy Markdown
Owner Author

@greptileai

}
platformLocationListener = null
}
view.onResume()
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.

Residual TOCTOU window around view.onResume()

The pre-check (lines 504–517) correctly guards the case where permission is already revoked before ON_RESUME fires. However, when both state.hasLocationPermission and stillHasPermission are true (permission appears valid at line 504), the if-block is skipped and view.onResume() is called with no exception handling. If MapLibre's internal location engine calls LocationManager.requestLocationUpdates() during onResume() and the OS revokes the permission in the narrow window between lines 504 and 518, a SecurityException will be thrown uncaught here — the same crash this PR sets out to fix.

The two other code paths (onMapStyleLoaded catch at line 253 and the LaunchedEffect catch at line 440) both use try-catch precisely for this TOCTOU reason. The view.onResume() call should be given the same treatment:

try {
    view.onResume()
} catch (e: SecurityException) {
    Log.w("MapScreen", "SecurityException during onResume, disabling location component", e)
    mapLibreMap?.locationComponent?.let { lc ->
        if (lc.isLocationComponentActivated) {
            lc.isLocationComponentEnabled = false
        }
    }
    viewModel.onPermissionResult(false)
    platformLocationListener?.let {
        LocationCompat.removeLocationUpdates(context, it)
    }
    platformLocationListener = 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: 518

Comment:
**Residual TOCTOU window around `view.onResume()`**

The pre-check (lines 504–517) correctly guards the case where permission is already revoked before `ON_RESUME` fires. However, when both `state.hasLocationPermission` and `stillHasPermission` are `true` (permission appears valid at line 504), the if-block is skipped and `view.onResume()` is called with no exception handling. If MapLibre's internal location engine calls `LocationManager.requestLocationUpdates()` during `onResume()` and the OS revokes the permission in the narrow window between lines 504 and 518, a `SecurityException` will be thrown uncaught here — the same crash this PR sets out to fix.

The two other code paths (`onMapStyleLoaded` catch at line 253 and the `LaunchedEffect` catch at line 440) both use try-catch precisely for this TOCTOU reason. The `view.onResume()` call should be given the same treatment:

```kotlin
try {
    view.onResume()
} catch (e: SecurityException) {
    Log.w("MapScreen", "SecurityException during onResume, disabling location component", e)
    mapLibreMap?.locationComponent?.let { lc ->
        if (lc.isLocationComponentActivated) {
            lc.isLocationComponentEnabled = false
        }
    }
    viewModel.onPermissionResult(false)
    platformLocationListener?.let {
        LocationCompat.removeLocationUpdates(context, it)
    }
    platformLocationListener = null
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +513 to +516
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 fusedLocationClient not stopped on permission revocation

The cleanup here only removes the platform LocationListener (the non-GMS path). Per the comment at line 270 ("Platform LocationListener for cleanup when not using GMS"), platformLocationListener is null when useGms = true, making this block a no-op for GMS devices.

On the GMS path, startLocationUpdates registers a callback with fusedLocationClient. When permission is revoked, that callback is never removed here — the fusedLocationClient reference and its registered callback continue to exist until composition leaves. While FusedLocationProviderClient will eventually stop delivering updates on its own after revocation, explicitly removing the callback is the right pattern and mirrors what the non-GMS branch already does.

Consider also removing GMS callbacks in this revocation path (requires tracking the LocationCallback returned from startLocationUpdates, similar to how platformLocationListener tracks the non-GMS callback).

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: 513-516

Comment:
**GMS `fusedLocationClient` not stopped on permission revocation**

The cleanup here only removes the platform `LocationListener` (the non-GMS path). Per the comment at line 270 ("Platform LocationListener for cleanup when **not** using GMS"), `platformLocationListener` is `null` when `useGms = true`, making this block a no-op for GMS devices.

On the GMS path, `startLocationUpdates` registers a callback with `fusedLocationClient`. When permission is revoked, that callback is never removed here — the `fusedLocationClient` reference and its registered callback continue to exist until composition leaves. While `FusedLocationProviderClient` will eventually stop delivering updates on its own after revocation, explicitly removing the callback is the right pattern and mirrors what the non-GMS branch already does.

Consider also removing GMS callbacks in this revocation path (requires tracking the `LocationCallback` returned from `startLocationUpdates`, similar to how `platformLocationListener` tracks the non-GMS callback).

How can I resolve this? If you propose a fix, please make it concise.

@torlando-tech torlando-tech merged commit 1ace984 into release/v0.9.x Mar 11, 2026
14 checks passed
@torlando-tech torlando-tech deleted the fix/map-location-permission-crash-v0.9.x 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