Three permission dialogs for a working safety app on Android
A safety app I shipped for personal use on Android needs three distinct runtime permissions to actually do its job. Walking a first-time user through them in the right order, with the right fallback if they say no, took more thought than any other part of the build.
What was happening
The three permissions:
- Notifications (
POST_NOTIFICATIONS, Android 13+). Needed for any push at all — Expo push tokens require this on modern Android. - Foreground location (
ACCESS_FINE_LOCATION). The user has to grant this before background location is even askable. - Background location (
ACCESS_BACKGROUND_LOCATION). Required for the foreground service to keep tracking when the app isn't open. This is the one Android shows the scary "always allow" dialog for.
If you ask in the wrong order or all at once, Android either collapses them into one permission shelf the user doesn't understand, or pops the background dialog first and confuses the user.
What I found
The sequence that actually works for first-time users:
launch
→ check existing tokens (skip if signed in)
→ notification permission dialog (Allow)
→ Expo push token registers
→ foreground location dialog (Allow)
→ background location dialog ("Allow all the time")
→ foreground service starts
→ location ping posts every 60s, every 50m of movement
Each step has an explanation screen before the system dialog that says, in plain language, what the next dialog is for and what happens if they say no. Skipping the explanations means users panic at "always allow location."
Each permission also has a kill-switch in Settings — the user can turn the whole thing off without uninstalling the app, and each individual permission has an obvious "revoke" path that makes the app behave gracefully (no crashes, just degraded features).
The fix
The non-obvious thing is the order. Notifications first because they're the cheapest "yes" — most users say yes because they understand notifications. Foreground location second, with an explanation that "we need to know where you are when the app is open so we can detect arrival/departure." Background location last, with the most explanation, because "always allow" is genuinely a big ask.
Failure paths:
- If notifications are denied, the app still works but falls back to in-app indicators only. No SMS-equivalent on the device itself.
- If foreground location is denied, the app can still do manual check-ins but loses all geofencing and arrival detection.
- If background location is denied, foreground works fine when the app is open. The system reminds the user periodically that location access is limited.
Each path is explicit in the code with a PermissionState
enum, not implicit through "did this API call succeed?" The
state drives a banner in the UI ("Background location is off
— some features are disabled") so the user knows what they're
missing without having to debug it themselves.
What I'd do differently
Start with the kill-switches. Build the "user revokes permission X" path before you build the "user grants permission X" path. The granted path is the happy case and will work whether you wrote it carefully or not. The revoked path is where users actually end up, and it's where bad apps crash or behave mysteriously.
Also: Android's permission UX changes every couple of releases.
Background location was almost-but-not-quite available for a
while. POST_NOTIFICATIONS was opt-in for older targets and
mandatory for newer. Pinning your targetSdkVersion so you
know which dialog set you'll see is worth the maintenance cost.