How to initialize camera in compose - android

I want to get a camera view by compose androidView, but the following code seems not work for me.
#Composable
fun CameraPreviewScreen() {
val lifecycleOwner = LifecycleOwnerAmbient.current
val context = ContextAmbient.current
val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val previewView = remember { PreviewView(context) }
AndroidView(viewBlock = { previewView }) {
cameraProviderFuture.addListener(Runnable {
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build()
preview.setSurfaceProvider(it.surfaceProvider)
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(context))
}
}
The snapshot below.
The report error.
Permission Denial: can't use the camera
cannot open camera "0" without camera permission (code 1)

Since your problem seems related to camera permission, you can use Permissions APIs from Accompanist library to ask the user for permissions.
The docs show an example with camera
#Composable
private fun FeatureThatRequiresCameraPermission(
navigateToSettingsScreen: () -> Unit
) {
// Track if the user doesn't want to see the rationale any more.
var doNotShowRationale by rememberSaveable { mutableStateOf(false) }
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
PermissionRequired(
permissionState = cameraPermissionState,
permissionNotGrantedContent = {
if (doNotShowRationale) {
Text("Feature not available")
} else {
Column {
Text("The camera is important for this app. Please grant the permission.")
Spacer(modifier = Modifier.height(8.dp))
Row {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Ok!")
}
Spacer(Modifier.width(8.dp))
Button(onClick = { doNotShowRationale = true }) {
Text("Nope")
}
}
}
}
},
permissionNotAvailableContent = {
Column {
Text(
"Camera permission denied. See this FAQ with information about why we " +
"need this permission. Please, grant us access on the Settings screen."
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = navigateToSettingsScreen) {
Text("Open Settings")
}
}
}
) {
Text("Camera permission Granted")
}
}

Related

How to rotate image after capturing from front camera using CameraX in jetpack compose?

I am trying to rotate the output image flip horizontally but I am able to rotate it only flip vertically. I am using camerax for capturing the image of the front camera. I am successfully getting the mirror image from the front camera but I want the same image as the user sees.
here is my code for capturing the image:-
#OptIn(ExperimentalCoilApi::class)
#ExperimentalPermissionsApi
#ExperimentalCoroutinesApi
#Composable
fun CameraCapture(
modifier: Modifier = Modifier,
cameraSelector: CameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA,
onImageFile: (File) -> Unit = { }
) {
val context = LocalContext.current
Permission(
permission = Manifest.permission.CAMERA,
rationale = "You said you wanted a picture, so I'm going to have to ask for permission.",
permissionNotAvailableContent = {
Column(modifier) {
Text("O noes! No Camera!")
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
context.startActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
}
)
}
) {
Text("Open Settings")
}
}
}
) {
Box(modifier = modifier) {
val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
var previewUseCase by remember { mutableStateOf<UseCase>(Preview.Builder().build()) }
val imageCaptureUseCase by remember {
mutableStateOf(
ImageCapture.Builder()
.setCaptureMode(CAPTURE_MODE_MAXIMIZE_QUALITY)
.setTargetRotation(ROTATION_180)
.build()
)
}
Box {
CameraPreview(
modifier = Modifier.fillMaxSize(),
onUseCase = {
previewUseCase = it
}
)
Image(
modifier = Modifier.fillMaxSize(),
painter = rememberImagePainter(R.drawable.human_face_foreground),
contentDescription = stringResource(id = R.string.user_image)
)
Button(
modifier = Modifier
.wrapContentSize()
.padding(16.dp)
.align(Alignment.BottomCenter),
onClick = {
coroutineScope.launch {
imageCaptureUseCase.takePicture(context.executor).let {
onImageFile(it)
}
}
}
) {
Text("Click!")
}
}
LaunchedEffect(previewUseCase) {
val cameraProvider = context.getCameraProvider()
try {
// Must unbind the use-cases before rebinding them.
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner, cameraSelector, previewUseCase, imageCaptureUseCase
)
} catch (ex: Exception) {
Log.e("CameraCapture", "Failed to bind camera use cases", ex)
}
}
}
}
}
Someone please help me solving this issue.

How to stop camera from working when it no longer visible in the compisition?

I've got a bottomSheetScaffold, which contains a BottomSheet
That BottomSheet uses device's Camera, where I use CameraX alongside with Google's MLkit for bar scanning
Let's consider permission is accepted
What happens (Not correct): once I expand the bottomsheet upward, I show the CameraPreview, show camera preview, and ImageAnalyzer which analyzes the preview image.
Now the bottomSheet is expanded, the camera preview is visible and working as expected
then I collapse the bottomSheet, but the camera is still working (analyzer as well,
imageAnalysis.clearAnalyzer() clear the analyzing part)
The outcome: is not correct behavior I intended
so How can I stop camera from working, and using resources once the bottomSheetState is collapsed, and only allow camera when bottomSheetState is Expanded
How it works(Wrong):
The problem I got is, camera is binded to the lifecycle of the activity, and not the composable itself, when re-composition happens, it still consider the camera live, since it's not attached to the composition lifecycle
How does Composition work:
Code:
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheetContent(
modifier: Modifier = Modifier,
bottomSheetState: BottomSheetState
) {
Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(0.8f)
) {
PeekBar()
ScanningSerialTextTitle(modifier)
if (bottomSheetState.isExpanded) {
CameraBox(modifier)
} else {
EmptyBox()
}
}
}
#Composable
fun EmptyBox(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxSize()
.background(color = Color.DarkGray)
)
}
#OptIn(ExperimentalPermissionsApi::class)
#Composable
fun CameraBox(modifier: Modifier = Modifier) {
val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA)
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifeCycleOwner, effect = {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
cameraPermissionState.launchPermissionRequest()
}
}
lifeCycleOwner.lifecycle.addObserver(observer)
onDispose { lifeCycleOwner.lifecycle.removeObserver(observer) }
})
cameraPermissionState.handlePermissionCases(
ShouldShowRationaleContent = {
ShouldShowRationaleContent(cameraPermissionState = cameraPermissionState)
},
PermissionDeniedPermanentlyContent = {
PermissionDeniedPermanentContent()
}) {
val context = LocalContext.current
val barCodeVal = remember { mutableStateOf("") }
CameraPreview(onBarcodeDetected = { barcodes ->
barcodes.forEach { barcode ->
barcode.rawValue?.let { barcodeValue ->
barCodeVal.value = barcodeValue
Toast.makeText(context, barcodeValue, Toast.LENGTH_SHORT).show()
}
}
}, onBarcodeFailed = {}, onBarcodeNotFound = {})
}
}
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector = buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
try {
cameraProvider.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
camera.cameraControl.enableTorch(true)
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
private fun initPreviewView(androidViewContext: Context): PreviewView {
val previewView = PreviewView(androidViewContext).apply {
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
return previewView
}
private fun buildPreview(): Preview {
return Preview.Builder().build()
}
private fun buildImageAnalysis(imageAnalysisStrategy: Int): ImageAnalysis {
return ImageAnalysis.Builder()
.setBackpressureStrategy(imageAnalysisStrategy)
.build()
}
private fun buildCameraSelector(cameraLens: Int): CameraSelector {
return CameraSelector.Builder()
.requireLensFacing(cameraLens)
.build()
}
What I tried:
I tried passing down the state of BottomSheetState to the composable, and checking for state, which should triggers re-composition, but since I'm using Android's Camera as View, this doesn't solve the problem
First on CameraPreview Composable function in your code, define a variable of type ProcessCameraProvider, and assign it to null value
var cameraProvider: ProcessCameraProvider? = null
Then you will define a DisposableEffect, with key of cameraProvider and when it de-compose, you'll close the camera
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() } // closes the camera
}
}
Replace your old line of code
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
with our new cameraProvider
cameraProvider = cameraProviderFuture.get()
Then in your try-catch block, since we're using a null value, when need to check if it's null or not, so we'll use let
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
Complete Code:
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider: ProcessCameraProvider? = null
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() }
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector =
buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}
I found a solution, where I used DisposableEffect to shut the camera when composable is removed from composition
First on CameraPreview Composable function in your code, define a variable of type ProcessCameraProvider, and assign it to null value
var cameraProvider: ProcessCameraProvider? = null
Then you will define a DisposableEffect, with key of cameraProvider and when the composable de-compose, you'll close the camera
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() } // closes the camera
}
}
Replace your old line of code
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
with our new cameraProvider
cameraProvider = cameraProviderFuture.get()
Then in your try-catch block, since we're using a null value, when need to check if it's null or not, so we'll use let
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
Complete Code:
#Composable
fun CameraPreview(
modifier: Modifier = Modifier,
onBarcodeDetected: (barcodes: List<Barcode>) -> Unit,
onBarcodeFailed: (exception: Exception) -> Unit,
onBarcodeNotFound: (text: String) -> Unit,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var cameraProvider: ProcessCameraProvider? = null
DisposableEffect(key1 = cameraProvider) {
onDispose {
cameraProvider?.let { it.unbindAll() }
}
}
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { androidViewContext -> initPreviewView(androidViewContext) },
update = { previewView: PreviewView ->
val cameraSelector: CameraSelector =
buildCameraSelector(CameraSelector.LENS_FACING_BACK)
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
val preview = buildPreview().also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val barcodeAnalyser = BarCodeAnalyser(
onBarcodeDetected = onBarcodeDetected,
onBarcodeFailed = onBarcodeFailed,
onBarCodeNotFound = onBarcodeNotFound
)
val imageAnalysis: ImageAnalysis =
buildImageAnalysis(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST).also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider?.let {
it.unbindAll() //Make sure we only use 1 usecase related to camera
val camera = it.bindToLifecycle(
lifecycleOwner, cameraSelector, preview, imageAnalysis
)
camera.cameraControl.enableTorch(true) // TODO: Debug mode only
}
} catch (e: Exception) {
Log.d("TAG", "CameraPreview: ${e.localizedMessage}")
}
}, ContextCompat.getMainExecutor(context))
}
)
}

"#Composable invocations can only happen from the context of a #Composable function"

#Composable
fun CategorySection(context: Context) {
Column(modifier = Modifier.padding(8.dp)) {
Text(
text = "Api Data",
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(vertical = 7.dp, horizontal = 8.dp)
)
val coroutineScope = rememberCoroutineScope()
coroutineScope.launch(Dispatchers.Main) {
val response = ApiPhoto.apiService.getPhotos()
if (response.isSuccessful && response.body() != null) {
val photos = response.body()
photos?.let {
CategoryItemSection(it)
}
} else {
Toast.makeText(
context,
"Error Occurred: ${response.message()}",
Toast.LENGTH_LONG
).show()
}
}
}
}
#Composable
fun CategoryItemSection(photos: List<Photo>) {
LazyColumn(
modifier = Modifier.fillMaxWidth()
){
items(photos.size){
CardView(photos[it])
}
}
}
You cannot call CategoryItemSection from inside a coroutine. Your coding practice here is totally wrong. You shouldn't be making API calls to your data sources from within your composables. Use a viewmodel and state hoisting:
Read up on state hoisting and uni-directional data flow:
https://developer.android.com/jetpack/compose/state

Firebase Barcode scanner using Jetpack compose not working

Trying to migrate barcode scanner to Jetpack compose and updating camera and ML Kit dependencies to latest version.
The current shows the camera view correctly, but it is not scanning the barcodes.
The ImageAnalysis analyzer runs only once.
Code
#Composable
fun CameraPreview(
data: CameraPreviewData,
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
AndroidView(
modifier = Modifier
.fillMaxSize(),
factory = { AndroidViewContext ->
PreviewView(AndroidViewContext).apply {
this.scaleType = PreviewView.ScaleType.FILL_CENTER
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// Preview is incorrectly scaled in Compose on some devices without this
implementationMode = PreviewView.ImplementationMode.COMPATIBLE
}
},
update = { previewView ->
val cameraSelector: CameraSelector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor()
val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> =
ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
val preview: Preview = Preview.Builder()
.build()
.also {
// Attach the viewfinder's surface provider to preview use case
it.setSurfaceProvider(previewView.surfaceProvider)
}
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val barcodeAnalyser = BarcodeAnalyser { barcodes ->
barcodes.forEach { barcode ->
barcode.rawValue?.let { barcodeValue ->
logError("Barcode value detected: ${barcodeValue}.")
// Other handling code
}
}
}
val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(cameraExecutor, barcodeAnalyser)
}
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageAnalysis
)
} catch (exception: Exception) {
logError("Use case binding failed with exception : $exception")
}
}, ContextCompat.getMainExecutor(context))
},
)
}
BarcodeAnalyser
class BarcodeAnalyser(
private val onBarcodesDetected: (barcodes: List<Barcode>) -> Unit,
) : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
override fun analyze(
imageProxy: ImageProxy,
) {
logError("Inside analyze")
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
imageProxy.image?.let { imageToAnalyze ->
val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS)
.build()
val barcodeScanner = BarcodeScanning.getClient(options)
val imageToProcess =
InputImage.fromMediaImage(imageToAnalyze, imageProxy.imageInfo.rotationDegrees)
barcodeScanner.process(imageToProcess)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
logError("Scanned: $barcodes")
onBarcodesDetected(barcodes)
imageProxy.close()
} else {
logError("No barcode scanned")
}
}
.addOnFailureListener { exception ->
logError("BarcodeAnalyser: Something went wrong with exception: $exception")
imageProxy.close()
}
}
lastAnalyzedTimestamp = currentTimestamp
}
}
}
References
https://stackoverflow.com/a/66763853/9636037
Thanks to Adrian's comment.
It worked after the following changes.
In BarcodeAnalyser
Removed imageProxy.close() from addOnSuccessListener and addOnFailureListener. Added it to addOnCompleteListener.
Added imageProxy.close() in else condition as well.
class BarcodeAnalyser(
private val onBarcodesDetected: (barcodes: List<Barcode>) -> Unit,
) : ImageAnalysis.Analyzer {
private var lastAnalyzedTimestamp = 0L
override fun analyze(
imageProxy: ImageProxy,
) {
logError("Inside analyze")
val currentTimestamp = System.currentTimeMillis()
if (currentTimestamp - lastAnalyzedTimestamp >= TimeUnit.SECONDS.toMillis(1)) {
imageProxy.image?.let { imageToAnalyze ->
// ...Same code
barcodeScanner.process(imageToProcess)
.addOnSuccessListener { barcodes ->
if (barcodes.isNotEmpty()) {
logError("Scanned: $barcodes")
onBarcodesDetected(barcodes)
// imageProxy.close()
} else {
logError("No barcode scanned")
}
}
.addOnFailureListener { exception ->
logError("BarcodeAnalyser: Something went wrong with exception: $exception")
// imageProxy.close()
}
.addOnCompleteListener {
imageProxy.close()
}
}
lastAnalyzedTimestamp = currentTimestamp
} else {
imageProxy.close()
}
}
}

How request permissions with Jetpack Compose?

How should be implemented requesting permission from Jetpack Compose View? I'm trying implement application accessing Camera with Jetpack Compose. I tried example from How to get Current state or context in Jetpack Compose Unfortunately example is no longer working with dev06.
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
}
}
as compose_version = '1.0.0-beta04' and
implementation 'androidx.activity:activity-compose:1.3.0-alpha06'
you can do request permission as simple as this:
#Composable
fun ExampleScreen() {
val launcher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission Accepted: Do something
Log.d("ExampleScreen","PERMISSION GRANTED")
} else {
// Permission Denied: Do something
Log.d("ExampleScreen","PERMISSION DENIED")
}
}
val context = LocalContext.current
Button(
onClick = {
// Check permission
when (PackageManager.PERMISSION_GRANTED) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_EXTERNAL_STORAGE
) -> {
// Some works that require permission
Log.d("ExampleScreen","Code requires permission")
}
else -> {
// Asking for permission
launcher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
) {
Text(text = "Check and Request Permission")
}
}
Check out Google Accompanist's Jetpack Compose Permissions.
Bear in mind that, at the time of writing, the API is still considered experimental and will require the #ExperimentalPermissionsApi annotation when used.
Documentation and usage: https://google.github.io/accompanist/permissions/
Samples: https://github.com/google/accompanist/tree/main/sample/src/main/java/com/google/accompanist/sample/permissions
Google has a library called "Accompanist". It has many help libraries and one of them is the Permission Library.
Check:
Library: https://github.com/google/accompanist/
Documentation: https://google.github.io/accompanist/permissions/
Example:
Setup in build.gradle file:
repositories {
mavenCentral()
}
dependencies {
implementation "com.google.accompanist:accompanist-permissions:<latest_version>"
}
Implementation in Code
#Composable
private fun FeatureThatRequiresCameraPermission() {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
when (cameraPermissionState.status) {
// If the camera permission is granted, then show screen with the feature enabled
PermissionStatus.Granted -> {
Text("Camera permission Granted")
}
is PermissionStatus.Denied -> {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The camera is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Camera permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
}
/**
* Composable helper for permission checking
*
* onDenied contains lambda for request permission
*
* #param permission permission for request
* #param onGranted composable for [PackageManager.PERMISSION_GRANTED]
* #param onDenied composable for [PackageManager.PERMISSION_DENIED]
*/
#Composable
fun ComposablePermission(
permission: String,
onDenied: #Composable (requester: () -> Unit) -> Unit,
onGranted: #Composable () -> Unit
) {
val ctx = LocalContext.current
// check initial state of permission, it may be already granted
var grantState by remember {
mutableStateOf(
ContextCompat.checkSelfPermission(
ctx,
permission
) == PackageManager.PERMISSION_GRANTED
)
}
if (grantState) {
onGranted()
} else {
val launcher: ManagedActivityResultLauncher<String, Boolean> =
rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
grantState = it
}
onDenied { launcher.launch(permission) }
}
}
Don't forget to add
<uses-permission android:name="android.permission.CAMERA"/>
(when requesting camera permission) to your AndroidManifest.xml, otherwise it might end up with the permission denied state, when using the solutions others provided.
A little late but this might help as I had the problem today:
With ContextAmbient.current it is not guaranteed that you have an activity or fragment thus I created my own ambient for handling permissions.
val AmbientPermissionHandler: ProvidableAmbient<PermissionHandler> =
ambientOf { throw IllegalStateException("permission handler is not initialized") }
// Activity:
private val permissionHandler = PermissionHandler(this)
// onCreate:
setContent {
Providers(
AmbientPermissionHandler provides permissionHandler
) {/* Composable Contnent */}
Usage:
#Composable
fun PermissionHandler(
permissions: Array<out String>,
requestCode: Int,
granted: #Composable() () -> Unit,
denied: #Composable() () -> Unit,
deniedPermanently: (#Composable() () -> Unit)? = null,
rational: (#Composable() () -> Unit)? = null,
awaitResult: (#Composable() () -> Unit)? = null,
) {
val permissionHandler = AmbientPermissionHandler.current
val (permissionResult, setPermissionResult) = remember(permissions) { mutableStateOf<PermissionResult?>(null) }
LaunchedEffect(Unit) {
setPermissionResult(permissionHandler.requestPermissionsSuspend(requestCode, permissions))
}
when (permissionResult) {
is PermissionResult.PermissionGranted -> granted()
is PermissionResult.PermissionDenied -> denied()
is PermissionResult.PermissionDeniedPermanently -> deniedPermanently?.invoke()
is PermissionResult.ShowRational -> rational?.invoke()
null -> awaitResult?.invoke()
}
}
Implementation of PermissionHandler with dependency https://github.com/sagar-viradiya/eazypermissions
class PermissionHandler(
private val actualHandler: AppCompatActivity,
) {
suspend fun requestPermissionsSuspend(requestCode: Int, permissions: Array<out String>): PermissionResult {
return PermissionManager.requestPermissions(actualHandler, requestCode, *permissions)
}
fun requestPermissionsWithCallback(requestCode: Int, permissions: Array<out String>, onResult: (PermissionResult) -> Unit) {
actualHandler.lifecycleScope.launch {
onResult.invoke(PermissionManager.requestPermissions(actualHandler, requestCode, *permissions))
}
}
}
If you prefer a callback the second function works also.
private const val PERMISSIONS_REQUEST_CODE = 10
private val PERMISSIONS_REQUIRED = arrayOf(Manifest.permission.CAMERA)
#Composable
fun PermissionButton() {
val context = ContextAmbient.current
Button(onClick = {
if (!hasPermissions(context)) {
requestPermissions(
context as Activity,
PERMISSIONS_REQUIRED,
PERMISSIONS_REQUEST_CODE
)
}
}
) {}
}
fun hasPermissions(context: Context) = PERMISSIONS_REQUIRED.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
You can request multiples permissions.
class MainActivity : ComponentActivity() {
private val neededPermissions = arrayOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
)
#OptIn(ExperimentalMaterialApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MfmTheme {
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestMultiplePermissions()
) { maps ->
val granted = maps.values.reduce { acc, next -> (acc && next) }
if (granted) {
// all permission granted
} else {
// Permission Denied: Do something
}
// You can check one by one
maps.forEach { entry ->
Log.i("Permission = ${entry.key}", "Enabled ${entry.value}")
}
}
val context = LocalContext.current
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background,
onClick = {
when {
hasPermissions(context, *neededPermissions) -> {
// All permissions granted
}
else -> {
// Request permissions
launcher.launch(neededPermissions)
}
}
}
) {
Greeting("Android")
}
}
}
}
private fun hasPermissions(context: Context, vararg permissions: String): Boolean =
permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
#Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MfmTheme {
Greeting("Android")
}
}
//define permission in composable fun
val getPermission = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
//permission accepted do somthing
} else {
//permission not accepted show message
}
}
//i used SideEffect to launch permission request when screen recomposed
//you can call it inside a button click without SideEffect
SideEffect {
getPermission.launch(Manifest.permission.READ_CONTACTS)
}
and if you wanted to request multiple permission use this:
ActivityResultContracts.RequestMultiplePermissions()
The rememberPermissionState(permission: String) API allows you to request a certain permission to the user and check for the status of the permission.
**Step1:**
A library which provides Android runtime permissions support for Jetpack Compose.
implementation 'com.google.accompanist:accompanist-permissions:0.24.13-rc'
..
**Step2:**
In our AndroidManifeastxml we need to declare permission (in this example we are going to request location permission)
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
..
**Step3:**
In our MainActivity.kt we are calling this permission request function
class MainActivity : ComponentActivity() {
#OptIn(ExperimentalPermissionsApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Material3ComposeTheme {
RequestPermission(permission = Manifest.permission.ACCESS_FINE_LOCATION)
}
}
}
}
..
**Step4:**
In this SinglePermission.kt, we are going to request permission from user, if user already deny means we will show simple alert dialog info message otherwise will show custom full screen dialog.
package compose.material.theme
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import com.google.accompanist.permissions.*
#ExperimentalPermissionsApi
#Composable
fun RequestPermission(
permission: String,
rationaleMessage: String = "To use this app's functionalities, you need to give us the permission.",
) {
val permissionState = rememberPermissionState(permission)
HandleRequest(
permissionState = permissionState,
deniedContent = { shouldShowRationale ->
PermissionDeniedContent(
rationaleMessage = rationaleMessage,
shouldShowRationale = shouldShowRationale
) { permissionState.launchPermissionRequest() }
},
content = {
/* Content(
text = "PERMISSION GRANTED!",
showButton = false
) {}*/
}
)
}
#ExperimentalPermissionsApi
#Composable
fun HandleRequest(
permissionState: PermissionState,
deniedContent: #Composable (Boolean) -> Unit,
content: #Composable () -> Unit
) {
when (permissionState.status) {
is PermissionStatus.Granted -> {
content()
}
is PermissionStatus.Denied -> {
deniedContent(permissionState.status.shouldShowRationale)
}
}
}
#Composable
fun Content(showButton: Boolean = true, onClick: () -> Unit) {
if (showButton) {
val enableLocation = remember { mutableStateOf(true) }
if (enableLocation.value) {
CustomDialogLocation(
title = "Turn On Location Service",
desc = "Explore the world without getting lost and keep the track of your location.\n\nGive this app a permission to proceed. If it doesn't work, then you'll have to do it manually from the settings.",
enableLocation,
onClick
)
}
}
}
#ExperimentalPermissionsApi
#Composable
fun PermissionDeniedContent(
rationaleMessage: String,
shouldShowRationale: Boolean,
onRequestPermission: () -> Unit
) {
if (shouldShowRationale) {
AlertDialog(
onDismissRequest = {},
title = {
Text(
text = "Permission Request",
style = TextStyle(
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
fontWeight = FontWeight.Bold
)
)
},
text = {
Text(rationaleMessage)
},
confirmButton = {
Button(onClick = onRequestPermission) {
Text("Give Permission")
}
}
)
}
else {
Content(onClick = onRequestPermission)
}
}
..
**Step 5:**
In this CustomDialogLocation.kt, we make custom dialog in android jetpack compose.
package compose.material.theme
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
/*
This example demonstrates how to make custom dialog in android jetpack compose in android.
* Button : https://www.boltuix.com/2021/12/button_25.html
* Clip Modifier : https://www.boltuix.com/2021/12/clip-modifier_24.html
* Alert Dialog : https://www.boltuix.com/2021/12/alert-dialog_25.html
* Column : https://www.boltuix.com/2021/12/column-layout_25.html
* Box : https://www.boltuix.com/2021/12/box-layout_25.html
* Type.kt : https://www.boltuix.com/2021/12/typography_27.html
* Color.kt : https://www.boltuix.com/2022/05/google-material-design-color.html
* Dialog : https://www.boltuix.com/2022/07/compose-custom-animating-dialog.html
* */
#Composable
fun CustomDialogLocation(
title: String? = "Message",
desc: String? = "Your Message",
enableLocation: MutableState<Boolean>,
onClick: () -> Unit
) {
Dialog(
onDismissRequest = { enableLocation.value = false}
) {
Box(
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)
// .width(300.dp)
// .height(164.dp)
.background(
color = MaterialTheme.colorScheme.onPrimary,
shape = RoundedCornerShape(25.dp,5.dp,25.dp,5.dp)
)
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
//.........................Image: preview
Image(
painter = painterResource(id = R.drawable.permission_location),
contentDescription = null,
contentScale = ContentScale.Fit,
/* colorFilter = ColorFilter.tint(
color = MaterialTheme.colorScheme.primary
),*/
modifier = Modifier
.padding(top = 5.dp)
.height(320.dp)
.fillMaxWidth(),
)
//.........................Spacer
//.........................Text: title
Text(
text = title!!,
textAlign = TextAlign.Center,
modifier = Modifier
// .padding(top = 5.dp)
.fillMaxWidth(),
letterSpacing = 2.sp,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
)
Spacer(modifier = Modifier.height(8.dp))
//.........................Text : description
Text(
text = desc!!,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 10.dp, start = 25.dp, end = 25.dp)
.fillMaxWidth(),
letterSpacing = 1.sp,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.primary,
)
//.........................Spacer
Spacer(modifier = Modifier.height(24.dp))
//.........................Button : OK button
val cornerRadius = 16.dp
val gradientColors = listOf(Color(0xFFff669f), Color(0xFFff8961))
val roundedCornerShape = RoundedCornerShape(topStart = 30.dp,bottomEnd = 30.dp)
Button(
modifier = Modifier
.fillMaxWidth()
.padding(start = 32.dp, end = 32.dp),
onClick=onClick,
contentPadding = PaddingValues(),
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent
),
shape = RoundedCornerShape(cornerRadius)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.horizontalGradient(colors = gradientColors),
shape = roundedCornerShape
)
.padding(horizontal = 16.dp, vertical = 8.dp),
contentAlignment = Alignment.Center
) {
Text(
text ="Enable",
fontSize = 20.sp,
color = Color.White
)
}
}
//.........................Spacer
Spacer(modifier = Modifier.height(12.dp))
TextButton(onClick = {
enableLocation.value = false
}) { Text("Cancel", style = MaterialTheme.typography.labelLarge) }
Spacer(modifier = Modifier.height(24.dp))
}
}
}
}
Get source code & video: https://www.boltuix.com/2022/07/requesting-location-permission-in.html
There are two ways to get runtime permissions in jetpack compose.
Using activity result
Using the accompanist permissions library
Runtime permission using activity result
The first step is to define the permission in the manifest.xml file.
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera"/>
Create an activity result launcher to request the permission we defined. Once it’s launched it will return the result whether the permission is granted or not.
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
if (it) {
Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
cameraLauncher.launch(uri)
} else {
Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
Checking Permission
Before launching the request for permission, we need to check whether the permission is granted or not. If it’s already granted we can proceed with our regular flow. If permission is not provided, then we need to launch the permission request with the permission we wanted.
val permissionCheckResult = ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
cameraLauncher.launch(uri)
} else {
permissionLauncher.launch(android.Manifest.permission.CAMERA)
}
finally, the code for the runtime permission using the activity result will be like the below,
val context = LocalContext.current
val file = context.createImageFile()
val uri = FileProvider.getUriForFile(
Objects.requireNonNull(context),
BuildConfig.APPLICATION_ID + ".provider", file
)
var capturedImageUri by remember {
mutableStateOf<Uri>(Uri.EMPTY)
}
val cameraLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
capturedImageUri = uri
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) {
if (it) {
Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
cameraLauncher.launch(uri)
} else {
Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
}
}
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(12.dp)) {
Button(onClick = {
val permissionCheckResult = ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
cameraLauncher.launch(uri)
} else {
// Request a permission
permissionLauncher.launch(android.Manifest.permission.CAMERA)
}
}) {
Text(text = "Open Camera")
}
if (capturedImageUri.path?.isNotEmpty() == true) {
Image(
modifier = Modifier
.padding(16.dp, 8.dp)
.fillMaxWidth()
.size(400.dp),
painter = rememberImagePainter(capturedImageUri),
contentDescription = null
)
}
}
the output of the above code,

Categories

Resources