10 - Android Permissions
Permissions
Permissions are needed for:
- restricted data (system state)
- restricted actions (recording audio)
NB! When you include a library, you also inherit its permission requirements. Be aware of the permissions that each dependency requires and what those permissions are used for.
All permissions are listed here:
https://developer.android.com/reference/android/Manifest.permission
Permissions are first declared in manifest.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<application... />
Permissions can be categorized into:
- install-time
- run time
- special
General workflow
Install-time permissions
Install-time permissions are presented in app store, and are automatically granted during app install.
Install time permissions are divided into normal and signature.
- Normal - data and actions that extend past your app sandbox, but little risk.
- Signature - app is signed by the same certificate as the app or the OS that defines the permission (autofill, vpn, etc).
Run time permissions
Also known as dangerous permissions (location, contact, microphone)...
You need to request runtime permissions in your app before you can access the restricted data or perform restricted actions. And these need to be checked before every restricted access.
When your app requests a runtime permission, the system presents a runtime permission prompt:
Special permissions
Only the platform and OEMs can define special permissions - usally to protect powerful actions, such as drawing over other apps.
Look at the Special app access page
in system settings.
Hardware related permissions
Camera, bluetooth, etc...
https://developer.android.com/guide/topics/manifest/uses-feature-element#permissions-features
Is hardware actually required? Specify in manifest. If you require hardware permission in manifest, system by default assumes, that this hardware is required.
<uses-feature android:name="android.hardware.camera"
android:required="false" />
Detect hardware during app run...
if (applicationContext.packageManager.hasSystemFeature(
PackageManager.FEATURE_CAMERA_FRONT)) {
// do something with camera
} else {
// alternative flow
}
Request runtime permissions
General steps
- declare needed permissions in manifest
- during runtime - check, maybe you already have the permission. done.
- check, if rationale/explanation UI is needed (system decides). If so, display it, and wait for user acknowledgemnt
- request the runtime permission
- check response (granted or not)
- if not granted - gracefully degrade
Declare in manifest
<manifest ...>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
https://developer.android.com/reference/android/Manifest.permission
Check for existing permission grant
Pass needed permission to ContextCompat.checkSelfPermission()
. Returns PERMISSION_GRANTED
or PERMISSION_DENIED
.
If permission is granted - continue normally.
Check for rationale need
Users grant privileges much more readily when the need for them is explained well. Permissions dialog has no explanations, why your app needs the privileges.
If the ContextCompat.checkSelfPermission()
method returns PERMISSION_DENIED, call shouldShowRequestPermissionRationale()
- returns true or false. If true - show an educational UI.
Additionally, if your app requests a permission related to location, microphone, or camera, consider explaining why your app needs access to this information.
Add activity with following (for location, mic or camera):
<!-- android:exported required if you target Android 12. -->
<activity android:name=".DataAccessRationaleActivity"
android:permission="android.permission.START_VIEW_PERMISSION_USAGE"
android:exported="true">
<!-- VIEW_PERMISSION_USAGE shows a selectable information icon on
your app permission's page in system settings.
VIEW_PERMISSION_USAGE_FOR_PERIOD shows a selectable information
icon on the Privacy Dashboard screen. -->
<intent-filter>
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE_FOR_PERIOD" />
<category android:name="android.intent.category.DEFAULT" />
...
</intent-filter>
</activity>
- If you add the intent filter that contains the VIEW_PERMISSION_USAGE action, users see the icon on your app's permissions page in system settings. You can apply this action to all runtime permissions.
- If you add the intent filter that contains the VIEW_PERMISSION_USAGE_FOR_PERIOD action, users see the icon next to your app's name whenever your app appears in the Privacy Dashboard screen.
When users select that icon, your app's rationale activity is started.
Request permission
Use the RequestPermission
contract in AndroidX. Allows the system to manage the permission request code.
If needed you can also manage a request code yourself as part of permission request.
Add followin dependencies in build.gradle
- androidx.activity, version 1.2.0+ and androidx.fragment, version 1.3.0+.
- single permission, use
RequestPermission
. - multiple permissions, use
RequestMultiplePermissions
.
Follow these steps:
-
In your activity or fragment's initialization logic, pass in an implementation of ActivityResultCallback into a call to registerForActivityResult(). The ActivityResultCallback defines how your app handles the user's response to the permission request.
Keep a reference to the return value of registerForActivityResult(), which is of type ActivityResultLauncher. -
To display the system permissions dialog when necessary, call the launch() method on the instance of ActivityResultLauncher that you saved in the previous step.
After launch() is called, the system permissions dialog appears. When the user makes a choice, the system asynchronously invokes your implementation of ActivityResultCallback, which you defined in the previous step.
Full example code
MainActivity
package ee.taltech.permissionsdemo
import android.content.Context
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
class MainActivity : AppCompatActivity() {
companion object {
private val TAG = this::class.java.declaringClass!!.simpleName
}
// Register the permissions callback, which handles the user's response to the
// system permissions dialog. Save the return value, an instance of ActivityResultLauncher.
private val requestPermissionsLauncher =
registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { isGrantedMap: Map<String, Boolean> ->
isGrantedMap.forEach { (permission: String, isGranted: Boolean) ->
if (isGranted) {
// Permission was granted. Continue the action or workflow in your
// app.
Log.d(TAG, "Permission '$permission' was granted by the user")
} else {
// Explain to the user that the feature is unavailable because the
// feature 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.
Log.w(TAG, "Permission '$permission' was DENIED by the user")
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val permissionsInManifest = retrieveManifestPermissions(applicationContext)
handleAppPermissions(permissionsInManifest)
Log.d(TAG, "onCreate DONE!")
}
private fun retrieveManifestPermissions(context: Context): Array<String> {
val pkgName = context.getPackageName()
try {
return context
.packageManager
.getPackageInfo(pkgName, PackageManager.GET_PERMISSIONS)
.requestedPermissions
} catch (e: PackageManager.NameNotFoundException) {
return emptyArray<String>()
// Better to throw a custom exception since this should never happen unless the API has changed somehow.
}
}
private fun handleAppPermissions(permissions: Array<String>) {
val missingPermissions = arrayListOf<String>()
permissions.forEach { permission ->
// https://kotlinlang.org/docs/coding-conventions.html#if-versus-when
when {
ContextCompat.checkSelfPermission(
applicationContext,
permission
) == PackageManager.PERMISSION_GRANTED -> {
// You can now use the API that requires the permission. Attach locationListener etc.
Log.d(TAG, "Permission '$permission' is already granted")
}
ActivityCompat.shouldShowRequestPermissionRationale(
this, permission
) -> {
Log.w(TAG, "Permission rationale is needed for: '$permission'")
showAndHandlePermissionRationale(permission)
}
else -> {
Log.d(TAG, "Permission '$permission' not yet granted")
missingPermissions.add(permission)
}
}
}
// request all the missing permissions
if (missingPermissions.isNotEmpty()) {
Log.d(TAG, "Requesting missing permissions")
requestPermissionsLauncher.launch(missingPermissions.toTypedArray())
}
}
private fun showAndHandlePermissionRationale(permission: String) {
// https://developer.android.com/develop/ui/views/components/dialogs
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
builder
.setTitle("Permission required")
.setMessage("Permission '$permission' is required for app to work!")
.setPositiveButton("OK, Grant") { _, _ ->
requestPermissionsLauncher.launch(arrayOf(permission))
}
.setNegativeButton("NO") { _, _ ->
Toast.makeText(
this,
"Cannot work without '$permission' permission!",
Toast.LENGTH_LONG
).show()
// user denied the premission
// show error and redirect to settings
}
val dialog: AlertDialog = builder.create()
dialog.show()
}
}
Inspect and handle permissions while debugging
Inspect
adb shell dumpsys package PACKAGE_NAME
Remove user denial
adb shell pm clear-permission-flags PACKAGE_NAME PERMISSION_NAME user-set user-fixed
Revoke run-time permission
adb shell pm revoke PACKAGE_NAME PERMISSION_NAME
Grant run-time permission
adb shell pm grant PACKAGE_NAME PERMISSION_NAME
Uninstall the app
adb shell pm uninstall PACKAGE_NAME
adb shell pm clear PACKAGE_NAME
On osx adb is located in ~/Library/Android/sdk/platform-tools/
Handling location permissions
Location has several possible permissions.
- Foreground location
- Background location
Foreground location - app requires location info while activity is visible OR you are running foreground service (requires persistent notification).
Background location - app constantly shares location with other users or uses the Geofencing API. The system considers your app to be using background location if it accesses the device's current location in any situation other than the ones described in the foreground location.
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
Examples of background locations apps:
- Family location sharing app, feature lets users continuously share location with family members.
- IoT app, feature lets users configure their home devices such that they turn off when the user leaves their home and turn back on when the user returns home.