Track state of camera permission
This commit is contained in:
committed by
pynicolas
parent
2c64ebc972
commit
eb1f3b64ed
@@ -22,27 +22,34 @@ import androidx.activity.compose.ManagedActivityResultLauncher
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.runtime.Composable
|
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.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
fun hasCameraPermission(context: Context): Boolean {
|
private fun hasCameraPermission(context: Context): Boolean {
|
||||||
val camera = Manifest.permission.CAMERA
|
val camera = Manifest.permission.CAMERA
|
||||||
return ContextCompat.checkSelfPermission(context, camera) == PackageManager.PERMISSION_GRANTED
|
return ContextCompat.checkSelfPermission(context, camera) == PackageManager.PERMISSION_GRANTED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Stable
|
||||||
fun rememberCameraPermissionLauncher(
|
class CameraPermissionState internal constructor(
|
||||||
onGranted: () -> Unit = {},
|
private val context: Context,
|
||||||
onDenied: () -> Unit = {}
|
private val launcher: ManagedActivityResultLauncher<String, Boolean>
|
||||||
): ManagedActivityResultLauncher<String, Boolean> {
|
) {
|
||||||
val context = LocalContext.current
|
var isGranted by mutableStateOf(hasCameraPermission(context))
|
||||||
return rememberLauncherForActivityResult (
|
private set
|
||||||
ActivityResultContracts.RequestPermission()
|
|
||||||
) { isGranted ->
|
fun request() {
|
||||||
if (isGranted) {
|
launcher.launch(Manifest.permission.CAMERA)
|
||||||
onGranted()
|
}
|
||||||
} else {
|
|
||||||
onDenied()
|
internal fun update(granted: Boolean) {
|
||||||
|
isGranted = granted
|
||||||
|
if (!granted) {
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.camera_permission_denied),
|
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
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
val currentScreen by viewModel.currentScreen.collectAsStateWithLifecycle()
|
||||||
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
val liveAnalysisState by viewModel.liveAnalysisState.collectAsStateWithLifecycle()
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
|
val cameraPermission = rememberCameraPermissionState()
|
||||||
MyScanTheme {
|
MyScanTheme {
|
||||||
val navigation = Navigation(
|
val navigation = Navigation(
|
||||||
toHomeScreen = { viewModel.navigateTo(Screen.Home) },
|
toHomeScreen = { viewModel.navigateTo(Screen.Home) },
|
||||||
@@ -72,7 +73,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
when (val screen = currentScreen) {
|
when (val screen = currentScreen) {
|
||||||
is Screen.Home -> {
|
is Screen.Home -> {
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
hasCameraPermission = hasCameraPermission(this),
|
cameraPermission = cameraPermission,
|
||||||
currentDocument = document,
|
currentDocument = document,
|
||||||
navigation = navigation,
|
navigation = navigation,
|
||||||
onStartNewScan = navigation.toCameraScreen,
|
onStartNewScan = navigation.toCameraScreen,
|
||||||
@@ -85,6 +86,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
liveAnalysisState,
|
liveAnalysisState,
|
||||||
onImageAnalyzed = { image -> viewModel.liveAnalysis(image) },
|
onImageAnalyzed = { image -> viewModel.liveAnalysis(image) },
|
||||||
onFinalizePressed = { viewModel.navigateTo(Screen.Document()) },
|
onFinalizePressed = { viewModel.navigateTo(Screen.Document()) },
|
||||||
|
cameraPermission = cameraPermission
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is Screen.Document -> {
|
is Screen.Document -> {
|
||||||
|
|||||||
@@ -52,10 +52,9 @@ import androidx.core.graphics.scale
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import org.mydomain.myscan.CameraPermissionState
|
||||||
import org.mydomain.myscan.LiveAnalysisState
|
import org.mydomain.myscan.LiveAnalysisState
|
||||||
import org.mydomain.myscan.Point
|
import org.mydomain.myscan.Point
|
||||||
import org.mydomain.myscan.hasCameraPermission
|
|
||||||
import org.mydomain.myscan.rememberCameraPermissionLauncher
|
|
||||||
import org.mydomain.myscan.scaledTo
|
import org.mydomain.myscan.scaledTo
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
@@ -66,13 +65,12 @@ fun CameraPreview(
|
|||||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||||
captureController: CameraCaptureController,
|
captureController: CameraCaptureController,
|
||||||
onPreviewViewReady: (PreviewView) -> Unit,
|
onPreviewViewReady: (PreviewView) -> Unit,
|
||||||
|
cameraPermission: CameraPermissionState,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val requestPermissionLauncher = rememberCameraPermissionLauncher(onGranted = {}, onDenied = {})
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
val camera = android.Manifest.permission.CAMERA
|
if (!cameraPermission.isGranted) {
|
||||||
if (!hasCameraPermission(context)) {
|
cameraPermission.request()
|
||||||
requestPermissionLauncher.launch(camera)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import org.mydomain.myscan.CameraPermissionState
|
||||||
import org.mydomain.myscan.LiveAnalysisState
|
import org.mydomain.myscan.LiveAnalysisState
|
||||||
import org.mydomain.myscan.MainViewModel
|
import org.mydomain.myscan.MainViewModel
|
||||||
import org.mydomain.myscan.MainViewModel.CaptureState
|
import org.mydomain.myscan.MainViewModel.CaptureState
|
||||||
@@ -102,6 +103,7 @@ fun CameraScreen(
|
|||||||
liveAnalysisState: LiveAnalysisState,
|
liveAnalysisState: LiveAnalysisState,
|
||||||
onImageAnalyzed: (ImageProxy) -> Unit,
|
onImageAnalyzed: (ImageProxy) -> Unit,
|
||||||
onFinalizePressed: () -> Unit,
|
onFinalizePressed: () -> Unit,
|
||||||
|
cameraPermission: CameraPermissionState,
|
||||||
) {
|
) {
|
||||||
var previewView by remember { mutableStateOf<PreviewView?>(null) }
|
var previewView by remember { mutableStateOf<PreviewView?>(null) }
|
||||||
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
val document by viewModel.documentUiModel.collectAsStateWithLifecycle()
|
||||||
@@ -145,7 +147,8 @@ fun CameraScreen(
|
|||||||
CameraPreview(
|
CameraPreview(
|
||||||
onImageAnalyzed = onImageAnalyzed,
|
onImageAnalyzed = onImageAnalyzed,
|
||||||
captureController = captureController,
|
captureController = captureController,
|
||||||
onPreviewViewReady = { view -> previewView = view }
|
onPreviewViewReady = { view -> previewView = view },
|
||||||
|
cameraPermission = cameraPermission,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
pageListState =
|
pageListState =
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.mydomain.myscan.view
|
package org.mydomain.myscan.view
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
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.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.mydomain.myscan.CameraPermissionState
|
||||||
import org.mydomain.myscan.Navigation
|
import org.mydomain.myscan.Navigation
|
||||||
import org.mydomain.myscan.R
|
import org.mydomain.myscan.R
|
||||||
import org.mydomain.myscan.rememberCameraPermissionLauncher
|
import org.mydomain.myscan.rememberCameraPermissionState
|
||||||
import org.mydomain.myscan.ui.theme.MyScanTheme
|
import org.mydomain.myscan.ui.theme.MyScanTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
hasCameraPermission: Boolean,
|
cameraPermission: CameraPermissionState,
|
||||||
currentDocument: DocumentUiModel,
|
currentDocument: DocumentUiModel,
|
||||||
navigation: Navigation,
|
navigation: Navigation,
|
||||||
onStartNewScan: () -> Unit
|
onStartNewScan: () -> Unit
|
||||||
@@ -95,8 +95,8 @@ fun HomeScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
if (!hasCameraPermission) {
|
if (!cameraPermission.isGranted) {
|
||||||
CameraPermissionRationale()
|
CameraPermissionRationale(cameraPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentDocument.isEmpty()) {
|
if (!currentDocument.isEmpty()) {
|
||||||
@@ -115,8 +115,7 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CameraPermissionRationale() {
|
private fun CameraPermissionRationale(cameraPermission: CameraPermissionState) {
|
||||||
val requestPermissionLauncher = rememberCameraPermissionLauncher(onGranted = {}, onDenied = {})
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -128,9 +127,7 @@ private fun CameraPermissionRationale() {
|
|||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Button(onClick = {
|
Button(onClick = { cameraPermission.request() }) {
|
||||||
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
||||||
}) {
|
|
||||||
Text(stringResource(R.string.grant_permission))
|
Text(stringResource(R.string.grant_permission))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +180,7 @@ private fun SectionTitle(text: String) {
|
|||||||
fun HomeScreenPreviewOnFirstLaunch() {
|
fun HomeScreenPreviewOnFirstLaunch() {
|
||||||
MyScanTheme {
|
MyScanTheme {
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
hasCameraPermission = false,
|
cameraPermission = rememberCameraPermissionState(),
|
||||||
currentDocument = DocumentUiModel(listOf()) { _ -> null },
|
currentDocument = DocumentUiModel(listOf()) { _ -> null },
|
||||||
navigation = dummyNavigation(),
|
navigation = dummyNavigation(),
|
||||||
onStartNewScan = {}
|
onStartNewScan = {}
|
||||||
@@ -196,7 +193,7 @@ fun HomeScreenPreviewOnFirstLaunch() {
|
|||||||
fun HomeScreenPreviewWithCurrentDocument() {
|
fun HomeScreenPreviewWithCurrentDocument() {
|
||||||
MyScanTheme {
|
MyScanTheme {
|
||||||
HomeScreen(
|
HomeScreen(
|
||||||
hasCameraPermission = true,
|
cameraPermission = rememberCameraPermissionState(),
|
||||||
currentDocument = fakeDocument(
|
currentDocument = fakeDocument(
|
||||||
listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
listOf("gallica.bnf.fr-bpt6k5530456s-1.jpg"),
|
||||||
LocalContext.current),
|
LocalContext.current),
|
||||||
|
|||||||
Reference in New Issue
Block a user