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
}
}
}
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.
So i'm trying to return a value from a function to use inside the mainActivity class, but I get an error when I try to return the variable stating that the 'variable must be initialized' even though I have given a value to it. Any thoughts on this?
private fun getCLocation() : LocationClass {
var loc: LocationClass
if(checkPermission()){
if(isLocationEnabled()){
locationVariable.lastLocation.addOnCompleteListener(this) { task->
val location:Location? = task.result
if(location == null){
Toast.makeText(this,"NULL",Toast.LENGTH_LONG).show()
}
else{
loc = LocationClass(location.latitude.toString(),location.longitude.toString())
}
}
} else {
// location not enabled,open settings
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivity(intent)
}
} else {
//Permission Not Enabled
requestPermission()
}
return loc
}
Error is: Variable 'loc' must be initialized
The two problems with your code:
The user may not have granted the permission yet, so it would enter the else block, where the function requests a permission, but doesn't do anything to set the initial location value. The function couldn't possibly return a valid location in this circumstance.
Getting location updates is asynchronous. Even if the permission is already granted, your OnCompleteListener will not be called until some time in the future, after this function has already returned. You can read more explanations of what asynchronous APIs are here.
Here's a basic strategy for what to do. The function that gets a location takes a callback parameter instead of returning the LocationClass directly since it is asynchronous. You need a higher-level logic that can be retried after location or permissions become available.
private var triedPromptingLocationSetting = false
private var locationWorkflowInitiated = false // SET THIS BACK TO FALSE IN onViewDestroyed() IF IN FRAGMENT
private fun fetchCLocation(onReceived: (LocationClass?)->Unit) {
if (!checkPermission() || isLocationEnabled()) {
Log.e("Do not call getCLocation() before permission is granted and location is turned on! Ignoring.")
return
}
locationVariable.lastLocation.addOnCompleteListener(this) { task->
val location:Location? = task.result
if (location == null) {
onReceived(null)
}
else {
var loc = LocationClass(location.latitude.toString(),location.longitude.toString())
onReceived(loc)
}
}
}
// This is where the logic is for whatever you wanted to do with this location.
// In this example, it's assumed it would be called in onResume()
private fun doMyLocationWorkflow() {
if (!checkPermission()) {
requestPermission()
return
}
if (!isLocationEnabled()) {
if (triedPromptingLocationSetting) {
// can't infinitely loop back to settings.
showSomeUiRequestingUserToManuallyEnableLocation()
} else {
triedPromptingLocationSetting = true
val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)
startActivity(intent)
}
return
}
locationWorkflowInitiated = true
fetchCLocation { location ->
if (location == null) {
Toast.makeText(this,"NULL",Toast.LENGTH_LONG).show()
//...
return#fetchCLocation
}
// Do something with the LocationClass
}
}
override fun onResume() {
super.onResume()
if (!locationWorkflowInitiated) {
doMyLocationWorkflow()
}
}
private val locationPermissionRequest = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
when {
permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> {
doMyLocationWorkflow() // go back to original workflow
}
permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> {
doMyLocationWorkflow() // go back to original workflow
} else -> {
showSomeUiTellingUserPermissionMustBeGranted()
}
}
}
private fun requestPermission() {
locationPermissionRequest.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION))
}
I tried to implement request permissions for writing and reading from storage. Everything worked good but today Android showed me that the method onRequestPermissionsResult(...) is deprecated. There are so many questions about this topic in StackOverflow, but unfortunately, they are outdated.
I called the methods below in a fragment.
It was suggested simply to call:
requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
StorageKeys.STORAGE_PERMISSION_CODE)
instead of my approach:
ActivityCompat.requestPermissions(getActivity(),
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
StorageKeys.STORAGE_PERMISSION_CODE))
But both of them show that onRequestPermissionsResult(...) is deprecated.
Here is my onRequestPermissionsResult(...)-method:
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions,
#NonNull int[] grantResults) {
if (requestCode == StorageKeys.STORAGE_PERMISSION_CODE) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
exportBibTex.createBibFile();
exportBibTex.writeBibFile(exportBibTex
.getBibDataLibrary(libraryModel, bookDao, noteDao));
Toast.makeText(getContext(),
getString(R.string.exported_file_stored_in) + '\n'
+ File.separator + StorageKeys.DOWNLOAD_FOLDER + File.separator + fileName
+ StorageKeys.BIB_FILE_TYPE, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getContext(), R.string.storage_permission_denied,
Toast.LENGTH_SHORT).show();
}
}
}
Here is a simple alert dialog, in which I call the onRequestPermissionsResult(...):
private void showRequestPermissionDialog() {
AlertDialog.Builder reqAlertDialog = new AlertDialog.Builder(getContext());
reqAlertDialog.setTitle(R.string.storage_permission_needed);
reqAlertDialog.setMessage(R.string.storage_permission_alert_msg);
reqAlertDialog.setPositiveButton(R.string.ok,
(dialog, which) -> ActivityCompat.requestPermissions(getActivity(),
new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE},
StorageKeys.STORAGE_PERMISSION_CODE));
reqAlertDialog.setNegativeButton(R.string.cancel,
(dialog, which) -> dialog.dismiss());
reqAlertDialog.create().show();
}
Is there any alternative for onRequestPermissionsResult(...), that I can use?
The onRequestPermissionsResult() method is deprecated in androidx.fragment.app.Fragment.
So you may use registerForActivityResult() method instead of onRequestPermissionsResult().
You can refer this URL.
Following is Kotlin code, but you can refer it:
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
// PERMISSION GRANTED
} else {
// PERMISSION NOT GRANTED
}
}
// Ex. Launching ACCESS_FINE_LOCATION permission.
private fun startLocationPermissionRequest() {
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
I added java code from following URL.
How to get a permission request in new ActivityResult API (1.3.0-alpha05)?
private ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
new ActivityResultCallback<Boolean>() {
#Override
public void onActivityResult(Boolean result) {
if (result) {
// PERMISSION GRANTED
} else {
// PERMISSION NOT GRANTED
}
}
}
);
// Ex. Launch the permission window -- this is in onCreateView()
floatingActionButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION);
}
});
You can also request multiple permissions:
val requestMultiplePermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
permissions.entries.forEach {
Log.d("DEBUG", "${it.key} = ${it.value}")
}
}
requestMultiplePermissions.launch(
arrayOf(
Manifest.permission.READ_CONTACTS,
Manifest.permission.ACCESS_FINE_LOCATION
)
)
A simple way in Kotlin
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
class MyFragment : Fragment() {
companion object {
val TAG: String = MyFragment::class.java.simpleName
var PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
private val permReqLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val granted = permissions.entries.all {
it.value == true
}
if (granted) {
displayCameraFragment()
}
}
private fun takePhoto() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
displayCameraFragment()
return
}
activity?.let {
if (hasPermissions(activity as Context, PERMISSIONS)) {
displayCameraFragment()
} else {
permReqLauncher.launch(
PERMISSIONS
)
}
}
}
// util method
private fun hasPermissions(context: Context, permissions: Array<String>): Boolean = permissions.all {
ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
private fun displayCameraFragment() {
// open camera fragment
}
}
Alternative registerForActivityResult
Example use:
private fun permissionSetup() {
val permission = ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.READ_CONTACTS)
if (permission != PackageManager.PERMISSION_GRANTED) {
permissionsResultCallback.launch(Manifest.permission.READ_CONTACTS)
} else {
println("Permission isGranted")
}
}
private val permissionsResultCallback = registerForActivityResult(
ActivityResultContracts.RequestPermission()){
when (it) {
true -> { println("Permission has been granted by user") }
false -> {
Toast.makeText(requireContext(), "Permission denied", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
}
}
Register the permissions callback in your activity or fragment. which handle user permission
Example-Storage permission
private final ActivityResultLauncher<String> requestPermissionLauncher = registerForActivityResult(
new ActivityResultContracts.RequestPermission(),
result -> {
if (result) {
//Permission granted
} else {
//permission denied
if (ActivityCompat.shouldShowRequestPermissionRationale(activity, WRITE_EXTERNAL_STORAGE)) {
//show permission snackbar
} else {
//display error dialog
}
}
});
Ask for the permission.The registered ActivityResultCallback gets the result of this request.
if (checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PermissionChecker.PERMISSION_GRANTED) {
requestPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
} else {
//Permission already granted
}
This works for me - (kotlin):
private fun checkPermissions() {
if (mContext?.let {
ContextCompat.checkSelfPermission(
it,
READ_EXTERNAL_STORAGE
)
} != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Request Permissions")
requestMultiplePermissions.launch(
arrayOf(READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE))
} else {
Log.d(TAG, "Permission Already Granted")
}
}
private val requestMultiplePermissions =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissions.entries.forEach {
Log.d(TAG, "${it.key} = ${it.value}")
}
if (permissions[READ_EXTERNAL_STORAGE] == true && permissions[WRITE_EXTERNAL_STORAGE] == true) {
Log.d(TAG, "Permission granted")
} else {
Log.d(TAG, "Permission not granted")
}
}
Most of the answers address the OP requirement. But I have found few things that are missing so I thought to provide a complete example (in Koltin)
class ProfileFragment : Fragment(){
private lateinit var permissionRequest : ActivityResultLauncher<Array<String>>
companion object {
val LOCATION_PERMISSIONS = arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
}
private fun getGpsLocation() {
if(activity != null){
permissionRequest.launch(LOCATION_PERMISSIONS)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.locBtn.setOnClickListener { getGpsLocation() }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
registerPermissionRequest()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_profile, container, false)
return binding.root
}
private fun registerPermissionRequest(){
var permissionCount = 0
permissionRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissions.entries.forEach {
if(it.value){
permissionCount++
}
}
if(permissionCount == 2){
getMyLocation()
}
}
}
}
The things that are missing are
A)Fragments must call registerForActivityResult() before they are created (i.e. initialization, onAttach(), or onCreate()). Other wise it won't work and the app would crash.
Error:
java.lang.IllegalStateException: Fragment ProfileFragment{bf12414} (210ad5a1-3286-4586-a48f-deac1d8e3eef
id=0x7f09008b) is attempting to registerForActivityResult after being
created. Fragments must call registerForActivityResult() before they
are created (i.e. initialization, onAttach(), or onCreate()).
B) It is recommended to request permission when it is really needed. In my example when user clicks on Button with id locBtn, permission dialog is shown rather than showing when activity/fragment is created.
Please refer to the official documentation: https://developer.android.com/training/permissions/requesting
Review the following, which can be found in the documentation.
// Register the permissions callback, which handles the user's response to the
// system permissions dialog. Save the return value, an instance of
// ActivityResultLauncher. You can use either a val, as shown in this snippet,
// or a lateinit var in your onAttach() or onCreate() method.
val requestPermissionLauncher =
registerForActivityResult(RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// features requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
}
After which, launch the request
when {
ContextCompat.checkSelfPermission(
CONTEXT,
Manifest.permission.REQUESTED_PERMISSION
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
}
//Is not needed for it to work, but is a good practice as it plays a role
//in letting user know why the permission is needed.
shouldShowRequestPermissionRationale(...) -> {
// In an educational UI, explain to the user why your app requires this
// permission for a specific feature to behave as expected. In this UI,
// include a "cancel" or "no thanks" button that allows the user to
// continue using your app without granting the permission.
showInContextUI(...)
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
requestPermissionLauncher.launch(
Manifest.permission.REQUESTED_PERMISSION)
}
}
Suppose we need to take some permissions like audio and camera. Then we can put it on a variable of array.
private val REQUESTED_PERMISSIONS =
arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
This registerForActivityResult invoke when we launch with multiple permission array.
private val requestMultiplePermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
permissions.entries.forEach {
Log.d("DEBUG", "${it.key} = ${it.value}")
if (!it.value){
return#registerForActivityResult
}
}
init()
}
Now we make a function to check permission is already granted or not.
private fun checkSelfPermission(): Boolean {
return !(ContextCompat.checkSelfPermission(
requireContext(),
REQUESTED_PERMISSIONS[0]
) != PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
requireContext(),
REQUESTED_PERMISSIONS[1]
) != PackageManager.PERMISSION_GRANTED)
}
Now we check permission is granted or not from onCreate method. If permission is not granted then we invoke requestMultiplePermissions.launch(REQUESTED_PERMISSIONS) like
super.onCreate(savedInstanceState)
if (!checkSelfPermission()) {
requestMultiplePermissions.launch(REQUESTED_PERMISSIONS)
}
}
You can use some external library for permission handling to reduce some boilerplate code. I use Dexter library. Rx Permissions is also good choice if you are using RxJava2.
Try this in in your pubspec.yamal under flutter section:
plugin:
androidPackage: com.ly.permission
pluginClass: PermissionPlugin
It works with me.
I have this simple activity which popups the location permission dialog and based on negative or positive permission dialog button interaction I clear the activity.As a user when I dismiss the dialog for the second time and come back to the app again the dialog doesn't show up anymore and the activity just finishes right away. I would want to show the permission dialog every-time if the permission was not granted
override fun onResume() {
super.onResume()
setupPermission()
}
private fun setupPermission() {
val permission = PermissionChecker.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
if (permission != PackageManager.PERMISSION_GRANTED) {
permissionsResultCallback.launch(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
Log.d("dawg", "already granted")
finish()
}
}
private val permissionsResultCallback = registerForActivityResult(
ActivityResultContracts.RequestPermission()) { granted ->
when (granted) {
true -> {
Log.d("dawg", "granted now via dialog")
finish()
}
false -> {
Log.d("dawg", "denied via dialog")
finish()
}
}
}
You can use shouldShowRequestPermissionRationale. If the user deny for 2nd time you can show a snack bar where user can goto settings page and turn on notification manually.
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
showSnackBarForOpenSettings(
context,
"Camera Permission needed",
view
)
}
} else {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(android.Manifest.permission.CAMERA), 1
)
}
For opening app settings screen you can use this:
fun Context.openAppSystemSettings() {
startActivity(Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
data = Uri.fromParts("package", packageName, null)
})
}
use it like this context.openAppSystemSettings()
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.