In my app I have to ask for permission to write to external storage in order to save bitmap for ability to share it. Now looking at the code below, whenever I call it, alert dialog appears, but no permission request appears from Android itself. All I can see in the log is Let's launch permission request. How to appropriately show the permission window from Android to get that permission? Thank you.
#ExperimentalPermissionsApi
#Composable
fun Permission(
permission: String = android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
rationale: String = "This permission is important for this app. Please grant the permission.",
permissionNotAvailableContent: #Composable () -> Unit = { },
content: #Composable () -> Unit = { }
) {
val permissionState = rememberPermissionState(permission)
PermissionRequired(
permissionState = permissionState,
permissionNotGrantedContent = {
Rationale(
text = rationale,
onRequestPermission = {
println("Let's launch permission request.")
permissionState.launchPermissionRequest()
}
)
},
permissionNotAvailableContent = permissionNotAvailableContent,
content = content
)
}
#Composable
private fun Rationale(
text: String,
onRequestPermission: () -> Unit
) {
AlertDialog(
onDismissRequest = { /* Don't */ },
title = {
Text(text = "Permission request")
},
text = {
Text(text)
},
confirmButton = {
Button(onClick = onRequestPermission) {
Text("Ok")
}
}
)
}
Just so that no one misses the possible solution as mentioned by ADM in the comments.
The Solution by ADM: Check if you have added the permission in the manifest
Related
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()
}
For those looking for a similar scenario. To handle permissions in jetpack compose properly I followed the below steps:
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.
If it's not granted we will check the case for shouldShouldRational is false. If it's false we have two scenarios to check because the shouldShowRationale is false in two cases. First when the permission is permanently denied. Second when permission is not even asked at once. For managing, if permission is permanently denied or not I have used shared preferences. I have written extension functions for that which tell us if the permission is asked for once.
For the above first case, I'll show the snack bar telling the user that you permanently denied the permission open settings to allow the permission. For the above second case, I will launch the request for showing the system permission dialog and update the shared preference via the extension function.
And for the case in which shouldShowRationale is true. I'll show a snack bar to the user explaining why permission is required. Along with the action, to again request the system permission dialog.
Finally whenever permission is granted I can do the work needed in the rememberPermissionState callback.
val context = LocalContext.current
val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()
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
) { granted ->
if (granted) {
getImageLauncher.launch("image/*")
}
}
Button Composable
SecondaryOutlineButton(
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
buttonText = stringResource(
id = R.string.upload_image
),
buttonCornerRadius = 8.dp,
) {
// This is onClick Callback of My Custom Composable Button
with(readStoragePermissionState) {
when {
// If Permission is Already Granted to the Application
status.isGranted -> {
getImageLauncher.launch("image/*")
}
// If Permission is Asked First or Denied Permanently
!status.shouldShowRationale -> {
context.isPermissionAskedForFirstTime(
permission = permission
).also { result ->
if (result) {
launchPermissionRequest()
context.permissionAskedForFirsTime(
permission = permission
)
} else {
coroutineScope.launch {
with(scaffoldState.snackbarHostState) {
val snackbarResult =
showSnackbar(
message = context.getString(
R.string.read_storage_denied
),
actionLabel = context.getString(
R.string.settings
)
)
when (snackbarResult) {
// Open this Application General Settings.
SnackbarResult.ActionPerformed -> {
context.openApplicationSettings()
}
SnackbarResult.Dismissed -> Unit
}
}
}
}
}
}
// If You should Tell User Why this Permission Required
status.shouldShowRationale -> {
coroutineScope.launch {
with(scaffoldState.snackbarHostState) {
val snackbarResult = showSnackbar(
message = context.getString(
R.string.read_storage_rational
),
actionLabel = context.getString(
R.string.allow
)
)
when (snackbarResult) {
// Request for System Permission Dialog Again.
SnackbarResult.ActionPerformed -> {
launchPermissionRequest()
}
SnackbarResult.Dismissed -> Unit
}
}
}
}
else -> Unit
}
}
}
Extension Functions
fun Context.isPermissionAskedForFirstTime(
permission: String
): Boolean {
return getSharedPreferences(
packageName, MODE_PRIVATE
).getBoolean(permission, true)
}
fun Context.permissionAskedForFirsTime(
permission: String
) {
getSharedPreferences(
packageName, MODE_PRIVATE
).edit().putBoolean(permission, false).apply()
}
fun Context.openApplicationSettings() {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.parse("package:${packageName}")
})
}
I'm using implementation "com.google.accompanist:accompanist-permissions:0.25.0"
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
}
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.
i'm wanting to make a network call when location access has been granted. so i'm using LaunchedEffect(key1 = location.value){...} to decide when to make that network call to recompose, but facing some issues.
upon initial launch user is greeted with the location request (either precise or coarse). during this, the Toast.makeText(context, "Allow location access in order to see image", Toast.LENGTH_SHORT).show() get's called twice and shows up twice. when the user selects an option from the location request dialog, i would assume location.value would end up changing and viewModel.getImage(location.value!!) get's called. debugging through this, that all happens, but the image doesn't end up showing. i got it to work sometimes by force closing the app, then opening it again, then the image shows up. any insights? here is the location code in that same file:
val locationLiveData = LocationLiveData(context)
val location = locationLiveData.observeAsState()
val requestSinglePermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
when {
it.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
locationLiveData.startLocationUpdates()
}
it.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
locationLiveData.startLocationUpdates()
} else -> {
Toast.makeText(context, "Allow location access", Toast.LENGTH_SHORT).show()
}
}
}
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PermissionChecker.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PermissionChecker.PERMISSION_GRANTED) {
locationLiveData.startLocationUpdates()
} else {
// true so we execute once not again when we compose or so
LaunchedEffect(key1 = true) {
requestSinglePermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION))
}
}
EDIT 2
LocationLiveData
class LocationLiveData(var context: Context): LiveData<LocationDetails>() {
// used to get last known location
private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
// We have at least 1 observer or 1 component looking at us
// here we can get the last known location of the device
override fun onActive() {
super.onActive()
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
fusedLocationClient.lastLocation.addOnSuccessListener {
setLocationData(it)
}
}
// no one is looking at this live data anymore
override fun onInactive() {
super.onInactive()
fusedLocationClient.removeLocationUpdates(locationCallback)
}
internal fun startLocationUpdates() {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}
private fun setLocationData(location: Location) {
value = LocationDetails(longitude = location.longitude.toString(), latitude = location.latitude.toString())
}
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(p0: LocationResult) {
super.onLocationResult(p0)
for (location in p0.locations) {
setLocationData(location)
}
}
}
companion object {
private const val ONE_MINUTE: Long = 60_000
val locationRequest: LocationRequest = LocationRequest.create().apply {
interval = ONE_MINUTE
fastestInterval = ONE_MINUTE / 4
priority = Priority.PRIORITY_HIGH_ACCURACY
}
}
}
COMPOSABLE
#RequiresApi(Build.VERSION_CODES.N)
#Composable
fun HomeScreen(viewModel: HomeScreenViewModel = hiltViewModel(), navigateToAuthScreen: () -> Unit, navigateToAddImage: () -> Unit){
var text by remember { mutableStateOf(TextFieldValue("")) }
val context = LocalContext.current
val locationLiveData = remember { LocationLiveData(context) }
val location = locationLiveData.observeAsState()
val requestSinglePermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
when {
it.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
locationLiveData.startLocationUpdates()
}
it.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
locationLiveData.startLocationUpdates()
}
}
}
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PermissionChecker.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PermissionChecker.PERMISSION_GRANTED) {
locationLiveData.startLocationUpdates()
} else {
// true so we execute once not again when we compose or so
LaunchedEffect(key1 = true) {
requestSinglePermissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION))
}
}
Scaffold( topBar = {
HomeScreenTopBar()
},
floatingActionButton = {
FloatingActionButton(onClick = {
if (location.value != null) {
navigateToAddImageScreen()
} else {
Toast.makeText(context, "allow location access to add image", Toast.LENGTH_SHORT).show()
}
},
backgroundColor = MaterialTheme.colors.primary
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Save note"
)
}
}) {innerPadding ->
Column(modifier = Modifier
.fillMaxSize()
.padding(innerPadding)) {
LaunchedEffect(key1 = location.value) {
if (location.value != null) {
viewModel.getListings(location.value!!)
} else {
Toast.makeText(context, "Allow location access in order to see image", Toast.LENGTH_SHORT).show()
}
}
}
This line
val locationLiveData = LocationLiveData(context)
creates a new LocationLiveData instance on every recomposition.
You have to remember the same instance of LocationLiveData across recompositions, if you want it to hold any state or view state.
Change it to
// remember LocationLiveData across recompositions
// this does not survive configuration changes, nor other short Activity restarts
val locationLiveData = remember { LocationLiveData(context) }
As also mentioned in the code comment above, now locationLiveData will survive re-compositions, but it will still get reset on:
every configuration change (examples include but are not limited to:
orientation change, light/dark mode change, language change...)
every short Activity restart, caused by the system in some cases
also application death (but that is somewhat expected)
To solve 1. and 2. you can use rememberSaveable that can save primitive and other Parcelable types automatically (in your case you can also implement the Saver interface), to solve 3. you have to save the state to any of the persistent storage options and then restore as needed.
To learn more about working with state in Compose see the documentation section on Managing State. This is fundamental information to be able to work with state in Compose and trigger recompositions efficiently. It also covers the fundamentals of state hoisting. If you prefer a coding tutorial here is the code lab for State in Jetpack Compose.
An introduction to handling the state as the complexity increases is in the video from Google about Using Jetpack Compose's automatic state observation.
I'm trying to integrate One Tap Sign in with Google into my app which I'm building with Jetpack Compose. I'm using startIntentSenderForResult to launch an intent, but now the problem is that I'm unable to receive activity result from my composable function. I'm using rememberLauncherForActivityResult to get the result from an intent but still not getting anywhere. Any solutions?
LoginScreen
#Composable
fun LoginScreen() {
val activity = LocalContext.current as Activity
val activityResult = remember { mutableStateOf<ActivityResult?>(null) }
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
val oneTapClient = Identity.getSignInClient(activity)
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d("LOG", idToken)
} else {
Log.d("LOG", "Null Token")
}
Log.d("LOG", "ActivityResult")
if (result.resultCode == Activity.RESULT_OK) {
activityResult.value = result
}
}
activityResult.value?.let { _ ->
Log.d("LOG", "ActivityResultValue")
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
GoogleButton(
onClick = {
signIn(
activity = activity
)
}
)
}
}
fun signIn(
activity: Activity
) {
val oneTapClient = Identity.getSignInClient(activity)
val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build()
)
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
oneTapClient.beginSignIn(signInRequest)
.addOnSuccessListener(activity) { result ->
try {
startIntentSenderForResult(
activity, result.pendingIntent.intentSender, ONE_TAP_REQ_CODE,
null, 0, 0, 0, null
)
} catch (e: IntentSender.SendIntentException) {
Log.e("LOG", "Couldn't start One Tap UI: ${e.localizedMessage}")
}
}
.addOnFailureListener(activity) { e ->
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d("LOG", e.message.toString())
}
}
You aren't actually calling launch on the launcher you create, so you would never get a result back there.
Instead of using the StartActivityForResult contract, you need to use the StartIntentSenderForResult contract - that's the one that takes an IntentSender like the one you get back from your beginSignIn method.
This means your code should look like:
#Composable
fun LoginScreen() {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
) { result ->
if (result.resultCode != Activity.RESULT_OK) {
// The user cancelled the login, was it due to an Exception?
if (result.data?.action == StartIntentSenderForResult.ACTION_INTENT_SENDER_REQUEST) {
val exception: Exception? = result.data?.getSerializableExtra(StartIntentSenderForResult.EXTRA_SEND_INTENT_EXCEPTION)
Log.e("LOG", "Couldn't start One Tap UI: ${e?.localizedMessage}")
}
return#rememberLauncherForActivityResult
}
val oneTapClient = Identity.getSignInClient(context)
val credential = oneTapClient.getSignInCredentialFromIntent(result.data)
val idToken = credential.googleIdToken
if (idToken != null) {
// Got an ID token from Google. Use it to authenticate
// with your backend.
Log.d("LOG", idToken)
} else {
Log.d("LOG", "Null Token")
}
}
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
// Create a scope that is automatically cancelled
// if the user closes your app while async work is
// happening
val scope = rememberCoroutineScope()
GoogleButton(
onClick = {
scope.launch {
signIn(
context = context,
launcher = launcher
)
}
}
)
}
}
suspend fun signIn(
context: Context,
launcher: ActivityResultLauncher<IntentSenderRequest>
) {
val oneTapClient = Identity.getSignInClient(context)
val signInRequest = BeginSignInRequest.builder()
.setGoogleIdTokenRequestOptions(
BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
.setSupported(true)
// Your server's client ID, not your Android client ID.
.setServerClientId(CLIENT_ID)
// Only show accounts previously used to sign in.
.setFilterByAuthorizedAccounts(true)
.build()
)
// Automatically sign in when exactly one credential is retrieved.
.setAutoSelectEnabled(true)
.build()
try {
// Use await() from https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-play-services
// Instead of listeners that aren't cleaned up automatically
val result = oneTapClient.beginSignIn(signInRequest).await()
// Now construct the IntentSenderRequest the launcher requires
val intentSenderRequest = IntentSenderRequest.Builder(result.pendingIntent).build()
launcher.launch(intentSenderRequest)
} catch (e: Exception) {
// No saved credentials found. Launch the One Tap sign-up flow, or
// do nothing and continue presenting the signed-out UI.
Log.d("LOG", e.message.toString())
}
}
I wrote utility functions to request/check permissions in Composables (using CompositionLocal).
data class PermissionHandlerValue(
val hasPermission: (String) -> Boolean,
val hasPermissions: (Array<out String>) -> Array<Boolean>,
val requestPermission: (String) -> Unit,
val requestPermissions: (Array<out String>) -> Unit
)
val LocalPermissionHandler = compositionLocalOf<PermissionHandlerValue> { error("No implementation provided!") }
#Composable
fun ProvidePermissionHandler(content: #Composable () -> Unit) {
CompositionLocalProvider(LocalPermissionHandler provides permissionHandlerImpl()) {
content()
}
}
#Composable
fun permissionHandlerImpl(): PermissionHandlerValue {
val context = LocalContext.current
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {}
val hasPermission: (String) -> Boolean = { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED }
val hasPermissions: (Array<out String>) -> Array<Boolean> = { it.map { permission -> hasPermission(permission) }.toTypedArray() }
val requestPermission: (String) -> Unit = { launcher.launch(arrayOf(it)) }
val requestPermissions: (Array<out String>) -> Unit = { launcher.launch(it) }
return PermissionHandlerValue(hasPermission, hasPermissions, requestPermission, requestPermissions)
}
#Composable
fun RequirePermission(permission: String, fallback: (#Composable () -> Unit)? = null, content: #Composable () -> Unit) {
val permissionHandler = LocalPermissionHandler.current
if (permissionHandler.hasPermission(permission))
content()
else if (fallback != null)
fallback()
}
It works fine, I can request and check permissions. The problem is that its not reactive, here's an example:
setContent {
ProvidePermissionHandler {
val permissionHandler = LocalPermissionHandler.current
RequirePermission(
permission = Manifest.permission.READ_CONTACTS,
fallback = {
Button(onClick = { permissionHandler.requestPermission(Manifest.permission.READ_CONTACTS)
}) {
Text("Request permission")
}
}
) {
ContactsList()
}
}
}
This composable(RequirePermission) will only render ContactsList if the Manifest.permission.READ_CONTACTS was granted, Otherwise the fallback component is rendered with a button that when clicked will request the permission.
After permissionHandler.requestPermission() is called and I grant the permission on the screen the fallback still shows, instead of the ContactsList (I have to re-open the app to show it).
Basically the condition in RequirePermission() is not checked again because there is no recomposition. How can I force RequirePermission() to recompose?
I was trying to find out how to request permissions in Jetpack Compose. Found an article in official documentation, but I couldn't figure out how to use it in my case; there's also an answer on Stack Overflow, but I simply couldn't understand it.
I will appreciate if you show some of your examples with explanation, or help me understand the code from the answer that I mentioned.
For my case, it was quite simple, I just made a composable and called it in my MainActivity like this, in setContent:
checkNotificationPolicyAccess(notificationManager, this)
Basically, if the permission is not granted, I show a Dialog.
#Composable
fun checkNotificationPolicyAccess(
notificationManager: NotificationManager,
context: Context
): Boolean {
if (notificationManager.isNotificationPolicyAccessGranted) {
return true
} else {
NotificationPolicyPermissionDialog(context)
}
return false
}
OK, I understood it.
You need to implement this composable and use it when you need to use a feature that needs the permissions: you pass in an array of permissions, a requestCode (any Int), and two lambdas with composables: onGranted, which is used when the permissions were granted, and onDenied composable in the other case.
#Composable
fun PermissionsRequest(
permissions: Array<out String>,
requestCode: Int,
onGranted: #Composable () -> Unit,
onDenied: #Composable () -> Unit,
onDeniedPermanently: (#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.requestPermissions(requestCode, permissions))
}
when (permissionResult) {
is PermissionResult.PermissionGranted -> onGranted()
is PermissionResult.PermissionDenied -> onDenied()
is PermissionResult.PermissionDeniedPermanently -> onDeniedPermanently?.invoke()
is PermissionResult.ShowRational -> rational?.invoke()
null -> awaitResult?.invoke()
}
}
Also you need to implement an ambient. As I understand it, it is used to pass the value down the composables children. In our case AmbientPermissionHandler is going to be passed with its value — PermissionHandler — to the PermissionsRequest from Providers composable.
val AmbientPermissionHandler = ambientOf<PermissionHandler>()
The PermissionHandler implementation that would be passed to PermissionRequest composable as an AmbientPermissionHandler using Providers.
class PermissionHandler(private val context: AppCompatActivity) {
suspend fun requestPermissions(
requestCode: Int,
permissions: Array<out String>
): PermissionResult {
return PermissionManager.requestPermissions(context, requestCode, *permissions)
}
}
And then you use it like this:
class MainActivity : AppCompatActivity() {
private val permissionHandler = PermissionHandler(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Providers(
AmbientPermissionHandler provides permissionHandler
) {
PermissionsRequest(
permissions = arrayOf(Manifest.permission.READ_SMS),
requestCode = PERMISSION_REQUEST_CODE,
onGranted = { /* Here goes the composables when the permission is granted */ },
onDenied = { /* Is used when the permission is denied */ }
)
}
}
}
}
Initialize PermissionHandler in MainActivity and then provide it in Providers inside the setContent.
To use PermissionManager and LaunchedEffect you need those dependencies:
implementation 'com.sagar:coroutinespermission:2.0.3'
implementation 'androidx.compose.runtime:runtime:1.0.0-alpha11'
And thanks to 2jan222 for the sample code.
you don't need any additional library for request permission
//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.YOUR_PERMISSION_REQEUST)
}
and if you wanted to request multiple permission use this:
ActivityResultContracts.RequestMultiplePermissions()