diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt index ee54325ce1..296beaaa7a 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt @@ -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() } diff --git a/features/preferences/impl/src/main/res/values/localazy.xml b/features/preferences/impl/src/main/res/values/localazy.xml index d7b0f6dda2..d2a03f922f 100644 --- a/features/preferences/impl/src/main/res/values/localazy.xml +++ b/features/preferences/impl/src/main/res/values/localazy.xml @@ -8,8 +8,8 @@ "Custom Element Call base URL" "Set a custom base URL for Element Call." "Invalid URL, please make sure you include the protocol (http/https) and the correct address." - "Optimize for upload" - "Media" + "Upload photos and videos faster and reduce data usage" + "Optimise media quality" "Push notification provider" "Disable the rich text editor to type Markdown manually." "Read receipts" diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt index 1634918296..843df71afb 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt @@ -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() } } diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt index b1352cd7b6..3b40e3bf3c 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt @@ -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)) } } diff --git a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt index e224955164..e0a82e7773 100644 --- a/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt +++ b/libraries/matrixui/src/test/kotlin/io/element/android/libraries/matrix/ui/messages/reply/InReplyToMetadataKtTest.kt @@ -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(), diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt index 1ea1acae82..ca61aa0845 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessor.kt @@ -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() - .first() - .file - } else { - copyToTmpFile(uri) - } + val resultFile = videoCompressor.compress(uri, shouldBeCompressed) + .onEach { + // TODO handle progress + } + .filterIsInstance() + .first() + .file val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile) val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo) return MediaUploadInfo.Video( diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt index 69d2e41baa..a08a9222e0 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt @@ -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 = 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) } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/MimeTypeUtil.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/MimeTypeUtil.kt new file mode 100644 index 0000000000..6ab327e6cb --- /dev/null +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/MimeTypeUtil.kt @@ -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 +} diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt index 7258102eb3..95d3e2c4f8 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ThumbnailFactory.kt @@ -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 diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt index 760679b32f..8db2fd878a 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt @@ -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) diff --git a/libraries/mediaupload/impl/src/test/assets/image.jpeg b/libraries/mediaupload/impl/src/test/assets/image.jpeg new file mode 100644 index 0000000000..3d75513889 --- /dev/null +++ b/libraries/mediaupload/impl/src/test/assets/image.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77276f9b174f8823eaf787ab0a659199ef5d30c0361ec8b9b4f0890adb1907a1 +size 9986336 diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt index 832d82d210..09e316bd2f 100644 --- a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt @@ -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 diff --git a/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt new file mode 100644 index 0000000000..d923d9977a --- /dev/null +++ b/libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/Asset.kt @@ -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, +) diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt index 3fa33eab93..32d92a4a76 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt @@ -83,7 +83,7 @@ class DefaultSessionPreferencesStore( override fun isSessionVerificationSkipped(): Flow = get(skipSessionVerification) { false } override suspend fun setCompressMedia(compress: Boolean) = update(compressMedia, compress) - override fun doesCompressMedia(): Flow = get(compressMedia) { false } + override fun doesCompressMedia(): Flow = get(compressMedia) { true } override suspend fun clear() { dataStoreFile.safeDelete() diff --git a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt index dde117adc0..7bb2258c6e 100644 --- a/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt +++ b/libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt @@ -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) diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png index 74f6d0bd9d..a38b090e83 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13e7793d8dd6d08e182b128a9b3dac2877557e8bdd220561d36c2ce1650b94ff -size 48107 +oid sha256:cd172b454fdaf7966e8c85481959000c28310ab2ffccd1b13dd9af3b1e392a3d +size 54935 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png index 889f655996..34d54ae010 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1bac5247c3a4990eb9155a21f72a49059cbaa93288380ba1ab6be2def8b3a6a9 -size 47876 +oid sha256:cff335c3b36ad364bf237b5635577da8f5c82e86bdd35bfb1c138e222e616640 +size 54698 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png index 1fec0eae29..348c17ddf4 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11c969c1d04150cef68da64865bade2ea3bfc1aa5f0d790262315131b57d6233 -size 31702 +oid sha256:397049b4a74bd122c09e1e16d4ee72dcf5abc6562bccfe839d394388b1b0a2b9 +size 31741 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png index 0ab8ad267c..4b27052cd0 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc1aa9348e470e9d7e7e1e838e18a3403159c2effb49fa359b2b2db97dd81961 -size 47901 +oid sha256:7b5cee26851a31850647b550225071bd0a5c4d16026b42771f43e395fc1f0dcc +size 54766 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png index a1acf97a47..4a1809c127 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90d1184879c5a34dc27cc942ec3110e14ccd9a90c152b10a1844fde0566d54fe -size 47841 +oid sha256:21378e572926adcafa78ecbf16042f1abab81790ead55c2da6134d84d3e88145 +size 54742 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png index 1ca045272d..dd123faa03 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2beb4f4f190f6aca2d4da338d980e8611283ffb9bfd2f402482f8ecc629cf22 -size 46759 +oid sha256:9dcd26f37cea379f17d4b7d35b66c8dabdef2b83afe168f261c7007149ac7044 +size 53658 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png index 52825bc9d6..5a1be87eff 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b65acfd3126efc5cd4fe1a929a786468cc18ee371cb4462ef0cf9f6e8963fcea -size 46456 +oid sha256:d38e06cbbabfd6fdab433008ec2249f2ca3838a9a95a5f6300e79fce5b805733 +size 53366 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png index 528327c715..80fb2f3e56 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e90bbde9aac7e710703e836f293a00b2a5f35447d4d63c9732de21a0f291449 -size 29336 +oid sha256:2836655a2f3ef9aca1f2ba94222bc92f9bedab82589169e34ea0a325cb1c0e52 +size 29363 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png index 5a3f3c1ed3..53a47e0baf 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6da0cf1729162fa1745bcb4dd86be06f90d3bcf6bf034e4a64e1ee9119b6cdd5 -size 46501 +oid sha256:6823de7de81b167d27c1c9c8aadab6b027976673552590f1dbea9dad7748919f +size 53413 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png index 387f0e65d3..e549902114 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66109868b1e893bc569d70110e3e587d7a17d777838cfc3e5c3d3189338d924e -size 46423 +oid sha256:fc4b71bbb0d276540c887a4c6869eca003ff500ac1d8b319d63483be21cc04d0 +size 53377