Runtime System Permissions | Android M(>= API 23)

Simple SDK/Class to approach the workflow for requesting Runtime System Permission.

From Android Marshmallow (API >= 23), Google has introduced a runtime permission model. As a developer, implementing permissions in all the Activities/Fragments is a bit redundant. So, I have made a simple SDK that makes it easier to implement.

Summary

I’ve created the RuntimePermission class which required FragmentActivity(Base class for activities) to be passed in the constructor. It also inherits the LifecycleEventObserver to observe the lifecycle change and dispatch the result to the receiver.

Here, We will be using new ActivityResultLauncher API while asking for runtime permissions as onActivityResult API is deprecated, and because it doesn’t require request code, it’s also good at result callback.

How to Implement

First of all, we need to initialize the RuntimePermission class in Activity or Fragment and add a lifecycle observer.

In Activity:

val runtimePermission = RuntimePermission(this)
lifecycle.addObserver(runtimePermission)

In Fragment:

val runtimePermission = RuntimePermission(requireActivity())
lifecycle.addObserver(runtimePermission)

and in the onDestroy() method it’s required to remove the lifecycle observer

lifecycle.removeObserver(runtimePermission)

Request Permissions

The example below shows how to request permissions for the method. There are a few things to note:

  • To request permission use RuntimePermission#requestPermissions(...) which accepts any number of permissions.
  • The RuntimePermission#showRationalePermission(...) accepts the boolean, title and description. If we set boolean true only then Rational Dialog will be shown
  • In case we set showRationalePermission boolean to true (Defalut is false) RuntimePermission#setPositiveButton(...), RuntimePermission#setNegativeButton(...) (mandatory) and RuntimePermission#setNeutralButton(...), RuntimePermission#setRationaleDialogCancelable(...) (Defalut is true) (Optional)
  • In-case receiver require detailed permission then use RuntimePermission#setDetailedPermissionRequired(...) which returns detailed set receiver as requested (Default is false)
  • Finally, RuntimePermission#result{...} callback has DetailedPermissionResult(...), IsPermissionGranted(...), NegativeButtonClicked, NeutralButtonClicked. Incase IsPermissionGranted(false) show the RuntimePermission#settingsDialog(...) onClick of RuntimePermission#settingsDialog#PositiveButton naviagtes to application settings, where user has to maunlly provide the access.

Examples

Single permission request

Here in permission request RuntimePermission#setDetailedPermissionRequired(...) not at all called (Default false). So, RuntimePermission#PermissionResult#IsPermissionGranted(...) will be triggered which will return a boolean value that tells whether all permissions are granted or not.

And, RuntimePermission#showRationalePermission(...) set to true. If the user “deny” permission Rationale Dialog will be triggered where the user will act accordingly. In case the user select “Never Ask Again” RuntimePermission#PermissionResult#IsPermissionGranted(...) will always be false and the application has to act accordingly.

Note: Entered text shouldn’t be empty otherwise the application will throw IllegalArgumentException(...)

cameraButton.setOnClickListener {
            runtimePermission
                .requestPermissions(Manifest.permission.CAMERA)
                .showRationalePermission(
                    true,
                    "Permission Denied",
                    "To capture a profile picture we need this permission. Are you sure, you would like to cancel?"
                )
                .setPositiveButton("Re-try")
                .setNegativeButton("Cancel")
                .setNeutralButton("Maybe later")
                .setRationaleDialogCancelable(false)
                .result { result ->
                    when (result) {
                        is RuntimePermission.PermissionResult.DetailedPermissionResult -> {
                            result.detailedPermission.forEach {
                                Log.d(TAG, "Permission ${it.key} is allowed ${it.value}")
                            }
                        }
                        is RuntimePermission.PermissionResult.IsPermissionGranted -> {
                            if (result.isGranted) {
                                Log.d(TAG, "Permission Granted")
                                Toast.makeText(this, "Permission Granted", Toast.LENGTH_SHORT).show()
                            } else {
                                Log.e(TAG, "Permission Denied")
                                runtimePermission.settingsDialog(
                                    "To Continue give access to app",
                                    "Settings",
                                    true,
                                    Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                            }
                        }
                        RuntimePermission.PermissionResult.NegativeButtonClicked -> {
                            Log.d(TAG, "Rationale Dialog negative button clicked")
                        }
                        RuntimePermission.PermissionResult.NeutralButtonClicked -> {
                            Log.d(TAG, "Rationale Dialog neutral button clicked")
                        }
                    }
                }

        }

Multiple permission request

Here in permission request RuntimePermission#setDetailedPermissionRequired(...) set to true. So, RuntimePermission#PermissionResult#DetailedPermissionResult(...) will be triggered which will return detailed permission, a receiver as requested. Where the developer has to handle how the application should act accordingly.

And, RuntimePermission#showRationalePermission(...) method is not called. Therefore Rationale dialog will not be triggered in case the user “denies” permission, instead “isGranted = false” in the callback.

cameraLocationButton.setOnClickListener {
            runtimePermission
                .requestPermissions(
                    Manifest.permission.CAMERA,
                    Manifest.permission.ACCESS_FINE_LOCATION,
                    Manifest.permission.ACCESS_COARSE_LOCATION)
                .setDetailedPermissionRequired(true)
                .result { result ->
                    when (result) {
                        is RuntimePermission.PermissionResult.DetailedPermissionResult ->{
                            // Check which permission is allowed and which is not
                            // On the basis of it decide what to be done
                            result.detailedPermission.forEach {
                                Log.d(TAG, "Permission ${it.key} is allowed ${it.value}")
                            }

                            if (result.isGranted) {
                                Log.d(TAG, "Permission Granted")
                                Toast.makeText(this, "Permission Granted", Toast.LENGTH_SHORT).show()
                            } else {
                                Log.e(TAG, "Permission Denied")
                                runtimePermission.settingsDialog(
                                    "To Continue give access to app",
                                    "Settings",
                                    true,
                                    Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                            }
                        }
                        is RuntimePermission.PermissionResult.IsPermissionGranted -> {}
                        RuntimePermission.PermissionResult.NegativeButtonClicked -> {
                            Log.d(TAG, "Rationale Dialog negative button clicked")
                        }
                        RuntimePermission.PermissionResult.NeutralButtonClicked -> {
                            Log.d(TAG, "Rationale Dialog neutral button clicked")
                        }
                    }
                }
        }

Detailed permission request

As you know from Android 11 (API >= 30) WRITE_EXTERNAL_STORAGE and READ_EXTERNAL_STORAGEwere removed, instead, we need to use MANAGE_EXTERNAL_STORAGE(permission prompt dialog will not show, instead navigate to settings) to access storage. So, set RuntimePermission#setDetailedPermissionRequired(...) to true and act accordingly.

readWriteButton.setOnClickListener {
            val requestPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                arrayOf(
                    Manifest.permission.MANAGE_EXTERNAL_STORAGE
                )
            } else {
                arrayOf(
                    Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE
                )
            }

            runtimePermission
                .requestPermissions(*requestPermission)
                .showRationalePermission(
                    true,
                    "Permission Denied",
                    "To capture a profile picture we need this permission. Are you sure, you would like to cancel?"
                )
                .setPositiveButton("Re-try")
                .setNegativeButton("Cancel")
                .setNeutralButton("May be later")
                .setRationaleDialogCancelable(true)
                .setDetailedPermissionRequired(true)
                .result { result ->
                    when (result) {
                        is RuntimePermission.PermissionResult.DetailedPermissionResult -> {
                            var isStoragePermissionGranted = false
                            result.detailedPermission.forEach {
                                Log.d(TAG, "Permission ${it.key} is allowed ${it.value}")
                                when(it.key) {
                                    Manifest.permission.MANAGE_EXTERNAL_STORAGE -> {
                                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
                                            Environment.isExternalStorageManager()) {
                                            isStoragePermissionGranted = true
                                        }
                                    }
                                    Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE -> {
                                        isStoragePermissionGranted = it.value
                                        if (!it.value) {
                                            return@forEach
                                        }
                                    }
                                }
                            }

                            if (!isStoragePermissionGranted) {
                                val action = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                                    Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION
                                } else {
                                    Settings.ACTION_APPLICATION_DETAILS_SETTINGS
                                }
                                
                                runtimePermission.settingsDialog(
                                    "To Continue give access to app",
                                    "Settings",
                                    true,
                                    action)
                            } else {
                                Toast.makeText(this, "Permission Granted", Toast.LENGTH_SHORT).show()
                            }
                        }
                        is RuntimePermission.PermissionResult.IsPermissionGranted -> {}
                        RuntimePermission.PermissionResult.NegativeButtonClicked -> {
                            Log.d(TAG, "Rationale Dialog negative button clicked")
                        }
                        RuntimePermission.PermissionResult.NeutralButtonClicked -> {
                            Log.d(TAG, "Rationale Dialog neutral button clicked")
                        }

                    }
                }
        }

I hope this SDK will be helpful for you. Feel free to give your feedback. I would be very happy to get new suggestions and modifications in the code.

Happy coding ?

Reference

https://github.com/googlesamples/easypermissions

GitHub

View Github