Merge pull request #7449 from hayaksi1/fix/4112-wildcard-mime-type

Send a concrete mime type for media shared with a wildcard one
This commit is contained in:
Benoit Marty
2026-08-24 09:41:18 +02:00
committed by GitHub
5 changed files with 75 additions and 18 deletions
@@ -63,4 +63,17 @@ object MimeTypes {
val subType = components.last()
return subType.isNotBlank() && subType != "*"
}
/**
* A mime type with a wildcard subtype is a filter, not a media type, so replace it with a
* concrete default for its top level type.
*/
fun String?.ensureDefaultSubtype(): String = when {
this == null -> OctetStream
hasSubtype(this) -> this
this == Images -> Jpeg
this == Videos -> Mp4
this == Audio -> Mp3
else -> OctetStream
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.core.mimetype
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype
import org.junit.Test
class MimeTypesTest {
@Test
fun `ensureDefaultSubtype keeps a mime type that already has a subtype`() {
assertThat(MimeTypes.Png.ensureDefaultSubtype()).isEqualTo(MimeTypes.Png)
assertThat(MimeTypes.Mp4.ensureDefaultSubtype()).isEqualTo(MimeTypes.Mp4)
assertThat(MimeTypes.Pdf.ensureDefaultSubtype()).isEqualTo(MimeTypes.Pdf)
}
@Test
fun `ensureDefaultSubtype replaces a wildcard subtype with a concrete one`() {
assertThat(MimeTypes.Images.ensureDefaultSubtype()).isEqualTo(MimeTypes.Jpeg)
assertThat(MimeTypes.Videos.ensureDefaultSubtype()).isEqualTo(MimeTypes.Mp4)
assertThat(MimeTypes.Audio.ensureDefaultSubtype()).isEqualTo(MimeTypes.Mp3)
}
@Test
fun `ensureDefaultSubtype falls back to octet stream`() {
assertThat(MimeTypes.Any.ensureDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
assertThat("text/*".ensureDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
assertThat(null.ensureDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
}
}
@@ -11,7 +11,7 @@ package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MediaFile
import io.element.android.libraries.matrix.api.media.MediaSource
@@ -71,15 +71,8 @@ class RustMediaLoader(
val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource,
filename = filename,
mimeType = when {
mimeType == null -> MimeTypes.OctetStream
MimeTypes.hasSubtype(mimeType) -> mimeType
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
mimeType == MimeTypes.Images -> MimeTypes.Jpeg
mimeType == MimeTypes.Videos -> MimeTypes.Mp4
mimeType == MimeTypes.Audio -> MimeTypes.Mp3
else -> MimeTypes.OctetStream
},
// Fallback to a default mime type based on the main type, so that the SDK can create a file with the correct extension.
mimeType = mimeType.ensureDefaultSubtype(),
useCache = useCache,
tempDir = cacheDirectory.path,
)
@@ -26,6 +26,7 @@ import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.ensureDefaultSubtype
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
@@ -82,16 +83,17 @@ class AndroidMediaPreProcessor(
mediaOptimizationConfig: MediaOptimizationConfig,
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
runCatchingExceptions {
val resolvedMimeType = mimeType.ensureDefaultSubtype()
val result = when {
mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType)
mimeType.isMimeTypeImage() -> {
val imageMimeType = resolveImageMimeType(uri, mimeType)
resolvedMimeType == MimeTypes.Svg -> processSvgImage(uri, resolvedMimeType)
resolvedMimeType.isMimeTypeImage() -> {
val imageMimeType = resolveImageMimeType(uri, mimeType).ensureDefaultSubtype()
val shouldBeCompressed = mediaOptimizationConfig.compressImages && imageMimeType !in notCompressibleImageTypes
processImage(uri, imageMimeType, shouldBeCompressed)
}
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, mediaOptimizationConfig.videoCompressionPreset)
mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType)
else -> processFile(uri, mimeType)
resolvedMimeType.isMimeTypeVideo() -> processVideo(uri, resolvedMimeType, mediaOptimizationConfig.videoCompressionPreset)
resolvedMimeType.isMimeTypeAudio() -> processAudio(uri, resolvedMimeType)
else -> processFile(uri, resolvedMimeType)
}
if (deleteOriginal) {
tryOrNull {
@@ -65,6 +65,20 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
assertThat(height).isEqualTo(2268)
}
@Test
fun `test processing an image shared with a wildcard mime type`() = runTest {
val mediaUploadInfo = process(
asset = assetImageJpeg,
mimeType = MimeTypes.Images,
mediaOptimizationConfig = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
),
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Jpeg)
}
private suspend fun TestScope.process(
asset: Asset,
mediaOptimizationConfig: MediaOptimizationConfig,
@@ -124,7 +138,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
}
@Test
fun `a file that is not an image keeps the wildcard mime type it was shared with`() = runTest {
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,
@@ -135,7 +149,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
sdkIntVersion = Build.VERSION_CODES.Q,
)
val info = mediaUploadInfo as MediaUploadInfo.Image
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Images)
assertThat(info.imageInfo.mimetype).isEqualTo(MimeTypes.Jpeg)
}
@Test