compose-multiplatform-file-picker icon indicating copy to clipboard operation
compose-multiplatform-file-picker copied to clipboard

Exception on Android

Open joreilly opened this issue 1 year ago • 8 comments

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 {
                ....
            }
        }
    }

joreilly avatar Jan 14 '24 19:01 joreilly

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
    )
}

lusc8520 avatar Jan 17 '24 19:01 lusc8520

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

joreilly avatar Jan 20 '24 18:01 joreilly

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

Shahriyar13 avatar Feb 07 '24 07:02 Shahriyar13

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() } }

randyheaton avatar Feb 20 '24 04:02 randyheaton

Thanks @randyheaton its works with your workaround.

@vinceglb Did you know if the PR is mergeable ?

c4software avatar Mar 04 '24 20:03 c4software

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

vinceglb avatar Mar 05 '24 13:03 vinceglb

@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.

vinceglb avatar Mar 07 '24 20:03 vinceglb

Thanks for you feedback 👍

c4software avatar Mar 07 '24 21:03 c4software