Kahibaro
Discord Login Register

17.2 Runtime Permissions

Why Runtime Permissions Exist

Android introduced runtime permissions so that users can control access to sensitive data and hardware at the moment an app actually needs it. Instead of granting everything at install time, modern Android versions ask the user while the app is running. This affects how you design features and how you write permission related code.

Runtime permissions apply only to dangerous permissions, such as access to location, camera, microphone, contacts, or storage. Normal permissions are still granted automatically and do not require runtime checks.

Dangerous permissions must be requested and checked at runtime. Never assume you already have them just because they appear in AndroidManifest.xml.

Declaring Permissions in the Manifest

Before you can request a permission at runtime, you must first declare it in your AndroidManifest.xml. The manifest only states that your app may need this permission. The user still needs to approve it at runtime for dangerous permissions.

For example, to work with the camera, you declare:

<manifest ...>
    <uses-permission android:name="android.permission.CAMERA" />
    <application
        ... >
        ...
    </application>
</manifest>

If you forget this manifest entry, runtime requests for that permission will always fail.

Permission Flow in Modern Android

The runtime permission flow in an activity or fragment follows a clear pattern.

First, check whether the permission is already granted using ContextCompat.checkSelfPermission. If it is granted, you proceed with the protected operation. If it is not granted, you request the permission using the relevant API.

This is the basic pattern in an activity:

private val REQUEST_CAMERA = 100
private fun checkCameraPermissionAndOpen() {
    if (ContextCompat.checkSelfPermission(
            this,
            android.Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED
    ) {
        openCamera()
    } else {
        ActivityCompat.requestPermissions(
            this,
            arrayOf(android.Manifest.permission.CAMERA),
            REQUEST_CAMERA
        )
    }
}

Later, you handle the user's response in onRequestPermissionsResult. The exact way you register and handle this can differ between older callback based APIs and newer Activity Result APIs, but the flow is the same. You either get permission and continue, or you do not and must react appropriately.

Requesting Permissions with Activity Result APIs

Newer projects usually use the Activity Result APIs, which simplify permission requests and avoid overriding onRequestPermissionsResult directly. This approach works in activities and fragments.

You register a launcher:

private val requestCameraPermission =
    registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted: Boolean ->
        if (isGranted) {
            openCamera()
        } else {
            showPermissionDeniedMessage()
        }
    }

Then you start the process when needed:

private fun askForCameraPermission() {
    if (ContextCompat.checkSelfPermission(
            this,
            android.Manifest.permission.CAMERA
        ) == PackageManager.PERMISSION_GRANTED
    ) {
        openCamera()
    } else {
        requestCameraPermission.launch(
            android.Manifest.permission.CAMERA
        )
    }
}

Using this pattern keeps permission logic more readable and avoids manual request code management.

Showing a Rationale to the User

Sometimes, a simple popup from the system is not enough to convince a user. If a user denies a permission the first time, or if your app needs to clarify why it needs access, you can display additional information. Android provides a hint through shouldShowRequestPermissionRationale.

This method returns true in situations where your app should present an explanation before asking again. For example, when a user has previously denied the permission but did not select “Do not ask again”.

You might use it like this:

private fun askForLocationPermission() {
    val permission = android.Manifest.permission.ACCESS_FINE_LOCATION
    if (ContextCompat.checkSelfPermission(this, permission)
        == PackageManager.PERMISSION_GRANTED
    ) {
        startLocationUpdates()
    } else if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
        showLocationRationaleDialog()
    } else {
        requestLocationPermission.launch(permission)
    }
}

showLocationRationaleDialog typically displays a dialog that explains why the app needs the permission and then calls requestLocationPermission.launch(permission) if the user agrees.

Always give a clear, honest rationale in simple language for why a permission is needed. Avoid requesting permissions unexpectedly or without context.

Handling “Do Not Ask Again” and Permanent Denials

If a user checks “Do not ask again” when denying a permission, your app will no longer be able to display the runtime dialog for that permission. Subsequent requests will fail silently with isGranted as false in your callback.

In that case, shouldShowRequestPermissionRationale usually returns false, and you need to guide the user to the app settings screen if the feature is essential.

You can detect and react with a message like “You have permanently denied this permission. Enable it manually in settings to use this feature.” Then open the settings screen:

private fun openAppSettings() {
    val intent = Intent(
        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
        Uri.fromParts("package", packageName, null)
    )
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    startActivity(intent)
}

This pattern is important for critical features that cannot work without a specific permission.

Requesting Multiple Permissions Together

Sometimes, a feature needs more than one permission. For example, an app that records video may require both camera and audio recording permissions. Instead of asking for each separately, you can request them together.

Using Activity Result APIs, you can use RequestMultiplePermissions:

private val requestCameraAndAudioPermissions =
    registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val cameraGranted = permissions[android.Manifest.permission.CAMERA] == true
        val audioGranted = permissions[android.Manifest.permission.RECORD_AUDIO] == true
        if (cameraGranted && audioGranted) {
            startVideoRecording()
        } else {
            showPermissionDeniedMessage()
        }
    }
private fun askForVideoPermissions() {
    val neededPermissions = arrayOf(
        android.Manifest.permission.CAMERA,
        android.Manifest.permission.RECORD_AUDIO
    )
    requestCameraAndAudioPermissions.launch(neededPermissions)
}

You should still avoid requesting unrelated permissions together. Users are more likely to accept when the requested permissions clearly relate to what they are doing at that moment.

Foreground vs Background Location Permissions

Location access is a special case on newer Android versions. There is a difference between foreground location access, used while the app is visible, and background location access, used when the app is not on screen.

Foreground location uses ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION. Background location uses ACCESS_BACKGROUND_LOCATION. You must declare and request foreground first. Then you may request background location in a separate, clear step if your feature truly needs it.

The system often enforces that background location is not requested together with foreground permissions in one dialog. This means you design your flow so that users understand the benefits before they are asked for more powerful access.

Best Practices for Request Timing and UX

The timing of permission requests is as important as the code.

Ask for permissions only when needed, such as when the user taps a button to start a feature that requires that permission. Avoid showing permission dialogs at app launch without context.

Connect the permission request to a clear, immediate benefit. For instance, when the user taps “Take Photo,” then ask for camera access, ideally with a short explanation if necessary.

If the user denies a permission, respect that choice. Design your app so that other parts still work where possible, and indicate which features are limited. Only re prompt when a user intentionally tries again to use the blocked feature.

Never repeatedly nag users with permission dialogs without a clear action from them. Repeated, unsolicited prompts create a poor user experience and can lead to uninstalls.

Permissions on Different Android Versions

Runtime permissions behavior can change across Android versions, especially for newer permission types such as notification and location permissions. For example, starting with some newer releases, posting notifications can require a runtime permission similar to other dangerous permissions.

In practice, you often need to check the current API level with Build.VERSION.SDK_INT and selectively request permissions only when they exist on that version. This avoids crashes and unnecessary prompts on older devices.

A simplified pattern looks like this:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    // Request notification permission on Android 13 and above
    requestNotificationPermission.launch(
        android.Manifest.permission.POST_NOTIFICATIONS
    )
} else {
    // No runtime notification permission on older versions
    showNotificationsNormally()
}

You keep the logic in one place and handle each version according to its rules.

Testing Runtime Permissions

Runtime permissions need thorough testing because user behavior can vary. Even beginners should manually test scenarios like the first request, a simple denial, a denial with “Do not ask again”, and granting permissions after visiting system settings.

Use emulators with different Android versions and real devices if you can. Confirm that your app does not crash when a permission is denied and that it provides clear messages and logical fallbacks.

Runtime permissions are not only a technical requirement, but also a core part of user trust. Treat them as a design element in your app, not just a checkbox in your code.

Views: 1

Comments

Please login to add a comment.

Don't have an account? Register now!