Dangerous Permissions: Request at Runtime
In Android 6.0 and higher devices, permissions that are considered to be dangerous
not only have to be requested via <uses-permission>
elements, but you also have to ask the user to grant you those permissions at runtime. What you gain, though, is that users are not bothered with these permissions at install time, and you can elect to delay asking for certain permissions until such time as the user actually does something that needs them.
Let’s explore the runtime permissions system via a new series of questions.
Along the way, we will examine bits of the ContentEditor
sample module in the Sampler
and SamplerJ
projects. This app implements a tiny text editor, where the user can edit text from various sources. We will explore the file I/O portions of this code in an upcoming chapter. Here, we will focus on the WRITE_EXTERNAL_STORAGE
permission that this app needs, as that is a dangerous permission and one that we have to request at runtime.
What Permissions Are Affected By This?
Inside Android, permissions are organized into permission groups. For example, in Android 9.0, there are ten permission groups that contain dangerous permissions:
Permission Group | Permission |
---|---|
CALENDAR |
READ_CALENDAR , WRITE_CALENDAR |
CALL_LOG |
PROCESS_OUTGOING_CALLS , READ_CALL_LOG , WRITE_CALL_LOG |
CAMERA |
CAMERA |
CONTACTS |
GET_ACCOUNTS , READ_CONTACTS , WRITE_CONTACTS |
LOCATION |
ACCESS_COARSE_LOCATION , ACCESS_FINE_LOCATION |
MICROPHONE |
RECORD_AUDIO |
PHONE |
ADD_VOICEMAIL , ANSWER_PHONE_CALLS , CALL_PHONE , READ_PHONE_NUMBERS , READ_PHONE_STATE , USE_SIP |
SENSORS |
BODY_SENSORS |
SMS |
READ_SMS , RECEIVE_SMS , RECEIVE_MMS , RECEIVE_WAP_PUSH , SEND_SMS |
STORAGE |
READ_EXTERNAL_STORAGE , WRITE_EXTERNAL_STORAGE |
This roster changes over time. For example, in Android 6.0, the call log permissions were in the PHONE
category.
Users will be able to revoke permissions by group, through the Settings app. They can go into the page for your app, click on Permissions, and see a list of the permission groups for which you are requesting permissions:
What Goes in the Manifest?
The same <uses-permission>
elements as before. These declare the superset of all possible permissions that you can have. If you do not have a <uses-permission>
element for a particular permission, you cannot ask for it at runtime, and the user cannot grant it to you.
How Do I Ask the User For Permission?
To ask the user for one of the runtime permissions — or find out that we already have it — we can use ActivityResultContracts.RequestPermission
along with registerForActivityResult()
:
private final ActivityResultLauncher<String> requestPerm =
registerForActivityResult(new ActivityResultContracts.RequestPermission(),
new ActivityResultCallback<Boolean>() {
@Override
public void onActivityResult(Boolean wasGranted) {
if (wasGranted) {
loadFromDir(Environment.getExternalStorageDirectory());
}
else {
Toast.makeText(MainActivity.this, R.string.msg_sorry, Toast.LENGTH_LONG).show();
}
}
});
private val requestPerm = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it) {
loadFromDir(Environment.getExternalStorageDirectory())
} else {
Toast.makeText(this, R.string.msg_sorry, Toast.LENGTH_LONG).show()
}
}
We can then call launch()
on requestPerm
when we want to request permission, supplying the name of the permission that we want to request (Manifest.permission.WRITE_EXTERNAL_STORAGE
):
requestPerm.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE);
requestPerm.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
The callback will be called with a Boolean
value. If it is true
, we received permission; if false
, we did not. Based upon that value, we can determine what we want to do.
What Do I Do If the User Says “No”?
The user can click the “Deny” button for one or more pages in that system dialog and thereby reject granting you the requested permission(s). Your callback will get false
in this case, instead of true
.
If you were requesting permission as a direct response to some bit of user input (e.g., user tapped on an app bar item), and the user rejects the permission you need to do the work, obviously you cannot do the work. Depending on overall flow, showing a dialog or something to explain why you cannot do what the user asked for may be needed. In some cases, you may deem it to be obvious, by virtue of the fact that the user saw the permission-request dialog and said “deny”.
If you were requesting permission pre-emptively, such as when the activity starts, you will need to decide whether that decision needs to be reflected in the current UI (e.g., “no data available” messages, disabled app bar items).
What Do I Do If the User Says “No, And Please Stop Asking”?
The second time you ask a user for a particular runtime permission, the user will have a “Don’t ask again” option:
If the user chooses that, not only will you not get the runtime permission now, but all future requests will immediately call your callback indicating that your request for permission was denied. The only way the user can now grant you this permission is via the Settings app.
You need to handle this situation with grace and aplomb.
Choices include:
- Disabling UI input (e.g., app bar items) that cannot be performed because you lack permission
- Display a dialog, explaining the situation, with a button that links the user over to your app’s screen in Settings, so the user can grant you this permission
- Displaying inline messages about why you cannot show data (e.g., a count of contacts that you cannot show because the user did not grant you access), perhaps with a hyperlink that displays a screen with additional information about the situation
For permissions that, when denied, leave your app in a completely useless state, you may wind up just displaying a screen on app startup that says “sorry, but this app is useless to you”, with options for the user to uninstall the app or grant you the desired permissions.
How Do I Know If the User Takes Permissions Away From Me?
Permissions granted by a user can be revoked in a few ways:
- The user can remove it manually via the Settings app
- On Android 11+, the user can choose to grant a one-time permission, which your app will only hold until it leaves the foreground
- On Android 11+, the user can opt into having Android revoke the permission automatically if the user does not use your app for some time
If a permission is revoked, and your app is running at the time, your process is terminated.
Hence, while your code is running, you will have all permissions that you started with, plus any new ones that the user grants on the fly based upon your request. There should be no circumstance where your process is running yet you lose a permission.
What Happens When I Ship This to an Android 5.1 or Older Device?
Older devices behave as they always have. Since you still list the permissions in the manifest, those permissions will be granted to you if the user installs the app, and the user will be notified about those permissions as part of the installation process. If you are using ActivityResultContracts.RequestPermission
as described above, your code should just work.
What Happens if the User Clears My App’s Data?
If the user clears your app’s data through the Settings app, the runtime permissions are cleared as well. Behavior at this point will be as if your app had been just installed, and you will need to request the permissions.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.