Introduce MatrixMediaSource

This commit is contained in:
ganfra 2023-05-05 19:47:10 +02:00
parent c3a1297c18
commit 4236b69705
27 changed files with 298 additions and 84 deletions

View file

@ -20,5 +20,5 @@ data class FileInfo(
val mimetype: String?,
val size: Long?,
val thumbnailInfo: ThumbnailInfo?,
val thumbnailUrl: String?
val thumbnailSource: MatrixMediaSource?
)

View file

@ -22,6 +22,6 @@ data class ImageInfo(
val mimetype: String?,
val size: Long?,
val thumbnailInfo: ThumbnailInfo?,
val thumbnailUrl: String?,
val thumbnailSource: MatrixMediaSource?,
val blurhash: String?
)

View file

@ -16,14 +16,14 @@
package io.element.android.libraries.matrix.api.media
import java.nio.file.Path
import android.net.Uri
interface MatrixMediaLoader {
/**
* @param url to fetch the content for.
* @return a [Result] of ByteArray. It contains the binary data for the media.
*/
suspend fun loadMediaContent(url: String): Result<ByteArray>
suspend fun loadMediaContent(source: MatrixMediaSource): Result<ByteArray>
/**
* @param url to fetch the data for.
@ -31,12 +31,12 @@ interface MatrixMediaLoader {
* @param height: the desired height for rescaling the media as thumbnail
* @return a [Result] of ByteArray. It contains the binary data for the media.
*/
suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result<ByteArray>
suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result<ByteArray>
/**
* @param url to fetch the data for.
* @param mimeType: optional mime type
* @return a [Result] of [Path]. It's the path to the downloaded file.
* @return a [Result] of [Uri]. It's the uri of the downloaded file.
*/
suspend fun loadMediaFile(url: String, mimeType: String?): Result<Path>
suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri>
}

View file

@ -0,0 +1,25 @@
/*
* 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 android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
data class MatrixMediaSource(
val url: String
) : Parcelable

View file

@ -23,6 +23,6 @@ data class VideoInfo(
val mimetype: String?,
val size: Long?,
val thumbnailInfo: ThumbnailInfo?,
val thumbnailUrl: String?,
val thumbnailSource: MatrixMediaSource?,
val blurhash: String?
)

View file

@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ImageInfo
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import io.element.android.libraries.matrix.api.media.VideoInfo
sealed interface EventContent
@ -106,25 +107,25 @@ data class EmoteMessageType(
data class ImageMessageType(
val body: String,
val url: String,
val source: MatrixMediaSource,
val info: ImageInfo?
) : MessageType
data class AudioMessageType(
var body: String,
var url: String,
var info: AudioInfo?
val body: String,
val source: MatrixMediaSource,
val info: AudioInfo?
) : MessageType
data class VideoMessageType(
val body: String,
val url: String,
val source: MatrixMediaSource,
val info: VideoInfo?
) : MessageType
data class FileMessageType(
val body: String,
val url: String,
val source: MatrixMediaSource,
val info: FileInfo?
) : MessageType

View file

@ -17,13 +17,11 @@
package io.element.android.libraries.matrix.impl.media
import io.element.android.libraries.matrix.api.media.FileInfo
import io.element.android.libraries.matrix.api.media.ThumbnailInfo
import org.matrix.rustcomponents.sdk.FileInfo as RustFileInfo
import org.matrix.rustcomponents.sdk.ThumbnailInfo as RustThumbnailInfo
fun RustFileInfo.map(): FileInfo = FileInfo(
mimetype = mimetype,
size = size?.toLong(),
thumbnailInfo = thumbnailInfo?.map(),
thumbnailUrl = thumbnailSource?.useUrl()
thumbnailSource = thumbnailSource?.map()
)

View file

@ -25,6 +25,6 @@ fun RustImageInfo.map(): ImageInfo = ImageInfo(
mimetype = mimetype,
size = size?.toLong(),
thumbnailInfo = thumbnailInfo?.map(),
thumbnailUrl = thumbnailSource?.useUrl(),
thumbnailSource = thumbnailSource?.map(),
blurhash = blurhash
)

View file

@ -16,7 +16,8 @@
package io.element.android.libraries.matrix.impl.media
import org.matrix.rustcomponents.sdk.MediaSource
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import org.matrix.rustcomponents.sdk.use
import org.matrix.rustcomponents.sdk.MediaSource as RustMediaSource
fun MediaSource.useUrl(): String = use { it.url() }
fun RustMediaSource.map(): MatrixMediaSource = use { MatrixMediaSource(it.url()) }

View file

@ -16,14 +16,15 @@
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.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
import org.matrix.rustcomponents.sdk.use
import java.nio.file.Path
import kotlin.io.path.Path
import java.io.File
class RustMediaLoader(
private val dispatchers: CoroutineDispatchers,
@ -31,10 +32,10 @@ class RustMediaLoader(
) : MatrixMediaLoader {
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaContent(url: String): Result<ByteArray> =
override suspend fun loadMediaContent(source: MatrixMediaSource): Result<ByteArray> =
withContext(dispatchers.io) {
runCatching {
mediaSourceFromUrl(url).use { source ->
mediaSourceFromUrl(source.url).use { source ->
innerClient.getMediaContent(source).toUByteArray().toByteArray()
}
}
@ -42,13 +43,13 @@ class RustMediaLoader(
@OptIn(ExperimentalUnsignedTypes::class)
override suspend fun loadMediaThumbnail(
url: String,
source: MatrixMediaSource,
width: Long,
height: Long
): Result<ByteArray> =
withContext(dispatchers.io) {
runCatching {
mediaSourceFromUrl(url).use { mediaSource ->
mediaSourceFromUrl(source.url).use { mediaSource ->
innerClient.getMediaThumbnail(
mediaSource = mediaSource,
width = width.toULong(),
@ -58,15 +59,16 @@ class RustMediaLoader(
}
}
override suspend fun loadMediaFile(url: String, mimeType: String?): Result<Path> =
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> =
withContext(dispatchers.io) {
runCatching {
mediaSourceFromUrl(url).use { mediaSource ->
mediaSourceFromUrl(source.url).use { mediaSource ->
innerClient.getMediaFile(
mediaSource = mediaSource,
mimeType = mimeType ?: "application/octet-stream"
).use {
Path(it.path())
val file = File(it.path())
Uri.fromFile(file)
}
}
}

View file

@ -26,6 +26,6 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo(
mimetype = mimetype,
size = size?.toLong(),
thumbnailInfo = thumbnailInfo?.map(),
thumbnailUrl = thumbnailSource?.useUrl(),
thumbnailSource = thumbnailSource?.map(),
blurhash = blurhash
)

View file

@ -29,7 +29,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageTy
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType
import io.element.android.libraries.matrix.impl.media.map
import io.element.android.libraries.matrix.impl.media.useUrl
import org.matrix.rustcomponents.sdk.Message
import org.matrix.rustcomponents.sdk.MessageType
import org.matrix.rustcomponents.sdk.use
@ -42,13 +41,13 @@ class EventMessageMapper {
val type = message.msgtype().use { type ->
when (type) {
is MessageType.Audio -> {
AudioMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map())
AudioMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
}
is MessageType.File -> {
FileMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map())
FileMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
}
is MessageType.Image -> {
ImageMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map())
ImageMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
}
is MessageType.Notice -> {
NoticeMessageType(type.content.body, type.content.formatted?.map())
@ -60,7 +59,7 @@ class EventMessageMapper {
EmoteMessageType(type.content.body, type.content.formatted?.map())
}
is MessageType.Video -> {
VideoMessageType(type.content.body, type.content.source.useUrl(), type.content.info?.map())
VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map())
}
null -> {
UnknownMessageType

View file

@ -17,6 +17,7 @@
package io.element.android.libraries.matrix.impl.timeline.item.event
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseMessageLikeContent
import io.element.android.libraries.matrix.api.timeline.item.event.FailedToParseStateContent
import io.element.android.libraries.matrix.api.timeline.item.event.MembershipChange
@ -26,7 +27,6 @@ import io.element.android.libraries.matrix.api.timeline.item.event.RedactedConte
import io.element.android.libraries.matrix.api.timeline.item.event.RoomMembershipContent
import io.element.android.libraries.matrix.api.timeline.item.event.StateContent
import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent
import io.element.android.libraries.matrix.api.timeline.item.event.UnknownContent
import io.element.android.libraries.matrix.impl.media.map
@ -88,7 +88,7 @@ class TimelineEventContentMapper(private val eventMessageMapper: EventMessageMap
StickerContent(
body = kind.body,
info = kind.info.map(),
url = kind.url
url = kind.url,
)
}
is TimelineItemContentKind.UnableToDecrypt -> {

View file

@ -16,15 +16,16 @@
package io.element.android.libraries.matrix.test.media
import android.net.Uri
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import java.nio.file.Path
import kotlin.io.path.Path
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import java.io.File
class FakeMediaLoader : MatrixMediaLoader {
var shouldFail = false
override suspend fun loadMediaContent(url: String): Result<ByteArray> {
override suspend fun loadMediaContent(source: MatrixMediaSource): Result<ByteArray> {
return if (shouldFail) {
Result.failure(RuntimeException())
} else {
@ -32,7 +33,7 @@ class FakeMediaLoader : MatrixMediaLoader {
}
}
override suspend fun loadMediaThumbnail(url: String, width: Long, height: Long): Result<ByteArray> {
override suspend fun loadMediaThumbnail(source: MatrixMediaSource, width: Long, height: Long): Result<ByteArray> {
return if (shouldFail) {
Result.failure(RuntimeException())
} else {
@ -40,11 +41,11 @@ class FakeMediaLoader : MatrixMediaLoader {
}
}
override suspend fun loadMediaFile(url: String, mimeType: String?): Result<Path> {
override suspend fun loadMediaFile(source: MatrixMediaSource, mimeType: String?): Result<Uri> {
return if (shouldFail) {
Result.failure(RuntimeException())
} else {
return Result.success(Path("path"))
return Result.success(Uri.fromFile(File("path")))
}
}
}

View file

@ -17,13 +17,12 @@
package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
import kotlin.math.roundToLong
fun AvatarData.toMediaRequestData(): MediaRequestData? {
return url?.let {
MediaRequestData(
url = it,
kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong())
)
}
fun AvatarData.toMediaRequestData(): MediaRequestData {
return MediaRequestData(
source = url?.let { MatrixMediaSource(it) },
kind = MediaRequestData.Kind.Thumbnail(size.dp.value.roundToLong())
)
}

View file

@ -38,19 +38,15 @@ internal class CoilMediaFetcher(
ByteBuffer.wrap(data)
}.map { byteBuffer ->
imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch()
}
.fold(
{ result -> result },
{ failure -> throw failure }
)
}.getOrThrow()
}
private suspend fun loadMedia(): Result<ByteArray> {
if (mediaData == null) return Result.failure(IllegalStateException("No media data to fetch."))
if (mediaData?.source == null) return Result.failure(IllegalStateException("No media data to fetch."))
return when (mediaData.kind) {
is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(url = mediaData.url)
is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(source = mediaData.source)
is MediaRequestData.Kind.Thumbnail -> mediaLoader.loadMediaThumbnail(
url = mediaData.url,
source = mediaData.source,
width = mediaData.kind.width,
height = mediaData.kind.height
)

View file

@ -16,8 +16,10 @@
package io.element.android.libraries.matrix.ui.media
import io.element.android.libraries.matrix.api.media.MatrixMediaSource
data class MediaRequestData(
val url: String,
val source: MatrixMediaSource?,
val kind: Kind
) {

View file

@ -22,7 +22,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
internal class AvatarDataKeyer : Keyer<AvatarData> {
override fun key(data: AvatarData, options: Options): String? {
return data.toMediaRequestData()?.toKey()
return data.toMediaRequestData().toKey()
}
}
@ -32,4 +32,7 @@ internal class MediaRequestDataKeyer : Keyer<MediaRequestData> {
}
}
private fun MediaRequestData.toKey() = "${url}_${kind}"
private fun MediaRequestData.toKey(): String? {
if (source == null) return null
return "${source.url}_${kind}"
}