mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 12:07:28 +00:00
Send a concrete mime type for media shared with a wildcard one
Some apps share an image with the mime type image/* rather than a concrete one. That value was carried straight into the uploaded event, so the message reached the room with an unusable media type and bridges rejected it as unsupported. Resolve a wildcard subtype to a default for its top level type before processing the upload, reusing the mapping the download path already applies so the SDK can pick the right extension.
This commit is contained in:
+13
@@ -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?.withDefaultSubtype(): String = when {
|
||||
this == null -> OctetStream
|
||||
hasSubtype(this) -> this
|
||||
this == Images -> Jpeg
|
||||
this == Videos -> Mp4
|
||||
this == Audio -> Mp3
|
||||
else -> OctetStream
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -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.withDefaultSubtype
|
||||
import org.junit.Test
|
||||
|
||||
class MimeTypesTest {
|
||||
@Test
|
||||
fun `withDefaultSubtype keeps a mime type that already has a subtype`() {
|
||||
assertThat(MimeTypes.Png.withDefaultSubtype()).isEqualTo(MimeTypes.Png)
|
||||
assertThat(MimeTypes.Mp4.withDefaultSubtype()).isEqualTo(MimeTypes.Mp4)
|
||||
assertThat(MimeTypes.Pdf.withDefaultSubtype()).isEqualTo(MimeTypes.Pdf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `withDefaultSubtype replaces a wildcard subtype with a concrete one`() {
|
||||
assertThat(MimeTypes.Images.withDefaultSubtype()).isEqualTo(MimeTypes.Jpeg)
|
||||
assertThat(MimeTypes.Videos.withDefaultSubtype()).isEqualTo(MimeTypes.Mp4)
|
||||
assertThat(MimeTypes.Audio.withDefaultSubtype()).isEqualTo(MimeTypes.Mp3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `withDefaultSubtype falls back to octet stream`() {
|
||||
assertThat(MimeTypes.Any.withDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
|
||||
assertThat("text/*".withDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
|
||||
assertThat(null.withDefaultSubtype()).isEqualTo(MimeTypes.OctetStream)
|
||||
}
|
||||
}
|
||||
+3
-10
@@ -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.withDefaultSubtype
|
||||
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.withDefaultSubtype(),
|
||||
useCache = useCache,
|
||||
tempDir = cacheDirectory.path,
|
||||
)
|
||||
|
||||
+9
-7
@@ -29,6 +29,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
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
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes.withDefaultSubtype
|
||||
import io.element.android.libraries.di.annotations.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
import io.element.android.libraries.matrix.api.media.FileInfo
|
||||
@@ -82,15 +83,16 @@ class AndroidMediaPreProcessor(
|
||||
mediaOptimizationConfig: MediaOptimizationConfig,
|
||||
): Result<MediaUploadInfo> = withContext(coroutineDispatchers.computation) {
|
||||
runCatchingExceptions {
|
||||
val resolvedMimeType = mimeType.withDefaultSubtype()
|
||||
val result = when {
|
||||
mimeType == MimeTypes.Svg -> processSvgImage(uri, mimeType)
|
||||
mimeType.isMimeTypeImage() -> {
|
||||
val shouldBeCompressed = mediaOptimizationConfig.compressImages && mimeType !in notCompressibleImageTypes
|
||||
processImage(uri, mimeType, shouldBeCompressed)
|
||||
resolvedMimeType == MimeTypes.Svg -> processSvgImage(uri, resolvedMimeType)
|
||||
resolvedMimeType.isMimeTypeImage() -> {
|
||||
val shouldBeCompressed = mediaOptimizationConfig.compressImages && resolvedMimeType !in notCompressibleImageTypes
|
||||
processImage(uri, resolvedMimeType, 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 {
|
||||
|
||||
+16
-1
@@ -65,9 +65,24 @@ 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,
|
||||
mimeType: String = asset.mimeType,
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.P,
|
||||
deleteOriginal: Boolean = false,
|
||||
): MediaUploadInfo {
|
||||
@@ -81,7 +96,7 @@ class AndroidMediaPreProcessorTest : RobolectricTest() {
|
||||
val file = getFileFromAssets(context, asset.filename)
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = asset.mimeType,
|
||||
mimeType = mimeType,
|
||||
deleteOriginal = deleteOriginal,
|
||||
mediaOptimizationConfig = mediaOptimizationConfig,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user