Merge pull request #3779 from element-hq/feature/bma/mediaUpload
Optimize media upload
This commit is contained in:
@@ -37,7 +37,7 @@ class AdvancedSettingsPresenter @Inject constructor(
|
||||
.collectAsState(initial = true)
|
||||
val doesCompressMedia by sessionPreferencesStore
|
||||
.doesCompressMedia()
|
||||
.collectAsState(initial = false)
|
||||
.collectAsState(initial = true)
|
||||
val theme by remember {
|
||||
appPreferencesStore.getThemeFlow().mapToTheme()
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
<string name="screen_advanced_settings_element_call_base_url">"Custom Element Call base URL"</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_description">"Set a custom base URL for Element Call."</string>
|
||||
<string name="screen_advanced_settings_element_call_base_url_validation_error">"Invalid URL, please make sure you include the protocol (http/https) and the correct address."</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Optimize for upload"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Media"</string>
|
||||
<string name="screen_advanced_settings_media_compression_description">"Upload photos and videos faster and reduce data usage"</string>
|
||||
<string name="screen_advanced_settings_media_compression_title">"Optimise media quality"</string>
|
||||
<string name="screen_advanced_settings_push_provider_android">"Push notification provider"</string>
|
||||
<string name="screen_advanced_settings_rich_text_editor_description">"Disable the rich text editor to type Markdown manually."</string>
|
||||
<string name="screen_advanced_settings_send_read_receipts">"Read receipts"</string>
|
||||
|
||||
@@ -34,7 +34,7 @@ class AdvancedSettingsPresenterTest {
|
||||
assertThat(initialState.isDeveloperModeEnabled).isFalse()
|
||||
assertThat(initialState.showChangeThemeDialog).isFalse()
|
||||
assertThat(initialState.isSharePresenceEnabled).isTrue()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
assertThat(initialState.doesCompressMedia).isTrue()
|
||||
assertThat(initialState.theme).isEqualTo(Theme.System)
|
||||
}
|
||||
}
|
||||
@@ -76,11 +76,11 @@ class AdvancedSettingsPresenterTest {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitLastSequentialItem()
|
||||
assertThat(initialState.doesCompressMedia).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(awaitItem().doesCompressMedia).isTrue()
|
||||
assertThat(initialState.doesCompressMedia).isTrue()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
|
||||
assertThat(awaitItem().doesCompressMedia).isFalse()
|
||||
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
|
||||
assertThat(awaitItem().doesCompressMedia).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import io.element.android.features.roomdetails.aMatrixRoom
|
||||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditEvents
|
||||
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditPresenter
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.room.StateEventType
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
@@ -513,7 +514,7 @@ class RoomDetailsEditPresenterTest {
|
||||
initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
|
||||
initialState.eventSink(RoomDetailsEditEvents.Save)
|
||||
skipItems(4)
|
||||
updateAvatarResult.assertions().isCalledOnce().with(value("image/jpeg"), value(fakeFileContents))
|
||||
updateAvatarResult.assertions().isCalledOnce().with(value(MimeTypes.Jpeg), value(fakeFileContents))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.media.AudioInfo
|
||||
@@ -580,7 +581,7 @@ fun anImageInfo(): ImageInfo {
|
||||
return ImageInfo(
|
||||
height = 100,
|
||||
width = 100,
|
||||
mimetype = "image/jpeg",
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 1000,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = aMediaSource(),
|
||||
|
||||
@@ -125,9 +125,13 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||
val compressionResult = imageCompressor.compressToTmpFile(
|
||||
inputStreamProvider = { contentResolver.openInputStream(uri)!! },
|
||||
resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE),
|
||||
mimeType = mimeType,
|
||||
orientation = orientation,
|
||||
).getOrThrow()
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file)
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(
|
||||
file = compressionResult.file,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
val imageInfo = compressionResult.toImageInfo(
|
||||
mimeType = mimeType,
|
||||
thumbnailResult = thumbnailResult
|
||||
@@ -142,7 +146,10 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||
|
||||
suspend fun processImageWithoutCompression(): MediaUploadInfo {
|
||||
val file = copyToTmpFile(uri)
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(file)
|
||||
val thumbnailResult = thumbnailFactory.createImageThumbnail(
|
||||
file = file,
|
||||
mimeType = mimeType,
|
||||
)
|
||||
val imageInfo = contentResolver.openInputStream(uri).use { input ->
|
||||
val bitmap = BitmapFactory.decodeStream(input, null, null)!!
|
||||
ImageInfo(
|
||||
@@ -171,17 +178,13 @@ class AndroidMediaPreProcessor @Inject constructor(
|
||||
}
|
||||
|
||||
private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo {
|
||||
val resultFile = if (shouldBeCompressed) {
|
||||
videoCompressor.compress(uri)
|
||||
.onEach {
|
||||
// TODO handle progress
|
||||
}
|
||||
.filterIsInstance<VideoTranscodingEvent.Completed>()
|
||||
.first()
|
||||
.file
|
||||
} else {
|
||||
copyToTmpFile(uri)
|
||||
}
|
||||
val resultFile = videoCompressor.compress(uri, shouldBeCompressed)
|
||||
.onEach {
|
||||
// TODO handle progress
|
||||
}
|
||||
.filterIsInstance<VideoTranscodingEvent.Completed>()
|
||||
.first()
|
||||
.file
|
||||
val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile)
|
||||
val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo)
|
||||
return MediaUploadInfo.Video(
|
||||
|
||||
@@ -34,14 +34,16 @@ class ImageCompressor @Inject constructor(
|
||||
suspend fun compressToTmpFile(
|
||||
inputStreamProvider: () -> InputStream,
|
||||
resizeMode: ResizeMode,
|
||||
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
|
||||
mimeType: String,
|
||||
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
|
||||
desiredQuality: Int = 78,
|
||||
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
|
||||
runCatching {
|
||||
val format = mimeTypeToCompressFormat(mimeType)
|
||||
val extension = mimeTypeToCompressFileExtension(mimeType)
|
||||
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()
|
||||
// Encode bitmap to the destination temporary file
|
||||
val tmpFile = context.createTmpFile(extension = "jpeg")
|
||||
val tmpFile = context.createTmpFile(extension = extension)
|
||||
tmpFile.outputStream().use {
|
||||
compressedBitmap.compress(format, desiredQuality, it)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
||||
fun mimeTypeToCompressFormat(mimeType: String) = when (mimeType) {
|
||||
MimeTypes.Png -> Bitmap.CompressFormat.PNG
|
||||
else -> Bitmap.CompressFormat.JPEG
|
||||
}
|
||||
|
||||
fun mimeTypeToCompressFileExtension(mimeType: String) = when (mimeType) {
|
||||
MimeTypes.Png -> "png"
|
||||
else -> "jpeg"
|
||||
}
|
||||
|
||||
fun mimeTypeToThumbnailMimeType(mimeType: String) = when (mimeType) {
|
||||
MimeTypes.Png -> MimeTypes.Png
|
||||
else -> MimeTypes.Jpeg
|
||||
}
|
||||
@@ -53,8 +53,11 @@ class ThumbnailFactory @Inject constructor(
|
||||
private val sdkIntProvider: BuildVersionSdkIntProvider
|
||||
) {
|
||||
@SuppressLint("NewApi")
|
||||
suspend fun createImageThumbnail(file: File): ThumbnailResult? {
|
||||
return createThumbnail { cancellationSignal ->
|
||||
suspend fun createImageThumbnail(
|
||||
file: File,
|
||||
mimeType: String,
|
||||
): ThumbnailResult? {
|
||||
return createThumbnail(mimeType = mimeType) { cancellationSignal ->
|
||||
try {
|
||||
// This API works correctly with GIF
|
||||
if (sdkIntProvider.isAtLeast(Build.VERSION_CODES.Q)) {
|
||||
@@ -83,7 +86,7 @@ class ThumbnailFactory @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun createVideoThumbnail(file: File): ThumbnailResult? {
|
||||
return createThumbnail {
|
||||
return createThumbnail(mimeType = MimeTypes.Jpeg) {
|
||||
MediaMetadataRetriever().runAndRelease {
|
||||
setDataSource(context, file.toUri())
|
||||
getFrameAtTime(VIDEO_THUMB_FRAME)
|
||||
@@ -91,7 +94,10 @@ class ThumbnailFactory @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun createThumbnail(bitmapFactory: (CancellationSignal) -> Bitmap?): ThumbnailResult? = suspendCancellableCoroutine { continuation ->
|
||||
private suspend fun createThumbnail(
|
||||
mimeType: String,
|
||||
bitmapFactory: (CancellationSignal) -> Bitmap?,
|
||||
): ThumbnailResult? = suspendCancellableCoroutine { continuation ->
|
||||
val cancellationSignal = CancellationSignal()
|
||||
continuation.invokeOnCancellation {
|
||||
cancellationSignal.cancel()
|
||||
@@ -101,9 +107,11 @@ class ThumbnailFactory @Inject constructor(
|
||||
continuation.resume(null)
|
||||
return@suspendCancellableCoroutine
|
||||
}
|
||||
val thumbnailFile = context.createTmpFile(extension = "jpeg")
|
||||
val format = mimeTypeToCompressFormat(mimeType)
|
||||
val extension = mimeTypeToCompressFileExtension(mimeType)
|
||||
val thumbnailFile = context.createTmpFile(extension = extension)
|
||||
thumbnailFile.outputStream().use { outputStream ->
|
||||
bitmapThumbnail.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
|
||||
bitmapThumbnail.compress(format, 78, outputStream)
|
||||
}
|
||||
val blurhash = BlurHash.encode(bitmapThumbnail, 3, 3)
|
||||
val thumbnailResult = ThumbnailResult(
|
||||
@@ -111,7 +119,7 @@ class ThumbnailFactory @Inject constructor(
|
||||
info = ThumbnailInfo(
|
||||
height = bitmapThumbnail.height.toLong(),
|
||||
width = bitmapThumbnail.width.toLong(),
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
mimetype = mimeTypeToThumbnailMimeType(mimeType),
|
||||
size = thumbnailFile.length()
|
||||
),
|
||||
blurhash = blurhash
|
||||
|
||||
@@ -24,12 +24,20 @@ import javax.inject.Inject
|
||||
class VideoCompressor @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun compress(uri: Uri) = callbackFlow {
|
||||
fun compress(uri: Uri, shouldBeCompressed: Boolean) = callbackFlow {
|
||||
val tmpFile = context.createTmpFile(extension = "mp4")
|
||||
val future = Transcoder.into(tmpFile.path)
|
||||
.setVideoTrackStrategy(
|
||||
DefaultVideoStrategy.Builder()
|
||||
.addResizer(AtMostResizer(720, 480))
|
||||
.addResizer(
|
||||
AtMostResizer(
|
||||
if (shouldBeCompressed) {
|
||||
720
|
||||
} else {
|
||||
1080
|
||||
}
|
||||
)
|
||||
)
|
||||
.build()
|
||||
)
|
||||
.addDataSource(context, uri)
|
||||
|
||||
BIN
libraries/mediaupload/impl/src/test/assets/image.jpeg
LFS
Normal file
BIN
libraries/mediaupload/impl/src/test/assets/image.jpeg
LFS
Normal file
Binary file not shown.
@@ -35,113 +35,194 @@ import kotlin.time.Duration
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AndroidMediaPreProcessorTest {
|
||||
@Test
|
||||
fun `test processing image`() = runTest {
|
||||
private suspend fun TestScope.process(
|
||||
asset: Asset,
|
||||
compressIfPossible: Boolean,
|
||||
sdkIntVersion: Int = Build.VERSION_CODES.P,
|
||||
deleteOriginal: Boolean = false,
|
||||
): MediaUploadInfo {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val sut = createAndroidMediaPreProcessor(context, sdkIntVersion)
|
||||
val file = getFileFromAssets(context, asset.filename)
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = false,
|
||||
compressIfPossible = true,
|
||||
mimeType = asset.mimeType,
|
||||
deleteOriginal = deleteOriginal,
|
||||
compressIfPossible = compressIfPossible,
|
||||
)
|
||||
val data = result.getOrThrow()
|
||||
assertThat(data.file.path).endsWith("image.png")
|
||||
val info = data as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 109_908,
|
||||
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4484),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K13]7q%zWC00R4of%\$baad"
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
assertThat(data.file.path).endsWith(asset.filename)
|
||||
return data
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing image api Q`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context, sdkIntVersion = Build.VERSION_CODES.Q)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = false,
|
||||
fun `test processing png`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = true,
|
||||
)
|
||||
val data = result.getOrThrow()
|
||||
assertThat(data.file.path).endsWith("image.png")
|
||||
val info = data as MediaUploadInfo.Image
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = 2_026_433,
|
||||
ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Png, size = 91),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing png api Q`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = true,
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 109_908,
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = 2_026_433,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing image no compression`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = false,
|
||||
fun `test processing png no compression`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = false,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("image.png")
|
||||
val info = result as MediaUploadInfo.Image
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 1_856_786,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643),
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = assetImagePng.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Png, size = 91),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing image and delete`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "image.png")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Png,
|
||||
deleteOriginal = true,
|
||||
fun `test processing png and delete`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImagePng,
|
||||
compressIfPossible = false,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("image.png")
|
||||
val info = result as MediaUploadInfo.Image
|
||||
deleteOriginal = true,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 1_178,
|
||||
width = 1_818,
|
||||
mimetype = MimeTypes.Png,
|
||||
size = 1_856_786,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Jpeg, size = 643),
|
||||
height = assetImagePng.height,
|
||||
width = assetImagePng.width,
|
||||
mimetype = assetImagePng.mimeType,
|
||||
size = assetImagePng.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 25, width = 25, mimetype = MimeTypes.Png, size = 91),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
// Does not work
|
||||
// assertThat(file.exists()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = true,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 979,
|
||||
width = 3006,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 84_845,
|
||||
ThumbnailInfo(height = 244, width = 751, mimetype = MimeTypes.Jpeg, size = 7_178),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K07gBzX=j_D4xZjoaSe,s:"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg api Q`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = true,
|
||||
sdkIntVersion = Build.VERSION_CODES.Q,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 979,
|
||||
width = 3_006,
|
||||
mimetype = MimeTypes.Jpeg,
|
||||
size = 84_845,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg no compression`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = false,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = assetImageJpeg.height,
|
||||
width = assetImageJpeg.width,
|
||||
mimetype = assetImageJpeg.mimeType,
|
||||
size = assetImageJpeg.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 6, width = 6, mimetype = MimeTypes.Jpeg, size = 631),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing jpeg and delete`() = runTest {
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetImageJpeg,
|
||||
compressIfPossible = false,
|
||||
deleteOriginal = true,
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = assetImageJpeg.height,
|
||||
width = assetImageJpeg.width,
|
||||
mimetype = assetImageJpeg.mimeType,
|
||||
size = assetImageJpeg.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 6, width = 6, mimetype = MimeTypes.Jpeg, size = 631),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
@@ -152,70 +233,50 @@ class AndroidMediaPreProcessorTest {
|
||||
|
||||
@Test
|
||||
fun `test processing gif`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "animated_gif.gif")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Gif,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetAnimatedGif,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("animated_gif.gif")
|
||||
val info = result as MediaUploadInfo.Image
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Image
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.imageInfo).isEqualTo(
|
||||
ImageInfo(
|
||||
height = 600,
|
||||
width = 800,
|
||||
mimetype = MimeTypes.Gif,
|
||||
size = 687_979,
|
||||
height = assetAnimatedGif.height,
|
||||
width = assetAnimatedGif.width,
|
||||
mimetype = assetAnimatedGif.mimeType,
|
||||
size = assetAnimatedGif.size,
|
||||
thumbnailInfo = ThumbnailInfo(height = 50, width = 50, mimetype = MimeTypes.Jpeg, size = 691),
|
||||
thumbnailSource = null,
|
||||
blurhash = "K00000fQfQfQfQfQfQfQfQ",
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing file`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "text.txt")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.PlainText,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetText,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("text.txt")
|
||||
val info = result as MediaUploadInfo.AnyFile
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.AnyFile
|
||||
assertThat(info.fileInfo).isEqualTo(
|
||||
FileInfo(
|
||||
mimetype = MimeTypes.PlainText,
|
||||
size = 13,
|
||||
mimetype = assetText.mimeType,
|
||||
size = assetText.size,
|
||||
thumbnailInfo = null,
|
||||
thumbnailSource = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Ignore("Compressing video is not working with Robolectric")
|
||||
@Test
|
||||
fun `test processing video`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "video.mp4")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Mp4,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetVideo,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("video.mp4")
|
||||
val info = result as MediaUploadInfo.Video
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Video
|
||||
assertThat(info.thumbnailFile).isNotNull()
|
||||
assertThat(info.videoInfo).isEqualTo(
|
||||
VideoInfo(
|
||||
@@ -230,22 +291,16 @@ class AndroidMediaPreProcessorTest {
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Ignore("Compressing video is not working with Robolectric")
|
||||
@Test
|
||||
fun `test processing video no compression`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "video.mp4")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Mp4,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetVideo,
|
||||
compressIfPossible = false,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("video.mp4")
|
||||
val info = result as MediaUploadInfo.Video
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Video
|
||||
// Computing thumbnailFile is failing with Robolectric
|
||||
assertThat(info.thumbnailFile).isNull()
|
||||
assertThat(info.videoInfo).isEqualTo(
|
||||
@@ -263,22 +318,15 @@ class AndroidMediaPreProcessorTest {
|
||||
blurhash = null,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test processing audio`() = runTest {
|
||||
val context = InstrumentationRegistry.getInstrumentation().context
|
||||
val sut = createAndroidMediaPreProcessor(context)
|
||||
val file = getFileFromAssets(context, "sample3s.mp3")
|
||||
val result = sut.process(
|
||||
uri = file.toUri(),
|
||||
mimeType = MimeTypes.Mp3,
|
||||
deleteOriginal = false,
|
||||
val mediaUploadInfo = process(
|
||||
asset = assetAudio,
|
||||
compressIfPossible = true,
|
||||
).getOrThrow()
|
||||
assertThat(result.file.path).endsWith("sample3s.mp3")
|
||||
val info = result as MediaUploadInfo.Audio
|
||||
)
|
||||
val info = mediaUploadInfo as MediaUploadInfo.Audio
|
||||
assertThat(info.audioInfo).isEqualTo(
|
||||
AudioInfo(
|
||||
// Not available with Robolectric?
|
||||
@@ -287,7 +335,6 @@ class AndroidMediaPreProcessorTest {
|
||||
mimetype = MimeTypes.Mp3,
|
||||
)
|
||||
)
|
||||
assertThat(file.exists()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.mediaupload.impl
|
||||
|
||||
import io.element.android.libraries.core.mimetype.MimeTypes
|
||||
|
||||
data class Asset(
|
||||
val filename: String,
|
||||
val mimeType: String,
|
||||
val size: Long,
|
||||
val width: Long?,
|
||||
val height: Long?,
|
||||
)
|
||||
|
||||
/**
|
||||
* "image.png" is a 1_818 x 1_178 PNG image with a size of 1_856_786 bytes.
|
||||
*/
|
||||
val assetImagePng = Asset(
|
||||
filename = "image.png",
|
||||
mimeType = MimeTypes.Png,
|
||||
size = 1_856_786,
|
||||
width = 1_818,
|
||||
height = 1_178,
|
||||
)
|
||||
|
||||
/**
|
||||
* "image.jpeg" is a 12_024 x 3_916, JPEG image with a size of 9_986_336 bytes.
|
||||
*/
|
||||
val assetImageJpeg = Asset(
|
||||
filename = "image.jpeg",
|
||||
mimeType = MimeTypes.Jpeg,
|
||||
size = 9_986_336,
|
||||
width = 12_024,
|
||||
height = 3_916,
|
||||
)
|
||||
|
||||
/**
|
||||
* "video.mp4" is a 1_280 x 720, MP4 video with a size of 1_673_712 bytes.
|
||||
*/
|
||||
val assetVideo = Asset(
|
||||
filename = "video.mp4",
|
||||
mimeType = MimeTypes.Mp4,
|
||||
size = 1_673_712,
|
||||
width = 1_280,
|
||||
height = 720,
|
||||
)
|
||||
|
||||
/**
|
||||
* "sample3s.mp3" is a 3 seconds MP3 audio file with a size of 52_079 bytes.
|
||||
*/
|
||||
val assetAudio = Asset(
|
||||
filename = "sample3s.mp3",
|
||||
mimeType = MimeTypes.Mp3,
|
||||
size = 52_079,
|
||||
width = null,
|
||||
height = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* "text.txt" is a 13 bytes text file.
|
||||
*/
|
||||
val assetText = Asset(
|
||||
filename = "text.txt",
|
||||
mimeType = MimeTypes.PlainText,
|
||||
size = 13,
|
||||
width = null,
|
||||
height = null,
|
||||
)
|
||||
|
||||
/**
|
||||
* "animated_gif.gif" is a 800 x 600, GIF image with a size of 687_979 bytes.
|
||||
*/
|
||||
val assetAnimatedGif = Asset(
|
||||
filename = "animated_gif.gif",
|
||||
mimeType = MimeTypes.Gif,
|
||||
size = 687_979,
|
||||
width = 800,
|
||||
height = 600,
|
||||
)
|
||||
@@ -83,7 +83,7 @@ class DefaultSessionPreferencesStore(
|
||||
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
||||
|
||||
override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress)
|
||||
override fun doesCompressMedia(): Flow<Boolean> = get(compressMedia) { false }
|
||||
override fun doesCompressMedia(): Flow<Boolean> = get(compressMedia) { true }
|
||||
|
||||
override suspend fun clear() {
|
||||
dataStoreFile.safeDelete()
|
||||
|
||||
@@ -18,7 +18,7 @@ class InMemorySessionPreferencesStore(
|
||||
isSendTypingNotificationsEnabled: Boolean = true,
|
||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||
isSessionVerificationSkipped: Boolean = false,
|
||||
doesCompressMedia: Boolean = false,
|
||||
doesCompressMedia: Boolean = true,
|
||||
) : SessionPreferencesStore {
|
||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user