Android Runtime Permissions using Dexter

We all know that Android Marshmallow introduced runtime permissions letting user to allow or deny any permission at runtime. Implementing runtime permissions is a tedious process and developer needs to write lot of code just to get a single permission.

In this article, we are going to simplify the process by using Dexter library. Using this library, the permissions can be implemented in few minutes.

This is an introductory article about the Dexter covering basic features offered by the library. Dexter provides other features like using it with SnackBar, different types of listeners, error handling and few other. You can find more information on Dexter’s developer page.

1. Dexter Permissions Library

To get started with Dexter, add the dependency in your build.gradle
build.gradle

dependencies {
    // Dexter runtime permissions
    implementation 'com.karumi:dexter:6.2.3'
}

1.1 Requesting Single Permission

To request a single permission, you can use withPermission() method by passing the required permission. You also need a PermissionListener callback to receive the state of the permission.
  • The method onPermissionGranted() will be called once the permission is granted.
  • onPermissionDenied() will be called when the permission is denied. Here you can check whether the permission is permanently denied by using response.isPermanentlyDenied() condition.
The below example requests CAMERA permission.

Dexter.withContext(this)
            .withPermission(Manifest.permission.CAMERA)
            .withListener(object : PermissionListener {
                override fun onPermissionGranted(response: PermissionGrantedResponse) {
                    // Congrats! permission is granted. You can open the camera row
                    openCamera()
                }

                override fun onPermissionDenied(response: PermissionDeniedResponse) {
                    // check for permanent denial of permission
                    if (response.isPermanentlyDenied) {
                        showSettingsDialog()
                    }
                }

                override fun onPermissionRationaleShouldBeShown(
                    permission: PermissionRequest?,
                    token: PermissionToken
                ) {
                    token.continuePermissionRequest()
                }
            }).check()

1.2 Requesting Multiple Permissions

To request multiple permissions at the same time, you can use withPermissions() method. Below code requests READ_CONTACTS and READ_CALL_LOG permissions.

Dexter.withContext(this)
            .withPermissions(
                Manifest.permission.READ_CONTACTS,
                Manifest.permission.READ_CALL_LOG
            )
            .withListener(object : MultiplePermissionsListener {
                override fun onPermissionsChecked(report: MultiplePermissionsReport) {
                    // check if all permissions are granted
                    if (report.areAllPermissionsGranted()) {
                        Toast.makeText(
                            applicationContext,
                            "All permissions are granted!",
                            Toast.LENGTH_SHORT
                        ).show()
                    }


                    // check for permanent denial of any permission
                    if (report.isAnyPermissionPermanentlyDenied) {
                        // show alert dialog navigating to Settings
                        showSettingsDialog()
                    }
                }

                override fun onPermissionRationaleShouldBeShown(
                    permissions: List<PermissionRequest?>?,
                    token: PermissionToken
                ) {
                    token.continuePermissionRequest()
                }
            })
            .onSameThread()
            .check()

1.3 Error Handling

To catch any errors that occurred while requesting a permission, use PermissionRequestErrorListener.

Dexter.withContext(this)
            .withPermission(Manifest.permission.CAMERA)
            .withListener(object : ....)
            .withErrorListener {error ->
                Toast.makeText(applicationContext, "Error occurred! $error", Toast.LENGTH_SHORT).show();
            }
            .check()

2. Full example

Now let’s see how to use Dexter in an example project.
  1. Create a new project in Android Studio from File ⇒ New Project and select Basic Activity from templates.
  2. Add Dexter dependency to your build.gradle
    
    dependencies {
        // Dexter runtime permissions
        implementation 'com.karumi:dexter:6.2.3'
    }
    
  3. Open strings.xml and add the following strings
    strings.xml
    
    <resources>
        <string name="app_name">Runtime Permissions</string>
        <string name="camera_permission">Camera Permission</string>
        <string name="multiple_permissions">Multiple Permissions</string>
        <string name="permissions_title">Need Permissions</string>
        <string name="permissions_message">This app needs permission to use this feature. You can grant them in app settings.</string>
        <string name="cancel">Cancel</string>
        <string name="goto_settings">Go to settings</string>
    </resources>
    
  4. Open the layout file of your main activity activity_main.xml and add two buttons to test different permission methods.
    activity_main.xml
    
    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/default_padding"
        tools:context=".MainActivity">
    
        <Button
            android:id="@+id/btn_camera_permission"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/camera_permission"
            app:layout_constraintTop_toTopOf="parent" />
    
        <Button
            android:id="@+id/btn_multiple_permissions"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="@dimen/default_padding"
            android:text="@string/multiple_permissions"
            app:layout_constraintTop_toBottomOf="@id/btn_camera_permission" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  5. Open the MainActivity and do the modification as shown below.
    • Tapping camera button requests camera permission using requestCameraPermission() method.
    • Tapping request multiple permissions, requests READ_CONTACTS & READ_CALL_LOG permissions using requestMultiplePermissions().
    • If any permission is permanently denied, calling showSettingsDialog() shows a dialog that navigates to system settings where user can manually grant the permissions.
    MainActivity.kt
    
    package info.androidhive.runtime_permissions
    
    import android.Manifest
    import android.content.Intent
    import android.net.Uri
    import android.os.Bundle
    import android.provider.MediaStore
    import android.provider.Settings
    import android.widget.Toast
    import androidx.activity.result.contract.ActivityResultContracts
    import androidx.appcompat.app.AppCompatActivity
    import com.google.android.material.dialog.MaterialAlertDialogBuilder
    import com.karumi.dexter.Dexter
    import com.karumi.dexter.MultiplePermissionsReport
    import com.karumi.dexter.PermissionToken
    import com.karumi.dexter.listener.PermissionDeniedResponse
    import com.karumi.dexter.listener.PermissionGrantedResponse
    import com.karumi.dexter.listener.PermissionRequest
    import com.karumi.dexter.listener.multi.MultiplePermissionsListener
    import com.karumi.dexter.listener.single.PermissionListener
    import info.androidhive.runtime_permissions.databinding.ActivityMainBinding
    
    
    class MainActivity : AppCompatActivity() {
        private val binding by lazy(LazyThreadSafetyMode.NONE) {
            ActivityMainBinding.inflate(layoutInflater)
        }
    
        private var requestOpenCamera =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
            }
    
        private var requestOpenSettings =
            registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
    
            }
    
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
    
            binding.btnCameraPermission.setOnClickListener {
                requestCameraPermission()
            }
    
            binding.btnMultiplePermissions.setOnClickListener {
                requestMultiplePermissions()
            }
        }
    
        /**
         * Requesting multiple permissions (record audio and location) at once
         * This uses multiple permission model from dexter
         * On permanent denial opens settings dialog
         */
        private fun requestMultiplePermissions() {
            Dexter.withContext(this)
                .withPermissions(
                    Manifest.permission.READ_CONTACTS,
                    Manifest.permission.READ_CALL_LOG
                )
                .withListener(object : MultiplePermissionsListener {
                    override fun onPermissionsChecked(report: MultiplePermissionsReport) {
                        // check if all permissions are granted
                        if (report.areAllPermissionsGranted()) {
                            Toast.makeText(
                                applicationContext,
                                "All permissions are granted!",
                                Toast.LENGTH_SHORT
                            ).show()
                        }
    
    
                        // check for permanent denial of any permission
                        if (report.isAnyPermissionPermanentlyDenied) {
                            // show alert dialog navigating to Settings
                            showSettingsDialog()
                        }
                    }
    
                    override fun onPermissionRationaleShouldBeShown(
                        permissions: List<PermissionRequest?>?,
                        token: PermissionToken
                    ) {
                        token.continuePermissionRequest()
                    }
                }).withErrorListener {
                    Toast.makeText(
                        applicationContext,
                        "Error occurred! ",
                        Toast.LENGTH_SHORT
                    ).show()
                }
                .onSameThread()
                .check()
        }
    
        /**
         * Requesting camera permission
         * This uses single permission model from dexter
         * Once the permission granted, opens the camera
         * On permanent denial opens settings dialog
         */
        private fun requestCameraPermission() {
            Dexter.withContext(this)
                .withPermission(Manifest.permission.CAMERA)
                .withListener(object : PermissionListener {
                    override fun onPermissionGranted(response: PermissionGrantedResponse) {
                        // permission is granted
                        openCamera()
                    }
    
                    override fun onPermissionDenied(response: PermissionDeniedResponse) {
                        // check for permanent denial of permission
                        if (response.isPermanentlyDenied) {
                            showSettingsDialog()
                        }
                    }
    
                    override fun onPermissionRationaleShouldBeShown(
                        permission: PermissionRequest?,
                        token: PermissionToken
                    ) {
                        token.continuePermissionRequest()
                    }
                })
                .withErrorListener {error ->
                    Toast.makeText(applicationContext, "Error occurred! $error", Toast.LENGTH_SHORT).show();
                }
                .check()
        }
    
        /**
         * Showing Alert Dialog with Settings option
         * Navigates user to app settings
         * NOTE: Keep proper title and message depending on your app
         */
        private fun showSettingsDialog() {
            MaterialAlertDialogBuilder(this)
                .setTitle(resources.getString(R.string.permissions_title))
                .setMessage(resources.getString(R.string.permissions_message))
                .setNegativeButton(resources.getString(R.string.cancel)) { dialog, which ->
                    // Respond to neutral button press
                }
                .setPositiveButton(resources.getString(R.string.goto_settings)) { dialog, which ->
                    // Respond to positive button press
                    openSettings()
                }
                .show()
        }
    
        // navigating user to app settings
        private fun openSettings() {
            requestOpenSettings.launch(
                Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                    data = Uri.fromParts("package", packageName, null)
                })
        }
    
        private fun openCamera() {
            requestOpenCamera.launch(Intent(MediaStore.ACTION_IMAGE_CAPTURE))
        }
    }
    
Run the code and see it in action.
Let me know your queries in the comments section below.

Cheers!
Happy Coding 🤗
Previous Post Next Post

نموذج الاتصال