Media: refactor LocalMedia so the source of data is clear (uri or file)

This commit is contained in:
ganfra 2023-06-01 22:35:17 +02:00
parent c0470d4bb5
commit ab48735ddc
10 changed files with 77 additions and 78 deletions

View file

@ -25,6 +25,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.messages.impl.attachments.Attachment 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.Async
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.executeResult import io.element.android.libraries.architecture.executeResult
@ -84,7 +85,13 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
sendActionState: MutableState<Async<Unit>>, sendActionState: MutableState<Async<Unit>>,
) { ) {
suspend { 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) }.executeResult(sendActionState)
} }
} }

View file

@ -17,11 +17,11 @@
package io.element.android.features.messages.impl.attachments.preview package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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.attachments.Attachment
import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import java.io.File
open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> { open class AttachmentsPreviewStateProvider : PreviewParameterProvider<AttachmentsPreviewState> {
override val values: Sequence<AttachmentsPreviewState> override val values: Sequence<AttachmentsPreviewState>
@ -34,7 +34,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("path".toUri(), MimeTypes.Jpeg, "an image", 1000L), localMedia = LocalMedia(LocalMedia.Source.FromFile(File("path")), MimeTypes.Jpeg, "an image", 1000L),
compressIfPossible = true compressIfPossible = true
), ),
sendActionState = sendActionState, sendActionState = sendActionState,

View file

@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.media.local
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.file.getFileName import io.element.android.libraries.androidutils.file.getFileName
import io.element.android.libraries.androidutils.file.getFileSize 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.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaFile
import java.io.File
import javax.inject.Inject import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@ -34,8 +34,16 @@ class AndroidLocalMediaFactory @Inject constructor(
) : LocalMediaFactory { ) : LocalMediaFactory {
override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia { override fun createFromMediaFile(mediaFile: MediaFile, mimeType: String?): LocalMedia {
val uri = mediaFile.path().toUri() val resolvedMimeType = mimeType ?: MimeTypes.OctetStream
return createFromUri(uri, mimeType) 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 { override fun createFromUri(uri: Uri, mimeType: String?): LocalMedia {
@ -43,7 +51,7 @@ class AndroidLocalMediaFactory @Inject constructor(
val fileName = context.getFileName(uri) val fileName = context.getFileName(uri)
val fileSize = context.getFileSize(uri) val fileSize = context.getFileSize(uri)
return LocalMedia( return LocalMedia(
uri = uri, source = LocalMedia.Source.FromUri(uri),
mimeType = resolvedMimeType, mimeType = resolvedMimeType,
name = fileName, name = fileName,
size = fileSize size = fileSize

View file

@ -21,20 +21,27 @@ import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.io.File
@Parcelize @Parcelize
@Immutable @Immutable
data class LocalMedia( data class LocalMedia(
val uri: Uri, val source: Source,
val mimeType: String, val mimeType: String,
val name: String?, val name: String?,
val size: Long, val size: Long,
) : Parcelable { ) : Parcelable {
/** sealed interface Source : Parcelable {
* This tries to convert the uri to a file if applicable, otherwise keep it as uri. @Parcelize
*/ data class FromUri(val uri: Uri) : Source
@IgnoredOnParcel val model: Any by lazy {
UriToFileMapper.map(uri) ?: uri @Parcelize
data class FromFile(val file: File) : Source
}
@IgnoredOnParcel val model: Any = when (source) {
is Source.FromUri -> source.uri
is Source.FromFile -> source.file
} }
} }

View file

@ -31,13 +31,13 @@ import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView 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.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.R
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.ZoomSpec
@ -107,6 +107,7 @@ fun MediaVideoView(
onReady: () -> Unit, onReady: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val playerListener = object : Player.Listener { val playerListener = object : Player.Listener {
override fun onRenderedFirstFrame() { override fun onRenderedFirstFrame() {
@ -120,9 +121,9 @@ fun MediaVideoView(
this.prepare() this.prepare()
} }
} }
if (localMedia?.uri != null) { if (localMedia?.source != null) {
LaunchedEffect(localMedia.uri) { LaunchedEffect(localMedia.source) {
val mediaItem = MediaItem.fromUri(localMedia.uri) val mediaItem = localMedia.toMediaItem()
exoPlayer.setMediaItem(mediaItem) exoPlayer.setMediaItem(mediaItem)
} }
} else { } else {

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -16,11 +16,11 @@
package io.element.android.features.messages.impl.media.viewer package io.element.android.features.messages.impl.media.viewer
import android.net.Uri
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMedia
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import java.io.File
open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> { open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState> {
override val values: Sequence<MediaViewerState> override val values: Sequence<MediaViewerState>
@ -31,14 +31,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
aMediaViewerState( aMediaViewerState(
Async.Success( Async.Success(
LocalMedia( LocalMedia(
Uri.EMPTY, MimeTypes.IMAGE_JPEG, "an image file", 100L LocalMedia.Source.FromFile(File("")), MimeTypes.IMAGE_JPEG, "an image file", 100L
) )
), ),
), ),
aMediaViewerState( aMediaViewerState(
Async.Success( Async.Success(
LocalMedia( LocalMedia(
Uri.EMPTY, MimeTypes.VIDEO_MP4, "a video file", 100L LocalMedia.Source.FromFile(File("")), MimeTypes.VIDEO_MP4, "a video file", 100L
) )
), ),
) )

View file

@ -31,6 +31,7 @@ import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import io.element.android.features.messages.impl.attachments.Attachment 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.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.features.messages.impl.media.local.LocalMediaFactory
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.core.data.StableCharSequence
@ -191,8 +192,7 @@ class MessageComposerPresenter @Inject constructor(
when (attachment) { when (attachment) {
is Attachment.Media -> { is Attachment.Media -> {
sendMedia( sendMedia(
uri = attachment.localMedia.uri, media = attachment.localMedia,
mimeType = attachment.localMedia.mimeType,
attachmentState = attachmentState attachmentState = attachmentState
) )
} }
@ -226,11 +226,12 @@ class MessageComposerPresenter @Inject constructor(
} }
private suspend fun sendMedia( private suspend fun sendMedia(
uri: Uri, media: LocalMedia,
mimeType: String,
attachmentState: MutableState<AttachmentsState>, attachmentState: MutableState<AttachmentsState>,
) { ) {
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 { .onSuccess {
attachmentState.value = AttachmentsState.None attachmentState.value = AttachmentsState.None
}.onFailure { }.onFailure {

View file

@ -20,7 +20,6 @@ import android.net.Uri
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMedia
import io.mockk.mockk
fun aLocalMedia( fun aLocalMedia(
uri: Uri, uri: Uri,
@ -28,7 +27,7 @@ fun aLocalMedia(
name: String = "a media", name: String = "a media",
size: Long = 1000, size: Long = 1000,
) = LocalMedia( ) = LocalMedia(
uri = uri, source = LocalMedia.Source.FromUri(uri),
mimeType = mimeType, mimeType = mimeType,
name = name, name = name,
size = size, size = size,