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_IMAGESREAD_MEDIA_VIDEOREAD_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
- Never request permissions before the user understands why. Show an explanation screen or in-app prompt first.
- Request permissions at the point of need. Don't ask for camera permission on the welcome screen.
- Degrade gracefully. If the user denies a permission, offer alternative functionality.
- 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
SecurityExceptiongracefully.) - "How do you handle permissions for foreground services vs activities?" (Foreground services use
startForeground()with a notification; some permissions likeFOREGROUND_SERVICE_LOCATIONare auto-declared.) - "What is the difference between
ACCESS_FINE_LOCATIONandACCESS_COARSE_LOCATION?" (Fine uses GPS, coarse uses cell tower and Wi-Fi; fine includes coarse.)
Quiz
1. When were runtime permissions introduced in Android?
2. What does `shouldShowRequestPermissionRationale()` return after a user selects "Don't ask again"?
3. Which API is used to request multiple permissions simultaneously in modern Android?
4. Android 13 split READ_EXTERNAL_STORAGE into which new permissions?
Flashcards
Question
What is the difference between normal and dangerous permissions?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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 permissionRequestMultiplePermissions()— 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>