Media: refactor LocalMedia so the source of data is clear (uri or file)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user