Handling Android Permissions with Jetpack Compose API

אליאב לוסקי
5 min readOct 4, 2023

we are going to use the accompanist lib in order to achive our goal, however, the implementation of accompanist has limitations:

This permissions wrapper is built on top of the available Android platform APIs. We cannot extend the platform’s capabilities. For example, it’s not possible to differentiate between the it’s the first time requesting the permission vs the user doesn’t want to be asked again use cases.

To differentiate between never-asked before state and should-never-ask-again state you can keep track the value of shouldShowRationale and isGranted.

The user has (completely) denied the permission if:

  • the current value of shouldShowRationale is now false and was previously was true
    AND
    the current value of isGranted is false

we can keep track the value of shouldShowRationale by using mutableStateOf and LaunchedEffect.

// Boolean state variable to track if shouldShowRationale changed from true to false
var shouldShowRationaleBecameFalseFromTrue by remember { mutableStateOf(false) }

// Remember the previous value of shouldShowRationale
var prevShouldShowRationale by remember { mutableStateOf(bluetoothPermissionState.status.shouldShowRationale) }
// Track changes in shouldShowRationale
LaunchedEffect(bluetoothPermissionState.status.shouldShowRationale) {
if (prevShouldShowRationale && !bluetoothPermissionState.status.shouldShowRationale) {
shouldShowRationaleBecameFalseFromTrue = true
}
prevShouldShowRationale = bluetoothPermissionState.status.shouldShowRationale
}
// if shouldShowRationale changed from true to false and the permission is not granted,
// then the user denied the permission and checked the "Never ask again" checkbox
val userDeniedPermission =
shouldShowRationaleBecameFalseFromTrue && !bluetoothPermissionState.status.isGranted

let’s take the example from the accompanist lib:

@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun FeatureThatRequiresCameraPermission() {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
if (cameraPermissionState.status.isGranted) {
Text("Camera permission Granted")
} else {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The camera is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Camera permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}

let’s modify it with our implementation:

@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun FeatureThatRequiresBluetoothPermission() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
// Bluetooth permission state
val bluetoothPermissionState = rememberPermissionState(
Manifest.permission.BLUETOOTH_CONNECT
)
// Boolean state variable to track if shouldShowRationale changed from true to false
var shouldShowRationaleBecameFalseFromTrue by remember { mutableStateOf(false) }
// Remember the previous value of shouldShowRationale
var prevShouldShowRationale by remember { mutableStateOf(bluetoothPermissionState.status.shouldShowRationale) }
// Track changes in shouldShowRationale
LaunchedEffect(bluetoothPermissionState.status.shouldShowRationale) {
if (prevShouldShowRationale && !bluetoothPermissionState.status.shouldShowRationale) {
shouldShowRationaleBecameFalseFromTrue = true
}
prevShouldShowRationale = bluetoothPermissionState.status.shouldShowRationale
}
// if shouldShowRationale changed from true to false and the permission is not granted,
// then the user denied the permission and checked the "Never ask again" checkbox
val userDeniedPermission =
shouldShowRationaleBecameFalseFromTrue && !bluetoothPermissionState.status.isGranted

if (userDeniedPermission) {
Text(
"You denied the permission, in order for the app to work properly you need to grant the permission manually." +
"Open the app settings and grant the permission manually."
)
return
}
if (bluetoothPermissionState.status.isGranted) {
Text("Bluetooth permission Granted")
} else {
Column {
val textToShow = if (bluetoothPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The bluetooth is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Bluetooth permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { bluetoothPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}

and all within single composable component that is able to ask permissions, listen to permissions changes, and react to their change. Work like a charm:

now the user would be prompted to go to settings and change the permission manually whenever the denies the permission too many times.

Open settings

now lets add a button that send the use to settings if he denied the permission too many times:

let’s add to our composable:

val activity = LocalContext.current as Activity
val openAppSettings = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
activity.startActivity(intent)
}

so it becomes:

@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun FeatureThatRequiresBluetoothPermission(content: @Composable () -> Unit) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return

// Bluetooth permission state
val bluetoothPermissionState = rememberPermissionState(
Manifest.permission.BLUETOOTH_CONNECT
)

// Boolean state variable to track if shouldShowRationale changed from true to false
var shouldShowRationaleBecameFalseFromTrue by remember { mutableStateOf(false) }

// Remember the previous value of shouldShowRationale
var prevShouldShowRationale by remember { mutableStateOf(bluetoothPermissionState.status.shouldShowRationale) }

// Track changes in shouldShowRationale
LaunchedEffect(bluetoothPermissionState.status.shouldShowRationale) {
if (prevShouldShowRationale && !bluetoothPermissionState.status.shouldShowRationale) {
shouldShowRationaleBecameFalseFromTrue = true
}
prevShouldShowRationale = bluetoothPermissionState.status.shouldShowRationale
}

// if shouldShowRationale changed from true to false and the permission is not granted,
// then the user denied the permission and checked the "Never ask again" checkbox
val userDeniedPermission =
shouldShowRationaleBecameFalseFromTrue && !bluetoothPermissionState.status.isGranted

val activity = LocalContext.current as Activity
val openAppSettings = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", activity.packageName, null)
intent.data = uri
intent.addCategory(Intent.CATEGORY_DEFAULT)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
activity.startActivity(intent)
}

if (userDeniedPermission) {
Text(
"You denied the permission, in order for the app to work properly you need to grant the permission manually." +
"Open the app settings and grant the permission manually."
)
TextButton(
onClick = { openAppSettings() },
modifier = Modifier
.padding(16.dp)
) {
Text("Open Settings")
}
return
}

if (bluetoothPermissionState.status.isGranted) {
// Text("Bluetooth permission Granted")
content()

} else {
Column {
val textToShow = if (bluetoothPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The bluetooth is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Bluetooth permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { bluetoothPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}

and usage:

FeatureThatRequiresBluetoothPermission {
//Your app here
}

--

--