diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt index d80359e88c..537d259599 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt @@ -25,6 +25,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.attachments.Attachment +import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.executeResult @@ -84,7 +85,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( sendActionState: MutableState>, ) { suspend { - mediaSender.sendMedia(mediaAttachment.localMedia.uri, mediaAttachment.localMedia.mimeType, mediaAttachment.compressIfPossible) + when (mediaAttachment.localMedia.source) { + is LocalMedia.Source.FromUri -> { + mediaSender.sendMedia(mediaAttachment.localMedia.source.uri, mediaAttachment.localMedia.mimeType, mediaAttachment.compressIfPossible) + } + else -> error("Attachment should be defined by a uri") + } + }.executeResult(sendActionState) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt index 26565a226a..e7a3968386 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt @@ -17,11 +17,11 @@ package io.element.android.features.messages.impl.attachments.preview import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.core.net.toUri import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.mimetype.MimeTypes +import java.io.File open class AttachmentsPreviewStateProvider : PreviewParameterProvider { override val values: Sequence @@ -34,7 +34,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider = Async.Uninitialized) = AttachmentsPreviewState( attachment = Attachment.Media( - localMedia = LocalMedia("path".toUri(), MimeTypes.Jpeg, "an image", 1000L), + localMedia = LocalMedia(LocalMedia.Source.FromFile(File("path")), MimeTypes.Jpeg, "an image", 1000L), compressIfPossible = true ), sendActionState = sendActionState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt index bc2f1a066c..4ee00914ed 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/AndroidLocalMediaFactory.kt @@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.media.local import android.content.Context import android.net.Uri -import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileSize @@ -26,6 +25,7 @@ import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.MediaFile +import java.io.File import javax.inject.Inject @ContributesBinding(AppScope::class) @@ -34,8 +34,16 @@ class AndroidLocalMediaFactory @Inject constructor( ) : LocalMediaFactory { override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia { - val uri = mediaFile.path().toUri() - return createFromUri(uri, mimeType) + val resolvedMimeType = mimeType ?: MimeTypes.OctetStream + val file = File(mediaFile.path()) + val fileName = file.name + val fileSize = file.length() + return LocalMedia( + source = LocalMedia.Source.FromFile(file), + mimeType = resolvedMimeType, + name = fileName, + size = fileSize + ) } override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia { @@ -43,7 +51,7 @@ class AndroidLocalMediaFactory @Inject constructor( val fileName = context.getFileName(uri) val fileSize = context.getFileSize(uri) return LocalMedia( - uri = uri, + source = LocalMedia.Source.FromUri(uri), mimeType = resolvedMimeType, name = fileName, size = fileSize diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt index 8305c8eee7..5d1afd6bec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt @@ -21,20 +21,27 @@ import android.os.Parcelable import androidx.compose.runtime.Immutable import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import java.io.File @Parcelize @Immutable data class LocalMedia( - val uri: Uri, + val source: Source, val mimeType: String, val name: String?, val size: Long, ) : Parcelable { - /** - * This tries to convert the uri to a file if applicable, otherwise keep it as uri. - */ - @IgnoredOnParcel val model: Any by lazy { - UriToFileMapper.map(uri) ?: uri + sealed interface Source : Parcelable { + @Parcelize + data class FromUri(val uri: Uri) : Source + + @Parcelize + data class FromFile(val file: File) : Source + } + + @IgnoredOnParcel val model: Any = when (source) { + is Source.FromUri -> source.uri + is Source.FromFile -> source.file } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 3040f0bfbd..c1d9e2f119 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -31,13 +31,13 @@ import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle -import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.PlayerView import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper +import io.element.android.features.messages.impl.media.local.exoplayer.toMediaItem import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import me.saket.telephoto.zoomable.ZoomSpec @@ -107,6 +107,7 @@ fun MediaVideoView( onReady: () -> Unit, modifier: Modifier = Modifier, ) { + val context = LocalContext.current val playerListener = object : Player.Listener { override fun onRenderedFirstFrame() { @@ -120,9 +121,9 @@ fun MediaVideoView( this.prepare() } } - if (localMedia?.uri != null) { - LaunchedEffect(localMedia.uri) { - val mediaItem = MediaItem.fromUri(localMedia.uri) + if (localMedia?.source != null) { + LaunchedEffect(localMedia.source) { + val mediaItem = localMedia.toMediaItem() exoPlayer.setMediaItem(mediaItem) } } else { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt deleted file mode 100644 index d27b667883..0000000000 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/UriToFileMapper.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.features.messages.impl.media.local - -import android.content.ContentResolver -import android.net.Uri -import io.element.android.libraries.androidutils.uri.ASSET_FILE_PATH_ROOT -import io.element.android.libraries.androidutils.uri.firstPathSegment -import java.io.File - -/** - * Tries to convert a URI to a File. - * Extracted from Coil [coil.map.FileUriMapper] - */ -object UriToFileMapper { - - fun map(data: Uri): File? { - if (!isApplicable(data)) return null - return if (data.scheme == ContentResolver.SCHEME_FILE) { - data.path?.let(::File) - } else { - // If the scheme is not "file", it's null, representing a literal path on disk. - // Assume the entire input, regardless of any reserved characters, is valid. - File(data.toString()) - } - } - - private fun isApplicable(data: Uri): Boolean { - return !isAssetUri(data) && - data.scheme.let { it == null || it == ContentResolver.SCHEME_FILE } && - data.path.orEmpty().startsWith('/') && data.firstPathSegment != null - } - - private fun isAssetUri(uri: Uri): Boolean { - return uri.scheme == ContentResolver.SCHEME_FILE && uri.firstPathSegment == ASSET_FILE_PATH_ROOT - } -} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/LocalMediaExt.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/LocalMediaExt.kt new file mode 100644 index 0000000000..9a96a6a39b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/exoplayer/LocalMediaExt.kt @@ -0,0 +1,27 @@ +/* + * 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.features.messages.impl.media.local.exoplayer + +import androidx.media3.common.MediaItem +import io.element.android.features.messages.impl.media.local.LocalMedia + +fun LocalMedia.toMediaItem(): MediaItem { + return when (source) { + is LocalMedia.Source.FromFile -> MediaItem.fromUri(source.file.path) + is LocalMedia.Source.FromUri -> MediaItem.fromUri(source.uri) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 6c54eb3b05..c961f10caf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -16,11 +16,11 @@ package io.element.android.features.messages.impl.media.viewer -import android.net.Uri import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.media3.common.MimeTypes import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.architecture.Async +import java.io.File open class MediaViewerStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,14 +31,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider aMediaViewerState( Async.Success( LocalMedia( - Uri.EMPTY, MimeTypes.IMAGE_JPEG, "an image file", 100L + LocalMedia.Source.FromFile(File("")), MimeTypes.IMAGE_JPEG, "an image file", 100L ) ), ), aMediaViewerState( Async.Success( LocalMedia( - Uri.EMPTY, MimeTypes.VIDEO_MP4, "a video file", 100L + LocalMedia.Source.FromFile(File("")), MimeTypes.VIDEO_MP4, "a video file", 100L ) ), ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index ae975f4ddb..74f533c6fe 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -31,6 +31,7 @@ import androidx.media3.common.MimeTypes import androidx.media3.common.util.UnstableApi import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError +import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.data.StableCharSequence @@ -191,8 +192,7 @@ class MessageComposerPresenter @Inject constructor( when (attachment) { is Attachment.Media -> { sendMedia( - uri = attachment.localMedia.uri, - mimeType = attachment.localMedia.mimeType, + media = attachment.localMedia, attachmentState = attachmentState ) } @@ -226,11 +226,12 @@ class MessageComposerPresenter @Inject constructor( } private suspend fun sendMedia( - uri: Uri, - mimeType: String, + media: LocalMedia, attachmentState: MutableState, ) { - mediaSender.sendMedia(uri, mimeType, compressIfPossible = false) + if (media.source !is LocalMedia.Source.FromUri) error("Attachment should use Uri") + val uri = media.source.uri + mediaSender.sendMedia(uri, media.mimeType, compressIfPossible = false) .onSuccess { attachmentState.value = AttachmentsState.None }.onFailure { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt index 60a01a76d4..399c39cc88 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/media.kt @@ -20,7 +20,6 @@ import android.net.Uri import androidx.media3.common.MimeTypes import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.media.local.LocalMedia -import io.mockk.mockk fun aLocalMedia( uri: Uri, @@ -28,7 +27,7 @@ fun aLocalMedia( name: String = "a media", size: Long = 1000, ) = LocalMedia( - uri = uri, + source = LocalMedia.Source.FromUri(uri), mimeType = mimeType, name = name, size = size,