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)             
) 
...

EasyPermissions - Using Third Party Libraries

As you can see, requesting permissions is a tedious process but lucky for us, there's lots of third-party libraries (some functionality developed by an outside developer) that does the hard work for us so that we don't have to reinvent the wheel!

Here we will introduce the EasyPermissions library!

Installation

EasyPermissions-ktx is installed by adding the following dependency to your build.gradle file:

build.gradle
dependencies {
    implementation 'com.vmadalin:easypermissions-ktx:1.0.0'
}

Setup

To begin using EasyPermissions, have your Activity (or Fragment) override the onRequestPermissionsResult method:

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        // EasyPermissions handles the request result as opposed to regular Android
        EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
    }
}

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

To check for Permissions, we can leverage using EasyPermissions#hasPermissions(...) to check if the app already has the required permissions. This method can take any number of permissions as its final argument. To actually request the permissions, we use EasyPermissions#requestPermissions(...)

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // This can be put anywhere, e.g. a button event listener. However we put
        // it in the onCreate here for simplicity!
        if (!EasyPermissions.hasPermissions(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION,
                Manifest.permission.READ_CONTACTS
            )
        ) {
            // We don't actually have the permissions, so we need to request them from
            // the user.
            EasyPermissions.requestPermissions(
                host = this,
                // If they deny the permissions, the rationale appears the next 
                // time permissions are requested. Use this to explain why you 
                // need the specific permissions in your app.
                rationale = "Give us the permissions. We need it. Don't be stupid",
                // More on this later.
                requestCode = REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION,
                perms = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, 
                                Manifest.permission.READ_CONTACTS)
            )
        } else {
            Log.d("LOG", "Permission already granted, silly.")
            // Here the permissions are already granted, do whatever action 
            // you needed the permissions for!
            ...
        }
    }

Request Code

The request code provided in the call EasyPermissions#requestPermissions(...)should be unique to the request, as it's used by the library for identifying where to go after the permissions are granted. A useful place to define your request codes is in your companion object:

MainActivity.kt
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }
    
    companion object {
        const val REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION = 1
    }
}

What is the request code used for?

As mentioned before, the request code is used by the library for identifying where to go after the permissions are granted. We can define a function to be called after the permissions are granted to do whatever action we needed the permissions for using the request code:

MainActivity.kt
// If all of the permissions in a given request are granted, 
// all methods annotated with the proper request code will be 
// executed (be sure to have a unique request code). 
// The annotated method cannot return anything or have arguments.
@AfterPermissionGranted(REQUEST_CODE_LOCATION_AND_CONTACTS_PERMISSION)
override fun afterPermissionsGranted() {
    Log.d("LOG", "You have the permissions. Go crazy!")
}

Required Permissions

If the permission was denied previously, if requested again there will be a never ask again checkbox in the permission prompt. In some cases your app will not function properly without certain permissions. If the user denies these permissions with the "Never Ask Again" option, you will be unable to request these permissions from the user and they must be changed in app settings.

Luckily for us, EasyPermissions allows us to easily redirect the user to the settings to enable these permissions. You can use the method EasyPermissions.somePermissionPermanentlyDenied(...) to display a dialog to the user in this situation and direct them to the system setting screen for your app:

override fun onPermissionsDenied(requestCode: Int, perms: List<String>) {
    // Check whether the user denied any permissions and checked "NEVER ASK AGAIN."
    // This will display a dialog directing them to enable the permission in app settings.
    if (EasyPermissions.somePermissionPermanentlyDenied(this, perms)) {
        SettingsDialog.Builder(this).build().show()
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == DEFAULT_SETTINGS_REQ_CODE) {
        // Do something after user returned from app settings screen, e.g. check to see
        // if the permissions have been enabled!
    }
}

All done! Way more simple!

Last updated