Runtime Permissions

Runtime Permissions and the Permission Launcher

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:

Requesting the camera permission from a composable (ScanIsbnButton.kt, excerpt)Kotlin
  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 camera permission dialog on its second request
The camera permission dialog on its second request

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:

Reading, clearing and granting the camera permissionShell
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.