Add crop and rotate editing before sending images (#6363)

* feat(messages): add crop and rotate before image upload

* Update screenshots

* chore: trigger CI after screenshot update

* fix: resolve detekt violations in image editor and media viewer modules

* fix: require explicit edits param, use plurals for rotation a11y, remove redundant @Inject

* fix: require explicit edits param, use plurals for rotation a11y, remove redundant @Inject

* fix: use semantically correct RotateRight icon for image rotation action

* Update screenshots

* chore: trigger CI after screenshot update

---------

Co-authored-by: ElementBot <android@element.io>
Co-authored-by: Benoit Marty <benoitm@element.io>
This commit is contained in:
Gianluca Iavicoli 2026-05-21 15:08:26 +02:00 committed by Benoit Marty
parent 00efd9a01c
commit bcad1f9dce
17 changed files with 1517 additions and 49 deletions

View file

@ -9,7 +9,9 @@
package io.element.android.libraries.mediaviewer.impl.local
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.core.net.toUri
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
@ -17,6 +19,7 @@ import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.getFileSize
import io.element.android.libraries.androidutils.file.getMimeType
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.UserId
@ -85,8 +88,12 @@ class AndroidLocalMediaFactory(
waveform: List<Float>?,
duration: String?,
): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: ""
val resolvedMimeType = resolveMimeType(
uri = uri,
mimeType = mimeType,
fileName = fileName,
)
val fileSize = context.getFileSize(uri)
val calculatedFormattedFileSize = formattedFileSize ?: fileSizeFormatter.format(fileSize)
val fileExtension = fileExtensionExtractor.extractFromName(fileName)
@ -110,4 +117,36 @@ class AndroidLocalMediaFactory(
)
)
}
private fun resolveMimeType(
uri: Uri,
mimeType: String?,
fileName: String,
): String {
val explicitMimeType = mimeType.takeUnless { it.isNullOrBlank() || it == MimeTypes.OctetStream }
if (explicitMimeType != null) return explicitMimeType
val resolverMimeType = context.getMimeType(uri).takeUnless { it.isNullOrBlank() || it == MimeTypes.OctetStream }
if (resolverMimeType != null) return resolverMimeType
val decodedImageMimeType = decodeImageMimeType(uri)
if (decodedImageMimeType != null) return decodedImageMimeType
val extensionMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
fileExtensionExtractor.extractFromName(fileName)
)
if (!extensionMimeType.isNullOrBlank()) return extensionMimeType
return MimeTypes.OctetStream
}
private fun decodeImageMimeType(uri: Uri): String? {
return tryOrNull {
context.contentResolver.openInputStream(uri)?.use { inputStream ->
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeStream(inputStream, null, options)
options.outMimeType
}
}
}
}