Media: keep the name of the file when possible
This commit is contained in:
parent
c8c2cb8ff3
commit
4c19bd3644
10 changed files with 90 additions and 31 deletions
|
|
@ -25,5 +25,5 @@ import kotlinx.parcelize.Parcelize
|
||||||
sealed interface Attachment : Parcelable {
|
sealed interface Attachment : Parcelable {
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class Media(val localMedia: LocalMedia) : Attachment
|
data class Media(val localMedia: LocalMedia, val compressIfPossible: Boolean) : Attachment
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.attachments.preview
|
package io.element.android.features.messages.impl.attachments.preview
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -73,8 +72,7 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
when (attachment) {
|
when (attachment) {
|
||||||
is Attachment.Media -> {
|
is Attachment.Media -> {
|
||||||
sendMedia(
|
sendMedia(
|
||||||
uri = attachment.localMedia.uri,
|
mediaAttachment = attachment,
|
||||||
mimeType = attachment.localMedia.mimeType,
|
|
||||||
sendActionState = sendActionState
|
sendActionState = sendActionState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -82,12 +80,11 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun sendMedia(
|
private suspend fun sendMedia(
|
||||||
uri: Uri,
|
mediaAttachment: Attachment.Media,
|
||||||
mimeType: String,
|
|
||||||
sendActionState: MutableState<Async<Unit>>,
|
sendActionState: MutableState<Async<Unit>>,
|
||||||
) {
|
) {
|
||||||
suspend {
|
suspend {
|
||||||
mediaSender.sendMedia(uri, mimeType)
|
mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.mimeType, mediaAttachment.compressIfPossible)
|
||||||
}.executeResult(sendActionState)
|
}.executeResult(sendActionState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
|
||||||
fun anAttachmentsPreviewState(sendActionState: Async<Unit> = Async.Uninitialized) = AttachmentsPreviewState(
|
fun anAttachmentsPreviewState(sendActionState: Async<Unit> = Async.Uninitialized) = AttachmentsPreviewState(
|
||||||
attachment = Attachment.Media(
|
attachment = Attachment.Media(
|
||||||
localMedia = LocalMedia("".toUri(), mimeType = MimeTypes.OctetStream),
|
localMedia = LocalMedia("".toUri(), mimeType = MimeTypes.OctetStream),
|
||||||
|
compressIfPossible = true
|
||||||
),
|
),
|
||||||
sendActionState = sendActionState,
|
sendActionState = sendActionState,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
|
|
|
||||||
|
|
@ -70,16 +70,16 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mutableStateOf<AttachmentsState>(AttachmentsState.None)
|
mutableStateOf<AttachmentsState>(AttachmentsState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handlePickedMedia(uri: Uri?, mimeType: String? = null) {
|
fun handlePickedMedia(uri: Uri?, mimeType: String? = null, compressIfPossible: Boolean = true) {
|
||||||
val localMedia = localMediaFactory.createFromUri(uri, mimeType)
|
val localMedia = localMediaFactory.createFromUri(uri, mimeType)
|
||||||
attachmentsState.value = if (localMedia == null) {
|
attachmentsState.value = if (localMedia == null) {
|
||||||
AttachmentsState.None
|
AttachmentsState.None
|
||||||
} else {
|
} else {
|
||||||
val mediaAttachment = Attachment.Media(localMedia)
|
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
|
||||||
val isPreviewable = when {
|
val isPreviewable = when {
|
||||||
MimeTypes.isImage(mimeType) -> true
|
MimeTypes.isImage(localMedia.mimeType) -> true
|
||||||
MimeTypes.isVideo(mimeType) -> true
|
MimeTypes.isVideo(localMedia.mimeType) -> true
|
||||||
MimeTypes.isAudio(mimeType) -> true
|
MimeTypes.isAudio(localMedia.mimeType) -> true
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
if (isPreviewable) {
|
if (isPreviewable) {
|
||||||
|
|
@ -93,7 +93,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType ->
|
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker(onResult = { uri, mimeType ->
|
||||||
handlePickedMedia(uri, mimeType)
|
handlePickedMedia(uri, mimeType)
|
||||||
})
|
})
|
||||||
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes, onResult = { handlePickedMedia(it) })
|
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes, onResult = { handlePickedMedia(it, compressIfPossible = false) })
|
||||||
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) })
|
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(onResult = { handlePickedMedia(it, MimeTypes.IMAGE_JPEG) })
|
||||||
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) })
|
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker(onResult = { handlePickedMedia(it, MimeTypes.VIDEO_MP4) })
|
||||||
|
|
||||||
|
|
@ -221,7 +221,7 @@ class MessageComposerPresenter @Inject constructor(
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
attachmentState: MutableState<AttachmentsState>,
|
attachmentState: MutableState<AttachmentsState>,
|
||||||
) {
|
) {
|
||||||
mediaSender.sendMedia(uri, mimeType)
|
mediaSender.sendMedia(uri, mimeType, compressIfPossible = false)
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
attachmentState.value = AttachmentsState.None
|
attachmentState.value = AttachmentsState.None
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.androidutils.file
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun Context.getFileName(uri: Uri): String? = when (uri.scheme) {
|
||||||
|
ContentResolver.SCHEME_CONTENT -> getContentFileName(uri)
|
||||||
|
else -> uri.path?.let(::File)?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Context.getContentFileName(uri: Uri): String? = runCatching {
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
return@use cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME).let(cursor::getString)
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
|
@ -18,8 +18,6 @@ package io.element.android.libraries.androidutils.file
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -37,7 +35,7 @@ fun File.safeDelete() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File = withContext(Dispatchers.IO) {
|
fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File {
|
||||||
val suffix = extension?.let { ".$extension" }
|
val suffix = extension?.let { ".$extension" }
|
||||||
File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }
|
return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ interface MediaPreProcessor {
|
||||||
suspend fun process(
|
suspend fun process(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
deleteOriginal: Boolean = false
|
deleteOriginal: Boolean = false,
|
||||||
|
compressIfPossible: Boolean
|
||||||
): Result<MediaUploadInfo>
|
): Result<MediaUploadInfo>
|
||||||
|
|
||||||
data class Failure(override val cause: Throwable?) : RuntimeException(cause)
|
data class Failure(override val cause: Throwable?) : RuntimeException(cause)
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,14 @@ class MediaSender @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun sendMedia(uri: Uri, mimeType: String): Result<Unit> {
|
suspend fun sendMedia(uri: Uri, mimeType: String, compressIfPossible: Boolean): Result<Unit> {
|
||||||
return preProcessor
|
return preProcessor
|
||||||
.process(uri, mimeType, deleteOriginal = true)
|
.process(
|
||||||
|
uri = uri,
|
||||||
|
mimeType = mimeType,
|
||||||
|
deleteOriginal = true,
|
||||||
|
compressIfPossible = compressIfPossible
|
||||||
|
)
|
||||||
.flatMap { info ->
|
.flatMap { info ->
|
||||||
room.sendMedia(info)
|
room.sendMedia(info)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,9 @@ import android.net.Uri
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import androidx.exifinterface.media.ExifInterface
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.androidutils.file.createTmpFile
|
import io.element.android.libraries.androidutils.file.createTmpFile
|
||||||
|
import io.element.android.libraries.androidutils.file.getFileName
|
||||||
import io.element.android.libraries.androidutils.media.runAndRelease
|
import io.element.android.libraries.androidutils.media.runAndRelease
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.core.extensions.mapFailure
|
import io.element.android.libraries.core.extensions.mapFailure
|
||||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||||
|
|
@ -41,7 +43,6 @@ import io.element.android.libraries.matrix.api.media.VideoInfo
|
||||||
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
|
||||||
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
import io.element.android.libraries.mediaupload.api.MediaUploadInfo
|
||||||
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
|
import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.flow.filterIsInstance
|
import kotlinx.coroutines.flow.filterIsInstance
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
|
@ -58,6 +59,7 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val imageCompressor: ImageCompressor,
|
private val imageCompressor: ImageCompressor,
|
||||||
private val videoCompressor: VideoCompressor,
|
private val videoCompressor: VideoCompressor,
|
||||||
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
) : MediaPreProcessor {
|
) : MediaPreProcessor {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
|
|
@ -92,12 +94,13 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
deleteOriginal: Boolean,
|
deleteOriginal: Boolean,
|
||||||
|
compressIfPossible: Boolean,
|
||||||
): Result<MediaUploadInfo> = runCatching {
|
): Result<MediaUploadInfo> = runCatching {
|
||||||
val compressBeforeSending = (
|
val shouldBeCompressed = compressIfPossible &&
|
||||||
mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) ||
|
(mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) ||
|
||||||
mimeType.isMimeTypeVideo()
|
mimeType.isMimeTypeVideo()
|
||||||
|
|
||||||
val result = if (compressBeforeSending) {
|
val result = if (shouldBeCompressed) {
|
||||||
when {
|
when {
|
||||||
mimeType.isMimeTypeImage() -> processImage(uri)
|
mimeType.isMimeTypeImage() -> processImage(uri)
|
||||||
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType)
|
mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType)
|
||||||
|
|
@ -123,9 +126,26 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
contentResolver.delete(uri, null, null)
|
contentResolver.delete(uri, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result
|
result.postProcess(uri)
|
||||||
}.mapFailure { MediaPreProcessor.Failure(it) }
|
}.mapFailure { MediaPreProcessor.Failure(it) }
|
||||||
|
|
||||||
|
private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo {
|
||||||
|
|
||||||
|
fun File.rename(name: String): File {
|
||||||
|
return File(context.cacheDir, name).also {
|
||||||
|
renameTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val name = context.getFileName(uri) ?: return this
|
||||||
|
return when (this) {
|
||||||
|
is MediaUploadInfo.AnyFile -> copy(file = file.rename(name))
|
||||||
|
is MediaUploadInfo.Audio -> copy(file = file.rename(name))
|
||||||
|
is MediaUploadInfo.Image -> copy(file = file.rename(name))
|
||||||
|
is MediaUploadInfo.Video -> copy(file = file.rename(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun processImage(uri: Uri): MediaUploadInfo {
|
private suspend fun processImage(uri: Uri): MediaUploadInfo {
|
||||||
val compressedFileResult = contentResolver.openInputStream(uri).use { input ->
|
val compressedFileResult = contentResolver.openInputStream(uri).use { input ->
|
||||||
imageCompressor.compressToTmpFile(
|
imageCompressor.compressToTmpFile(
|
||||||
|
|
@ -176,7 +196,6 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
inputStream = inputStream,
|
inputStream = inputStream,
|
||||||
resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT),
|
resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT),
|
||||||
).getOrThrow()
|
).getOrThrow()
|
||||||
|
|
||||||
return thumbnailResult.toThumbnailProcessingInfo(MimeTypes.Jpeg)
|
return thumbnailResult.toThumbnailProcessingInfo(MimeTypes.Jpeg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +210,7 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun createTmpFileWithInput(inputStream: InputStream): File? {
|
private suspend fun createTmpFileWithInput(inputStream: InputStream): File? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(coroutineDispatchers.io) {
|
||||||
tryOrNull {
|
tryOrNull {
|
||||||
val tmpFile = context.createTmpFile()
|
val tmpFile = context.createTmpFile()
|
||||||
tmpFile.outputStream().use { inputStream.copyTo(it) }
|
tmpFile.outputStream().use { inputStream.copyTo(it) }
|
||||||
|
|
@ -203,7 +222,6 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailUrl: String?, thumbnailInfo: ThumbnailProcessingInfo?): VideoInfo =
|
private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailUrl: String?, thumbnailInfo: ThumbnailProcessingInfo?): VideoInfo =
|
||||||
MediaMetadataRetriever().runAndRelease {
|
MediaMetadataRetriever().runAndRelease {
|
||||||
setDataSource(context, Uri.fromFile(file))
|
setDataSource(context, Uri.fromFile(file))
|
||||||
|
|
||||||
VideoInfo(
|
VideoInfo(
|
||||||
duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L,
|
duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L,
|
||||||
width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0L,
|
width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0L,
|
||||||
|
|
@ -229,7 +247,6 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||||
inputStream = inputStream,
|
inputStream = inputStream,
|
||||||
resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT),
|
resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT),
|
||||||
)
|
)
|
||||||
|
|
||||||
result.getOrThrow().toThumbnailProcessingInfo(MimeTypes.Jpeg)
|
result.getOrThrow().toThumbnailProcessingInfo(MimeTypes.Jpeg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,12 @@ class FakeMediaPreProcessor : MediaPreProcessor {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override suspend fun process(uri: Uri, mimeType: String, deleteOriginal: Boolean): Result<MediaUploadInfo> = result
|
override suspend fun process(
|
||||||
|
uri: Uri,
|
||||||
|
mimeType: String,
|
||||||
|
deleteOriginal: Boolean,
|
||||||
|
compressIfPossible: Boolean
|
||||||
|
): Result<MediaUploadInfo> = result
|
||||||
|
|
||||||
fun givenResult(value: Result<MediaUploadInfo>) {
|
fun givenResult(value: Result<MediaUploadInfo>) {
|
||||||
this.result = value
|
this.result = value
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue