androidandroid-cameraandroid-jetpack-composeandroid-fileproviderandroid-compose-image

Camera image from FileProvider Uri only displaying for first time


My Jetpack Compose camera app is targeting API Level 32 and is being tested on an Android 11 phone. I'm generating a Uri with FileProvider to take a photo with the stock camera app. Logcat shows the Uri every time I snap a picture, but the image is displayed in the UI only for the first time. Subsequent camera snapshots don't show the image although the Uri is displayed in Logcat. And exiting the app with a back button click and then opening the app to take a snapshot, again, only works for the first time. How can I fix this issue?

Manifest File

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.testsoft.camtest">

    <uses-feature android:name="android.hardware.camera" />
    <uses-permission android:name="android.permission.CAMERA" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Camtest"
        tools:targetApi="31">

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Camtest">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

File Paths XML File

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path
        name="my_images"
        path="Android/data/com.testsoft.camtest/files/Pictures"/>
    <external-files-path
        name="my_debug_images"
        path="/storage/emulated/0/Android/data/com.testsoft.camtest/files/Pictures/"/>
    <external-files-path
        name="my_root_images"
        path="/"/>
</paths>

MainScreen Composable

@Composable
fun MainScreen() {
    var hasImage by remember {
        mutableStateOf(false)
    }
    var imageUri by remember {
        mutableStateOf<Uri?>(null)
    }

    val context = LocalContext.current

    var grantCameraState by remember {
        mutableStateOf(
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.CAMERA
            ) == PackageManager.PERMISSION_GRANTED
        )
    }

    val cameraPermissionlauncher: ManagedActivityResultLauncher<String, Boolean> =
        rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
            grantCameraState = it
        }

    val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { success ->
        Log.i("UriContent@Snapshot", imageUri.toString())
        hasImage = success
    }

    Column {
        Button(
            modifier = Modifier.align(alignment = Alignment.CenterHorizontally),
            onClick = {
            if (grantCameraState) {
                val uri = getCamImageUri(context)
                imageUri = uri
                cameraLauncher.launch(uri)
            } else {
                cameraPermissionlauncher.launch(Manifest.permission.CAMERA)
            }
        }) {
            Text(text = "Take photo")
        }

        Spacer(Modifier.width(10.dp))

        if(hasImage && imageUri != null){
            Log.i("UriContent@Render", imageUri.toString())

            AsyncImage(
                imageUri,
                contentDescription = null,
                modifier = Modifier.fillMaxWidth()
            )

        /*
            //Also tried this but had the same issue:

            Image(
                painter = rememberAsyncImagePainter(imageUri),
                contentDescription = null,
                modifier = Modifier.fillMaxWidth()
            )
        */
        }
    }
}

fun getCamImageUri(context: Context): Uri? {
    var uri: Uri? = null
    val file = createImageFile(context)
    try {
        uri = FileProvider.getUriForFile(context, "com.testsoft.camtest.fileprovider", file)
    } catch (e: Exception) {
        Log.e(ContentValues.TAG, "Error: ${e.message}")
    }
    return uri
}

private fun createImageFile(context: Context) : File {
    val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
    val imageDirectory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    return File.createTempFile(
        "Camtest_Image_${timestamp}",
        ".jpg",
        imageDirectory
    )
}

Logcat Output

I/UriContent@Snapshot: content://com.testsoft.camtest.fileprovider/my_root_images/Pictures/Camtest_Image_20220702_1002472475660413794636578.jpg

I/UriContent@Render: content://com.testsoft.camtest.fileprovider/my_root_images/Pictures/Camtest_Image_20220702_1002472475660413794636578.jpg

Logcat message with subsequent camera launch

D/skia: --- Failed to create image decoder with message 'unimplemented'

Solution

  • I found the issue. After debugging the app, I discovered that I needed to set the hasImage state variable to false in the Take photo button's onclick logic like below:

     Button(
        modifier = Modifier.align(alignment = Alignment.CenterHorizontally),
        onClick = {
            if (grantCameraState) {
               val uri = getCamImageUri(context)
               imageUri = uri
    
               // Set it to false here
               hasImage = false
    
               cameraLauncher.launch(uri)
            } else {
            cameraPermissionlauncher.launch(Manifest.permission.CAMERA)
           }
      }) {
        Text(text = "Take photo")
     }