Android Permissions Unveiled: A Developer's Insight

Android Permissions Unveiled: A Developer's Insight


Published in ProAndroidDev

This article was published in ProAndroidDev so you could also read it here

I believe that writing code is not hard, but understanding why we write it is. Let’s start with a brief introduction to permissions in Android.

Permissions are safeguards that protect a user’s privacy by restricting the access to sensitive data and device’s functionality which can be misused. These permissions ensure that apps only have access to the necessary components and data, enhancing user privacy and security.

Types of Permission

  1. Install-time
  2. Runtime
  3. Special

Each of these permissions are assigned a Protection Level

Before we dive into details of different permissions let’s first understand what is Protection Level.

Protection Level

A Protection Level signifies the nature of the permission i.e, whether a user needs to grant that permission, whether a third party app can even be provided with that permission, what specific apps can have that permission etc.  Understanding Protection Level really eases our work with Permissions.

A Protection Level is made up of two parts

  1. Protection (mandatory)
  2. Protection Flag (optional)

Protection level = Protection | Protection Flag 

There can only be one Protection but many Protection Flags in a Protection level.

Protection level = Protection | Protection Flag | Protection Flag

| signifies OR and in this context it means that the permission will be granted if either of the Protection or any Protection Flag is satisfied by our app

Protection is further classified into 4 parts

  1. normal: Permission will be granted if declared in the Manifest and user’s consent is not needed.
  2. dangerous: A user needs to be asked for the permission to be granted.
  3. signature: This one’s a little tricky, the official definition is

The system grants a signature permission to an app only when the app is signed by the same certificate as the app or the OS that defines the permission.

Confusing huh? Let’s simplify this

when the app is signed by the same certificate as the app

In native android development we can define our own custom permissions and suppose we created an app which has some sensitive functionality or data. If we want to make sure that this functionality or data shouldn’t be accessible to any other app on the device and just to the app(s) that we want, we can protect this functionality or data by hiding them behind a custom permission with the signature Protection Level.

But how the OS will recognise the app(s) which can have access to the functionality or data?

Answer: While uploading apps to Playstore we sign the bundle/apk with a certificate which proves it’s authenticity. if the app that has the said functionality or data and it defends the same with a permission of protection level of type signature then the app(s) which want to have the access to it must be signed with the same certificate.

OS that defines the permission

Some permissions are not to be provided to the third party apps and are there for the use by System apps only, those also fall in this category.

Note: By System apps here I don’t mean all the apps which came pre-installed and which cannot be deleted, I meant only those apps which are signed by the OS itself. For example the permission BIND_ACCESSIBILITY_SERVICE has Protection level signature associated with it which means no third party app can ask for the it and only System apps can utilise it.

  1. internal: As I said that a Protection level is the combination of Protection and Protection flag (More about them in the next paragraph). When a permission has to be provided solely on the basis of a Protection flag then then the Protection assigned to that permission is internal.  For eg. the permission MANAGE_DEVICE_LOCK_STATE has Protection level internal|role signifying that the permission will only be granted if the role protection flag is satisfied by the app. Protection Flags are further classified into
  2. appop: Permissions with this flag give access to some powerful action that accesses some core feature of the OS such as setting alarms. A user needs to understand the usecase of permission having this flag before granting them. Example is the SCHEDULE_EXACT_ALARM permission
  3. preinstalled: No third party apps that are installed from an apk, app bundle or are side-loaded can be granted the permission which has this flag. Although, if the app is preinstalled on a device then the permission could be provided. The permission could be asked for and provided in the newer updates of the app too.
  4. privileged: If an app is installed at system/priv or system/app directory i.e. if the app is marked as a system app then only the permission which has this flag can be provided.  Note: If a privileged app is updated then it can’t ask and neither is provided the permissions with this flag hence only preinstalled apk or app bundle can have such permissions.
  5. development: Any permission with this flag can be provided with the help of shell or terminal for testing purposes. 

Some examples:  ACCESS_WIFI_STATE has Protection level of normal which means it can be granted to an app if it is declared in the manifest and will be granted when the app is installed.

BLUETOOTH_SCAN has Protection level of dangerous which means it can be granted to an app if it is declared in the manifest and granted by the user.

BATTERY_STATS has Protection level of signature|privileged|development which means it can be granted to an app if the app has been signed by the same certificate as the OS or the app is a System app but the permission can also be provided through terminal.

Before diving into various kinds of permissions I want to provide a figure segregating them on the basis of whether a user needs to be asked for providing them and the Protection level assigned to them.

Install-time Permissions

Every android permission which comes under this category offers limited access to restricted data and restricted actions. As they offer limited access so providing them to an app poses the least risk to both user and the device itself which in-turn makes them easiest to handle.

As developers we just need to declare them in the Manifest file of the app and are provided to the app by the OS when the app is getting installed on user’s device. A user can see what Install-time permissions will be granted to the app when it is installed in the app details section on Playstore. List of install-time permissions required by an app as seen from Playstore.

These permissions are further divided into two categories

  1. Normal permissions: These present limited risk to user’s personal data and the integrity of the system. They are assigned the protection level normal. E.g. ACCESS_NETWORK_STATE
  2. Signature permissions: These permissions are those which have the signature protection level associated with them as described Protection Level section above. E.g. BIND_AUTOFILL_SERVICE

Runtime permissions

These permissions provide more access to restricted data and actions hence possess a greater risk to user privacy and system integrity. These need to be granted by the user and declaring them in Manifest is not enough. The System creates the dialog box for asking the runtime permissions so we don’t have to create or manage one ourselves.

These permissions can be revoked by the user, so whenever performing an action in the app which requires a runtime permission it must always be checked that the permission is already granted or not. If a user denies a runtime permission twice than the dialog asking for the permission is not shown again. They are associated with protection level dangerous. E.g. ANSWER_PHONE_CALLS

How to ask for Runtime Permissions?

As per the official documentation this is how a flow asking for a runtime permission should look like below.

figure: 1 (officially recommended flow)

figure: 1 (officially recommended flow)

Roughly in code it would look like

   val requestPermissionLauncher =
       registerForActivityResult(
           ActivityResultContracts.RequestPermission()
       ) { isGranted: Boolean ->
           if (isGranted) {
               Toast.makeText(this, "Permission Granted", Toast.LENGTH_SHORT).show()
           } else {
               Toast.makeText(this, "Permission Denied", Toast.LENGTH_SHORT).show()
           }
       }           

if (ContextCompat.checkSelfPermission(
                   this,
                   Manifest.permission.Required_Runtime_Permission
               ) == PackageManager.PERMISSION_GRANTED
           ) {
               Toast.makeText(this, "Permission Already Present", Toast.LENGTH_SHORT).show()
           } else {
               if (ActivityCompat.shouldShowRequestPermissionRationale(
                       this, Manifest.permission.Required_Runtime_Permission
                   )
               ) {
                   Toast.makeText(this, "Rationale Required", Toast.LENGTH_SHORT).show()
                   AlertDialog.Builder(this).setMessage("permission required because...")
                       .setPositiveButton(
                           "Ok"
                       ) { p0, p1 -> requestPermissionLauncher.launch(Manifest.permission.Required_Runtime_Permission) }
                       .setNegativeButton(
                           "Deny"
                       ) { p0, p1 -> p0.cancel() }.show()
               } else {
                   requestPermissionLauncher.launch(Manifest.permission.Required_Runtime_Permission)
               }
           }
       }

As you can see I have marked a region in the flow as Region of dispute and this is because the official recommendation is to gracefully degrade the app’s experience and not provide the functionality. But as far as I know this is not an acceptable way for most of the usecases.

If the permission is denied twice then the system generated prompt asking the user for permission won’t appear throughout the app lifetime (unless it’s data is cleared) for the said permission and this should be the case too as per the official flow. The only way the app can get the permission is if the user manually gives the permission from the info screen of the app.

System generated prompt for asking a runtime permission

How to overcome it?

As we know that the prompt will only appear twice but the option of directing the users to settings screen is always available but still that cannot be the primary way to go forward as the prompt gives an intuitive UX to the users where they have to just read and choose, with the second option though we can re-direct the user to the app info page from the app itself (by using Intent) but still the user would have to understand by himself which permission he/she has to allow.

We must take the advantage of the prompt as well and resort to directing the user to settings page only when the permission has been denied permanently

As the official recommendation is to degrade the experience gracefully so by default there is no api or function which can tell the developer that the permission has been denied twice or permanently denied. Also the function responsible for knowing whether a Rationale should be shown to the user returns false when the permission has been denied twice.

ActivityCompat.shouldShowRequestPermissionRationale(
                        this, Manifest.permission.Required_Runtime_Permission
                    )

See the video describing the issue

One way to overcome this is by making sure that the first call to the function shouldShowRequestPermissionRationale() is made when the permission has already been denied once.  By this approach shouldShowRequestPermissionRationale() will return true after the first call whereas if the permission is denied again it will return false as by design the permission will be considered permanently denied and neither the prompt will appear and nor rationale should be shown.

By this we can get whether a permission has been permanently denied or not.

I have tweaked the the official flow (see figure: 1) to address the solution I mentioned. Same sections from the figure: 1 have same colour here too as for the ease of understanding.

figure: 2 (flow recommended for getting whether the permission has been denied permanently)Special permissions

Special permissions

These permission can only be defined by the platform itself and are used to protect access to powerful actions which can put a load on the system or harm it’s integrity to a great extent such as setting exact alarms. The protection level of these permissions is appop. Examples: SCHEDULE_EXACT_ALARMS, MANAGE_EXTERNAL_STORAGE

How to ask for Special Permissions?

Unlike Runtime permission the system doesn’t shows a prompt to the user for granting them, they can be granted from the Special App Access page which can be accessed by Settings > Apps > Special app access.

This page can be hard for users to find which are not very well versed with the device’s settings screen so it’s important that the user must be directed from within the app and this can done via Intent too.  Each special permission has a it’s own Intent action for e.g. for the permission SCHEDULE_EXACT_ALARMS an Intent to direct the user to the page from where this permission can be granted looks like this

Intent(
           Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM,
           Uri.fromParts("package", (context as Activity).packageName, null)
       )

But some special permissions as per the android version of the device can be provided to the apps when they are installed as in the case of SCHEDULE_EXACT_ALARMS. It is provided at install time to the apps which have declared it in the Manifest and are installed on devices running Android version 11 or below whereas it has to be provided by the user for the devices running on any Android version greater than 11 such as 12, 12L, 13 etc.

These permissions can also be revoked at any time so it’s important to check each time whether they have been granted when user triggers an action which requires them. To check this each special permission has it’s own custom function provided by the framework itself. For SCHEDULE_EXACT_ALARMS it is canScheduleExactAlarms() which returns true if the permission is already granted i.e. if the app is installed on a device with Android version less than 12 or the permission has already been granted whereas will return false if not already granted from the Special access page for devices running Android 12 or greater.

As the user can be asked for these permission so showing a rationale in that case is officially recommend but the function determining whether to show one is same as used for Runtime Permissions and also works the same so again keeping a check for whether the special permission has been denied permanently can be required.

recommended flow for asking for special permissions

If you have read till here then I hope you have liked it and if this is the case indeed, do show some appreciation by clapping for it and sharing it. Thanks for your time :) References: https://developer.android.com/guide/topics/permissions/overview