Search code examples
kotlinandroid-jetpack-composeandroid-camera

ActivityResultContracts TakePicture it is always returning false as a result


I'm using Jetpack Compose, and when I call the method to take a picture with the camera, the result of ActivityResultContracts.TakePicture is always false.

Sample code:

@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun SomeScreen() {
    val photoUri by remember { mutableStateOf(value = Uri.EMPTY) }

    val cameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicture(),
        onResult = { success ->
            if (success) {
                println("success")
                println("photo uri: $photoUri")
            } else println("result failed")
        }
    )

    val cameraPermissionState = rememberPermissionState(
        permission = Manifest.permission.CAMERA,
        onPermissionResult = { granted ->
            if (granted) cameraLauncher.launch(photoUri)
            else print("camera permission is denied")
        }
    )

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Button(onClick = cameraPermissionState::launchPermissionRequest) {
            Text(text = "Take a photo with Camera")
        }
    }
}

I used the accompanist-permissions library to make it easier, the part of opening the camera app and taking the photo is apparently working normally, but the result from cameraLauncher is always false...

Can anyone guide me to solve this problem?


Solution

  • The problem with your code is that you are passing an empty Uri to launch, and the Camera app can't save the image in that Uri. If you open the TakePicture class or place the mouse cursor over it, you will see the following information:

    An ActivityResultContract to take a picture saving it into the provided content-Uri. Returns true if the image was saved into the given Uri.

    In other words, the TakePicture class will not automatically create a File for you, you will have to create a File yourself and provide the Uri.

    I'll assume a simple scenario where you want to take a photo for some temporary task within the app. To achieve this goal, you need to understand some steps that are missing in your code:

    1. As you need to create a File and expose it to the Camera app, you need to create rules with the File Provider and declare it in the Manifest file.
    2. The function that creates a File and exposes its Uri with the FileProvider.

    Lets start with file_paths.xml (inside res/xml):

    <?xml version="1.0" encoding="utf-8"?>
    <paths>
        <cache-path
            name="cache_pictures"
            path="/" />
    
    </paths>
    

    I'm using cache-path here following the idea of keeping the files temporarily.

    In the Manifest file:

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
    
        <uses-permission android:name="android.permission.CAMERA" />
    
        <application ...>
    
            <activity .../>
    
            <provider
                android:name="androidx.core.content.FileProvider"
                android:authorities="${applicationId}.provider"
                android:exported="false"
                android:grantUriPermissions="true">
                <meta-data
                    android:name="android.support.FILE_PROVIDER_PATHS"
                    android:resource="@xml/file_paths" />
            </provider>
    
        </application>
    
    </manifest>
    

    Extension to create a File and return a Uri:

    fun Context.createTempPictureUri(
        provider: String = "${BuildConfig.APPLICATION_ID}.provider",
        fileName: String = "picture_${System.currentTimeMillis()}",
        fileExtension: String = ".png"
    ): Uri {
        val tempFile = File.createTempFile(
            fileName, fileExtension, cacheDir
        ).apply {
            createNewFile()
        }
    
        return FileProvider.getUriForFile(applicationContext, provider, tempFile)
    }
    

    The cache folder is being used in this example with cacheDir. If changing here to filesDir, be sure to change in the cache-path on file_paths.xml to files-path.

    Now in Composable Screen you can have something like this:

    @OptIn(ExperimentalPermissionsApi::class)
    @Composable
    fun SomeScreen() {
        val context = LocalContext.current
        var currentPhotoUri by remember { mutableStateOf(value = Uri.EMPTY) }
        var tempPhotoUri by remember { mutableStateOf(value = Uri.EMPTY) }
    
        val cameraLauncher = rememberLauncherForActivityResult(
            contract = ActivityResultContracts.TakePicture(),
            onResult = { success ->
                if (success) currentPhotoUri = tempPhotoUri
            }
        )
    
        val cameraPermissionState = rememberPermissionState(
            permission = Manifest.permission.CAMERA,
            onPermissionResult = { granted ->
                if (granted) {
                    tempPhotoUri = context.createTempPictureUri()
                    cameraLauncher.launch(tempPhotoUri)
                } else print("camera permission is denied")
            }
        )
    
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            AnimatedVisibility(visible = currentPhotoUri.toString().isNotEmpty()) {
                // from coil library 
                AsyncImage(
                    modifier = Modifier.size(size = 240.dp),
                    model = currentPhotoUri,
                    contentDescription = null
                )
            }
    
            Button(onClick = cameraPermissionState::launchPermissionRequest) {
                Text(text = "Take a photo with Camera")
            }
        }
    }