androidkotlincamerauriandroid-fileprovider

Android Camera: The displayed image fails to refresh after the initial photo is taken


I'm developing an app that enables users to capture photos using the built-in camera of their phones and then display the captured photo within the app using an AsyncImage object from Coli. The initial capture works as expected, but subsequent attempts to capture additional photos result in the displayed image remaining unchanged from the first one. It fails to update with the newly captured picture.

I attempted to place the getUriForFile function within the button so that the URI would be refreshed every time the button is clicked. However, the issue escalated to the point where the displayed image becomes empty after the second capture. I suspect that the problem may stem from the way I assigned the URI value, but despite spending an entire day grappling with it, I haven't been able to resolve it.

Here are the codes that I'm working with:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    MyApp()
                }
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun MyApp() {
    val context = LocalContext.current
    val uri = context.createImageFile()
    var imageUri by remember {
        mutableStateOf<Uri>(Uri.EMPTY)
    }

    // Take photo
    val cameraLauncher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
        imageUri = uri
    }

    // Camera Permission
    val cameraPermissionLauncher = 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()
        }
    }

    Scaffold (
        topBar = {
            CenterAlignedTopAppBar(
                title = {
                    Text(
                        text = stringResource(R.string.title),
                        fontWeight = FontWeight.Bold,
                        fontSize = 30.sp
                    )
                },
                colors = TopAppBarDefaults.largeTopAppBarColors(
                    containerColor = colorResource(R.color.theme)
                )
            )
        }
    ) {
        innerPadding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(innerPadding)
        ) {
            // Create AsyncImage object if image exists
            if (imageUri.path?.isNotEmpty() == true) {
                AsyncImage(
                    model = imageUri,
                    modifier = Modifier.fillMaxWidth(),
                    contentDescription = stringResource(R.string.image_content_description)
                )
            }
            Column(
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
                modifier = Modifier
                    .align(Alignment.BottomCenter)
                    .padding(bottom = dimensionResource(R.dimen.padding_vertical))
            ) {
                Text(
                    text = stringResource(R.string.upload),
                    style = MaterialTheme.typography.titleLarge
                )
                // Take photo
                Button(
                    onClick = {
                        val permissionCheckResult =
                            ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                        if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
                            cameraLauncher.launch(uri)
                        } else {
                            // Request a permission
                            cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
                        }
                    },
                    shape = RoundedCornerShape(dimensionResource(R.dimen.button_radius)),
                    colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.theme)),
                    modifier = Modifier
                        .width(dimensionResource(R.dimen.button_width))
                        .height(dimensionResource(R.dimen.button_height))
                        .padding(top = dimensionResource(R.dimen.button_padding))
                ) {
                    Image(
                        painter = painterResource(R.drawable.photo_camera),
                        contentDescription = null,
                        modifier = Modifier.weight(0.5f)
                    )
                    Text(
                        text = stringResource(R.string.take_photo),
                        style = MaterialTheme.typography.titleLarge,
                        color = Color.Black,
                        fontWeight = FontWeight.Bold,
                        modifier = Modifier.weight(1.5f)
                    )
                }
            }
        }
    }
}

private fun Context.createImageFile(): Uri {
    val directory = File(filesDir, "images")
    if (!directory.exists()) {
        directory.mkdirs()
    }
    val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
    val file = File.createTempFile(
        "image_${timeStamp}_",
        ".png",
        directory
    )
    return FileProvider.getUriForFile(
        Objects.requireNonNull(this),
        packageName + ".FileProvider",
        file
    )
}

Solution

  • I've managed to find a solution, so I'll share it here.

    Initially, I made the uri variable mutable thanks to Akshay's suggestion. This allows the uri value to change every time the button is clicked. However, the issue persisted.

    Upon further investigation, I came across this post that shed light on the problem. It turns out that passing the cameraLauncher into the cameraPermissionLauncher resulted in the camera launching with the same uri each time it checked for permission.

    Here is the working solution:

    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyTheme {
                    // A surface container using the 'background' color from the theme
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        MyApp()
                    }
                }
            }
        }
    }
    
    @OptIn(ExperimentalMaterial3Api::class)
    @Composable
    private fun MyApp() {
        val context = LocalContext.current
        var uri by rememberSaveable {
            mutableStateOf<Uri>(Uri.EMPTY)
        }
        var imageUri by rememberSaveable {
            mutableStateOf<Uri>(Uri.EMPTY)
        }
    
        // Take photo
        val cameraLauncher = rememberLauncherForActivityResult(
            ActivityResultContracts.TakePicture()
        ) { success ->
            if (success) {
                imageUri = uri
            } else {
                Toast.makeText(context, "Failed to capture photo", Toast.LENGTH_SHORT).show()
            }
        }
    
        // Camera Permission
        val cameraPermissionLauncher = rememberLauncherForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted ->
            if (granted) {
                Toast.makeText(context, "Permission Granted", Toast.LENGTH_SHORT).show()
            } else {
                Toast.makeText(context, "Permission Denied", Toast.LENGTH_SHORT).show()
            }
        }
    
        Scaffold (
            topBar = {
                CenterAlignedTopAppBar(
                    title = {
                        Text(
                            text = stringResource(R.string.title),
                            fontWeight = FontWeight.Bold,
                            fontSize = 30.sp
                        )
                    },
                    colors = TopAppBarDefaults.largeTopAppBarColors(
                        containerColor = colorResource(R.color.theme)
                    )
                )
            }
        ) {
            innerPadding ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(innerPadding)
            ) {
                // Create AsyncImage object if image exists
                AnimatedVisibility(visible = imageUri.toString().isNotEmpty()) {
                    AsyncImage(
                        model = ImageRequest.Builder(context)
                            .data(imageUri)
                            .crossfade(true)
                            .build(),
                        modifier = Modifier.fillMaxWidth(),
                        contentDescription = stringResource(R.string.image_content_description)
                    )
                }
                Column(
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .align(Alignment.BottomCenter)
                        .padding(bottom = dimensionResource(R.dimen.padding_vertical))
                ) {
                    Text(
                        text = stringResource(R.string.upload),
                        style = MaterialTheme.typography.titleLarge
                    )
                    // Take photo
                    Button(
                        onClick = {
                            val permissionCheckResult =
                                ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA)
                            if (permissionCheckResult == PackageManager.PERMISSION_GRANTED) {
                                uri = context.createImageFile()
                                cameraLauncher.launch(uri)
                            } else {
                                // Request a permission
                                cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
                            }
                        },
                        shape = RoundedCornerShape(dimensionResource(R.dimen.button_radius)),
                        colors = ButtonDefaults.buttonColors(containerColor = colorResource(R.color.theme)),
    modifier = Modifier
                            .width(dimensionResource(R.dimen.button_width))
                            .height(dimensionResource(R.dimen.button_height))
                            .padding(top = dimensionResource(R.dimen.button_padding))
                    ) {
                        Image(
                            painter = painterResource(R.drawable.photo_camera),
                            contentDescription = null,
                            modifier = Modifier.weight(0.5f)
                        )
                        Text(
                            text = stringResource(R.string.take_photo),
                            style = MaterialTheme.typography.titleLarge,
                            color = Color.Black,
                            fontWeight = FontWeight.Bold,
                            modifier = Modifier.weight(1.5f)
                        )
                    }
                }
            }
        }
    }
    
    private fun Context.createImageFile(): Uri {
        val directory = File(filesDir, "images")
        if (!directory.exists()) {
            directory.mkdirs()
        }
        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
        val file = File.createTempFile(
            "image_${timeStamp}_",
            ".png",
            directory
        )
        return FileProvider.getUriForFile(
            Objects.requireNonNull(this),
            packageName + ".FileProvider",
            file
        )
    }