Wavesonics / compose-multiplatform-file-picker

A multiplatform compose widget for picking files
MIT License
346 stars 21 forks source link

Exception on Android #107

Open joreilly opened 8 months ago

joreilly commented 8 months ago

I have code that's working fine in other Compose clients but on Android I'm getting following when calling getFileByteArray

01-14 19:37:45.657 22788 22788 E AndroidRuntime: java.lang.IllegalArgumentException: Uri lacks 'file' scheme: content://com.android.providers.media.documents/document/image%3A1000050843
01-14 19:37:45.657 22788 22788 E AndroidRuntime:    at androidx.core.net.UriKt.toFile(Uri.kt:43)
01-14 19:37:45.657 22788 22788 E AndroidRuntime:    at com.darkrockstudios.libraries.mpfilepicker.AndroidFile.getFileByteArray(AndroidFilePicker.kt:15)

following is code I have

    val coroutineScope = rememberCoroutineScope()

    val fileExtensions = listOf("jpg", "png")
    FilePicker(show = show, fileExtensions = fileExtensions) { file ->
        coroutineScope.launch {
            val data = file?.getFileByteArray()
            data?.let {
                ....
            }
        }
    }
lusc8520 commented 7 months ago

I had the same problem and found a weird workaround (inspired by https://github.com/Wavesonics/compose-multiplatform-file-picker/pull/104)

I have a global variable in commainMain to store the byte array (this is not necessary, only if you want to do more than just displaying the picked image):

// this variable is set from "outside" in the jvmMain and androidMain
var imageBytes : ByteArray = byteArrayOf()

this is my commonMain part:

var showFilePicker by remember { mutableStateOf(false) }
        colorButton(onClick = {showFilePicker = true}, text = "Choose Image")
        var showImage by remember {mutableStateOf(false)}
        val fileType = listOf("jpg", "png")
        var file by remember { mutableStateOf<MPFile<Any>?>(null) }
        FilePicker(
            show = showFilePicker,
            fileExtensions = fileType,
            onFileSelected = {
                showFilePicker = false
                scope.launch {
                    file = it
                    showImage = true
                }
            }
        )
        if (showImage) {
            ImageFromFile(file)
        }

then in commonMain i have this expect function declared:

@Composable
expect fun ImageFromFile(file: MPFile<Any>?)

The actual implementation in androidMain:

@Composable
actual fun ImageFromFile(file: MPFile<Any>?) {
    var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
    if (file != null) {
        val uri = Uri.parse(file.path)
        val stream = LocalContext.current.contentResolver.openInputStream(uri)
        if (stream != null) {
            val bytes = stream.readBytes()
            imageBytes = bytes // sets the global byteArray variable in commonMain
            stream.close()
            if (bytes.isNotEmpty()) {
                imageBitmap.prepareToDraw()
                imageBitmap = BitmapFactory.decodeByteArray(
                    bytes,
                    0,
                    bytes.size
                ).asImageBitmap()
            }
        }
    }
    Image(
        bitmap = imageBitmap,
        ""
    )
}

actual implementation in jvmMain:

@Composable
actual fun ImageFromFile(file: MPFile<Any>?) {
    var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
    val scope = rememberCoroutineScope()
    scope.launch {
        if (file == null) return@launch
        imageBitmap.prepareToDraw()
        imageBytes = file.getFileByteArray() // sets the global byteArray variable in commonMain
        imageBitmap = Image.makeFromEncoded(file.getFileByteArray()).toComposeImageBitmap()
    }
    Image(
        bitmap = imageBitmap,
        ""
    )
}

Furthermore, I have these functions for displaying an Image from just a byte Array:

// commonMain expect function
@Composable
expect fun ImageFromByteArray(byteArray: ByteArray, modifier:Modifier = Modifier, scale: ContentScale = ContentScale.Fit)

// androidMain actual function
@Composable
actual fun ImageFromByteArray(byteArray: ByteArray, modifier: Modifier, scale: ContentScale) {
    var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
    imageBitmap = BitmapFactory.decodeByteArray(byteArray,
        0,
        byteArray.size).asImageBitmap()
    Image(
        modifier = modifier,
        bitmap = imageBitmap,
        contentDescription = "",
        contentScale = scale
    )
}

// jvmMain actual function
@Composable
actual fun ImageFromByteArray(byteArray: ByteArray, modifier: Modifier, scale: ContentScale) {
    val scope = rememberCoroutineScope()
    var imageBitmap by remember { mutableStateOf(ImageBitmap(height = 1, width = 1)) }
    scope.launch {
        imageBitmap.prepareToDraw()
        imageBitmap = Image.makeFromEncoded(byteArray).toComposeImageBitmap()
    }
    Image(
        modifier = modifier,
        bitmap = imageBitmap,
        contentDescription = "",
        contentScale = scale
    )
}
joreilly commented 7 months ago

Thanks @lusc8520 ....using adapted version of that now in https://github.com/joreilly/GeminiKMP/blob/main/composeApp/src/androidMain/kotlin/actual.kt

Shahriyar13 commented 7 months ago

Could you try this?: https://stackoverflow.com/a/8370299/9133703

randyheaton commented 6 months ago

While folks are waiting on that pr, here's some of the guts of it worked as an extension function so that you can use the library as is. This just steals the main punchline from vinceglb's pr of using contentResolver to turn a Uri into a ByteArray. Assumes the original works correctly in iOS. Handling the non-null assertion and supplying the Android context is dealt with elsewhere.

Common: expect suspend fun MPFile<Any>.getBytes(): ByteArray

iOS: actual suspend fun MPFile<Any>.getBytes(): ByteArray { return this.getFileByteArray() }

Android: actual suspend fun MPFile<Any>.getBytes(): ByteArray { return AndroidApplication.getApplicationContext().contentResolver.openInputStream(this.platformFile as Uri).use { stream -> stream!!.readBytes() } }

c4software commented 6 months ago

Thanks @randyheaton its works with your workaround.

@vinceglb Did you know if the PR is mergeable ?

vinceglb commented 6 months ago

@c4software I sent a message to other maintainers, I'll let you know as soon as I have any news 👍

vinceglb commented 6 months ago

@c4software I discuss with Wavesonics, it supposed to have a reviewer before merging. So, I'm waiting to a maintainer to review it. If it takes too long, I'll check with Wavesonics how to proceed.

c4software commented 6 months ago

Thanks for you feedback 👍