How to Request Android Runtime Permissions using Jetpack Compose?

A simple app example and proper way to request Android runtime permission workflow using Accompanist Permissions library for Jetpack Compose

ยท

6 min read

How to Request Android Runtime Permissions using Jetpack Compose?

There are 2 types of permissions:

  • Install-time permissions

  • Runtime permissions

An example of the most common install-time permissions is internet permissions. The Android OS system automatically grants your app the permission when the app is installed. You just need to declare the permission in the AndroidManifest.xml.

Unlike runtime permissions, in addition to declaring the permissions in the AndroidManifest.xml, you also need to manually request the permissions in your app. There are some gotchas on requesting runtime permissions, which will be discussed later.

Why Accompanist Permissions?

After playing around with different Android permission implementations in Jetpack Compose, I prefer to use Accompanist permissions library over rememberLauncherForActivityResult() for the following reasons:

  • Permissions state checking is slightly easier using rememberPermissionState() or rememberMultiplePermissionsState() instead of waiting for callback results from the activity result launcher.

  • Accessing to shouldShowRequestPermissionRationale() in composable function is not that straight forward. You probably want to override the shouldShowRequestPermissionRationale() in Activity and do something like this, and figure out how this can be accessed from your composable function.

      class MainActivity : ComponentActivity() {
          /*...*/
          override fun shouldShowRequestPermissionRationale(
              permission: String) : Boolean 
          {
              return if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                  super.shouldShowRequestPermissionRationale(permission)
              } else {
                  false
              }
          }
      }
    

    In Accompanist permission, you can just access the PermissionStatus.shouldShowRationale variable from PermissionState.

Request Runtime Permission Gotchas

Permission Request Dialog is Dismissible

Request runtime permission allows user to dismiss the permission dialog in certain Android version (I think is starting from Android 11/ API level 30). You can either click outside the permission request dialog or simply press the back button to dismiss the permission request dialog.

The problem is, when the permission dialog is dismissed, the PermissionState data remains unchanged. So we have no idea, the user has dismissed the permission request dialog.

What I did to workaround with this issue is I launch the permission request on top of another OptionalLaunchPermissionDialog() which is the wrapper for AlertDialog().

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
    permission: String,
) {
    /*...*/
    OptionalLaunchPermissionDialog(
        permission,
        permissionState,
        dismissCallback = { launchPermissionDialog = false}
    )

    SideEffect {
        permissionState.launchPermissionRequest()
    }        
}

So, when the permission request is dismissed, the AlertDialog() is shown to allow user to relaunch or cancel the permission request (if the permission request is optional).

Don't Launch Permission Request when shouldShowRationale is True

When shouldShowRationale is True, it means the permission has been denied before. The launchPermissionRequest() was first called before. If you call launchPermissionRequest() the second time, it still works. However, if you deny the permission this time, the shouldShowRationale is now set to false. You call launchPermissionRequest(), nothing will happen.

So after this stage, you completely lost the permission state status. You do not know the runtime permission request has been permanently denied. So, the best practice here in my opinion is to show the rational UI to guide user to enable the permission manually when shouldShowRationale is set to True.

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
    permission: String,
) {
    val permissionState = rememberPermissionState(permission)

    if (permissionState.status.isGranted) {
        // permission granted - do nothing here
    } else if (permissionState.status.shouldShowRationale) {
        // permission denied - show rational UI here
    } else {
        // launch runtime permission request here
    }
}

Request Runtime Permission App Demo

This demo app has the following examples:

  • Optional Single Runtime Permission Request

  • Required Single Runtime Permission Request

  • Optional Multiple Runtime Permissions Request

  • Required Multiple Runtime Permissions Request

The step-by-step guide here refers to Optional Single Runtime Permission Request. For the rest, you can refer to the source code.

1. Declare Permission in Your Manifest File

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.CALL_PHONE" />
    <!-- ... -->
</manifest>

2. Add Accompanist Permissions Dependency

dependencies {
    /*...*/
    implementation ("com.google.accompanist:accompanist-permissions:0.31.0-alpha")
}

3. Implement Permission Request Logic

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalSinglePermissionScreen(
    permission: String,
) {
    var permissionStatusText by remember { mutableStateOf("") }
    val permissionState = rememberPermissionState(permission)

    var launchPermissionDialog by remember { mutableStateOf(true) }
    var showRationale by remember { mutableStateOf(true) }

    if (permissionState.status.isGranted) {
        permissionStatusText = "Granted"
    }

    else if (permissionState.status.shouldShowRationale) {
        permissionStatusText = "Denied"

        if(showRationale) {
            OptionalRationalPermissionDialog(
                permission,
                dismissCallback = {showRationale = false}
            )
        }

    } else {
        permissionStatusText = "N/A"
        if (launchPermissionDialog) {
            OptionalLaunchPermissionDialog(
                permission,
                permissionState,
                dismissCallback = { launchPermissionDialog = false}
            )

            SideEffect {
                permissionState.launchPermissionRequest()
            }
        }

    }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text("Optional Permission status: $permissionStatusText")
    }
}

This is the OptionalLaunchPermissionDialog() that allows you to relaunch the permission request when the permission request dialog is dismissed.

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun OptionalLaunchPermissionDialog (
    permission: String,
    permissionState: PermissionState,
    dismissCallback: () -> Unit
) {
    val context = LocalContext.current
    val permissionLabel = stringResource(
        context.packageManager.getPermissionInfo(permission, 0).labelRes
    )

    AlertDialog(
        onDismissRequest = { dismissCallback()},
        title = { Text(text = "Permission Required!") },
        text = { Text(text = permissionLabel) },
        confirmButton = {
            Button(onClick = {
                permissionState.launchPermissionRequest()
            }) {
                Text(text = "Launch")
            }
        },
        dismissButton = {
            Button(onClick = {
                dismissCallback()
            }) {
                Text(text = "Cancel")
            }
        }
    )
}

  • This stringResource(context.packageManager.getPermissionInfo(permission, 0).labelRes) provides a more user-friendly permission string.

  • Since this is an optional permission request, we allow the user to dismiss this dialog.

OptionalRationalPermissionDialog() shows the reasons why the app needs the permission and guide the user to grant the permission manually.

@Composable
fun OptionalRationalPermissionDialog (
    permission: String,
    dismissCallback: () -> Unit
) {
    val context = LocalContext.current
    val permissionLabel = stringResource(
        context.packageManager.getPermissionInfo(permission, 0).labelRes
    )

    AlertDialog(
        onDismissRequest = { dismissCallback()},
        title = { Text(text = "Permission Required!") },
        text = { Text(text = permissionLabel) },
        confirmButton = {
            Button(onClick = {
                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                    .apply {
                        data = Uri.fromParts("package", context.packageName, null)
                    }
                ContextCompat.startActivity(context, intent, null)
            }) {
                Text(text = "Go to settings")
            }
        },
        dismissButton = {
            Button(onClick = {
                dismissCallback()
            }) {
                Text(text = "Cancel")
            }
        }
    )
}

This creates an activity intent that brings you to the app setting which you manually grant the permission to your app.

val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
    .apply {
        data = Uri.fromParts("package", context.packageName, null)
    }
ContextCompat.startActivity(context, intent, null)

Well, that is it!

4. Revoke Runtime Permissions

For testing, we often want to revoke the runtime permissions and rerun the app again. So I gave this ADB command a try.

 adb shell pm revoke vtsen.hashnode.dev.runtimepermissiondemoapp android.permission.CALL_PHONE

It does remove the permission, but it has an unexpected behavior. The shouldShowRationale is now set to True. I want it to be False. To completely revoke the runtime permissions, I either need to reinstall the app or clear the app storage.

Conclusion

Multiple permissions request is similar. Instead of having single PermissionState, it has List<PermissionState>. I will probably use it by default in my app because it can also support single permission request. Last but not least, when shouldShowRationale is True, do NOT launch the runtime permission request.

Source Code

GitHub Repository: Demo_RuntimePermission

Did you find this article valuable?

Support Vincent Tsen - AndroidDev Blog by becoming a sponsor. Any amount is appreciated!

ย