Asking at the right moment
A shop assistant who asks for your phone number the moment you walk in gets a no. One who asks at the till, “so we can text you when your order is ready”, usually gets a yes. Same question, different moment.
Ask for a permission when the user taps the feature that needs it: the camera permission when they tap “Scan receipt”, location when they tap “Find stores near me”. The reason is obvious then, and the feature works right after they agree.
Every request has three possible outcomes, and the app must handle all of them:
- Granted: continue with the feature.
- Denied: the feature can’t work; explain briefly and offer an alternative (type the address instead).
- Denied permanently: after the second refusal, Android doesn’t show the dialog anymore. The only way back is the app’s page in Settings.
Use the Activity Result API (ActivityResultContracts.RequestPermission / RequestMultiplePermissions, or Accompanist’s/compose permission helpers) rather than onRequestPermissionsResult. The system shows the dialog at most twice per permission before treating further requests as denied without UI; shouldShowRequestPermissionRationale is true after one denial (show your explanation) and false both before the first request and after a permanent denial, so you need your own “have I asked before” flag to distinguish them. Prefer permission-free alternatives where they exist: the Photo Picker instead of storage permissions, approximate location instead of precise, ACTION_IMAGE_CAPTURE intents instead of the camera permission when you only need a photo.
@Composable
fun ScanReceiptButton(onScan: () -> Unit) {
val context = LocalContext.current
var showRationale by remember { mutableStateOf(false) }
var permanentlyDenied by remember { mutableStateOf(false) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) onScan()
else permanentlyDenied = !context.findActivity()
.shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)
}
Button(onClick = {
when {
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED -> onScan()
context.findActivity().shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) ->
showRationale = true
else -> launcher.launch(Manifest.permission.CAMERA)
}
}) { Text("Scan receipt") }
if (showRationale) {
AlertDialog(
onDismissRequest = { showRationale = false },
title = { Text("Camera access") },
text = { Text("The camera is used only to read the receipt. Photos aren't saved.") },
confirmButton = {
TextButton(onClick = { showRationale = false; launcher.launch(Manifest.permission.CAMERA) }) {
Text("Continue")
}
},
dismissButton = { TextButton(onClick = { showRationale = false }) { Text("Not now") } },
)
}
if (permanentlyDenied) {
AlertDialog(
onDismissRequest = { permanentlyDenied = false },
text = { Text("Camera access is off. You can turn it on in Settings, or type the amount instead.") },
confirmButton = {
TextButton(onClick = {
permanentlyDenied = false
context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", context.packageName, null)))
}) { Text("Open Settings") }
},
dismissButton = { TextButton(onClick = { permanentlyDenied = false }) { Text("Type it") } },
)
}
}- Check first: if the permission is already granted, just start the feature.
- Rationale: after the user said no once, Android suggests explaining why before asking again. The explanation is your own dialog, in your own words.
- Permanently denied: after a refusal with no rationale needed, the system won’t show its dialog again, so offer a button to the app’s Settings page, and an alternative that works without the permission.
- Check before every use: users can turn permissions off in Settings at any time while the app is in the background.
Predict the output
The user denied the location permission twice. The app calls launcher.launch(ACCESS_FINE_LOCATION) a third time when they tap 'Stores near me'. What does the user see?
After two denials, Android treats the permission as permanently denied for that app: launch returns false to the callback right away, and no dialog appears. To the user, the button seems broken. Detect this case (denied, and shouldShowRequestPermissionRationale is false) and offer a way to Settings plus an alternative, like searching by city name.
Plan the permissions for a running app
A running app needs: precise location during a run, notifications for “run paused” reminders, and access to photos to set a profile picture. Decide when to ask for each, and whether any can be avoided.
›
For each one, find the first tap that actually needs it. Then check whether Android offers a way to do the job without asking at all.
SolutionTry it yourself first
- Location: ask when the user taps Start run, with a short explanation on the screen before the system dialog (“Your route is recorded while you run”). Precise location is needed for route tracking; if the user grants only approximate, show a notice that the map will be less accurate. Background location during a locked-screen run is better done with a foreground service of type
locationstarted from the visible app, which avoids the separate background location permission in most cases. - Notifications: ask after the first run, when offering “Remind me if I forget to resume a paused run”, not at first launch.
- Photos: don’t ask at all. The Photo Picker (
PickVisualMedia) lets the user choose one picture without any storage permission.
Permission hygiene
- Request the minimum: approximate location, Photo Picker,
ACTION_IMAGE_CAPTURE, Credential Manager, and the share sheet avoid whole categories of permissions. - One-time and auto-reset: users can grant “only this time”, and Android revokes permissions of apps unused for months; never cache a “granted” flag.
- Play policy: sensitive permissions (background location, all-files access, SMS, call log, exact alarms) require declarations and justification in the Play Console, and get apps rejected when unused.
- Testing:
GrantPermissionRulefor instrumented tests, andadb shell pm revoketo test denial paths; UI Automator can tap the system dialog.
- Activity Result API; ask in context; handle granted, denied (rationale), permanently denied (Settings + alternative). Track your own “asked before” flag, since the rationale flag is false both before the first ask and after permanent denial.
- Dialog shown at most twice; one-time grants and auto-reset: check before every use.
- Prefer permission-free alternatives (Photo Picker, approximate location, capture intents); sensitive permissions need Play declarations.
Check this part
When should an app ask for the camera permission?
The user permanently denied a permission. What can the app do?
How does an app pick one profile photo without any storage permission?