Fix IndexOutOfBounds crash when sending media with an undecodable preview (#7050)

* android, desktop: skip media with no decodable preview to fix IndexOutOfBounds on send

processPickedMedia appended to MediaPreview.content unconditionally for videos
(and size-ok animated images) but only appended to images when a preview bitmap
existed. getBitmapFromVideo returns a null preview for undecodable/corrupted
videos without throwing, desyncing the two lists; sendMessageAsync then indexes
images[index] past its end and crashes. Pair both appends behind a non-null
bitmap so the lists stay equal-length and index-aligned, and skip only the bad
item so the rest of the picked batch still sends.

A video skipped this way shows showVideoDecodingException(), guarded by
hasAlertsShown() so it neither stacks across multiple bad items nor duplicates
the alert already shown on getBitmapFromVideo's exception path. Image decode
failures are already surfaced earlier by getBitmapFromUri.

* docs: add plan justifying media preview alignment fix
This commit is contained in:
Narasimha-sc authored and GitHub committed 2026-06-17 19:22:34 +01:00
1 parent a4cfa2ae97
commit cf6a60882f
2 files changed
+119 -5

No files matched your search

@@ -330,7 +330,7 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
val imagesPreview = ArrayList<String>()
uris.forEach { uri ->
var bitmap: ImageBitmap?
when {
val uploadContent: UploadContent? = when {
isImage(uri) -> {
// Image
val drawable = getDrawableFromUri(uri)
@@ -340,16 +340,19 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
// It's a gif or webp
val fileSize = getFileSize(uri)
if (fileSize != null && fileSize <= maxFileSize) {
content.add(UploadContent.AnimatedImage(uri))
UploadContent.AnimatedImage(uri)
} else {
bitmap = null
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.large_file),
String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize))
)
null
}
} else if (bitmap != null) {
content.add(UploadContent.SimpleImage(uri))
UploadContent.SimpleImage(uri)
} else {
null
}
}
else -> {
@@ -357,11 +360,22 @@ suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text:
val res = getBitmapFromVideo(uri, withAlertOnException = true)
bitmap = res.preview
val durationMs = res.duration
content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0))
UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)
}
}
if (bitmap != null) {
// content and imagesPreview must stay index-aligned and equal-length: both consumers
// (ComposeImageView and sendMessageAsync) cross-index one list by the other's index.
// Only pair them when a preview bitmap exists; otherwise skip the media entirely.
if (bitmap != null && uploadContent != null) {
content.add(uploadContent)
imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000))
} else if (uploadContent is UploadContent.Video && !AlertManager.shared.hasAlertsShown()) {
// A corrupted/undecodable video can yield a null preview frame without throwing, so
// getBitmapFromVideo shows no alert. Skip it (other picked media still send) and tell
// the user instead of dropping it silently. hasAlertsShown guards against stacking the
// alert across multiple bad items and against duplicating the one already shown on the
// exception path. Image decode failures are already surfaced by getBitmapFromUri above.
showVideoDecodingException()
}
}
if (imagesPreview.isNotEmpty()) {