coil-kt / coil

Image loading for Android and Compose Multiplatform.
https://coil-kt.github.io/coil/
Apache License 2.0
10.87k stars 668 forks source link

[Coil 3] Frequent `ImageDecoder$DecodeException: Input was incomplete.` when displaying an Uri from the Android PhotoPicker in Compose Multiplatform. #2434

Closed kihaki closed 3 months ago

kihaki commented 3 months ago

Describe the bug I implemented an expect/actual wrapper for the Android PhotoPicker and use it to pick photos which reside in the google cloud.

I am returning the Uri of the picked images as a String and load them in coil for compose multiplatform like the following:

val uriFromPhotoPicker: String = /** This is the uri returned from the native android photo picker as String **/
AsyncImage(
  model = uriFromPhotoPicker,
  contentDescription = null,
)

This works sometimes, e.g. some of the photos picked are displayed fine, some of them produce the error below. I have not found a pattern to this yet, as all photos picked reside in the google cloud (the photo picker downloads them before returning them to my app). Subjectively it feels like about 40% success, 60% failure rate, although I have not measured this yet. To date I have never had this issue when developing a completely native Android App with Jetpack Compose and Coil (2.x.x)

I have granted persisted media access to all returned uris as described here.

The uris that work and those that don't work also have the same pattern, here is one that works: content://media/picker/0/com.google.android.apps.photos.cloudpicker/media/05ad89e2-eefe-4cb4-b1fe-d725f0984415-1_all_48803

Here is one that produces the error below: content://media/picker/0/com.google.android.apps.photos.cloudpicker/media/05ad89e2-eefe-4cb4-b1fe-d725f0984415-1_all_48822

The images that work and don't work are from the exact same source (iPhone photo capture HEIC, uploaded into the Google Fotos app and then used on an Android Phone, through the Google Photos cloud, probably converted on the way somewhere).

Logs/Screenshots

android.graphics.ImageDecoder$DecodeException: Input was incomplete.
    at android.graphics.ImageDecoder.onPartialImage(ImageDecoder.java:2086)
    at android.graphics.ImageDecoder.nDecodeBitmap(Native Method)
    at android.graphics.ImageDecoder.decodeBitmapInternal(ImageDecoder.java:1674)
    at android.graphics.ImageDecoder.decodeBitmapImpl(ImageDecoder.java:1863)
    at android.graphics.ImageDecoder.decodeBitmap(ImageDecoder.java:1848)
    at coil3.decode.StaticImageDecoder.decode(StaticImageDecoder.kt:143)
    at coil3.intercept.EngineInterceptor.decode(EngineInterceptor.kt:191)
    at coil3.intercept.EngineInterceptor.access$decode(EngineInterceptor.kt:32)
    at coil3.intercept.EngineInterceptor$execute$executeResult$1.invokeSuspend(EngineInterceptor.kt:121)
    at coil3.intercept.EngineInterceptor$execute$executeResult$1.invoke(Unknown Source:8)
    at coil3.intercept.EngineInterceptor$execute$executeResult$1.invoke(Unknown Source:4)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:61)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:163)
    at kotlinx.coroutines.BuildersKt.withContext(Unknown Source:1)
    at coil3.intercept.EngineInterceptor.execute(EngineInterceptor.kt:120)
    at coil3.intercept.EngineInterceptor.access$execute(EngineInterceptor.kt:32)
    at coil3.intercept.EngineInterceptor$intercept$2.invokeSuspend(EngineInterceptor.kt:67)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)

Version My coil version is 3.0.0-alpha09, the artifacts are io.coil-kt.coil3:coil, io.coil-kt.coil3:coil-compose and io.coil-kt.coil3:coil-network-ktor2

kihaki commented 3 months ago

I have just tested the same images on Coil 2.x and Jetpack Compose in an Android Native app and the images show fine, no DecodeException - if that helps

colinrtwhite commented 3 months ago

If you set ExifOrientationPolicy.IGNORE in ImageLoader.Builder does it fix the issue? Setting that will force ImageLoader to use BitmapFactory internally instead of ImageDecoder and this sounds like it could be an underlying issue with ImageDecoder unfortunately. What Android version are you testing on?

revonateB0T commented 3 months ago

I suppose this is because https://github.com/coil-kt/coil/pull/1990, but I can't reproduce it. If ContentProvider provide a AssetFileDescriptor that inner FileDescriptor doesn't support lseek, then we cannot share that AFD. Literally we need to do a fallback for AFDs that doesn't support seek(i.e. fallback to not share, use URI directly)

val afd = metadata.assetFileDescriptor
val imageSource = try {
    Os.lseek(afd.fileDescriptor, afd.startOffset, SEEK_SET)
    ImageDecoder.createSource { metadata.assetFileDescriptor }
} catch (e: ErrnoException) {
    ImageDecoder.createSource(options.context.contentResolver, metadata.uri.toAndroidUri())
}

See https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/graphics/java/android/graphics/ImageDecoder.java;drc=18311bd177e42e4a29ac81d4897ddfe6ded5b96c;l=377 IDK if this helps.

Actually ContentProvider have no reason to provide a fd not support seek, fd that normally backed by file on disk always supports seek.

Edit: https://github.com/coil-kt/coil/pull/1990 should not lead to this. Error message is incomplete source but AFDs that doesn't support lseek will only lead to truncated header(i.e. malformed data) https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/graphics/java/android/graphics/ImageDecoder.java#727

revonateB0T commented 3 months ago

IMO currently workaround could be forbid StaticImageDecoder create sources from certain blacklist(com.google.android.apps.photos.cloudpicker) of ContentProvider. As mostly DocumentsProvider is the common ContentProvider and this should be the optimal path.

revonateB0T commented 3 months ago

So I mean, If you can still reproduce that, you try ImageDecoder.createSource from that URI, and decode that source, if you still got "Incomplete input", then we know it's either a ImageDecoder internal bug or "Google Photos" bug and we need to fallback to BitmapFactory for "Google Photo"'s ContentProvider, if this always success(i.e. call ImageDecoder.createSource and decode manually), then it must be caused by https://github.com/coil-kt/coil/pull/1990 and we need https://github.com/coil-kt/coil/issues/2434#issuecomment-2293577105 as solution.

colinrtwhite commented 3 months ago

@revonateB0T Thanks for taking a look! I don't think we can effectively maintain a blocklist for cases like this as it will very tough to maintain. Is there any way to detect if file descriptor supports lseek? We can fall back to BitmapFactoryDecoder if it doesn't support it.

revonateB0T commented 3 months ago

@revonateB0T Thanks for taking a look! I don't think we can effectively maintain a blocklist for cases like this as it will very tough to maintain. Is there any way to detect if file descriptor supports lseek? We can fall back to BitmapFactoryDecoder if it doesn't support it.

Yeah! just try lseek on that fd.

val afd = metadata.assetFileDescriptor
val imageSource = try {
    Os.lseek(afd.fileDescriptor, afd.startOffset, SEEK_SET)
    ImageDecoder.createSource { metadata.assetFileDescriptor }
} catch (e: ErrnoException) {
    ImageDecoder.createSource(options.context.contentResolver, metadata.uri.toAndroidUri())
}
kihaki commented 2 months ago

I just got back to this and looks like it was solved in the meantime, so I will just say you guys are amazing