1

I'm using the accompanist library for handling permissions in jetpack compose. The sample code in the docs doesn't have a scenario to handle permissions such as checking permission on button clicks.

So My scenario is I wanted to check runtime permission on the button click and if the permission is granted do the required work or show the snackbar if not granted. But I can't figure out how can i check if permission was denied permanently or not.

I want a similar behavior like this library has https://github.com/Karumi/Dexter

    val getImageLauncher = rememberLauncherForActivityResult(
        contract = GetContent()
    ) { uri ->

        uri?.let {
            viewModel.imagePicked.value = it.toString()
        }
    }

    // Remember Read Storage Permission State
    val readStoragePermissionState = rememberPermissionState(
        permission = READ_EXTERNAL_STORAGE
    ) { result ->

        if (result) {
            getImageLauncher.launch("image/*")
        } else {

            // How can i check here if permission permanently denied?
            
            coroutineScope.launch {

                scaffoldState.snackbarHostState.showSnackbar(
                    context.getString(R.string.read_storage_denied)
                )
                
            }
        }
    }

Here's the code of the button on which when I click I want to check the permission

    SecondaryOutlineButton(
        modifier = Modifier
            .fillMaxWidth()
            .height(48.dp),
        buttonText = stringResource(
            id = R.string.upload_image
        ),
        buttonCornerRadius = 8.dp,
    ) {
        readStoragePermissionState.launchPermissionRequest()
    }

Malik Bilal
  • 869
  • 8
  • 25

4 Answers4

2

For those looking for a similar scenario. To handle permissions in jetpack compose properly I followed the below steps:

  1. When the button is clicked, first check if the permission is already granted. If it's already granted then simply do the work you needed to do.
  2. If it's not granted we will request the permission. And in the callback of rememberPermissionState(){granted -> } we will check if the permission is granted or not.
  3. If permission is granted then simply do the work you needed to do. Otherwise, we have 2 scenarios to check now. Check if the shouldShowRequestPermissionRationale returns true or false.
  4. As we have requested the permission already so we already neglected the scenario when shouldShowRequestPermissionRationale returns false for the first time before requesting the permission.
  5. So if the shouldShowRequestPermissionRationale returns false that means the permission is permanently denied we can show a message to the user to go to settings to grant permission otherwise it might be denied for once only then we can show some message to the user telling why we need access to that permission.

val context = LocalContext.current

val coroutineScope = rememberCoroutineScope()

val snackBarState = remember { SnackbarHostState() }

val getImageLauncher = rememberLauncherForActivityResult(
    contract = GetContent()
) { uri ->
    //Todo
}

// Remember Read Storage Permission State

val readStoragePermissionState = rememberPermissionState(
  permission = READ_EXTERNAL_STORAGE
) { granted ->
  if (granted) {
    getImageLauncher.launch("image/*")
  } else {
    context.findActivity()?.apply {
      when {
        shouldShowRationale(READ_EXTERNAL_STORAGE) -> {
          snackbarState.showSnackBar(
            message = context.getString(
              R.string.read_storage_rational
            ),
            coroutineScope = coroutineScope,
          )
        }
        else -> {
          snackbarState.showSnackBar(
            action = context.getString(
              R.string.settings
            ),
            message = context.getString(
              R.string.read_storage_denied
            ),
            coroutineScope = coroutineScope,
            onSnackBarAction = {
              context.gotoApplicationSettings()
            },
          )
        }
      }
    }
  }
}

Button Composable


SecondaryOutlineButton(
    modifier = Modifier
        .fillMaxWidth()
        .height(48.dp),
    buttonText = stringResource(
        id = R.string.upload_image
    ),
    buttonCornerRadius = 8.dp,
) {

    if (context.hasPickMediaPermission()) {
        launcher.launch(
            getImageLauncher.launch("image/*")
        )
    } else {
        permission.launchPermissionRequest()
    }
}

Extension Functions

fun Context.isPermissionGranted(name: String): Boolean {
    return ContextCompat.checkSelfPermission(
        this, name
    ) == PackageManager.PERMISSION_GRANTED
}

fun Activity.shouldShowRationale(name: String): Boolean {
    return shouldShowRequestPermissionRationale(name)
}

fun Context.hasPickMediaPermission(): Boolean {

    return when {
        // If Android Version is Greater than Android Pie!
        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> true

        else -> isPermissionGranted(name = READ_EXTERNAL_STORAGE)
    }
}

fun Context.gotoApplicationSettings() {
    startActivity(Intent().apply {
        action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
        data = Uri.parse("package:${packageName}")
    })
}

fun Context.findActivity(): Activity? {
    return when (this) {
        is Activity -> this
        is ContextWrapper -> {
            baseContext.findActivity()
        }

        else -> null
    }
}

fun SnackbarHostState.showSnackBar(
    message: String? = null,
    action: String? = null,
    duration: SnackbarDuration = Short,
    coroutineScope: CoroutineScope,
    onSnackBarAction: () -> Unit = {},
    onSnackBarDismiss: () -> Unit = {},
) {
    if (!message.isNullOrEmpty()) {

        coroutineScope.launch {

            when (showSnackbar(
                message = message,
                duration = duration,
                actionLabel = action,
                withDismissAction = duration == Indefinite,
            )) {
                SnackbarResult.Dismissed -> onSnackBarDismiss.invoke()
                SnackbarResult.ActionPerformed -> onSnackBarAction.invoke()
            }
        }
    }
}

I'm using implementation "com.google.accompanist:accompanist-permissions:0.25.0"

Malik Bilal
  • 869
  • 8
  • 25
  • 1
    You may want to consider the case of the permission being reset by the system, though. In that case your stored preference is out of sync. Instead: when showRationale is false, always try to request permission once. When showRationale is still false after that you know that permission has been permanently denied. – Uli Oct 01 '22 at 01:01
  • @Uli Can you provide any snippets? – Malik Bilal Oct 01 '22 at 01:12
  • You can use something like this to keep track of permission request count, but it may be enough for you to just use a Boolean to keep track of the first permission request vs. later ones: https://github.com/google/accompanist/issues/819#issuecomment-1059477255 – Uli Oct 01 '22 at 13:38
1

I used Philipp Lackner's tutorial for this. He creates an extension method in case the permission is permanently denied.

So in your button Code you would have a method doing this:

Manifest.permission.CAMERA -> {
                    when {
                        perm.status.isGranted -> {                                                                 
                          PermissionText(text = "Camera permission accepted.")
                          }

                        perm.status.shouldShowRationale -> {
                            PermissionText(text = "Camera permission is needed to take pictures.")
                        }

                        perm.isPermanentlyDenied() -> {
                            PermissionText(text = "Camera permission was permanently denied. You can enable it in the app settings.")
                        }
                    }
                }

And the extension would be:

@ExperimentalPermissionsApi
fun PermissionState.isPermanentlyDenied(): Boolean {
    return !status.shouldShowRationale && !status.isGranted
}
Code Poet
  • 6,222
  • 2
  • 29
  • 50
0

Here is the code that does exactly what you are asking:

Click a button (FAB), if the permission is already granted, start working. If the permission is not granted, check if we need to display more info to the user (shouldShowRationale) before requesting and display a SnackBar if needed. Otherwise just ask for the permission (and start work if then granted).

Keep in mind that it is no longer possible to check if a permission is permanently denied. shouldShowRationale() works differently in different versions of Android. What you can do instead (see code), is to display your SnackBar if shouldShowRationale() returns true.

@Composable
fun OptionalPermissionScreen() {
    val context = LocalContext.current.applicationContext

    val state = rememberPermissionState(Manifest.permission.CAMERA)
    val scaffoldState = rememberScaffoldState()
    val launcher = rememberLauncherForActivityResult(RequestPermission()) { wasGranted ->
        if (wasGranted) {
            // TODO do work (ie forward to viewmodel)
            Toast.makeText(context, " Photo in 3..2..1", Toast.LENGTH_SHORT).show()
        }
    }
    Scaffold(
        modifier = Modifier.fillMaxSize(),
        scaffoldState = scaffoldState,
        floatingActionButton = {
            val scope = rememberCoroutineScope()
            val snackbarHostState = scaffoldState.snackbarHostState

            FloatingActionButton(onClick = {
                when (state.status) {
                    PermissionStatus.Granted -> {
                        // TODO do work (ie forward to viewmodel)
                        Toast.makeText(context, " Photo in 3..2..1", Toast.LENGTH_SHORT).show()
                    }
                    else -> {
                        if (state.status.shouldShowRationale) {
                            scope.launch {
                                val result =
                                    snackbarHostState.showSnackbar(
                                        message = "Permission required",
                                        actionLabel = "Go to settings"
                                    )
                                if (result == SnackbarResult.ActionPerformed) {
                                    val intent = Intent(
                                        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                                        Uri.fromParts("package", context.packageName, null)
                                    )
                                    startActivity(intent)
                                }
                            }
                        } else {
                            launcher.launch(Manifest.permission.CAMERA)
                        }
                    }
                }
            }) {
                Icon(Icons.Rounded.Camera, contentDescription = null)
            }
        }) {
        // the rest of your screen
    }
}

Video of how this works by clicking here.

this is part of a blog post I wrote on permissions in Jetpack Compose.

Alex Styl
  • 3,982
  • 2
  • 28
  • 47
0

All answers based on shouldShowRationale won't work well because the rationale is different depending on versions of Android (lol). Check my implementation based on time difference: https://stackoverflow.com/a/77027650/12544067

mxkmn
  • 79
  • 1
  • 6