Android Runtime Permissions in 2026: Camera, Location, and Notifications
Android's runtime permission model has evolved significantly since its introduction in Android 6.0. As we move into 2026, developers must understand the latest best practices for requesting and handling permissions at runtime, especially for sensitive features like camera, location tracking, and notifications. This article covers the modern approach using Jetpack's ActivityResultContracts and Jetpack Compose.
Understanding Runtime Permissions
Runtime permissions exist to protect user privacy. Unlike install-time permissions in Android 5 and earlier, modern Android requires explicit user consent when an app needs access to sensitive resources. Users can revoke permissions at any time through Settings, and apps must handle this gracefully.
The Android permission system operates on a principle of least privilege: users should only grant the minimum permissions necessary for the app to function. Developers should request permissions at the moment they're needed, not upfront during app launch.
The Modern Approach: ActivityResultContracts
The older approach using startActivityForResult() with request codes has been deprecated. Modern Android development leverages ActivityResultContracts, which provide type-safe, reusable permission request handlers.
Here's the recommended pattern:
val requestPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission granted, proceed with camera access
openCamera()
} else {
// Permission denied, show explanation or fallback UI
showPermissionDeniedMessage()
}
}
This approach is clean, type-safe, and integrates seamlessly with Jetpack Compose.
Permission Rationale: Why You Need It
Before requesting a permission, check if you should show a rationale explaining why your app needs it. The shouldShowRequestPermissionRationale() function helps with this:
val shouldShowRationale = shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)
if (shouldShowRationale) {
// Show a dialog explaining why you need the permission
showPermissionRationaleDialog {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
} else {
// Request directly
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
This is crucial for user trust and app store guidelines.
Camera Permission: Complete Example
Here's a complete Compose example for camera access:
@Composable
fun CameraPermissionExample() {
val context = LocalContext.current
var cameraGranted by remember { mutableStateOf(false) }
var showRationale by remember { mutableStateOf(false) }
val cameraLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
cameraGranted = isGranted
if (!isGranted) {
showRationale = true
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(onClick = {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
cameraGranted = true
} else {
cameraLauncher.launch(Manifest.permission.CAMERA)
}
}) {
Text("Request Camera Access")
}
if (cameraGranted) {
Text("Camera permission granted!", color = Color.Green)
}
if (showRationale) {
AlertDialog(
onDismissRequest = { showRationale = false },
title = { Text("Camera Access Needed") },
text = { Text("We need camera access to capture photos in your profile.") },
confirmButton = {
Button(onClick = {
showRationale = false
cameraLauncher.launch(Manifest.permission.CAMERA)
}) {
Text("Grant")
}
},
dismissButton = {
Button(onClick = { showRationale = false }) {
Text("Deny")
}
}
)
}
}
}
Location Permission: Fine and Coarse Variants
Location permissions have two levels: fine (GPS, ~10m accuracy) and coarse (network-based, ~1-10km accuracy). Request only what you need:
val locationLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val fineGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
val coarseGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false
when {
fineGranted -> {
// Use fine location
startLocationTracking(LocationAccuracy.HIGH)
}
coarseGranted -> {
// Use coarse location as fallback
startLocationTracking(LocationAccuracy.LOW)
}
else -> {
showError("Location access denied")
}
}
}
Button(onClick = {
locationLauncher.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}) {
Text("Request Location")
}
Notification Permission (Android 13+)
Post-notification permission was introduced in Android 13. Always check before posting:
val notificationLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
sendNotification()
} else {
showPermissionDeniedMessage("Notifications disabled")
}
}
Button(onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
// Older Android versions don't need this permission
sendNotification()
}
}) {
Text("Enable Notifications")
}
Handling Permanent Denial and Settings Redirect
When users deny a permission twice (or check "Don't ask again"), the OS flags it as permanently denied. At this point, requestPermissionLauncher.launch() won't show the dialog again. You must redirect users to app Settings:
fun redirectToAppSettings(context: Context) {
val intent = Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", context.packageName, null)
)
context.startActivity(intent)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Check if permission is permanently denied
if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)
&& ContextCompat.checkSelfPermission(
context,
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
AlertDialog(
onDismissRequest = {},
title = { Text("Permission Required") },
text = { Text("Camera permission is permanently denied. Please enable it in Settings.") },
confirmButton = {
Button(onClick = { redirectToAppSettings(context) }) {
Text("Open Settings")
}
}
)
}
}
AndroidManifest Declaration
All runtime permissions must be declared in AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Declaring permissions in the manifest is still required; runtime requests supplement this declaration.
Best Practices Summary
- Request Just-in-Time: Ask for permissions when the user triggers an action that needs them, not at app launch.
-
Show Rationale: Explain why you need the permission using
shouldShowRequestPermissionRationale(). - Handle Denial Gracefully: Provide fallback UI or features when permissions are denied.
- Detect Permanent Denial: Redirect to Settings when users permanently deny a permission.
-
Use ActivityResultContracts: Avoid deprecated
startActivityForResult()with request codes. - Test on Real Devices: Permission behavior varies across Android versions; test thoroughly.
- Respect User Choice: Never spam permission requests or use dark patterns to pressure users.
Conclusion
Runtime permissions are a cornerstone of Android security and user privacy. By following modern patterns with ActivityResultContracts, showing clear rationales, and handling denials gracefully, you build trust with your users and meet app store compliance requirements.
All 8 templates follow permission best practices. https://myougatheax.gumroad.com
Top comments (0)