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.
Related
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 am working with camera permissions. when the user clicks on take photo button, user will be shown with run time permissions menu and lets say user deny's it and then clicks on take photo button, run time permissions will be shown second time.
after this clicking the take photo button nothing happens.
What I want to do is, after the second attempt, i want to show a popup telling the user to go to settings to change the permissions.
How can I know if the user has denied the permission twice.
this is what I have coded so far
takePhotoBtn.setOnClickListener {
takePhoto()
}
private fun takePhoto() {
activity?.let {
if (isCameraPermissionsAllowed()) {
capturePhoto()
} else {
permReqLauncher.launch(
CAMERA_PERMISSION
)
}
}
}
private val permReqLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val granted = permissions.entries.all {
it.value == true
}
if (granted) {
capturePhoto()
}
}
private fun capturePhoto() {
onUtilityBillTypeListener.onUtilityBillTypePhotoLink(true)
}
where is the right place to add this dinielDialog
private fun showPermissionDeniedDialog() {
AlertDialog.Builder(this.requireContext())
.setTitle("Permission Denied")
.setMessage("Permission is denied, Please allow permissions from App Settings.")
.setPositiveButton("Settings",
DialogInterface.OnClickListener { dialogInterface, i ->
// send to app settings if permission is denied permanently
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts("package", getPackageName(this.requireContext()), null)
intent.data = uri
startActivity(intent)
})
.setNegativeButton("Cancel", null)
.show()
}
You could write something like this
if (isCameraPermissionsAllowed()) {
capturePhoto()
} else {
if (permissionDeniedFlag) {
showPermissionDeniedDialog()
} else {
permissionDeniedFlag = True
permReqLauncher.launch(
CAMERA_PERMISSION
)
}
With an initial declaration of var permissionDeniedFlag = False.
On your code where you're getting permission result you can do it like this :
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
isGranted: Boolean ->
if (isGranted) //perform action
else {
val builder = AlertDialog.Builder(requireContext())
builder.setTitle("Permission Required!")
builder.setMessage("We need permission in order to perform this action.")
builder.setPositiveButton("OK") { dialog, _ ->
showPermRationale()
dialog.cancel()
}
builder.setNegativeButton("CANCEL") { dialog, _ ->
dialog.cancel()
}
builder.show()
}
}
private fun showPermRationale() {
val intent = Intent()
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts(
"package", requireActivity().packageName, null)
intent.data = uri
requireActivity().startActivity(intent)
}
resolved it by adding this code
private fun takePhoto() {
activity?.let {
if (isCameraPermissionsAllowed()) {
capturePhoto()
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this.requireActivity(),
Manifest.permission.CAMERA
)
) {
showPermissionDeniedDialog()
}
else{
permReqLauncher.launch(
CAMERA_PERMISSION
)
}
}
}
}
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
I am trying to write a program to communicate with ESP32 modules via bluetooth. For the program to work, Bt must be turned on and the FINE_LOCATION permission granted. I am using API 29.
The code below works, but it can be done much better.
I am a beginner, this is the only way I can do it.
I have a few questions :
Can I use shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) together with ActivityResultContracts.RequestPermission(), if yes how?
To achieve my goal if the user refuses the first time
to grant permissions, I run an almost identical contract with a different dialog.How can this code be reduced?
How to simplify this constant checking:
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
Half my code seems redundant, I don't know how to get rid of it.
All these problems actually concern the first run, then everything is fine.
Code:
const val TAG = "DEBUG"
data class Conditions (var isBtEnabled : Boolean , var permissionsOk :Boolean){
fun isReady():Boolean{
if (isBtEnabled && permissionsOk) return true
else return false
}
fun log(){
Log.d("DEBUG","Conditions-> $isBtEnabled , $permissionsOk")
}}
class MainActivity : AppCompatActivity() {
private lateinit var bind: ActivityMainBinding
private lateinit var broadcastReceiver: BroadcastReceiver
private lateinit var bluetoothAdapter: BluetoothAdapter
private var conditions = Conditions(false, false)
private var requestBluetoothEnable =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
Log.d(TAG, "IT RESULT CODE: ${it.resultCode.toString()}")
//kiedy bt jest wlaczone , result -1 , kiedy wylaczone i wlaczamy i akceptujemy tez -1
//a jak odrzucamy to 0
if (it.resultCode == -1) {
conditions.log()
conditions.isBtEnabled = true
}
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
}
//use it when user denied first time
private val requestPermissionLocationSecond =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
Log.d(TAG, "Permission granted by contract 2")
conditions.permissionsOk = checkPermissions()
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
} else {
val builder = AlertDialog.Builder(this#MainActivity)
builder.setTitle("V2 - Hi!")
builder.setMessage(
" Please go to the app settings and manually turn on " +
"\"location permission\". Without this permission, I do not work. "
)
builder.setPositiveButton("Ok") { dialog, which -> }
val dialog: AlertDialog = builder.create()
dialog.show()
Log.d(TAG, " V2-> Permission denied, - contract 2")
}
}
// first try to get permission
private var requestPermissionLocation =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
Log.d(TAG, "Permission granted by contract 1")
conditions.permissionsOk = checkPermissions()
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
//shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
Log.d(TAG, "Permission denied by contract 1")
val builder = AlertDialog.Builder(this#MainActivity)
builder.setTitle("V2 - Uprawnienie do lokalizacji")
builder.setMessage("I need these permissions to work with Bt devices ")
builder.setPositiveButton("YES") { dialog, which ->
requestPermissionLocationSecond.launch(android.Manifest.permission.ACCESS_FINE_LOCATION)
}
builder.setNegativeButton("No") { dialog, which -> }
val dialog: AlertDialog = builder.create()
dialog.show()
conditions.permissionsOk = checkPermissions()
if (conditions.isReady()) {
buildInterfaceOk()
} else buildInterfaceError()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
bind = ActivityMainBinding.inflate(layoutInflater)
super.onCreate(savedInstanceState)
setContentView(bind.root)
Log.d(TAG, "BUild version : ${Build.VERSION.SDK_INT} -> ${Build.VERSION.CODENAME}")
val currentDebug = getString(R.string.app_name)
Log.d(TAG, "CURRENT DEBUG : $currentDebug")
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
requestBluetoothEnable.launch(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
requestPermissionLocation.launch(Manifest.permission.ACCESS_FINE_LOCATION)
conditions.isBtEnabled = bluetoothAdapter.isEnabled
conditions.permissionsOk = checkPermissions()
Log.d(TAG, "FIRST conditions check :")
if (conditions.isReady()) {
conditions.log()
buildInterfaceOk()
}
}
private fun buildInterfaceOk() {
Log.d(TAG, "BUILDING INTERFACE : all is fine")
bind.tvInfo.text = "All is fine i can build interface"
}
private fun buildInterfaceError() {
Log.d(TAG, "BUILDING INTERFACE : errors")
bind.tvInfo.text = "Some errors..."
}
private fun checkPermissions(): Boolean {
val permissionsRequired =
arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
//Manifest.permission.BLUETOOTH_CONNECT, //to znow wymagane od S(API 31) ??
//Manifest.permission.BLUETOOTH_SCAN, //to znow wymagane od S(API 31) ??
Manifest.permission.ACCESS_FINE_LOCATION
//Manifest.permission.ACCESS_BACKGROUND_LOCATION
)
var permissionsOk = true
permissionsRequired.forEach { requiredPermission ->
if (ContextCompat.checkSelfPermission(
this.applicationContext,
requiredPermission
) == PackageManager.PERMISSION_GRANTED
) {
Log.d(TAG, "PERMISSION : $requiredPermission -> GRANTED")
} else {
Log.d(TAG, "PERMISSION : $requiredPermission -> NOT GRANTED")
permissionsOk = false
}
}
return permissionsOk
}
}
What I would do is display an AlertDialog first saying, you MUST ACCEPT all permissions in order to precede then Request Permissions until the user agrees to them all.
Check Permission -> Pass -> Start App
Check Permission -> Fail -> Alert Dialog "You must accept all permissions for the app to start."
Request Permission -> Check Permission -> Pass -> Start App
Request Permission -> Check Permission -> Fail -> Request Permission
Request Permission -> Check Permission -> Fail & Never ask again ->
Alert Dialog "Go to setting to turn on permissions" -> onPositive "OK" -> Request Permission
The only problem with this is the user can choose "Never ask again", meaning you can no longer Request Permissions.
Luckily you can tell if the user has chosen "Never ask again", this should get you started
Android M - check runtime permission - how to determine if the user checked "Never ask again"?
This way will encourage code reuse because you are essentially doing the same things in a loop until the user accepts permissions. The only way out of the loop is permission granted across the board.
Since introducing new location permission changes on Android 11 (https://developer.android.com/about/versions/11/privacy/location) there is a need to more carefully work with permissions. We are now able to request background location permission only twice
I want to show the user a dialog dependent on the state of their location permission.
The issue is that system handles click outside of the dialog the same as denying the permission, but it apparently doesn't count it in PermissionsUtil.shouldShowRequestStoragePermissionRationale limit. So it is then hard to distinguish which state the user is in.
My initial question was: How to deal with this specific situation?
But I guess more useful is the general question:
How to recognize if the user pressed outside of the system permission dialog or if he directly denied the permission?.
Here is how I solved it:
private val btRequestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val allPermissionsGranted = permissions.values.none { !it }
if (allPermissionsGranted) {
callback?.invoke().apply { this#PermissionActivity.callback = null }
} else if (Manifest.permission.BLUETOOTH_CONNECT in requiredPermissions || Manifest.permission.ACCESS_FINE_LOCATION in requiredPermissions) {
if (!shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT) && !shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) && (this.getPreferenceBoolean("NEVER_SHOW_AGAIN", false) || shouldShowRationaleBefore)) {
this.getPreferenceBoolean("NEVER_SHOW_AGAIN", true)
//A dialog box with GO TO SETTING button
} else {
shouldShowRationaleBefore = true
requestBluetoothPermissions()
}
} else
requestBluetoothPermissions()
}
private var shouldShowRationaleBefore = false
protected fun requestBluetoothPermissions(callBack: (() -> Unit)? = null) {
this.callback = callBack
when {
areAllPermissionsGranted() -> {
callBack?.invoke()
}
shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT) -> {
//A dialog box with CONTINUE button and callBack to btRequestPermissionLauncher.launch(...)
}
shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) -> {
//A dialog box with CONTINUE button and callBack to btRequestPermissionLauncher.launch(...)
}
!areAllPermissionsGranted() -> {
if (!shouldShowRationaleBefore)
btRequestPermissionLauncher.launch(requiredPermissions)
else {
private var shouldShowRationale = false
//A dialog box with CONTINUE button and callBack to btRequestPermissionLauncher.launch(...)
//This is where the click outside the permission dialog box is handled
}
}
}
}
Based on the result per permission you can add it to a list and then you could know if the user denied / rejected the permissions completely . It can be done in the following way :
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { result: MutableMap<String, Boolean> ->
val deniedList: List<String> = result.filter {
!it.value
}.map {
it.key
}
when {
deniedList.isNotEmpty() -> {
val map = deniedList.groupBy { permission ->
if (shouldShowRequestPermissionRationale(permission)) DENIED else EXPLAINED
}
map[DENIED]?.let {
// request denied , request again
}
map[EXPLAINED]?.let {
//request denied ,send to settings
}
}
else -> {
//All request are permitted
}
}
}