Media: refactor LocalMedia so the source of data is clear (uri or file)
This commit is contained in:
parent
c0470d4bb5
commit
ab48735ddc
10 changed files with 77 additions and 78 deletions
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
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
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue