Dangerous permissions (camera, microphone, location, contacts) are granted by the user at run time. Ask when the user taps the feature: check checkSelfPermission(), then launch the RequestPermission contract. BookNest's Scan an ISBN button asks for the camera:
val launcher = rememberLauncherForActivityResult(RequestPermission()) { granted ->
status = when {
granted -> "granted"
activity.shouldShowRequestPermissionRationale(CAMERA) -> "denied, explain why"
else -> "denied, don't ask again"
}
Log.i("BookNest", "camera permission: $status")
}
Button(onClick = {
if (activity.checkSelfPermission(CAMERA) == PERMISSION_GRANTED) status = "granted"
else launcher.launch(CAMERA) // shows the system dialog
}) { Text(stringResource(R.string.scan_isbn)) }
The rationale check returns true after one denial, your cue to explain the feature. Don't allow on the second dialog means "don't ask again": the next launch() delivered false at once, with no dialog, so only Settings can grant it now. adb reads and resets the flags:
adb shell dumpsys package com.example.booknest | grep "CAMERA: granted"
adb shell pm clear-permission-flags com.example.booknest \
android.permission.CAMERA user-set user-fixed
adb shell pm grant com.example.booknest android.permission.CAMERA
adb shell dumpsys package com.example.booknest | grep "CAMERA: granted"Output
android.permission.CAMERA: granted=false, flags=[ USER_SET|USER_FIXED|USER_SENSITIVE_WHEN_GRANT ED|USER_SENSITIVE_WHEN_DENIED] android.permission.CAMERA: granted=true, flags=[ USER_SENSITIVE_WHEN_GRANTED|USER_SENSITIVE_WHE N_DENIED]
Keep the app usable when the answer is no, and test all three paths.