Merge develop into the wildcard mime type branch

Develop gained resolveImageMimeType, which reads the image header so a
share that declares a broad type is encoded as the format it really is.
Resolving the wildcard first would hand that helper a concrete image/jpeg
and make it return early, so a transparent PNG shared as image/* would be
flattened and an animated GIF would be recompressed. The header is now
read from the type the sender declared and the default subtype is applied
to whatever comes back, so the sniffing still runs and an undecodable file
no longer keeps a wildcard.

A non-image shared as image/* is therefore sent as image/jpeg rather than
image/*, which is the whole point of this branch, so the test covering
that case now expects the default subtype.
This commit is contained in:
hayaksi1
2026-08-20 14:26:57 +03:00
418 changed files with 6374 additions and 4183 deletions
@@ -63,10 +63,10 @@ class AndroidMediaPreProcessor(
/**
* Used for calculating `inSampleSize` for bitmaps.
*
* *Note*: Ideally, this should result in images of up to (but not included) 1280x1280 being sent. However, images with very different width and height
* *Note*: Ideally, this should result in images of up to (but not included) 2560x2560 being sent. However, images with very different width and height
* values may surpass this limit. (i.e.: an image of `480x3000px` would have `inSampleSize=1` and be sent as is).
*/
private const val IMAGE_SCALE_REF_SIZE = 640
private const val IMAGE_SCALE_REF_SIZE = 1280
private val notCompressibleImageTypes = listOf(MimeTypes.Gif, MimeTypes.WebP)
}
@@ -87,8 +87,9 @@ class AndroidMediaPreProcessor(
val result = when {
resolvedMimeType == MimeTypes.Svg -> processSvgImage(uri, resolvedMimeType)
resolvedMimeType.isMimeTypeImage() -> {
val shouldBeCompressed = mediaOptimizationConfig.compressImages && resolvedMimeType !in notCompressibleImageTypes
processImage(uri, resolvedMimeType, shouldBeCompressed)
val imageMimeType = resolveImageMimeType(uri, mimeType).ensureDefaultSubtype()
val shouldBeCompressed = mediaOptimizationConfig.compressImages && imageMimeType !in notCompressibleImageTypes
processImage(uri, imageMimeType, shouldBeCompressed)
}
resolvedMimeType.isMimeTypeVideo() -> processVideo(uri, resolvedMimeType, mediaOptimizationConfig.videoCompressionPreset)
resolvedMimeType.isMimeTypeAudio() -> processAudio(uri, resolvedMimeType)
@@ -157,6 +158,19 @@ class AndroidMediaPreProcessor(
}
}
/**
* The declared mime type can be broader than the image actually is - a wildcard from a share intent, or a mislabelled file - and the encoder is picked
* from it. Re-encoding a transparent image as JPEG flattens its alpha channel to black, so resolve the real format from the image header first.
*/
private fun resolveImageMimeType(uri: Uri, declaredMimeType: String): String {
if (declaredMimeType == MimeTypes.Png || declaredMimeType == MimeTypes.Jpeg) return declaredMimeType
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
tryOrNull {
contentResolver.openInputStream(uri)?.use { BitmapFactory.decodeStream(it, null, options) }
}
return options.outMimeType ?: declaredMimeType
}
private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo {
Timber.d("Processing image ${uri.path.orEmpty().hash()}")
@@ -82,9 +82,9 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
private suspend fun TestScope.process(
asset: Asset,
mediaOptimizationConfig: MediaOptimizationConfig,
mimeType: String = asset.mimeType,
sdkIntVersion: Int = Build.VERSION_CODES.P,
deleteOriginal: Boolean = false,
mimeType: String = asset.mimeType,
): MediaUploadInfo {
val context = InstrumentationRegistry.getInstrumentation().context
val deleteCallback = lambdaRecorder<Uri?, Unit> {}
@@ -106,6 +106,52 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
return data
}
@Test
fun `a png shared with a wildcard mime type is still encoded as a png`() = runTest {
val mediaUploadInfo = process(
asset = assetImagePng,
mimeType = MimeTypes.Images,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
sdkIntVersion = Build.VERSION_CODES.Q,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Png)
}
@Test
fun `a gif shared with a wildcard mime type is recognised and left uncompressed`() = runTest {
val mediaUploadInfo = process(
asset = assetAnimatedGif,
mimeType = MimeTypes.Images,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
sdkIntVersion = Build.VERSION_CODES.Q,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Gif)
assertThat(info.imageInfo.size).isEqualTo(assetAnimatedGif.size)
}
@Test
fun `a file that is not an image falls back to the default subtype of the wildcard it was shared with`() = runTest {
val mediaUploadInfo = process(
asset = assetText,
mimeType = MimeTypes.Images,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
sdkIntVersion = Build.VERSION_CODES.Q,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Jpeg)
}
@Test
@Ignore("Ignore now that min API for enterprise is 33")
fun `test processing png`() = runTest {
@@ -223,10 +269,10 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
assertThat(info.thumbnailFile).isNotNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 979,
width = 3006,
height = 1958,
width = 6012,
mimetype = MimeTypes.Jpeg,
size = 84_845,
size = 682_492,
ThumbnailInfo(height = 244, width = 751, mimetype = MimeTypes.Jpeg, size = 7_178),
thumbnailSource = null,
blurhash = "K07gBzX=j_D4xZjoaSe,s:"
@@ -248,10 +294,10 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
assertThat(info.thumbnailFile).isNull()
assertThat(info.imageInfo).isEqualTo(
ImageInfo(
height = 979,
width = 3_006,
height = 1_958,
width = 6_012,
mimetype = MimeTypes.Jpeg,
size = 84_845,
size = 682_492,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,