From f45b07e8274bf80a0851bb2df9a2f65206abbbbc Mon Sep 17 00:00:00 2001 From: bifri Date: Mon, 10 Nov 2025 12:58:31 +0200 Subject: [PATCH] Preserve captured photo result after process death and camera reinitialization --- .../dialogs/compose/CameraPicker.android.kt | 48 +++++++++++++++++ .../dialogs/compose/CameraPicker.ios.kt | 54 +++++++++++++++++++ .../filekit/dialogs/compose/CameraPicker.kt | 13 +++++ .../dialogs/compose/FileKitCompose.mobile.kt | 40 -------------- .../compose/FileKitResultLauncher.mobile.kt | 7 +-- .../src/androidMain/kotlin/App.android.kt | 7 +-- .../composeApp/src/iosMain/kotlin/App.ios.kt | 12 ++--- 7 files changed, 124 insertions(+), 57 deletions(-) create mode 100644 filekit-dialogs-compose/src/androidMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.android.kt create mode 100644 filekit-dialogs-compose/src/iosMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.ios.kt create mode 100644 filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.kt diff --git a/filekit-dialogs-compose/src/androidMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.android.kt b/filekit-dialogs-compose/src/androidMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.android.kt new file mode 100644 index 00000000..740c45c0 --- /dev/null +++ b/filekit-dialogs-compose/src/androidMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.android.kt @@ -0,0 +1,48 @@ +package io.github.vinceglb.filekit.dialogs.compose + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.dialogs.CustomTakePicture +import io.github.vinceglb.filekit.dialogs.FileKitCameraFacing +import io.github.vinceglb.filekit.dialogs.FileKitOpenCameraSettings +import io.github.vinceglb.filekit.dialogs.toAndroidUri +import io.github.vinceglb.filekit.path +import kotlin.let + +@Composable +public actual fun rememberCameraPickerLauncher( + cameraFacing: FileKitCameraFacing, + openCameraSettings: FileKitOpenCameraSettings, + onResult: (PlatformFile?) -> Unit, +): PhotoResultLauncher { + val currentOnResult by rememberUpdatedState(onResult) + var currentPhotoPath by rememberSaveable { mutableStateOf(null) } + + val takePictureLauncher = rememberLauncherForActivityResult( + CustomTakePicture(cameraFacing) + ) { success -> + if (success) { + currentOnResult(currentPhotoPath?.let(::PlatformFile)) + } else { + currentOnResult(null) + } + } + + val authority = openCameraSettings.authority + val returnedLauncher = remember(authority) { + PhotoResultLauncher { _, destinationFile -> + val newPhotoPath = destinationFile.path + currentPhotoPath = newPhotoPath + val uri = destinationFile.toAndroidUri(authority) + takePictureLauncher.launch(uri) + } + } + return returnedLauncher +} diff --git a/filekit-dialogs-compose/src/iosMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.ios.kt b/filekit-dialogs-compose/src/iosMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.ios.kt new file mode 100644 index 00000000..99953842 --- /dev/null +++ b/filekit-dialogs-compose/src/iosMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.ios.kt @@ -0,0 +1,54 @@ +package io.github.vinceglb.filekit.dialogs.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import io.github.vinceglb.filekit.FileKit +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.dialogs.FileKitCameraFacing +import io.github.vinceglb.filekit.dialogs.FileKitOpenCameraSettings +import io.github.vinceglb.filekit.dialogs.openCameraPicker +import kotlinx.coroutines.launch + +@Composable +public actual fun rememberCameraPickerLauncher( + cameraFacing: FileKitCameraFacing, + openCameraSettings: FileKitOpenCameraSettings, + onResult: (PlatformFile?) -> Unit, +): PhotoResultLauncher { + // Init FileKit + InitFileKit() + + // Coroutine + val coroutineScope = rememberCoroutineScope() + + // Updated state + val currentOnResult by rememberUpdatedState(onResult) + + // FileKit + val fileKit = remember { FileKit } + + // FileKit launcher + // FIXME: Add openCameraSettings as a key to remember(). + // To support this safely, openCameraSettings should be a data class (or override equals() and hashCode()) + // and annotated with @Immutable. + // Note: On iOS, openCameraSettings is currently an empty class and not actually used, + // so it's safe to temporarily skip it as a key. + val returnedLauncher = remember(cameraFacing) { + PhotoResultLauncher { type, destinationFile -> + coroutineScope.launch { + val result = fileKit.openCameraPicker( + type = type, + cameraFacing = cameraFacing, + destinationFile = destinationFile, + openCameraSettings = openCameraSettings, + ) + currentOnResult(result) + } + } + } + + return returnedLauncher +} diff --git a/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.kt b/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.kt new file mode 100644 index 00000000..cfae2311 --- /dev/null +++ b/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/CameraPicker.kt @@ -0,0 +1,13 @@ +package io.github.vinceglb.filekit.dialogs.compose + +import androidx.compose.runtime.Composable +import io.github.vinceglb.filekit.PlatformFile +import io.github.vinceglb.filekit.dialogs.FileKitCameraFacing +import io.github.vinceglb.filekit.dialogs.FileKitOpenCameraSettings + +@Composable +public expect fun rememberCameraPickerLauncher( + cameraFacing: FileKitCameraFacing = FileKitCameraFacing.Back, + openCameraSettings: FileKitOpenCameraSettings = FileKitOpenCameraSettings.createDefault(), + onResult: (PlatformFile?) -> Unit, +): PhotoResultLauncher diff --git a/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitCompose.mobile.kt b/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitCompose.mobile.kt index 9c6df959..545c2bd3 100644 --- a/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitCompose.mobile.kt +++ b/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitCompose.mobile.kt @@ -1,53 +1,13 @@ package io.github.vinceglb.filekit.dialogs.compose import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.rememberUpdatedState import io.github.vinceglb.filekit.FileKit -import io.github.vinceglb.filekit.PlatformFile -import io.github.vinceglb.filekit.dialogs.FileKitOpenCameraSettings import io.github.vinceglb.filekit.dialogs.FileKitShareSettings -import io.github.vinceglb.filekit.dialogs.openCameraPicker import io.github.vinceglb.filekit.dialogs.shareFile import kotlinx.coroutines.launch -@Composable -public fun rememberCameraPickerLauncher( - openCameraSettings: FileKitOpenCameraSettings = FileKitOpenCameraSettings.createDefault(), - onResult: (PlatformFile?) -> Unit, -): PhotoResultLauncher { - // Init FileKit - InitFileKit() - - // Coroutine - val coroutineScope = rememberCoroutineScope() - - // Updated state - val currentOnResult by rememberUpdatedState(onResult) - - // FileKit - val fileKit = remember { FileKit } - - // FileKit launcher - val returnedLauncher = remember { - PhotoResultLauncher { type, cameraFacing, destinationFile -> - coroutineScope.launch { - val result = fileKit.openCameraPicker( - type = type, - cameraFacing = cameraFacing, - destinationFile = destinationFile, - openCameraSettings = openCameraSettings, - ) - currentOnResult(result) - } - } - } - - return returnedLauncher -} - @Composable public fun rememberShareFileLauncher( shareSettings: FileKitShareSettings = FileKitShareSettings.createDefault(), diff --git a/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitResultLauncher.mobile.kt b/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitResultLauncher.mobile.kt index 57383664..4bb2617a 100644 --- a/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitResultLauncher.mobile.kt +++ b/filekit-dialogs-compose/src/mobileMain/kotlin/io/github/vinceglb/filekit/dialogs/compose/FileKitResultLauncher.mobile.kt @@ -3,7 +3,6 @@ package io.github.vinceglb.filekit.dialogs.compose import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.PlatformFile import io.github.vinceglb.filekit.cacheDir -import io.github.vinceglb.filekit.dialogs.FileKitCameraFacing import io.github.vinceglb.filekit.dialogs.FileKitCameraType import io.github.vinceglb.filekit.div import kotlin.uuid.ExperimentalUuidApi @@ -12,17 +11,15 @@ import kotlin.uuid.Uuid public class PhotoResultLauncher( private val onLaunch: ( type: FileKitCameraType, - cameraFacing: FileKitCameraFacing, destinationFile: PlatformFile, ) -> Unit, ) { @OptIn(ExperimentalUuidApi::class) public fun launch( type: FileKitCameraType = FileKitCameraType.Photo, - cameraFacing: FileKitCameraFacing = FileKitCameraFacing.Back, - destinationFile: PlatformFile = FileKit.cacheDir / "${Uuid.random()}.jpg", + destinationFile: PlatformFile = FileKit.cacheDir / "${Uuid.random()}.jpg" ) { - onLaunch(type, cameraFacing, destinationFile) + onLaunch(type, destinationFile) } } diff --git a/samples/sample-compose/composeApp/src/androidMain/kotlin/App.android.kt b/samples/sample-compose/composeApp/src/androidMain/kotlin/App.android.kt index aba0d57d..fe42b21b 100644 --- a/samples/sample-compose/composeApp/src/androidMain/kotlin/App.android.kt +++ b/samples/sample-compose/composeApp/src/androidMain/kotlin/App.android.kt @@ -1,4 +1,3 @@ - import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share @@ -24,6 +23,7 @@ import io.github.vinceglb.filekit.filesDir actual fun TakePhoto(onPhotoTaken: (PlatformFile?) -> Unit) { val context = LocalContext.current val takePhotoLauncher = rememberCameraPickerLauncher( + cameraFacing = FileKitCameraFacing.Back, openCameraSettings = FileKitOpenCameraSettings( authority = "${context.packageName}.fileprovider" ) @@ -34,10 +34,7 @@ actual fun TakePhoto(onPhotoTaken: (PlatformFile?) -> Unit) { Button( onClick = { val destinationFile = FileKit.filesDir / "photo_${System.currentTimeMillis()}.jpg" - takePhotoLauncher.launch( - destinationFile = destinationFile, - cameraFacing = FileKitCameraFacing.Front - ) + takePhotoLauncher.launch(destinationFile = destinationFile) } ) { Text("Take photo") diff --git a/samples/sample-compose/composeApp/src/iosMain/kotlin/App.ios.kt b/samples/sample-compose/composeApp/src/iosMain/kotlin/App.ios.kt index 7e4bc388..435e61e0 100644 --- a/samples/sample-compose/composeApp/src/iosMain/kotlin/App.ios.kt +++ b/samples/sample-compose/composeApp/src/iosMain/kotlin/App.ios.kt @@ -22,17 +22,15 @@ import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) @Composable actual fun TakePhoto(onPhotoTaken: (PlatformFile?) -> Unit) { - val takePhotoLauncher = rememberCameraPickerLauncher { - onPhotoTaken(it) - } + val takePhotoLauncher = rememberCameraPickerLauncher( + cameraFacing = FileKitCameraFacing.Back, + onResult = onPhotoTaken, + ) Button( onClick = { val destinationFile = FileKit.filesDir / "photo_${Clock.System.now().toEpochMilliseconds()}.jpg" - takePhotoLauncher.launch( - cameraFacing = FileKitCameraFacing.Front, - destinationFile = destinationFile, - ) + takePhotoLauncher.launch(destinationFile = destinationFile) } ) { Text("Take photo")