Image edition before sending: crop and rotate. (#6842)

* 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>

* Update design and UX

Update and add tests
Improve preview

* Update screenshots

* For quality issue and improve preview

* Update screenshots

* Remove default value of data class.

* Rename file.

* Fix tests.

* Allow detecting touch events outside the image by applying the drag detection to the parent node and offsetting the touch events

* Improve touch detection.

* Update screenshots

* Remove useless line.

---------

Co-authored-by: Gianluca Iavicoli <gianluca.iavicoli04@gmail.com>
Co-authored-by: ElementBot <android@element.io>
Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
Benoit Marty 2026-05-26 10:07:05 +02:00 committed by GitHub
commit 6fb79d6e06
35 changed files with 2030 additions and 90 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
}
}
}
}