Track state of camera permission

This commit is contained in:
Pierre-Yves Nicolas
2025-08-23 17:32:58 +02:00
committed by pynicolas
parent 2c64ebc972
commit eb1f3b64ed
5 changed files with 59 additions and 34 deletions

View File

@@ -22,27 +22,34 @@ import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
fun hasCameraPermission(context: Context): Boolean {
private fun hasCameraPermission(context: Context): Boolean {
val camera = Manifest.permission.CAMERA
return ContextCompat.checkSelfPermission(context, camera) == PackageManager.PERMISSION_GRANTED
}
@Composable
fun rememberCameraPermissionLauncher(
onGranted: () -> Unit = {},
onDenied: () -> Unit = {}
): ManagedActivityResultLauncher<String, Boolean> {
val context = LocalContext.current
return rememberLauncherForActivityResult (
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
onGranted()
} else {
onDenied()
@Stable
class CameraPermissionState internal constructor(
private val context: Context,
private val launcher: ManagedActivityResultLauncher<String, Boolean>
) {
var isGranted by mutableStateOf(hasCameraPermission(context))
private set
fun request() {
launcher.launch(Manifest.permission.CAMERA)
}
internal fun update(granted: Boolean) {
isGranted = granted
if (!granted) {
Toast.makeText(
context,
context.getString(R.string.camera_permission_denied),
@@ -51,3 +58,21 @@ fun rememberCameraPermissionLauncher(
}
}
}
@Composable
fun rememberCameraPermissionState(): CameraPermissionState {
val context = LocalContext.current
lateinit var state: CameraPermissionState
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
state.update(granted)
}
state = remember {
CameraPermissionState(context, launcher)
}
return state
}

View File

@@ -60,6 +60,7 @@ class MainActivity : ComponentActivity() {
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
val cameraPermission = rememberCameraPermissionState()
MyScanTheme {
val navigation = Navigation(
toHomeScreen = { viewModel.navigateTo(Screen.Home) },
@@ -72,7 +73,7 @@ class MainActivity : ComponentActivity() {
when (val screen = currentScreen) {
is Screen.Home -> {
HomeScreen(
hasCameraPermission = hasCameraPermission(this),
cameraPermission = cameraPermission,
currentDocument = document,
navigation = navigation,
onStartNewScan = navigation.toCameraScreen,
@@ -85,6 +86,7 @@ class MainActivity : ComponentActivity() {
liveAnalysisState,
onImageAnalyzed = { image -> viewModel.liveAnalysis(image) },
onFinalizePressed = { viewModel.navigateTo(Screen.Document()) },
cameraPermission = cameraPermission
)
}
is Screen.Document -> {

View File

@@ -52,10 +52,9 @@ import androidx.core.graphics.scale
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.google.common.util.concurrent.ListenableFuture
import org.mydomain.myscan.CameraPermissionState
import org.mydomain.myscan.LiveAnalysisState
import org.mydomain.myscan.Point
import org.mydomain.myscan.hasCameraPermission
import org.mydomain.myscan.rememberCameraPermissionLauncher
import org.mydomain.myscan.scaledTo
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -66,13 +65,12 @@ fun CameraPreview(
onImageAnalyzed: (ImageProxy) -> Unit,
captureController: CameraCaptureController,
onPreviewViewReady: (PreviewView) -> Unit,
cameraPermission: CameraPermissionState,
) {
val context = LocalContext.current
val requestPermissionLauncher = rememberCameraPermissionLauncher(onGranted = {}, onDenied = {})
LaunchedEffect(Unit) {
val camera = android.Manifest.permission.CAMERA
if (!hasCameraPermission(context)) {
requestPermissionLauncher.launch(camera)
if (!cameraPermission.isGranted) {
cameraPermission.request()
}
}

View File

@@ -75,6 +75,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import org.mydomain.myscan.CameraPermissionState
import org.mydomain.myscan.LiveAnalysisState
import org.mydomain.myscan.MainViewModel
import org.mydomain.myscan.MainViewModel.CaptureState
@@ -102,6 +103,7 @@ fun CameraScreen(
liveAnalysisState: LiveAnalysisState,
onImageAnalyzed: (ImageProxy) -> Unit,
onFinalizePressed: () -> Unit,
cameraPermission: CameraPermissionState,
) {
var previewView by remember { mutableStateOf<PreviewView?>(null) }
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
@@ -145,7 +147,8 @@ fun CameraScreen(
CameraPreview(
onImageAnalyzed = onImageAnalyzed,
captureController = captureController,
onPreviewViewReady = { view -> previewView = view }
onPreviewViewReady = { view -> previewView = view },
cameraPermission = cameraPermission,
)
},
pageListState =

View File

@@ -14,7 +14,6 @@
*/
package org.mydomain.myscan.view
import android.Manifest
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -46,15 +45,16 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mydomain.myscan.CameraPermissionState
import org.mydomain.myscan.Navigation
import org.mydomain.myscan.R
import org.mydomain.myscan.rememberCameraPermissionLauncher
import org.mydomain.myscan.rememberCameraPermissionState
import org.mydomain.myscan.ui.theme.MyScanTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
hasCameraPermission: Boolean,
cameraPermission: CameraPermissionState,
currentDocument: DocumentUiModel,
navigation: Navigation,
onStartNewScan: () -> Unit
@@ -95,8 +95,8 @@ fun HomeScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
if (!hasCameraPermission) {
CameraPermissionRationale()
if (!cameraPermission.isGranted) {
CameraPermissionRationale(cameraPermission)
}
if (!currentDocument.isEmpty()) {
@@ -115,8 +115,7 @@ fun HomeScreen(
}
@Composable
private fun CameraPermissionRationale() {
val requestPermissionLauncher = rememberCameraPermissionLauncher(onGranted = {}, onDenied = {})
private fun CameraPermissionRationale(cameraPermission: CameraPermissionState) {
Card(
modifier = Modifier
.fillMaxWidth()
@@ -128,9 +127,7 @@ private fun CameraPermissionRationale() {
style = MaterialTheme.typography.bodyMedium
)
Spacer(Modifier.height(8.dp))
Button(onClick = {
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}) {
Button(onClick = { cameraPermission.request() }) {
Text(stringResource(R.string.grant_permission))
}
}
@@ -183,7 +180,7 @@ private fun SectionTitle(text: String) {
fun HomeScreenPreviewOnFirstLaunch() {
MyScanTheme {
HomeScreen(
hasCameraPermission = false,
cameraPermission = rememberCameraPermissionState(),
currentDocument = DocumentUiModel(listOf()) { _ -> null },
navigation = dummyNavigation(),
onStartNewScan = {}
@@ -196,7 +193,7 @@ fun HomeScreenPreviewOnFirstLaunch() {
fun HomeScreenPreviewWithCurrentDocument() {
MyScanTheme {
HomeScreen(
hasCameraPermission = true,
cameraPermission = rememberCameraPermissionState(),
currentDocument = fakeDocument(
listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
LocalContext.current),