Browse Source

Media: improve media viewer

feature/jme/open-room-member-details-when-clicking-on-user-data
ganfra 1 year ago
parent
commit
80adbd4bd1
  1. 12
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt
  2. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt
  3. 22
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt
  4. 22
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt
  5. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt
  6. 66
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt
  7. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt
  8. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt
  9. 44
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt
  10. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt
  11. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt
  12. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt
  13. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt
  14. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt
  15. 6
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt
  16. 27
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.kt
  17. 31
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt
  18. 14
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt
  19. 27
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaFile.kt
  20. 7
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt

12
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt

@ -62,7 +62,11 @@ class MessagesFlowNode @AssistedInject constructor(
object Messages : NavTarget object Messages : NavTarget
@Parcelize @Parcelize
data class MediaViewer(val title: String, val mediaSource: MatrixMediaSource) : NavTarget data class MediaViewer(
val title: String,
val mediaSource: MatrixMediaSource,
val mimeType: String?
) : NavTarget
@Parcelize @Parcelize
data class AttachmentPreview(val attachment: Attachment) : NavTarget data class AttachmentPreview(val attachment: Attachment) : NavTarget
@ -89,7 +93,7 @@ class MessagesFlowNode @AssistedInject constructor(
createNode<MessagesNode>(buildContext, listOf(callback)) createNode<MessagesNode>(buildContext, listOf(callback))
} }
is NavTarget.MediaViewer -> { is NavTarget.MediaViewer -> {
val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource) val inputs = MediaViewerNode.Inputs(navTarget.title, navTarget.mediaSource, navTarget.mimeType)
createNode<MediaViewerNode>(buildContext, listOf(inputs)) createNode<MediaViewerNode>(buildContext, listOf(inputs))
} }
is NavTarget.AttachmentPreview -> { is NavTarget.AttachmentPreview -> {
@ -103,12 +107,12 @@ class MessagesFlowNode @AssistedInject constructor(
when (event.content) { when (event.content) {
is TimelineItemImageContent -> { is TimelineItemImageContent -> {
val mediaSource = event.content.mediaSource val mediaSource = event.content.mediaSource
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource) val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
backstack.push(navTarget) backstack.push(navTarget)
} }
is TimelineItemVideoContent -> { is TimelineItemVideoContent -> {
val mediaSource = event.content.videoSource val mediaSource = event.content.videoSource
val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource) val navTarget = NavTarget.MediaViewer(event.content.body, mediaSource, event.content.mimeType)
backstack.push(navTarget) backstack.push(navTarget)
} }
else -> Unit else -> Unit

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMedia.kt

@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.media.local
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class LocalMedia( data class LocalMedia(

22
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt

@ -22,6 +22,8 @@ import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -84,13 +86,17 @@ fun MediaVideoView(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val exoPlayer = ExoPlayer.Builder(LocalContext.current).build() val exoPlayer = remember {
val mediaItem = MediaItem.Builder() ExoPlayer.Builder(context).build()
.setUri(uri) .apply {
.build() this.playWhenReady = true
exoPlayer.playWhenReady this.prepare()
}
}
LaunchedEffect(uri) {
val mediaItem = MediaItem.fromUri(uri)
exoPlayer.setMediaItem(mediaItem) exoPlayer.setMediaItem(mediaItem)
exoPlayer.prepare() }
AndroidView( AndroidView(
factory = { factory = {
@ -98,8 +104,10 @@ fun MediaVideoView(
player = exoPlayer player = exoPlayer
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
controllerShowTimeoutMs = 3000
} }
}, modifier = modifier.fillMaxSize() },
modifier = modifier.fillMaxSize()
) )
OnLifecycleEvent { _, event -> OnLifecycleEvent { _, event ->

22
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerEvents.kt

@ -0,0 +1,22 @@
/*
* 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.viewer
sealed interface MediaViewerEvents {
object RetryLoading : MediaViewerEvents
object SaveOnDisk : MediaViewerEvents
}

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerNode.kt

@ -39,11 +39,12 @@ class MediaViewerNode @AssistedInject constructor(
data class Inputs( data class Inputs(
val name: String, val name: String,
val mediaSource: MatrixMediaSource, val mediaSource: MatrixMediaSource,
val mimeType: String?
) : NodeInputs ) : NodeInputs
private val inputs: Inputs = inputs() private val inputs: Inputs = inputs()
private val presenter = presenterFactory.create(inputs.name, inputs.mediaSource) private val presenter = presenterFactory.create(inputs)
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {

66
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerPresenter.kt

@ -17,8 +17,14 @@
package io.element.android.features.messages.impl.media.viewer package io.element.android.features.messages.impl.media.viewer
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.core.net.toUri
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -27,37 +33,65 @@ import io.element.android.features.messages.impl.media.local.LocalMediaFactory
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.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.api.media.MediaFile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
class MediaViewerPresenter @AssistedInject constructor( class MediaViewerPresenter @AssistedInject constructor(
@Assisted private val name: String, @Assisted private val inputs: MediaViewerNode.Inputs,
@Assisted private val mediaSource: MatrixMediaSource,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
private val client: MatrixClient, private val client: MatrixClient,
) : Presenter<MediaViewerState> { ) : Presenter<MediaViewerState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(name: String, mediaSource: MatrixMediaSource): MediaViewerPresenter fun create(inputs: MediaViewerNode.Inputs): MediaViewerPresenter
} }
@Composable @Composable
override fun present(): MediaViewerState { override fun present(): MediaViewerState {
val localMedia by produceState<Async<LocalMedia>>(initialValue = Async.Uninitialized) { val coroutineScope = rememberCoroutineScope()
value = Async.Loading(null) var loadMediaTrigger by remember { mutableStateOf(0) }
//TODO we are missing some permissions to use this API val mediaFile: MutableState<MediaFile?> = remember {
client.mediaLoader.loadMediaFile(mediaSource, null) mutableStateOf(null)
.onSuccess { }
val localMedia = localMediaFactory.createFromUri(uri = it, null) val localMedia: MutableState<Async<LocalMedia>> = remember {
Async.Success(localMedia) mutableStateOf(Async.Uninitialized)
}.onFailure { }
Async.Failure(it, null) DisposableEffect(loadMediaTrigger) {
coroutineScope.loadMedia(mediaFile, localMedia)
onDispose {
mediaFile.value?.close()
}
}
fun handleEvents(mediaViewerEvents: MediaViewerEvents) {
when (mediaViewerEvents) {
MediaViewerEvents.RetryLoading -> loadMediaTrigger++
MediaViewerEvents.SaveOnDisk -> TODO()
} }
} }
return MediaViewerState( return MediaViewerState(
name = name, name = inputs.name,
downloadedMedia = localMedia, downloadedMedia = localMedia.value,
eventSink = ::handleEvents
) )
} }
private fun CoroutineScope.loadMedia(mediaFile: MutableState<MediaFile?>, localMedia: MutableState<Async<LocalMedia>>) = launch {
mediaFile.value = null
localMedia.value = Async.Loading()
client.mediaLoader.loadMediaFile(inputs.mediaSource, inputs.mimeType)
.onSuccess {
mediaFile.value = it
}.mapCatching {
val uri = it.path().toUri()
localMediaFactory.createFromUri(uri, inputs.mimeType)!!
}.onSuccess {
localMedia.value = Async.Success(it)
}.onFailure {
localMedia.value = Async.Failure(it)
}
}
} }

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerState.kt

@ -21,5 +21,6 @@ import io.element.android.libraries.architecture.Async
data class MediaViewerState( data class MediaViewerState(
val name: String, val name: String,
val downloadedMedia: Async<LocalMedia> val downloadedMedia: Async<LocalMedia>,
val eventSink: (MediaViewerEvents) -> Unit
) )

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt

@ -29,5 +29,6 @@ open class MediaViewerStateProvider : PreviewParameterProvider<MediaViewerState>
fun aMediaViewerState() = MediaViewerState( fun aMediaViewerState() = MediaViewerState(
name = "A media", name = "A media",
downloadedMedia = Async.Uninitialized downloadedMedia = Async.Uninitialized,
eventSink = {}
) )

44
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt

@ -19,37 +19,50 @@
package io.element.android.features.messages.impl.media.viewer package io.element.android.features.messages.impl.media.viewer
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.element.android.features.messages.impl.media.local.LocalMediaView import io.element.android.features.messages.impl.media.local.LocalMediaView
import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.R.string as StringR
@Composable @Composable
fun MediaViewerView( fun MediaViewerView(
state: MediaViewerState, state: MediaViewerState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
fun onRetry() {
state.eventSink(MediaViewerEvents.RetryLoading)
}
Scaffold(modifier) { Scaffold(modifier) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.padding(it),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (state.downloadedMedia) { when (state.downloadedMedia) {
is Async.Success -> LocalMediaView(state.downloadedMedia.state) is Async.Success -> LocalMediaView(state.downloadedMedia.state)
is Async.Failure -> ErrorDialog( is Async.Failure -> ErrorView("Error while downloading", ::onRetry)
content = "Error while downloading the media",
)
else -> CircularProgressIndicator( else -> CircularProgressIndicator(
strokeWidth = 2.dp, strokeWidth = 2.dp,
) )
@ -58,6 +71,27 @@ fun MediaViewerView(
} }
} }
@Composable
private fun ErrorView(
errorMessage: String,
onRetry: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = errorMessage)
Spacer(modifier = Modifier.size(8.dp))
Button(
onClick = onRetry
) {
Text(text = stringResource(id = StringR.action_retry))
}
}
}
@Preview @Preview
@Composable @Composable
fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) = fun MediaViewerViewLightPreview(@PreviewParameter(MediaViewerStateProvider::class) state: MediaViewerState) =

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt

@ -52,6 +52,7 @@ class TimelineItemContentMessageFactory @Inject constructor() {
body = messageType.body, body = messageType.body,
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
width = messageType.info?.width?.toInt(), width = messageType.info?.width?.toInt(),
mimeType = messageType.info?.mimetype,
mediaSource = messageType.source, mediaSource = messageType.source,
blurhash = messageType.info?.blurhash, blurhash = messageType.info?.blurhash,
aspectRatio = aspectRatio aspectRatio = aspectRatio
@ -69,7 +70,7 @@ class TimelineItemContentMessageFactory @Inject constructor() {
body = messageType.body, body = messageType.body,
thumbnailSource = messageType.info?.thumbnailSource, thumbnailSource = messageType.info?.thumbnailSource,
videoSource = messageType.source, videoSource = messageType.source,
mimetype = messageType.info?.mimetype, mimeType = messageType.info?.mimetype,
width = messageType.info?.width?.toInt(), width = messageType.info?.width?.toInt(),
height = messageType.info?.height?.toInt(), height = messageType.info?.height?.toInt(),
duration = messageType.info?.duration ?: 0L, duration = messageType.info?.duration ?: 0L,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaSource
data class TimelineItemImageContent( data class TimelineItemImageContent(
val body: String, val body: String,
val mediaSource: MatrixMediaSource, val mediaSource: MatrixMediaSource,
val mimeType: String?,
val blurhash: String?, val blurhash: String?,
val width: Int?, val width: Int?,
val height: Int?, val height: Int?,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt

@ -34,6 +34,7 @@ fun aTimelineItemImageContent() = TimelineItemImageContent(
mediaSource = MatrixMediaSource(""), mediaSource = MatrixMediaSource(""),
blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", blurhash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr",
aspectRatio = 0.5f, aspectRatio = 0.5f,
mimeType = "null",
height = null, height = null,
width = null width = null
) )

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt

@ -27,7 +27,7 @@ data class TimelineItemVideoContent(
val blurhash: String?, val blurhash: String?,
val height: Int?, val height: Int?,
val width: Int?, val width: Int?,
val mimetype: String?, val mimeType: String?,
) : TimelineItemEventContent { ) : TimelineItemEventContent {
override val type: String = "TimelineItemImageContent" override val type: String = "TimelineItemImageContent"
} }

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContentProvider.kt

@ -18,7 +18,6 @@ package io.element.android.features.messages.impl.timeline.model.event
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.ui.media.MediaRequestData
open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineItemVideoContent> { open class TimelineItemVideoContentProvider : PreviewParameterProvider<TimelineItemVideoContent> {
override val values: Sequence<TimelineItemVideoContent> override val values: Sequence<TimelineItemVideoContent>
@ -38,5 +37,5 @@ fun aTimelineItemVideoContent() = TimelineItemVideoContent(
videoSource = MatrixMediaSource(""), videoSource = MatrixMediaSource(""),
height = null, height = null,
width = null, width = null,
mimetype = null mimeType = null
) )

6
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MatrixMediaLoader.kt

@ -16,8 +16,6 @@
package io.element.android.libraries.matrix.api.media package io.element.android.libraries.matrix.api.media
import android.net.Uri
interface MatrixMediaLoader { interface MatrixMediaLoader {
/** /**
* @param url to fetch the content for. * @param url to fetch the content for.
@ -36,7 +34,7 @@ interface MatrixMediaLoader {
/** /**
* @param url to fetch the data for. * @param url to fetch the data for.
* @param mimeType: optional mime type * @param mimeType: optional mime type
* @return a [Result] of [Uri]. It's the uri of the downloaded file. * @return a [Result] of [MediaFile]
*/ */
suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<MediaFile>
} }

27
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/MediaFile.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.libraries.matrix.api.media
import java.io.Closeable
/**
* A wrapper around a media file on the disk.
* When closed the file will be removed from the disk.
*/
interface MediaFile : Closeable {
fun path(): String
}

31
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/MediaHandle.kt

@ -0,0 +1,31 @@
/*
* 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.libraries.matrix.impl.media
import io.element.android.libraries.matrix.api.media.MediaFile
import org.matrix.rustcomponents.sdk.MediaFileHandle
class RustMediaFile(private val inner: MediaFileHandle) : MediaFile {
override fun path(): String {
return inner.path()
}
override fun close() {
inner.close()
}
}

14
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/RustMediaLoader.kt

@ -16,15 +16,14 @@
package io.element.android.libraries.matrix.impl.media package io.element.android.libraries.matrix.impl.media
import android.net.Uri
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.api.media.MediaFile
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import java.io.File
class RustMediaLoader( class RustMediaLoader(
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
@ -59,19 +58,16 @@ class RustMediaLoader(
} }
} }
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> = override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<MediaFile> =
withContext(dispatchers.io) { withContext(dispatchers.io) {
runCatching { runCatching {
mediaSourceFromUrl(source.url).use { mediaSource -> mediaSourceFromUrl(source.url).use { mediaSource ->
innerClient.getMediaFile( val mediaFile = innerClient.getMediaFile(
mediaSource = mediaSource, mediaSource = mediaSource,
mimeType = mimeType ?: "application/octet-stream" mimeType = mimeType ?: "application/octet-stream"
).use { )
val file = File(it.path()) RustMediaFile(mediaFile)
Uri.fromFile(file)
} }
} }
} }
}
} }

27
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaFile.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.libraries.matrix.test.media
import io.element.android.libraries.matrix.api.media.MediaFile
class FakeMediaFile(private val path: String) : MediaFile {
override fun path(): String {
return path
}
override fun close() = Unit
}

7
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt

@ -16,10 +16,9 @@
package io.element.android.libraries.matrix.test.media package io.element.android.libraries.matrix.test.media
import android.net.Uri
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MatrixMediaSource import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import java.io.File import io.element.android.libraries.matrix.api.media.MediaFile
class FakeMediaLoader : MatrixMediaLoader { class FakeMediaLoader : MatrixMediaLoader {
@ -41,11 +40,11 @@ class FakeMediaLoader : MatrixMediaLoader {
} }
} }
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> { override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<MediaFile> {
return if (shouldFail) { return if (shouldFail) {
Result.failure(RuntimeException()) Result.failure(RuntimeException())
} else { } else {
return Result.success(Uri.fromFile(File("path"))) return Result.success(FakeMediaFile(""))
} }
} }
} }

Loading…
Cancel
Save