When developing on Android, it is common to ask for permission. Several features of the phone, such as accessing files or using the bluetooth adapter, require the user's explicit approval.
As a central part of the Android system, permissions have evolved a lot, both for users and developers.
When do we ask for permission?
Up to Android 5.1, permissions were granted by the user when downloading an app. At that point, the user could not know exactly what the app looked like and which part of it required which permission. Android 6 resolved the issue by allowing developers to request permissions at runtime, allowing users to browse the app and only then grant permissions to access specific screens and features. Google’s guidelines specifically request developers to use this system and not to ask for all permissions when starting the app.
With Android 11, Google went a step further and gave users the choice to grant permissions only for their current session.
Best practices
From the developer's point of view, there are several things to keep in mind when requesting permissions:
- We should only ask for permissions when we need them
- We need to be able to deal with a denial of permission
- We need to take in account the fact that permission dialog depends on the Android version, and that the permission can be denied forever using the don't show again option (which is automatically assumed after the second denial on Android 11)
The Android guidelines also recommend making sure that the user understands why the requested feature is needed.
whenever possible, you should provide an explanation of your request both at the time of the request and in a follow-up dialog if the user denies the request.
— Official guideline from https://developer.android.com/training/permissions/usage-notes
Furthermore, the importance of having the permission may vary depending on our use case:
Some permissions might be necessary to use the main feature of an app. In this case, it makes sense to display a warning to the user if the permission has been permanently denied to explain why a fundamental part of the app is not accessible.
Easy permission request
In android, the basic way to request a permission is to call ActivityCompat.requestPermissions
. This function takes the requesting activity as an argument. We get a callback when the user grants or denies the request by overridingonRequestPermissionsResult
@Override
override fun onRequestPermissionsResult(int requestCode,String permissions[], int[] grantResults)
This approach has several problems:
- The activity must handle the callback, which will lead to a large amount of code in the activity when coding with a single activity, multi fragments app
- The code is not reusable
Fortunately, the androidx library provides solutions to the activity callback problem by introducing ActivityResultLauncher
and ActivityResultContracts.RequestMultiplePermissions()
This allows us to call the request and get its result from a fragment. Also, it allows us to abstract the callback logic in a class to check if it is the right time to display an explanation popup to the user. Here is an example wrapper that:
- Checks whether the permission is granted
- Displays an explanation popup before the actual permission request
- Displays a warning popup to explain why a feature is not accessible and pointing to the permission settings of the phone
abstract class BasePermissionRequester(
private val fragment: Fragment,
private val onGranted: () -> Unit,
protected val onDismissed: () -> Unit,
) {
protected abstract val permissions: Array<String>
protected abstract val titleResId: Int
protected abstract val descriptionResId: Int
protected abstract val descriptionWhenDeniedResId: Int
private val permissionRequest: ActivityResultLauncher<Array<out String>> = fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grants ->
onPermissionsResult(grants)
}
fun checkPermissions(context: Context) {
val allGranted = permissions.all { p -> PermissionChecker.checkSelfPermission(context, p) == PermissionChecker.PERMISSION_GRANTED }
val shouldDisplayExplanation = permissions.any { p -> fragment.shouldShowRequestPermissionRationale(p) }
if (shouldDisplayExplanation) {
displayPermissionExplanationDialog(context, then = { permissionRequest.launch(permissions) })
} else if (!allGranted) {
permissionRequest.launch(permissions)
} else {
onGranted()
}
}
private fun onPermissionsResult(grants: Map<String, Boolean>) {
if (grants.all { grant -> grant.value }) {
onGranted()
} else {
val userHadPreviouslyDenied = grants.keys.none { p -> fragment.shouldShowRequestPermissionRationale(p) }
if (userHadPreviouslyDenied) {
displayPermissionDeniedDialog(fragment.requireContext())
} else {
// User clicked a one time deny, show him the main screen
onDismissed()
}
}
}
private fun displayPermissionExplanationDialog(context: Context, then: () -> Unit) {
AlertDialog.Builder(context)
.setTitle(context.getString(titleResId))
.setMessage(context.getString(descriptionResId))
.setPositiveButton(R.string.alert_ok) { _: DialogInterface?, _: Int -> then() }
.setCancelable(false)
.show()
}
private fun displayPermissionDeniedDialog(context: Context) {
AlertDialog.Builder(context)
.setTitle(context.getString(R.string.android_permission_denied_title))
.setMessage(context.getString(descriptionWhenDeniedResId))
.setPositiveButton(R.string.android_permission_denied_open_settings) { _: DialogInterface?, _: Int -> openSettings(context) }
.setNeutralButton(R.string.android_permission_denied_not_now) { _: DialogInterface?, _: Int -> onDismissed() }
.setCancelable(false)
.show()
}
protected fun openSettings(context: Context) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
context.startActivity(intent)
}
}
To request a specific permission, we simply override this class to provide the texts to be displayed in each popup
class BluetoothPermissionRequester(
fragment: Fragment,
onGranted: () -> Unit,
onDismissed: () -> Unit,
) : BasePermissionRequester(fragment, onGranted, onDismissed) {
override val permissions: Array<String> = arrayOf(Manifest.permission.BLUETOOTH, Manifest.permission.BLUETOOTH_ADMIN, Manifest.permission.ACCESS_FINE_LOCATION)
override val titleResId = R.string.android_permission_bluetooth_title
override val descriptionResId = R.string.android_permission_bluetooth_description
override val descriptionWhenDeniedResId = R.string.android_permission_bluetooth_denied_description
}
The usage is then straightforward. In any fragment that needs to display a permission request, you create an instance of the requester and call it whenever the permission is needed:
class SomeFragment : BaseFragment() {
// connect is the callback function on permission granted,
// noBluetoothPermission is the callback function on permission denied
private val permissionRequester = BluetoothPermissionRequester(this, ::connect, ::noBluetoothPermission)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val bindings = SomeFragmentBinding.inflate(inflater, container, false)
// Show a permission popup when user click on a button
bindings.connectButton.setOnClickListener {
permissionRequester.checkPermissions(requireContext())
}
return bindings.root
}
private fun connect() {
// Whatever call to connect using bluetooth, once here the permission is granted
}
private fun noBluetoothPermission() {
// Whatever you need to do if the permission has been denied
}
}