Handle permanently denied camera permission with Settings redirect

Detect when camera permission is permanently denied (not granted and
rationale not shown) and offer an "Open Settings" button instead of
a non-functional "Grant" button. Use centralized AppColors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-05 12:23:34 +01:00
commit ee2b11941a

View file

@ -1,7 +1,10 @@
package no.naiv.tiltshift package no.naiv.tiltshift
import android.Manifest import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
@ -25,6 +28,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -38,7 +42,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.google.accompanist.permissions.shouldShowRationale
import no.naiv.tiltshift.ui.CameraScreen import no.naiv.tiltshift.ui.CameraScreen
import no.naiv.tiltshift.ui.theme.AppColors
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -96,12 +102,17 @@ private fun TiltShiftApp() {
} }
} }
else -> { else -> {
// Permanently denied: not granted AND rationale not shown
val cameraPermanentlyDenied = !cameraPermission.status.isGranted &&
!cameraPermission.status.shouldShowRationale
// Show permission request UI // Show permission request UI
PermissionRequestScreen( PermissionRequestScreen(
onRequestCamera = { cameraPermission.launchPermissionRequest() }, onRequestCamera = { cameraPermission.launchPermissionRequest() },
onRequestLocation = { locationPermissions.launchMultiplePermissionRequest() }, onRequestLocation = { locationPermissions.launchMultiplePermissionRequest() },
cameraGranted = cameraPermission.status.isGranted, cameraGranted = cameraPermission.status.isGranted,
locationGranted = locationPermissions.allPermissionsGranted locationGranted = locationPermissions.allPermissionsGranted,
cameraPermanentlyDenied = cameraPermanentlyDenied
) )
} }
} }
@ -113,7 +124,8 @@ private fun PermissionRequestScreen(
onRequestCamera: () -> Unit, onRequestCamera: () -> Unit,
onRequestLocation: () -> Unit, onRequestLocation: () -> Unit,
cameraGranted: Boolean, cameraGranted: Boolean,
locationGranted: Boolean locationGranted: Boolean,
cameraPermanentlyDenied: Boolean
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -146,6 +158,7 @@ private fun PermissionRequestScreen(
title = "Camera", title = "Camera",
description = "Required to take photos", description = "Required to take photos",
isGranted = cameraGranted, isGranted = cameraGranted,
isPermanentlyDenied = cameraPermanentlyDenied,
onRequest = onRequestCamera onRequest = onRequestCamera
) )
@ -168,8 +181,10 @@ private fun PermissionItem(
title: String, title: String,
description: String, description: String,
isGranted: Boolean, isGranted: Boolean,
isPermanentlyDenied: Boolean = false,
onRequest: () -> Unit onRequest: () -> Unit
) { ) {
val context = LocalContext.current
Box( Box(
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
@ -182,7 +197,7 @@ private fun PermissionItem(
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = if (isGranted) Color(0xFF4CAF50) else Color(0xFFFFB300), tint = if (isGranted) AppColors.Success else AppColors.Accent,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
) )
@ -201,13 +216,31 @@ private fun PermissionItem(
if (!isGranted) { if (!isGranted) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Button( if (isPermanentlyDenied) {
onClick = onRequest, Button(
colors = ButtonDefaults.buttonColors( onClick = {
containerColor = Color(0xFFFFB300) context.startActivity(
) Intent(
) { Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Text("Grant", color = Color.Black) Uri.fromParts("package", context.packageName, null)
)
)
},
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.Accent
)
) {
Text("Open Settings", color = Color.Black)
}
} else {
Button(
onClick = onRequest,
colors = ButtonDefaults.buttonColors(
containerColor = AppColors.Accent
)
) {
Text("Grant", color = Color.Black)
}
} }
} }
} }