diff --git a/app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt b/app/src/main/java/org/mydomain/myscan/CameraPermission.kt similarity index 61% rename from app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt rename to app/src/main/java/org/mydomain/myscan/CameraPermission.kt index 6dbe884..4248a09 100644 --- a/app/src/main/java/org/mydomain/myscan/PermissionHelpers.kt +++ b/app/src/main/java/org/mydomain/myscan/CameraPermission.kt @@ -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 { - 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 +) { + 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 +} diff --git a/app/src/main/java/org/mydomain/myscan/MainActivity.kt b/app/src/main/java/org/mydomain/myscan/MainActivity.kt index f18d1c6..7d6898c 100644 --- a/app/src/main/java/org/mydomain/myscan/MainActivity.kt +++ b/app/src/main/java/org/mydomain/myscan/MainActivity.kt @@ -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 -> { diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt b/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt index 0302366..fc93fef 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraPreview.kt @@ -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() } } diff --git a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt index aec1a7e..9f82f89 100644 --- a/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/CameraScreen.kt @@ -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(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 = diff --git a/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt b/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt index c06cd29..fc4f7fe 100644 --- a/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt +++ b/app/src/main/java/org/mydomain/myscan/view/HomeScreen.kt @@ -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),