Requesting Multiple Runtime Permission Made EASY

Requesting Multiple Runtime Permission Made EASY

Table of contents

No heading

No headings in the article.

As a developer dealing with permission in android is time-consuming, writing too much boilerplate code. Recently in Android 11, users can give permission “only once” or “while using the app,” They can revoke the consent if the app is not in use for a long time.

But today, we will learn a new way to request multiple runtime permission with minimal boilerplate code.

Step 1: First, we need to declare our permission in the manifest file before requesting

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Here we will access permission to read contacts, access location and storage.

Step 2: Inside our activity, we need to declare our ActivityResultLauncher<I> and variable which keeps track of our permission

class MainActivity : AppCompatActivity() {
    private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
    private var isContactPermissionGranted = false
    private var isLocationPermissionGranted = false
    private var isStoragePermissionGranted = false
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

We use ActivityResultLauncher<I> to launch the other activity where I is the type of input required to launch.

Step 3: We need to initialize our permissionLauncher

class MainActivity : AppCompatActivity() {
    private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
    private var isContactPermissionGranted = false
    private var isLocationPermissionGranted = false
    private var isStoragePermissionGranted = false
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

         permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
                isContactPermissionGranted = permissions[Manifest.permission.READ_CONTACTS] ?: isContactPermissionGranted
                isLocationPermissionGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: isLocationPermissionGranted
                isStoragePermissionGranted = permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: isStoragePermissionGranted
            }

    }
}

The ActivityResult API provides registerForActivityResult API for registering the result callback. In simple terms, it will register the output which we will receive from the permission dialog (if the user has granted permission or denied it)

registerForActivityResult() takes an ActivityResultContract and an ActivityResultCallback and returns an ActivityResultLauncher which you’ll use to launch the other activity.

What is ActivityResultContract?
A contract specifying that an activity can be called with an input of type I and produce an output of type O.

What is ActivityResultCallback?
It is a single method interface with an onActivityResult() method that takes an object of the output type defined in the ActivityResultContract. When the output is available onActivityResult() is executed in simple terms.

As we have to request multiple permission for users, we need to define a contract that can handle multiple runtime permission that we have. Here we are using ActivityResultContracts.RequestMultiplePermissions.

Let's dive deeper and check how this RequestMultiplePermissions() works

public static final class RequestMultiplePermissions
            extends ActivityResultContract<String[], java.util.Map<String, Boolean>> {
              ..............
    }

RequestMultiplePermissions() is a static final class that extends ActivityResultContract<I,O> for input; we need to pass an array of strings. We get output as Map where the key is of String type, and the value is of Boolean type.

Now let's see how this class generates the output

RequestMultiplePermissions Implements a method createIntent() which will create that permission dialog and request permission.

When the result is obtained from the user inside RequestMultiplePermissions, we have a method parseResult() which will convert the result obtained from onActivityResult to desired output.

Let's look inside the code for parse result here as we can see it creates Map With permission name and a boolean which is checking if permission is granted, and then we return that result.

So inside our ActivityResultCallback() { of registerForActivityResult() as described above } we will receive a Map as output.

public static final class RequestMultiplePermissions
            extends ActivityResultContract<String[], java.util.Map<String, Boolean>> {

        @NonNull
        @Override
        public Map<String, Boolean> parseResult(int resultCode,
                @Nullable Intent intent) {
            if (resultCode != Activity.RESULT_OK) return emptyMap();
            if (intent == null) return emptyMap();

            String[] permissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS);
            int[] grantResults = intent.getIntArrayExtra(EXTRA_PERMISSION_GRANT_RESULTS);
            if (grantResults == null || permissions == null) return emptyMap();

            Map<String, Boolean> result = new HashMap<>();
            for (int i = 0, size = permissions.length; i < size; i++) {
                result.put(permissions[i], grantResults[i] == PackageManager.PERMISSION_GRANTED);
            }
            return result;
        }

        @NonNull
        static Intent createIntent(@NonNull String[] input) {
            return new Intent(ACTION_REQUEST_PERMISSIONS).putExtra(EXTRA_PERMISSIONS, input);
        }
    }

While registerForActivityResult() registers your callback, it does not launch the other activity and kick off the request for a result. Instead, this is the responsibility of the returned ActivityResultLauncher instance. (our permissionLauncher variable holds the instance of ActivityResultLauncher)

In simple term registerForActivityResult() keeps track of our permissions that are granted or denied.

ActivityResultlauncher is used to launch other activities. (permission dialog in this case)

Step 4: Request Permission

class MainActivity : AppCompatActivity() {

    private lateinit var permissionLauncher: ActivityResultLauncher<Array<String>>
    private var isContactPermissionGranted = false
    private var isLocationPermissionGranted = false
    private var isStoragePermissionGranted = false
    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
                Log.d(TAG, "permissions :$permissions")
                isContactPermissionGranted = permissions[Manifest.permission.READ_CONTACTS] ?: isContactPermissionGranted
                isLocationPermissionGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: isLocationPermissionGranted
                isStoragePermissionGranted = permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: isStoragePermissionGranted
            }

        requestPermissions()

    }

    private fun requestPermissions() {
        isContactPermissionGranted = ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.READ_CONTACTS
        ) == PackageManager.PERMISSION_GRANTED


        isLocationPermissionGranted = ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED

        isStoragePermissionGranted = ContextCompat.checkSelfPermission(
            this,
            Manifest.permission.READ_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED

        val permissionRequestList = ArrayList<String>()

        if (!isContactPermissionGranted) {
            permissionRequestList.add(Manifest.permission.READ_CONTACTS)
        }

        if (!isLocationPermissionGranted) {
            permissionRequestList.add(Manifest.permission.ACCESS_FINE_LOCATION)
        }

        if (!isStoragePermissionGranted) {
            permissionRequestList.add(Manifest.permission.READ_EXTERNAL_STORAGE)
        }

        if (permissionRequestList.isNotEmpty()) {
            permissionLauncher.launch(permissionRequestList.toTypedArray())
        }
    }
}

Here in our requestForPermission() method, we first check whether all the permission is granted or denied.

I have declared a permissionRequestList variable that holds a list of all the permission not granted.

Then we check if permissionRequestList is not empty, which means the user has not given access to the permissions.

So Then we launch!!

What Launch Does?
If input exists, the launcher takes the input that matches the type of the ActivityResultContract Here input type is an array of strings because of ActivityResultContract.RequestMultiplePermission() takes an array of strings as input, as discussed earlier.

Calling launch() starts the process of producing the result. When the user is done with the subsequent activity and returns, the onActivityResult() from the ActivityResultCallback is executed.

So here we receive a Map<String, Boolean> which we get from parseResult() method as discussed earlier and check if the permission is granted and set their variables accordingly.

permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
                isContactPermissionGranted = permissions[Manifest.permission.READ_CONTACTS] ?: isContactPermissionGranted
                isLocationPermissionGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: isLocationPermissionGranted
                isStoragePermissionGranted = permissions[Manifest.permission.READ_EXTERNAL_STORAGE] ?: isStoragePermissionGranted
            }

Did you find this article valuable?

Support Krish Parekh by becoming a sponsor. Any amount is appreciated!