Skip to content
intermediate Phase 12 · Security

Runtime Permissions

Request and handle runtime permissions with the permissions API and rationale flows.

35m
2 problems
Topic Progress 0%

The Android Permission Model

Why Runtime Permissions Matter

Before Android 6.0 (API 23), all permissions were granted at install time. Users had to accept every permission upfront or not install the app at all. This created a poor user experience and led to apps requesting excessive permissions.

Android 6.0 introduced the runtime permission model. Dangerous permissions—those that access user data or system features—must now be requested while the app is running. The user can grant or revoke these permissions at any time.

Permission Categories

Permissions fall into two categories:

Category Example Behavior
Normal INTERNET, VIBRATE Granted automatically at install
Dangerous CAMERA, LOCATION, READ_CONTACTS Must be requested at runtime

The Permissions API

The modern way to request permissions uses ActivityResultContracts. The old requestPermissions() / onRequestPermissionsResult() pattern is deprecated.

// Register the permission launcher in your Activity/Fragment
private val cameraPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { isGranted ->
    if (isGranted) {
        openCamera()
    } else {
        showPermissionDeniedMessage()
    }
}

// Launch the request
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)

For requesting multiple permissions at once, use RequestMultiplePermissions:

private val multiplePermissionsLauncher = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
    val allGranted = permissions.values.all { it }
    if (allGranted) {
        proceedWithFeature()
    } else {
        val denied = permissions.filterValues { !it }.keys
        showDeniedExplanation(denied)
    }
}

multiplePermissionsLauncher.launch(
    arrayOf(
        Manifest.permission.ACCESS_FINE_LOCATION,
        Manifest.permission.ACCESS_COARSE_LOCATION
    )
)

Checking Permissions Before Use

Always check if a permission is already granted before requesting it. Unnecessary prompts frustrate users.

fun hasCameraPermission(): Boolean {
    return ContextCompat.checkSelfPermission(
        this, Manifest.permission.CAMERA
    ) == PackageManager.PERMISSION_GRANTED
}

if (hasCameraPermission()) {
    openCamera()
} else {
    cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}

Android 13+ Permission Changes

Android 13 (API 33) split the old READ_EXTERNAL_STORAGE permission into granular media permissions:

  • READ_MEDIA_IMAGES
  • READ_MEDIA_VIDEO
  • READ_MEDIA_AUDIO

Apps targeting Android 13+ must request these new permissions instead of the legacy storage permission.

Permission Request Flow and Rationale

The Permission Request Flow

A robust permission flow handles three outcomes: granted, denied, and permanently denied.

class LocationActivity : AppCompatActivity() {

    private val locationPermissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        when {
            permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true -> {
                startLocationTracking()
            }
            permissions[Manifest.permission.ACCESS_COARSE_LOCATION] == true -> {
                startApproximateLocationTracking()
            }
            else -> {
                val shouldShowRationale =
                    shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION)
                
                if (shouldShowRationale) {
                    showRationaleDialog()
                } else {
                    // User selected "Don't ask again" — direct them to settings
                    showSettingsDialog()
                }
            }
        }
    }

    private fun checkAndRequestLocation() {
        when {
            hasFineLocation() -> startLocationTracking()
            hasCoarseLocation() -> startApproximateLocationTracking()
            shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_FINE_LOCATION) -> {
                showRationaleDialog()
            }
            else -> {
                locationPermissionLauncher.launch(
                    arrayOf(
                        Manifest.permission.ACCESS_FINE_LOCATION,
                        Manifest.permission.ACCESS_COARSE_LOCATION
                    )
                )
            }
        }
    }
}

Writing Effective Rationale Dialogs

Explain why your app needs the permission in terms the user understands.

private fun showRationaleDialog() {
    AlertDialog.Builder(this)
        .setTitle("Location Permission Required")
        .setMessage(
            "This app uses your location to show nearby stores. " +
            "Without location access, we cannot display relevant results."
        )
        .setPositiveButton("Grant") { _, _ ->
            locationPermissionLauncher.launch(
                arrayOf(Manifest.permission.ACCESS_FINE_LOCATION)
            )
        }
        .setNegativeButton("Cancel") { dialog, _ ->
            dialog.dismiss()
        }
        .show()
}

Permanent Denial and Settings Redirect

When shouldShowRequestPermissionRationale() returns false after a denial, the user has selected "Don't ask again." You cannot re-request the permission programmatically. Instead, guide the user to app settings.

private fun showSettingsDialog() {
    AlertDialog.Builder(this)
        .setTitle("Permission Required")
        .setMessage(
            "Location permission was permanently denied. " +
            "Please enable it in app settings."
        )
        .setPositiveButton("Open Settings") { _, _ ->
            val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                data = Uri.fromParts("package", packageName, null)
            }
            settingsLauncher.launch(intent)
        }
        .setNegativeButton("Cancel") { dialog, _ ->
            dialog.dismiss()
        }
        .show()
}

Best Practices

  1. Never request permissions before the user understands why. Show an explanation screen or in-app prompt first.
  2. Request permissions at the point of need. Don't ask for camera permission on the welcome screen.
  3. Degrade gracefully. If the user denies a permission, offer alternative functionality.
  4. Test all denial paths. Use "Don't test only the happy path" in the emulator permission settings.

Package Visibility and Manifest Declarations

Package Visibility (Android 11+)

Android 11 introduced package visibility restrictions. Your app can no longer query for all installed apps on the device by default. If your app needs to interact with specific other apps, you must declare intent filters or specific packages in your manifest.

<queries>
    <!-- Query for a specific app -->
    <package android:name="com.example.otherapp" />
    
    <!-- Query by intent -->
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
    </intent>
</queries>

This is critical when implementing features like:

  • Payment app selection (UPI, wallets)
  • Social login (checking if a social app is installed)
  • Deep linking to specific apps

Manifest Permission Declarations

Every dangerous permission must be declared in AndroidManifest.xml:

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

<!-- For Android 11+ package visibility -->
<queries>
    <intent>
        <action android:name="android.media.action.IMAGE_CAPTURE" />
    </intent>
</queries>

Testability

Use ADB commands to test permission handling during development:

# Revoke a permission
adb shell pm revoke com.your.package android.permission.CAMERA

# Grant a permission
adb shell pm grant com.your.package android.permission.CAMERA

# Reset all permissions
adb shell pm reset-permissions com.your.package

Interview Focus

Interviewers often ask:

  • "What happens if a user revokes a permission while your app is in the background?" (Your app loses access immediately and must handle the SecurityException gracefully.)
  • "How do you handle permissions for foreground services vs activities?" (Foreground services use startForeground() with a notification; some permissions like FOREGROUND_SERVICE_LOCATION are auto-declared.)
  • "What is the difference between ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION?" (Fine uses GPS, coarse uses cell tower and Wi-Fi; fine includes coarse.)

Quiz

1. When were runtime permissions introduced in Android?

Question 1 options

2. What does `shouldShowRequestPermissionRationale()` return after a user selects "Don't ask again"?

Question 2 options

3. Which API is used to request multiple permissions simultaneously in modern Android?

Question 3 options

4. Android 13 split READ_EXTERNAL_STORAGE into which new permissions?

Question 4 options

Flashcards

Question

What is the difference between normal and dangerous permissions?

Answer

Normal permissions (INTERNET, VIBRATE) are granted automatically at install. Dangerous permissions (CAMERA, LOCATION) must be requested and granted at runtime by the user.

Question

How do you handle permanent permission denial on Android?

Answer

When shouldShowRequestPermissionRationale() returns false after a denial, direct the user to app settings using ACTION_APPLICATION_DETAILS_SETTINGS with the app's package URI.

Question

What changed in Android 11 regarding package visibility?

Answer

Android 11 introduced package visibility restrictions. Apps must declare which other apps they need to query via <queries> in the manifest, rather than seeing all installed apps by default.

Question

What is the correct modern API for requesting permissions?

Answer

Use ActivityResultContracts.RequestPermission() for single permissions or RequestMultiplePermissions() for multiple, registered via registerForActivityResult().

Revision Notes

Key Takeaways

  • 1. Always check if a permission is granted before requesting it to avoid unnecessary prompts
  • 2. Show rationale before requesting permissions the user has already denied once
  • 3. Handle permanent denial by directing users to app settings
  • 4. Android 13 replaced READ_EXTERNAL_STORAGE with granular media permissions
  • 5. Package visibility on Android 11+ requires <queries> declarations

Interview Tips

  • Explain the full permission flow: check → rationale → request → handle result → handle denial
  • Know the difference between ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION
  • Be ready to discuss how permissions work with foreground services and background work
  • Explain why runtime permissions were introduced and the security tradeoffs

Cheat Sheet

Runtime Permissions Cheat Sheet

Permission Categories:

  • Normal: Granted automatically (INTERNET, VIBRATE)
  • Dangerous: Runtime request required (CAMERA, LOCATION)

Modern API:

  • RequestPermission() — single permission
  • RequestMultiplePermissions() — multiple permissions
  • Register via registerForActivityResult()

Key Checks:

  • ContextCompat.checkSelfPermission() — is it granted?
  • shouldShowRequestPermissionRationale() — did user deny?
    • true: show explanation, then request
    • false after denial: direct to Settings

Android 13+:

  • READ_EXTERNAL_STORAGE → READ_MEDIA_IMAGES / VIDEO / AUDIO

ADB Testing:

  • adb shell pm grant <pkg> <permission>
  • adb shell pm revoke <pkg> <permission>