3.5 Permissions

The manifest file also controls what permissions your application has requested from the user. Apps must be granted the right to use certain functions of the device, from accessing the camera to accessing the internet. Only once the app has requested access, will the system grant the benign ones, and ask the user to explicitly hand over control of the more dangerous ones. For example, knowing the state of the network will be granted automatically, while knowing device location must go through the user first.

The app must publicize all required permissions in AndroidManifest.xml, where they all sit in <uses-permission> tags in the manifest root as a sibling child to the <application> element. The following line declares that this application will request the geo-location of users.

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

When dealing with code that involves permissions, developers must be able to adequately respond to situations where the user has denied the permission to the application (and not crash because of an exception because the permission wasn’t granted).

How can we request permissions?

Before checking out this section, make sure you first check out this!

Step 1: Declare the permission in the Android Manifest file: In Android, permissions are declared in the AndroidManifest.xml file using the uses-permission tag.

<manifest xlmns:android...>
 ...
 <uses-permission android:name=”android.permission.PERMISSION_NAME”/>
 <application ...
</manifest>

Step 2: Check whether permission is already granted or not. If permission isn’t already granted, request the user for the permission: In order to use any service or feature, the permissions are required. Hence we have to ensure that the permissions are given for that. If not, then the permissions are requested.

This step is compromised of multiple steps.

Step 2.1: Register the permissions callback, which handles the user's response to the system permissions dialog. In this case, we can use the ActivityResultLauncher introduced during the explicit intents section to automatically handle actually requesting the permission to the user and giving us the result:


    private lateinit var requestPermissionLauncher: ActivityResultLauncher<String>
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        requestPermissionLauncher =
            registerForActivityResult(ActivityResultContracts.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.
                }
            }
        ...
    }

Step 2.2: Check or request the given permission. This code snippet demonstrates the recommended process of checking for a permission, and requesting a permission from the user when necessary:

when {
    ContextCompat.checkSelfPermission(
            <CONTEXT>, // Usually <this> if in an activity
            Manifest.permission.<REQUESTED_PERMISSION>
            ) == PackageManager.PERMISSION_GRANTED -> {
        // Permission is granted. Continue the action or workflow in your app.
    }
    // If the permission was denied previously, if requested again there will be 
    // a never ask again checkbox in the permission prompt. 
    // shouldShowRequestPermissionRationale checks to see if the 
    // user checked said checkbox. shouldShowRequestPermissionRationale 
    // method returns false only if the user selected never ask again 
    // or device policy prohibits the app from having that permission and true
    // otherwise.
    ActivityCompat.shouldShowRequestPermissionRationale(context, Manifest.permission.<REQUESTED_PERMISSION>) -> {
        // 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.
    }
    else -> {
        // You can directly ask for the permission.
        // The registered ActivityResultCallback gets the result of this request.
        requestPermissionLauncher.launch(
                Manifest.permission.<REQUESTED_PERMISSION>)
    }
}

How does this work if we want to request multiple permissions at once?

Similarly, multiple permissions can be requested at the same time by passing an array of Permissions instead of single permission as input and we get a MutableMap with permissions as keys and grant result as values in the activityResultCallback.

The request permission launcher changes as so:

    private lateinit var requestPermissionLauncher: ActivityResultLauncher<Array<String>>
    requestPermissionLauncher =
        registerForActivityResult(
           ActivityResultContracts.RequestMultiplePermissions())   
             { permissions ->
                // Handle Permission granted/rejected
               permissions.entries.forEach {
                  val permissionName = it.key
                  val isGranted = it.value
                  // Feel free to also check for in your 
                  // conditions the permissionName as well! Below is just 
                  // a bsaic example that only checks to see if it's granted.
                  if (isGranted) {
                     // Permission is granted
                  } 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.
                  }
              }
              
              // Can also opt for something like this, which, 
              // if you need to check to see if multiple/all your permissions 
              // are granted before proceeding may be useful:
              if (permissions.entries.all {
                    it.value == true }) {
                    // All permissions are granted
              } else {
                 // Explain to user why you need to access all of these permissions!
              }
              
              // Since permissions is a map of the permissions to their isGranted
              // status, can also do something like this:
              if (permissions[Manifest.permission.<SOME_SPECIFIC_PERMISSION>] == true
              && permissions[Manifest.permission.<SOME_OTHER_PERMISSION>] == true) {
                 // The given permissions are granted
              } else {
                 ...
              }
           }
        }

Ultimately just tailor the conditions to your needs!

The when else block in step 2.2 is dependent on your needs of your permissions but the structure is relatively the same. If you need multiple permissions at once for some API / UI / functionality, expand the first if case to check to see if all the respective permissions are granted.

If I needed to access both the camera and external storage I could do something like this:

when {
    ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED && 
    ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.READ_EXTERNAL_STORAGE
        ) == PackageManager.PERMISSION_GRANTED  -> {
        // Permissions are granted, carry through with action
    }
    ...
 }

This process can be abstracted away as a function!

private fun hasPermissions(context: Context, vararg permissions: String): Boolean = permissions.all {
        ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
 }
 
 ...
 
 when {
        hasPermissions(this,
            Manifest.permission.READ_CONTACTS,
            Manifest.permission.READ_EXTERNAL_STORAGE) -> {
        // Permissions are granted, carry through with action
        }
        ...
}

When multiple permissions are requested, the permission prompts are shown one after another instead of all at once, so users could deny some but allow others, thus you must check if you shouldShowRequestPermissionRationale for each individual permission!

Lastly:

How do we use the new launcher to request multiple permissions?

Instead of passing a singular permission, we pass in an array of the permissions we want to ask for, here's an example below:

...
requestPermissionLauncher.launch(
arrayOf(Manifest.permission.CAMERA,    
        Manifest.permission.READ_EXTERNAL_STORAGE)             
) 
...

Accompanist - Simplifying Permissions

Requesting permissions can be a complex process, but with the help of libraries like Accompanist, handling permissions becomes much easier and streamlined, especially in Jetpack Compose. Here, we introduce the Accompanist library, which simplifies permission handling in Compose-based applications.

Installation

To get started with Accompanist Permissions, add the following dependency to your build.gradle.kts file:

build.gradle
dependencies {
    implementation("com.google.accompanist:accompanist-permissions:0.34.0")
}

Setup

Declare any required permissions in the AndroidManifest.xml. We'll be working with the ACCESS_FINE_LOCATION and the READ_CONTACTS permissions which changes our manifest as so:

AndroidManifest.xml
<manifest xlmns:android...>
 ...
 <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_CONTACTS" />
 
 <application 
 ...
 </application>
</manifest>

Checking and Requesting Permissions

Accompanist makes checking and requesting permissions straightforward by using rememberPermissionState. Permissions are managed as states in Compose, allowing UI updates based on permission status.

Here’s an example of how to request permissions for fine location and contacts:

MainActivity.kt
fun PermissionScreen() {
    // Permission states for location and contacts
    val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_FINE_LOCATION)
    val contactsPermissionState = rememberPermissionState(Manifest.permission.READ_CONTACTS)

    // Handle UI based on permission status
    Column {
        Button(onClick = {
            if (!locationPermissionState.hasPermission) {
                locationPermissionState.launchPermissionRequest()
            }
        }) {
            Text("Request Location Permission")
        }

        Button(onClick = {
            if (!contactsPermissionState.hasPermission) {
                contactsPermissionState.launchPermissionRequest()
            }
        }) {
            Text("Request Contacts Permission")
        }

        // Example of showing rationale if permission is denied
        if (locationPermissionState.shouldShowRationale) {
            Text("We need location permission to show nearby places.")
        }

        if (contactsPermissionState.shouldShowRationale) {
            Text("Contacts permission is needed to access your contacts.")
        }
    }
}

Requesting Permissions Flow

  1. Check Permission State: Use rememberPermissionState to check if a permission is granted or not.

  2. Launch Permission Request: If a permission is not granted, call launchPermissionRequest() to show the system permission dialog.

  3. Handle Rationale: If the user denies the permission, shouldShowRationale can be used to show a rationale message explaining why the app needs the permission.

All done! Way more simple!

Last updated