How request permissions with Jetpack Compose?
Asked Answered
D

14

60

How should be implemented requesting permission from Jetpack Compose View? I'm trying implement application accessing Camera with Jetpack Compose. I tried example from How to get Current state or context in Jetpack Compose Unfortunately example is no longer working with dev06.

        fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
    }
}
Dronski answered 9/3, 2020 at 20:45 Comment(2)
updated answer to the linked question for dev06: https://mcmap.net/q/134838/-how-to-get-context-in-jetpack-composeDexterous
#73899307Genni
E
35

Check out Google Accompanist's Jetpack Compose Permissions.

Bear in mind that, at the time of writing, the API is still considered experimental and will require the @ExperimentalPermissionsApi annotation when used.

Engorge answered 27/6, 2021 at 20:56 Comment(5)
Do you know if it is possible to execute something right after the permission was granted?Whopping
Yes, you can execute your code after permission was granted. Can you please explain what is it exactly you are trying to achieve?Engorge
@emilioburrito This is how I did it: val lifecycleOwner = LocalLifecycleOwner.current var requestingPermission by remember { mutableStateOf(false) } if (requestingPermission) { SideEffect { lifecycleOwner.lifecycleScope.launchWhenResumed { onPermissionResult(permissionState.hasPermission) } } requestingPermission = false } and: requestingPermission = true permissionState.launchPermissionRequest()Methylene
This helps but it forces us to declare all of the libraries at once and I feel it's a bit tightly coupled thing to implementShortcut
@emilioburrito this is my code : LaunchedEffect(permissionState.status.isGranted) { if(permissionState.status.isGranted) //do your stuff here } Lightning
M
61

as compose_version = '1.0.0-beta04' and

implementation 'androidx.activity:activity-compose:1.3.0-alpha06'

you can do request permission as simple as this:

@Composable
fun ExampleScreen() {
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            // Permission Accepted: Do something
            Log.d("ExampleScreen","PERMISSION GRANTED")

        } else {
            // Permission Denied: Do something
            Log.d("ExampleScreen","PERMISSION DENIED")
        }
    }
    val context = LocalContext.current

    Button(
        onClick = {
            // Check permission
            when (PackageManager.PERMISSION_GRANTED) {
                ContextCompat.checkSelfPermission(
                    context,
                    Manifest.permission.READ_EXTERNAL_STORAGE
                ) -> {
                    // Some works that require permission
                    Log.d("ExampleScreen","Code requires permission")
                }
                else -> {
                    // Asking for permission
                    launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
                }
            }
        }
    ) {
        Text(text = "Check and Request Permission")
    }
}

Mcglothlin answered 16/4, 2021 at 6:55 Comment(1)
PackageManager.PERMISSION_GRANTED is a static final int 0, so the when is when (0). Is that correct? It looks like the when condition and the first match need to be swapped?Retinue
E
35

Check out Google Accompanist's Jetpack Compose Permissions.

Bear in mind that, at the time of writing, the API is still considered experimental and will require the @ExperimentalPermissionsApi annotation when used.

Engorge answered 27/6, 2021 at 20:56 Comment(5)
Do you know if it is possible to execute something right after the permission was granted?Whopping
Yes, you can execute your code after permission was granted. Can you please explain what is it exactly you are trying to achieve?Engorge
@emilioburrito This is how I did it: val lifecycleOwner = LocalLifecycleOwner.current var requestingPermission by remember { mutableStateOf(false) } if (requestingPermission) { SideEffect { lifecycleOwner.lifecycleScope.launchWhenResumed { onPermissionResult(permissionState.hasPermission) } } requestingPermission = false } and: requestingPermission = true permissionState.launchPermissionRequest()Methylene
This helps but it forces us to declare all of the libraries at once and I feel it's a bit tightly coupled thing to implementShortcut
@emilioburrito this is my code : LaunchedEffect(permissionState.status.isGranted) { if(permissionState.status.isGranted) //do your stuff here } Lightning
S
17

Google has a library called "Accompanist". It has many help libraries and one of them is the Permission Library.

Check: Library: https://github.com/google/accompanist/

Documentation: https://google.github.io/accompanist/permissions/

Example:

Setup in build.gradle file:

repositories {
    mavenCentral()
}

dependencies {

implementation "com.google.accompanist:accompanist-permissions:<latest_version>"

}

Implementation in Code

@Composable
private fun FeatureThatRequiresCameraPermission() {

    // Camera permission state
    val cameraPermissionState = rememberPermissionState(
        android.Manifest.permission.CAMERA
    )

    when (cameraPermissionState.status) {
        // If the camera permission is granted, then show screen with the feature enabled
        PermissionStatus.Granted -> {
            Text("Camera permission Granted")
        }
        is PermissionStatus.Denied -> {
            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")
                }
            }
        }
    }
}
Syracuse answered 13/7, 2021 at 5:27 Comment(2)
This code is outdated, there is no more PermissionRequired(...) method in the library. You should update this.Engorge
@mr.Gauss I have updated my code. You can check it now.Syracuse
C
7
/**
 * Composable helper for permission checking
 *
 * onDenied contains lambda for request permission
 *
 * @param permission permission for request
 * @param onGranted composable for [PackageManager.PERMISSION_GRANTED]
 * @param onDenied composable for [PackageManager.PERMISSION_DENIED]
 */
@Composable
fun ComposablePermission(
    permission: String,
    onDenied: @Composable (requester: () -> Unit) -> Unit,
    onGranted: @Composable () -> Unit
) {
    val ctx = LocalContext.current

    // check initial state of permission, it may be already granted
    var grantState by remember {
        mutableStateOf(
            ContextCompat.checkSelfPermission(
                ctx,
                permission
            ) == PackageManager.PERMISSION_GRANTED
        )
    }
    if (grantState) {
        onGranted()
    } else {
        val launcher: ManagedActivityResultLauncher<String, Boolean> =
            rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
                grantState = it
            }
        onDenied { launcher.launch(permission) }
    }
}
Candice answered 2/6, 2021 at 17:53 Comment(0)
M
5

Don't forget to add

<uses-permission android:name="android.permission.CAMERA"/>

(when requesting camera permission) to your AndroidManifest.xml, otherwise it might end up with the permission denied state, when using the solutions others provided.

Maleficent answered 25/11, 2021 at 10:28 Comment(0)
L
3

A little late but this might help as I had the problem today:

With ContextAmbient.current it is not guaranteed that you have an activity or fragment thus I created my own ambient for handling permissions.

val AmbientPermissionHandler: ProvidableAmbient<PermissionHandler> =
    ambientOf { throw IllegalStateException("permission handler is not initialized") }

// Activity:
private val permissionHandler = PermissionHandler(this)
// onCreate:
setContent {
    Providers(
        AmbientPermissionHandler provides permissionHandler
    ) {/* Composable Contnent */}

Usage:

@Composable
fun PermissionHandler(
    permissions: Array<out String>,
    requestCode: Int,
    granted: @Composable() () -> Unit,
    denied: @Composable() () -> Unit,
    deniedPermanently: (@Composable() () -> Unit)? = null,
    rational: (@Composable() () -> Unit)? = null,
    awaitResult: (@Composable() () -> Unit)? = null,
) {
    val permissionHandler = AmbientPermissionHandler.current
    val (permissionResult, setPermissionResult) = remember(permissions) { mutableStateOf<PermissionResult?>(null) }
    LaunchedEffect(Unit) {
        setPermissionResult(permissionHandler.requestPermissionsSuspend(requestCode, permissions))
    }
    when (permissionResult) {
        is PermissionResult.PermissionGranted -> granted()
        is PermissionResult.PermissionDenied -> denied()
        is PermissionResult.PermissionDeniedPermanently -> deniedPermanently?.invoke()
        is PermissionResult.ShowRational -> rational?.invoke()
        null -> awaitResult?.invoke()
    }
}

Implementation of PermissionHandler with dependency https://github.com/sagar-viradiya/eazypermissions

class PermissionHandler(
    private val actualHandler: AppCompatActivity,
) {
    suspend fun requestPermissionsSuspend(requestCode: Int, permissions: Array<out String>): PermissionResult {
        return PermissionManager.requestPermissions(actualHandler, requestCode, *permissions)
    }

    fun requestPermissionsWithCallback(requestCode: Int, permissions: Array<out String>, onResult: (PermissionResult) -> Unit) {
        actualHandler.lifecycleScope.launch {
            onResult.invoke(PermissionManager.requestPermissions(actualHandler, requestCode, *permissions))
        }
    }
}

If you prefer a callback the second function works also.

Ludendorff answered 26/9, 2020 at 17:56 Comment(0)
B
2
//define permission in composable fun
val getPermission = rememberLauncherForActivityResult( 
    ActivityResultContracts.RequestPermission()
) { isGranted ->
    if (isGranted) {
       //permission accepted do somthing
    } else {
       //permission not accepted show message
    }
}
//i used SideEffect to launch permission request when screen recomposed
//you can call it inside a button click without SideEffect
SideEffect {
    getPermission.launch(Manifest.permission.READ_CONTACTS)
}

and if you wanted to request multiple permission use this:

ActivityResultContracts.RequestMultiplePermissions()
Backstairs answered 13/7, 2022 at 7:58 Comment(0)
S
2

Permission Sample UI

The rememberPermissionState(permission: String) API allows you to request a certain permission to the user and check for the status of the permission.

**Step1:**

A library which provides Android runtime permissions support for Jetpack Compose.

implementation 'com.google.accompanist:accompanist-permissions:0.24.13-rc'
..

**Step2:**

In our AndroidManifeastxml we need to declare permission (in this example we are going to request location permission)

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
..

**Step3:**

In our MainActivity.kt we are calling this permission request function

class MainActivity : ComponentActivity() {
    @OptIn(ExperimentalPermissionsApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Material3ComposeTheme {
                RequestPermission(permission = Manifest.permission.ACCESS_FINE_LOCATION)
            }
        }
    }
}
..

**Step4:**

In this SinglePermission.kt, we are going to request permission from user, if user already deny means we will show simple alert dialog info message otherwise will show custom full screen dialog.

package compose.material.theme

import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import com.google.accompanist.permissions.*

@ExperimentalPermissionsApi
@Composable
fun RequestPermission(
    permission: String,
    rationaleMessage: String = "To use this app's functionalities, you need to give us the permission.",
) {
    val permissionState = rememberPermissionState(permission)

    HandleRequest(
        permissionState = permissionState,
        deniedContent = { shouldShowRationale ->
            PermissionDeniedContent(
                rationaleMessage = rationaleMessage,
                shouldShowRationale = shouldShowRationale
            ) { permissionState.launchPermissionRequest() }
        },
        content = {
         /*   Content(
                text = "PERMISSION GRANTED!",
                showButton = false
            ) {}*/
        }
    )
}

@ExperimentalPermissionsApi
@Composable
fun HandleRequest(
    permissionState: PermissionState,
    deniedContent: @Composable (Boolean) -> Unit,
    content: @Composable () -> Unit
) {
    when (permissionState.status) {
        is PermissionStatus.Granted -> {
            content()
        }
        is PermissionStatus.Denied -> {
            deniedContent(permissionState.status.shouldShowRationale)
        }
    }
}

@Composable
fun Content(showButton: Boolean = true, onClick: () -> Unit) {
    if (showButton) {
        val enableLocation = remember { mutableStateOf(true) }
        if (enableLocation.value) {
            CustomDialogLocation(
                title = "Turn On Location Service",
                desc = "Explore the world without getting lost and keep the track of your location.\n\nGive this app a permission to proceed. If it doesn't work, then you'll have to do it manually from the settings.",
                enableLocation,
                onClick
            )
        }
    }
}

@ExperimentalPermissionsApi
@Composable
fun PermissionDeniedContent(
    rationaleMessage: String,
    shouldShowRationale: Boolean,
    onRequestPermission: () -> Unit
) {

    if (shouldShowRationale) {

        AlertDialog(
            onDismissRequest = {},
            title = {
                Text(
                    text = "Permission Request",
                    style = TextStyle(
                        fontSize = MaterialTheme.typography.headlineLarge.fontSize,
                        fontWeight = FontWeight.Bold
                    )
                )
            },
            text = {
                Text(rationaleMessage)
            },
            confirmButton = {
                Button(onClick = onRequestPermission) {
                    Text("Give Permission")
                }
            }
        )
        
    }
    else {
        Content(onClick = onRequestPermission)
    }

}
.. 

**Step 5:**

In this CustomDialogLocation.kt, we make custom dialog in android jetpack compose.

package compose.material.theme

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog

/*

This example demonstrates how to make custom dialog in android jetpack compose in android.

*  Button        : https://www.boltuix.com/2021/12/button_25.html
*  Clip Modifier : https://www.boltuix.com/2021/12/clip-modifier_24.html
*  Alert Dialog  : https://www.boltuix.com/2021/12/alert-dialog_25.html
*  Column        : https://www.boltuix.com/2021/12/column-layout_25.html
*  Box           : https://www.boltuix.com/2021/12/box-layout_25.html
*  Type.kt       : https://www.boltuix.com/2021/12/typography_27.html
*  Color.kt      : https://www.boltuix.com/2022/05/google-material-design-color.html
*  Dialog        : https://www.boltuix.com/2022/07/compose-custom-animating-dialog.html
* */

@Composable
fun CustomDialogLocation(
    title: String? = "Message",
    desc: String? = "Your Message",
    enableLocation: MutableState<Boolean>,
    onClick: () -> Unit
) {
    Dialog(
        onDismissRequest = { enableLocation.value = false}
    ) {
        Box(
            modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)
                // .width(300.dp)
                // .height(164.dp)
                .background(
                    color = MaterialTheme.colorScheme.onPrimary,
                    shape = RoundedCornerShape(25.dp,5.dp,25.dp,5.dp)
                )
                .verticalScroll(rememberScrollState())

        ) {
            Column(
                modifier = Modifier.padding(16.dp),
                horizontalAlignment = Alignment.CenterHorizontally
            ) {


                //.........................Image: preview
                Image(
                    painter = painterResource(id = R.drawable.permission_location),
                    contentDescription = null,
                    contentScale = ContentScale.Fit,
                    /*  colorFilter  = ColorFilter.tint(
                          color = MaterialTheme.colorScheme.primary
                      ),*/
                    modifier = Modifier
                        .padding(top = 5.dp)
                        .height(320.dp)
                        .fillMaxWidth(),

                    )
                //.........................Spacer
                //.........................Text: title
                Text(
                    text = title!!,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                      //  .padding(top = 5.dp)
                        .fillMaxWidth(),
                    letterSpacing = 2.sp,
                    fontWeight = FontWeight.Bold,
                    style = MaterialTheme.typography.titleLarge,
                    color = MaterialTheme.colorScheme.primary,
                )
                Spacer(modifier = Modifier.height(8.dp))
                //.........................Text : description
                Text(
                    text = desc!!,
                    textAlign = TextAlign.Center,
                    modifier = Modifier
                        .padding(top = 10.dp, start = 25.dp, end = 25.dp)
                        .fillMaxWidth(),
                    letterSpacing = 1.sp,
                    style = MaterialTheme.typography.bodyLarge,
                    color = MaterialTheme.colorScheme.primary,
                )
                //.........................Spacer
                Spacer(modifier = Modifier.height(24.dp))

                //.........................Button : OK button
                val cornerRadius = 16.dp
                val gradientColors = listOf(Color(0xFFff669f), Color(0xFFff8961))
                val roundedCornerShape = RoundedCornerShape(topStart = 30.dp,bottomEnd = 30.dp)

                Button(
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(start = 32.dp, end = 32.dp),
                    onClick=onClick,
                    contentPadding = PaddingValues(),
                    colors = ButtonDefaults.buttonColors(
                        containerColor = Color.Transparent
                    ),
                    shape = RoundedCornerShape(cornerRadius)
                ) {

                    Box(
                        modifier = Modifier
                            .fillMaxWidth()
                            .background(
                                brush = Brush.horizontalGradient(colors = gradientColors),
                                shape = roundedCornerShape
                            )
                            .padding(horizontal = 16.dp, vertical = 8.dp),
                        contentAlignment = Alignment.Center
                    ) {
                        Text(
                            text ="Enable",
                            fontSize = 20.sp,
                            color = Color.White
                        )
                    }
                }


                //.........................Spacer
                Spacer(modifier = Modifier.height(12.dp))


                TextButton(onClick = {
                    enableLocation.value = false
                }) { Text("Cancel", style = MaterialTheme.typography.labelLarge) }


                Spacer(modifier = Modifier.height(24.dp))

            }
        }
    }
}

Get source code & video: https://www.boltuix.com/2022/07/requesting-location-permission-in.html

Spragens answered 19/7, 2022 at 4:5 Comment(0)
S
2

There are two ways to get runtime permissions in jetpack compose.

  • Using activity result
  • Using the accompanist permissions library

Runtime permission using activity result

The first step is to define the permission in the manifest.xml file.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera"/>

Create an activity result launcher to request the permission we defined. Once it’s launched it will return the result whether the permission is granted or not.

val permissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) {
        if (it) {
            Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
            cameraLauncher.launch(uri)
        } else {
            Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
        }

    }

Checking Permission

Before launching the request for permission, we need to check whether the permission is granted or not. If it’s already granted we can proceed with our regular flow. If permission is not provided, then we need to launch the permission request with the permission we wanted.

val permissionCheckResult = ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)

            if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
                cameraLauncher.launch(uri)
            } else {
  permissionLauncher.launch(android.Manifest.permission.CAMERA)
            }

finally, the code for the runtime permission using the activity result will be like the below,

val context = LocalContext.current
    val file = context.createImageFile()
    val uri = FileProvider.getUriForFile(
        Objects.requireNonNull(context),
        BuildConfig.APPLICATION_ID + ".provider", file
    )

    var capturedImageUri by remember {
        mutableStateOf<Uri>(Uri.EMPTY)
    }

    val cameraLauncher =
        rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
            capturedImageUri = uri
        }
    val permissionLauncher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) {
        if (it) {
            Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
            cameraLauncher.launch(uri)
        } else {
            Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
        }

    }

    Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(12.dp)) {
        Button(onClick = {
            val permissionCheckResult = ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)

            if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
                cameraLauncher.launch(uri)
            } else {
                // Request a permission
                permissionLauncher.launch(android.Manifest.permission.CAMERA)
            }

        }) {
            Text(text = "Open Camera")
        }

        if (capturedImageUri.path?.isNotEmpty() == true) {
            Image(
                modifier = Modifier
                    .padding(16.dp, 8.dp)
                    .fillMaxWidth()
                    .size(400.dp),
                painter = rememberImagePainter(capturedImageUri),
                contentDescription = null
            )
        }
    }

the output of the above code,

Runtime permission screenshot

Sinker answered 5/12, 2022 at 16:51 Comment(0)
C
2

Accompanist Permissions isn't very cool, as it doesn't allow you to be notified of permanently rejected permissions. In addition, it still requires the @ExperimentalPermissionsApi annotation to be used everywhere, despite Google recently announcing that Accompanist Permissions will not be getting new features and it's smth like stable. This forced me to implement getting the permissions myself.

I took only what I needed from the current version of Accompanist Permissions and wrote it into a single file, complete with the ability to perform actions on different user actions - for when the user granted permissions, when they didn't, and when they denied completely. For this purpose I have highlighted the following aspects:

  1. All answers based on shouldShowRationale won't work well because their implementation is different on different versions of Android. I was able to implement this for Android < 11, but it didn't work with newer versions due to differences in the SDK.
  2. Since Android does not provide data that permanent reject has occurred, we have to find out about it based on an external factor. To do this we can calculate the time difference between request launch and request completed - if it is less than 200ms, then there was no user click, which means the system itself refused to show the dialogue and we need to direct the user to the settings.
  3. In many cases you need to provide several permissions at once, so I did not copy rememberPermissionState from Accompanist and copypasted only rememberMultiplePermissionsState (now it is called rememberPermissionsState).
  4. If a certain feature of the application allows to work only partially (for example, recording video without sound when there is no access to the microphone), it is more reasonable to grant access step by step (first camera, then microphone). In this case it is better to make two different MultiplePermissionsState (using rememberPermissionsState), granting permission for each separate PermissionState will be inconvenient and complicated. So I commented out some code from Accompanist that is responsible for granting permissions separately, and you can remove it (or uncomment it if I didn't consider your usecase).

So this is just a doped Accompanist Permissions with the ability to have different actions on different permission statuses. To use my implementation in an application, create a file (e.g. PermissionHandler.kt) and add this code:

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver

private const val PERMISSIONS_CLICK_DELAY_MS = 200

private var lastPermissionRequestLaunchedAt = 0L

@Composable
fun rememberPermissionsState(
    permissions: List<String>,
    onGrantedAction: () -> Unit = {},
    onDeniedAction: (List<String>) -> Unit = {},
    onPermanentlyDeniedAction: (List<String>) -> Unit = {}
): MultiplePermissionsState {
    // Create mutable permissions that can be requested individually
    val mutablePermissions = rememberMutablePermissionsState(permissions)

    // Refresh permissions when the lifecycle is resumed.
    PermissionsLifecycleCheckerEffect(mutablePermissions)

    val multiplePermissionsState = remember(permissions) {
        MultiplePermissionsState(mutablePermissions)
    }

    // Remember RequestMultiplePermissions launcher and assign it to multiplePermissionsState
    val launcher = rememberLauncherForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissionsResult ->
        multiplePermissionsState.updatePermissionsStatus(permissionsResult)

        if (!permissionsResult.containsValue(false)) {
            onGrantedAction()
        } else if (System.currentTimeMillis() - PERMISSIONS_CLICK_DELAY_MS
            < lastPermissionRequestLaunchedAt) {
            onPermanentlyDeniedAction(permissionsResult.filter { !it.value }.keys.toList())
        } else {
            onDeniedAction(permissionsResult.filter { !it.value }.keys.toList())
        }
    }
    DisposableEffect(multiplePermissionsState, launcher) {
        multiplePermissionsState.launcher = launcher
        onDispose {
            multiplePermissionsState.launcher = null
        }
    }

    return multiplePermissionsState
}

@Composable
private fun rememberMutablePermissionsState(
    permissions: List<String>
): List<PermissionState> {
    val context = LocalContext.current
    val activity = context.findActivity()

    val mutablePermissions: List<PermissionState> = remember(permissions) {
        return@remember permissions.map { PermissionState(it, context, activity) }
    }
    // Update each permission with its own launcher
//  for (permissionState in mutablePermissions) {
//      key(permissionState.name) {
//          // Remember launcher and assign it to the permissionState
//          val launcher = rememberLauncherForActivityResult(
//              ActivityResultContracts.RequestPermission()
//          ) {
//              permissionState.refreshPermissionStatus()
//          }
//          DisposableEffect(launcher) {
//              permissionState.launcher = launcher
//              onDispose {
//                  permissionState.launcher = null
//              }
//          }
//      }
//  }

    return mutablePermissions
}

@Composable
private fun PermissionsLifecycleCheckerEffect(
    permissions: List<PermissionState>,
    lifecycleEvent: Lifecycle.Event = Lifecycle.Event.ON_RESUME
) {
    // Check if the permission was granted when the lifecycle is resumed.
    // The user might've gone to the Settings screen and granted the permission.
    val permissionsCheckerObserver = remember(permissions) {
        LifecycleEventObserver { _, event ->
            if (event == lifecycleEvent) {
                for (permission in permissions) {
                    // If the permission is revoked, check again. We don't check if the permission
                    // was denied as that triggers a process restart.
                    if (permission.status != PermissionStatus.Granted) {
                        permission.refreshPermissionStatus()
                    }
                }
            }
        }
    }
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    DisposableEffect(lifecycle, permissionsCheckerObserver) {
        lifecycle.addObserver(permissionsCheckerObserver)
        onDispose { lifecycle.removeObserver(permissionsCheckerObserver) }
    }
}



@Stable
sealed interface PermissionStatus {
    data object Granted : PermissionStatus
    data class Denied(
        val shouldShowRationale: Boolean
    ) : PermissionStatus
}

val PermissionStatus.isGranted: Boolean
    get() = this == PermissionStatus.Granted

val PermissionStatus.shouldShowRationale: Boolean
    get() = when (this) {
        PermissionStatus.Granted -> false
        is PermissionStatus.Denied -> shouldShowRationale
    }

@Stable
class PermissionState(
    /**
     * The full name of the permission in the Android SDK,
     * e.g. android.permission.ACCESS_FINE_LOCATION
     */
    val name: String,
    private val context: Context,
    private val activity: Activity
) {
    var status: PermissionStatus by mutableStateOf(getPermissionStatus())
        private set

//  fun launchPermissionRequest() {
//      launcher?.launch(name) ?: throw IllegalStateException("ActivityResultLauncher cannot be null")
//  }
//
//  internal var launcher: ActivityResultLauncher<String>? = null

    internal fun refreshPermissionStatus() {
        status = getPermissionStatus()
    }

    private fun getPermissionStatus(): PermissionStatus {
        val hasPermission = context.checkPermission(name)
        return if (hasPermission) {
            PermissionStatus.Granted
        } else {
            PermissionStatus.Denied(activity.shouldShowRationale(name))
        }
    }
}

/**
 * A state object that can be hoisted to control and observe multiple permission status changes.
 *
 * In most cases, this will be created via [rememberPermissionsState].
 *
 * @param mutablePermissions list of mutable permissions to control and observe.
 */
@Stable
class MultiplePermissionsState(
    private val mutablePermissions: List<PermissionState>
) {
    val permissions: List<PermissionState> = mutablePermissions

    val revokedPermissions: List<PermissionState> by derivedStateOf {
        permissions.filter { it.status != PermissionStatus.Granted }
    }

    val allPermissionsGranted: Boolean by derivedStateOf {
        permissions.all { it.status.isGranted } || // Up to date when the lifecycle is resumed
            revokedPermissions.isEmpty() // Up to date when the user launches the action
    }

    val shouldShowRationale: Boolean by derivedStateOf {
        permissions.any { it.status.shouldShowRationale }
    }

    /**
     * Request the [permissions] to the user and use actions declared in [rememberPermissionsState].
     *
     * This should always be triggered from non-composable scope, for example, from a side-effect
     * or a non-composable callback. Otherwise, this will result in an IllegalStateException.
     *
     * This triggers a system dialog that asks the user to grant or revoke the permission.
     * Note that this dialog might not appear on the screen if the user doesn't want to be asked
     * again or has denied the permission multiple times.
     * This behavior varies depending on the Android level API.
     */
    fun launchPermissionRequestsAndAction() {
        lastPermissionRequestLaunchedAt = System.currentTimeMillis()

        launcher?.launch(
            permissions.map { it.name }.toTypedArray()
        ) ?: throw IllegalStateException("ActivityResultLauncher cannot be null")
    }

    internal var launcher: ActivityResultLauncher<Array<String>>? = null

    internal fun updatePermissionsStatus(permissionsStatus: Map<String, Boolean>) {
        // Update all permissions with the result
        for (permission in permissionsStatus.keys) {
            mutablePermissions.firstOrNull { it.name == permission }?.apply {
                permissionsStatus[permission]?.let {
                    this.refreshPermissionStatus()
                }
            }
        }
    }
}

private fun Activity.shouldShowRationale(permission: String): Boolean {
    return ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
}

private fun Context.checkPermission(permission: String): Boolean {
    return ContextCompat.checkSelfPermission(this, permission) ==
        PackageManager.PERMISSION_GRANTED
}

/**
 * Find the closest Activity in a given Context.
 */
private fun Context.findActivity(): Activity {
    var context = this
    while (context is ContextWrapper) {
        if (context is Activity) return context
        context = context.baseContext
    }
    throw IllegalStateException("Permissions should be called in the context of an Activity")
}

Next, create an object in your @Composable using rememberPermissionsState in which you declare the required permissions and actions. For example, to use Bluetooth LE I use this:

val context = LocalContext.current
val permissions = rememberPermissionsState(
    permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        listOf(
            Manifest.permission.BLUETOOTH_SCAN,
            Manifest.permission.BLUETOOTH_CONNECT
        )
    } else {
        listOf(
            Manifest.permission.ACCESS_FINE_LOCATION
        )
    },
    onGrantedAction = {
        // find the device and connect to it
    },
    onPermanentlyDeniedAction = {
        context.goToSettings(it)
    }
)

Hint: add this extension function to the project to go to the settings:

fun Context.goToSettings(revokedPermissions: List<String> = emptyList()) {
    if (revokedPermissions.isNotEmpty()) {
        Toast.makeText(
            this,
            this.getString(
                R.string.provide_permissions_via_settings, // add to strings.xml: Permission(s) %s must be granted through settings
                revokedPermissions.joinToString(
                    transform = { it.replace("android.permission.", "") },
                    separator = ", "
                )
            ),
            Toast.LENGTH_LONG
        ).show()
    }

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

All you have to do is call permissions.launchPermissionRequestsAndAction() in onClick or something similar to invoke the dialog box and make the actions work.

With this implementation, you can remove the Accompanist Permissions dependency if you already have it in your project.

Tested on a slow device with Android 6.0 and fast ones with Android 10, 12, 13. In all cases the solution works perfectly.

Charybdis answered 2/9, 2023 at 9:6 Comment(2)
Can't you just use a listener instead of copying the Accompanist code?Sennight
@Sennight you can add lastPermissionRequestLaunchedAt to your UI code, change it on every permissions.launch() and add identical checks to onPermissionResult every time you need perms in app, but that leads to writing a lot of boilerplate, doesn't it? Also I couldn't override Accompanist functions or write extra class properly, so I just copypasted and modified that code. Anyway, the main feature is calculating the time difference, I came up with this implementation myself and have not seen it elsewhere, and you can implement it in many ways if you don't like mine.Charybdis
D
1
private const val PERMISSIONS_REQUEST_CODE = 10
private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)

@Composable
fun PermissionButton() {

    val context = ContextAmbient.current

    Button(onClick = {
        if (!hasPermissions(context)) {
            requestPermissions(
                context as Activity,
                PERMISSIONS_REQUIRED,
                PERMISSIONS_REQUEST_CODE
            )
        }
    }
    ) {}
}

fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
    ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
Dronski answered 9/3, 2020 at 22:36 Comment(2)
I would not assume that ContextAmbient.current will necessarily point to an Activity.Kat
During recomposition you will lose your permission state.Coverture
A
1

You can request multiples permissions.

class MainActivity : ComponentActivity() {

    private val neededPermissions = arrayOf(
        Manifest.permission.ACCESS_COARSE_LOCATION,
        Manifest.permission.ACCESS_FINE_LOCATION
    )

    @OptIn(ExperimentalMaterialApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MfmTheme {
                val launcher = rememberLauncherForActivityResult(
                    contract = ActivityResultContracts.RequestMultiplePermissions()
                ) { maps ->
                    val granted = maps.values.reduce { acc, next -> (acc && next) }
                    if (granted) {
                        // all permission granted
                    } else {
                        // Permission Denied: Do something
                    }
                    // You can check one by one
                    maps.forEach { entry ->
                        Log.i("Permission = ${entry.key}", "Enabled ${entry.value}")
                    }
                }
                val context = LocalContext.current
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background,
                    onClick = {
                        when {
                            hasPermissions(context, *neededPermissions) -> {
                                // All permissions granted
                               
                            }
                            else -> {
                                // Request permissions
                                launcher.launch(neededPermissions)
                            }
                        }
                    }
                ) {
                    Greeting("Android")
                }
            }
        }
    }

    private fun hasPermissions(context: Context, vararg permissions: String): Boolean =
        permissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    MfmTheme {
        Greeting("Android")
    }
}
Accusal answered 25/2, 2022 at 14:26 Comment(0)
G
0

I've written an answer that uses accompanist lib, using Jetpack compose, here: https://mcmap.net/q/93324/-how-do-we-distinguish-never-asked-from-stop-asking-in-android-m-39-s-runtime-permissions

it also able to differentiate between first-time permission ask and should-not-ask-permission which is bit tricky currently using the android API:

@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")
            }
        }
    }
}

enter image description here

Groceryman answered 4/10, 2023 at 18:53 Comment(0)
W
-1

According to https://google.github.io/accompanist/permissions/

@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")
            }
        }
    }
}
Winniewinnifred answered 25/5, 2023 at 13:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.