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.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<Async<Unit>>,
) {
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)
}
}

View File

@@ -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<AttachmentsPreviewState> {
override val values: Sequence<AttachmentsPreviewState>
@@ -34,7 +34,7 @@ open class AttachmentsPreviewStateProvider : PreviewParameterProvider<Attachment
fun anAttachmentsPreviewState(sendActionState: Async<Unit> = 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,

View File

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

View File

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

View File

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

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
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<MediaViewerState> {
override val values: Sequence<MediaViewerState>
@@ -31,14 +31,14 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
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
)
),
)

View File

@@ -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<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 {
attachmentState.value = AttachmentsState.None
}.onFailure {

View File

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