Don't compress images sent through the Files attachment picker (#6755)

* Don't compress images sent through the Files attachment picker

Images and videos picked through the "Attachment" picker are now
uploaded without re-encoding, regardless of the "Optimize media quality"
setting. The gallery and camera pickers keep the existing behaviour,
matching what Element Web/Desktop and most other messengers do.

Fixes #6365

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Make sure we select the right video preset for sending as file

Wait for the video size estimations to be calculated before preprocessing the video file

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
cizra 2026-05-20 15:19:08 +00:00 committed by GitHub
parent 933b18f6c2
commit a33d717aa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 357 additions and 30 deletions

View file

@ -16,5 +16,12 @@ import kotlinx.parcelize.Parcelize
@Immutable
sealed interface Attachment : Parcelable {
@Parcelize
data class Media(val localMedia: LocalMedia) : Attachment
data class Media(
val localMedia: LocalMedia,
// When true, the media was picked through the "Files" picker and should be
// uploaded without image recompression; videos still use the highest available
// / best-fit preset rather than an additional size-reduction optimization pass.
// See https://github.com/element-hq/element-x-android/issues/6365
val sendAsFile: Boolean = false,
) : Attachment
}

View file

@ -23,6 +23,8 @@ import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorPresenter
import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState
import io.element.android.features.messages.impl.attachments.video.VideoCompressionPresetSelector
import io.element.android.libraries.androidutils.file.TemporaryUriDeleter
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.androidutils.hash.hash
@ -61,6 +63,7 @@ class AttachmentsPreviewPresenter(
private val permalinkBuilder: PermalinkBuilder,
private val temporaryUriDeleter: TemporaryUriDeleter,
private val mediaOptimizationSelectorPresenterFactory: MediaOptimizationSelectorPresenter.Factory,
private val videoCompressionPresetSelector: VideoCompressionPresetSelector,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
@ -96,7 +99,10 @@ class AttachmentsPreviewPresenter(
val mediaAttachment = attachment as Attachment.Media
val mediaOptimizationSelectorPresenter = remember {
mediaOptimizationSelectorPresenterFactory.create(mediaAttachment.localMedia)
mediaOptimizationSelectorPresenterFactory.create(
localMedia = mediaAttachment.localMedia,
sendAsFile = mediaAttachment.sendAsFile,
)
}
val mediaOptimizationSelectorState by rememberUpdatedState(mediaOptimizationSelectorPresenter.present())
@ -104,14 +110,25 @@ class AttachmentsPreviewPresenter(
var displayFileTooLargeError by remember { mutableStateOf(false) }
LaunchedEffect(mediaOptimizationSelectorState.displayMediaSelectorViews) {
LaunchedEffect(
mediaOptimizationSelectorState.displayMediaSelectorViews,
mediaOptimizationSelectorState.videoSizeEstimations,
) {
// If the media optimization selector is not displayed, we can pre-process the media
// to prepare it for sending. This is done to avoid blocking the UI thread when the
// user clicks on the send button.
if (mediaOptimizationSelectorState.displayMediaSelectorViews == false) {
preprocessMediaJob = preProcessAttachment(
if (mediaOptimizationSelectorState.displayMediaSelectorViews == false && preprocessMediaJob == null) {
if (mediaAttachment.localMedia.info.mimeType.isMimeTypeVideo() && mediaOptimizationSelectorState.videoSizeEstimations.dataOrNull() == null) {
Timber.d("Waiting for video size estimations to be able to select the best video compression preset before pre-processing the media")
return@LaunchedEffect
}
val config = getAutoPreprocessMediaOptimizationConfig(
mediaAttachment = mediaAttachment,
mediaOptimizationSelectorState = mediaOptimizationSelectorState,
) ?: return@LaunchedEffect
preprocessMediaJob = coroutineScope.preProcessAttachment(
attachment = attachment,
mediaOptimizationConfig = mediaOptimizationConfigProvider.get(),
mediaOptimizationConfig = config,
displayProgress = false,
sendActionState = sendActionState,
)
@ -233,6 +250,28 @@ class AttachmentsPreviewPresenter(
)
}
private suspend fun getAutoPreprocessMediaOptimizationConfig(
mediaAttachment: Attachment.Media,
mediaOptimizationSelectorState: MediaOptimizationSelectorState,
): MediaOptimizationConfig? {
return if (mediaAttachment.sendAsFile) {
// If we're sending the media as a file, we can skip image compression and we should select the highest video compression preset that still fits
// the upload limit (if the estimations are available)
val videoCompressionPreset = videoCompressionPresetSelector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.HIGH,
videoSizeEstimations = mediaOptimizationSelectorState.videoSizeEstimations,
).dataOrNull() ?: VideoCompressionPreset.HIGH
MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = videoCompressionPreset,
)
} else {
// Otherwise, we just rely on the user preferences for media optimization
mediaOptimizationConfigProvider.get()
}
}
private fun CoroutineScope.preProcessAttachment(
attachment: Attachment,
mediaOptimizationConfig: MediaOptimizationConfig,

View file

@ -37,9 +37,11 @@ import kotlin.math.roundToLong
@AssistedInject
class DefaultMediaOptimizationSelectorPresenter(
@Assisted private val localMedia: LocalMedia,
@Assisted private val sendAsFile: Boolean,
private val maxUploadSizeProvider: MaxUploadSizeProvider,
private val featureFlagService: FeatureFlagService,
private val mediaOptimizationConfigProvider: MediaOptimizationConfigProvider,
private val videoCompressionPresetSelector: VideoCompressionPresetSelector,
mediaExtractorFactory: VideoMetadataExtractor.Factory,
) : MediaOptimizationSelectorPresenter {
@ContributesBinding(SessionScope::class)
@ -47,6 +49,7 @@ class DefaultMediaOptimizationSelectorPresenter(
interface Factory : MediaOptimizationSelectorPresenter.Factory {
override fun create(
localMedia: LocalMedia,
sendAsFile: Boolean,
): DefaultMediaOptimizationSelectorPresenter
}
@ -55,7 +58,9 @@ class DefaultMediaOptimizationSelectorPresenter(
@Composable
override fun present(): MediaOptimizationSelectorState {
val displayMediaSelectorViews by produceState<Boolean?>(null) {
value = featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality)
// When sending as a raw file, never show the optimization selector: images skip
// recompression, while videos use the highest available best-fit preset.
value = !sendAsFile && featureFlagService.isFeatureEnabled(FeatureFlags.SelectableMediaQuality)
}
var displayVideoPresetSelectorDialog by remember { mutableStateOf(false) }
@ -123,12 +128,23 @@ class DefaultMediaOptimizationSelectorPresenter(
var selectedVideoOptimizationPreset by remember { mutableStateOf<AsyncData<VideoCompressionPreset>>(AsyncData.Loading()) }
LaunchedEffect(videoSizeEstimations.dataOrNull()) {
if (sendAsFile) {
// Send-as-file path: pin to no image compression, and pick the highest-quality
// video preset that still fits the upload limit (we have no true "do not re-encode
// video" path in the pre-processor right now).
selectedImageOptimization = AsyncData.Success(false)
selectedVideoOptimizationPreset = videoCompressionPresetSelector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.HIGH,
videoSizeEstimations = videoSizeEstimations,
)
return@LaunchedEffect
}
val mediaOptimizationConfig = mediaOptimizationConfigProvider.get()
selectedImageOptimization = AsyncData.Success(mediaOptimizationConfig.compressImages)
// Find the best video preset based on the default preset and the video size estimations
// Since the estimation for the current preset may be way too large to upload, we check the ones that provide lower file sizes
selectedVideoOptimizationPreset = findBestVideoPreset(
defaultVideoPreset = mediaOptimizationConfig.videoCompressionPreset,
selectedVideoOptimizationPreset = videoCompressionPresetSelector.selectBestVideoPreset(
expectedVideoPreset = mediaOptimizationConfig.videoCompressionPreset,
videoSizeEstimations = videoSizeEstimations,
)
}
@ -176,20 +192,4 @@ class DefaultMediaOptimizationSelectorPresenter(
eventSink = ::handleEvent,
)
}
private fun findBestVideoPreset(
defaultVideoPreset: VideoCompressionPreset,
videoSizeEstimations: AsyncData<ImmutableList<VideoUploadEstimation>>,
): AsyncData<VideoCompressionPreset> {
val estimations = videoSizeEstimations.dataOrNull() ?: return AsyncData.Loading()
// This will find the best video preset that can be used to produce a video that can be uploaded
val bestEstimation = estimations.find { it.preset.ordinal >= defaultVideoPreset.ordinal && it.canUpload }?.preset
return if (bestEstimation != null) {
AsyncData.Success(bestEstimation)
} else {
AsyncData.Failure(
IllegalStateException("No suitable video preset found for default preset: $defaultVideoPreset")
)
}
}
}

View file

@ -15,6 +15,7 @@ fun interface MediaOptimizationSelectorPresenter : Presenter<MediaOptimizationSe
interface Factory {
fun create(
localMedia: LocalMedia,
sendAsFile: Boolean,
): MediaOptimizationSelectorPresenter
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.impl.attachments.video
import dev.zacsweers.metro.Inject
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.collections.immutable.ImmutableList
@Inject
class VideoCompressionPresetSelector {
fun selectBestVideoPreset(
expectedVideoPreset: VideoCompressionPreset,
videoSizeEstimations: AsyncData<ImmutableList<VideoUploadEstimation>>,
): AsyncData<VideoCompressionPreset> {
val estimations = videoSizeEstimations.dataOrNull() ?: return AsyncData.Loading()
val bestEstimation = estimations.find { it.preset.ordinal >= expectedVideoPreset.ordinal && it.canUpload }?.preset
return if (bestEstimation != null) {
AsyncData.Success(bestEstimation)
} else {
AsyncData.Failure(
IllegalStateException("No suitable video preset found for expected preset: $expectedVideoPreset")
)
}
}
}

View file

@ -179,7 +179,7 @@ class MessageComposerPresenter(
handlePickedMedia(uri, mimeType)
}
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType ->
handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream)
handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
handlePickedMedia(uri, MimeTypes.Jpeg)
@ -605,6 +605,7 @@ class MessageComposerPresenter(
private fun handlePickedMedia(
uri: Uri?,
mimeType: String? = null,
sendAsFile: Boolean = false,
) {
uri ?: return
val localMedia = localMediaFactory.createFromUri(
@ -613,7 +614,7 @@ class MessageComposerPresenter(
name = null,
formattedFileSize = null
)
val mediaAttachment = Attachment.Media(localMedia)
val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile)
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
navigator.navigateToPreviewAttachments(persistentListOf(mediaAttachment), inReplyToEventId)

View file

@ -17,6 +17,7 @@ import io.element.android.features.messages.impl.attachments.preview.Attachments
import io.element.android.features.messages.impl.attachments.preview.OnDoneListener
import io.element.android.features.messages.impl.attachments.preview.SendActionState
import io.element.android.features.messages.impl.attachments.video.MediaOptimizationSelectorState
import io.element.android.features.messages.impl.attachments.video.VideoCompressionPresetSelector
import io.element.android.features.messages.impl.attachments.video.VideoUploadEstimation
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
import io.element.android.features.messages.test.attachments.video.FakeMediaOptimizationSelectorPresenterFactory
@ -45,6 +46,7 @@ import io.element.android.libraries.mediaupload.test.FakeMediaOptimizationConfig
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.api.aVideoMediaInfo
import io.element.android.libraries.mediaviewer.api.anApkMediaInfo
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
@ -548,10 +550,87 @@ class AttachmentsPreviewPresenterTest {
}
}
@Test
fun `present - sendAsFile attachment is pre-processed without image compression`() = runTest {
// Even though the user has enabled "Optimize media quality" globally, picking the file
// through the Files picker (sendAsFile = true) must skip compression. Regression test
// for https://github.com/element-hq/element-x-android/issues/6365
val mediaPreProcessor = FakeMediaPreProcessor()
val presenter = createAttachmentsPreviewPresenter(
localMedia = aLocalMedia(mockMediaUrl, anImageMediaInfo()),
sendAsFile = true,
mediaPreProcessor = mediaPreProcessor,
// Selector views are hidden in the sendAsFile flow, which triggers the auto pre-process path.
displayMediaQualitySelectorViews = false,
mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(
config = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
)
),
)
presenter.test {
consumeItemsUntilPredicate { mediaPreProcessor.processCallCount > 0 }
assertThat(mediaPreProcessor.lastMediaOptimizationConfig).isEqualTo(
MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.HIGH,
)
)
}
}
@Test
fun `present - sendAsFile video is pre-processed with best fitting preset`() = runTest {
val mediaPreProcessor = FakeMediaPreProcessor()
val presenter = createAttachmentsPreviewPresenter(
localMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()),
sendAsFile = true,
mediaPreProcessor = mediaPreProcessor,
// Selector views are hidden in the sendAsFile flow, which triggers the auto pre-process path.
displayMediaQualitySelectorViews = false,
mediaOptimizationSelectorPresenterFactory = FakeMediaOptimizationSelectorPresenterFactory {
MediaOptimizationSelectorState(
maxUploadSize = AsyncData.Success(250_000_000L),
videoSizeEstimations = AsyncData.Success(
persistentListOf(
VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 513_216_000L, canUpload = false),
VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 228_096_000L, canUpload = true),
VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 57_024_000L, canUpload = true),
)
),
isImageOptimizationEnabled = false,
selectedVideoPreset = VideoCompressionPreset.STANDARD,
displayMediaSelectorViews = false,
displayVideoPresetSelectorDialog = false,
eventSink = {},
)
},
mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(
config = MediaOptimizationConfig(
compressImages = true,
videoCompressionPreset = VideoCompressionPreset.LOW,
)
),
)
presenter.test {
consumeItemsUntilPredicate { mediaPreProcessor.processCallCount > 0 }
assertThat(mediaPreProcessor.lastMediaOptimizationConfig).isEqualTo(
MediaOptimizationConfig(
compressImages = false,
videoCompressionPreset = VideoCompressionPreset.STANDARD,
)
)
}
}
private fun TestScope.createAttachmentsPreviewPresenter(
localMedia: LocalMedia = aLocalMedia(
uri = mockMediaUrl,
),
sendAsFile: Boolean = false,
room: JoinedRoom = FakeJoinedRoom(),
timelineMode: Timeline.Mode = Timeline.Mode.Live,
permalinkBuilder: PermalinkBuilder = FakePermalinkBuilder(),
@ -573,9 +652,10 @@ class AttachmentsPreviewPresenterTest {
}
),
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
videoCompressionPresetSelector: VideoCompressionPresetSelector = VideoCompressionPresetSelector(),
): AttachmentsPreviewPresenter {
return AttachmentsPreviewPresenter(
attachment = aMediaAttachment(localMedia),
attachment = aMediaAttachment(localMedia, sendAsFile = sendAsFile),
onDoneListener = onDoneListener,
mediaSenderFactory = MediaSenderFactory { timelineMode ->
DefaultMediaSender(
@ -592,6 +672,7 @@ class AttachmentsPreviewPresenterTest {
sessionCoroutineScope = this,
dispatchers = testCoroutineDispatchers(),
mediaOptimizationSelectorPresenterFactory = mediaOptimizationSelectorPresenterFactory,
videoCompressionPresetSelector = videoCompressionPresetSelector,
timelineMode = timelineMode,
inReplyToEventId = null,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,

View file

@ -210,19 +210,88 @@ class DefaultMediaOptimizationSelectorPresenterTest {
}
}
@Test
fun `present - sendAsFile hides selector views and disables image compression for images`() = runTest {
val presenter = createDefaultMediaOptimizationSelectorPresenter(
localMedia = aLocalMedia(mockMediaUrl, anImageMediaInfo()),
// Even with the feature flag on, sendAsFile must hide the selector.
featureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)),
// And it must override the user's "optimize images" preference.
mediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
sendAsFile = true,
)
presenter.test {
// Initial loading state
skipItems(1)
awaitItem().run {
assertThat(displayMediaSelectorViews).isFalse()
assertThat(isImageOptimizationEnabled).isFalse()
}
}
}
@Test
fun `present - sendAsFile picks HIGH video preset when the video fits the upload limit`() = runTest {
val presenter = createDefaultMediaOptimizationSelectorPresenter(
// Plenty of room: even HIGH preset will fit.
maxUploadSizeProvider = MaxUploadSizeProvider { Result.success(Long.MAX_VALUE) },
mediaExtractorFactory = FakeVideoMetadataExtractorFactory(
FakeVideoMetadataExtractor(
sizeResult = Result.success(Size(1920, 1080)),
duration = Result.success(10.minutes)
)
),
sendAsFile = true,
)
presenter.test {
// Initial loading state, then the one with size estimations loaded.
skipItems(1)
awaitItem().run {
assertThat(displayMediaSelectorViews).isFalse()
assertThat(selectedVideoPreset).isEqualTo(VideoCompressionPreset.HIGH)
}
}
}
@Test
fun `present - sendAsFile picks lower video preset when HIGH exceeds the upload limit`() = runTest {
val presenter = createDefaultMediaOptimizationSelectorPresenter(
maxUploadSizeProvider = MaxUploadSizeProvider { Result.success(250_000_000L) },
mediaExtractorFactory = FakeVideoMetadataExtractorFactory(
FakeVideoMetadataExtractor(
sizeResult = Result.success(Size(1920, 1080)),
duration = Result.success(10.minutes)
)
),
sendAsFile = true,
)
presenter.test {
// Initial loading state, then the one with size estimations loaded.
skipItems(1)
awaitItem().run {
assertThat(displayMediaSelectorViews).isFalse()
assertThat(selectedVideoPreset).isEqualTo(VideoCompressionPreset.STANDARD)
}
}
}
private fun createDefaultMediaOptimizationSelectorPresenter(
localMedia: LocalMedia = aLocalMedia(mockMediaUrl, aVideoMediaInfo()),
maxUploadSizeProvider: MaxUploadSizeProvider = MaxUploadSizeProvider { Result.success(1_000L) },
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(mapOf(FeatureFlags.SelectableMediaQuality.key to true)),
mediaExtractorFactory: FakeVideoMetadataExtractorFactory = FakeVideoMetadataExtractorFactory(),
mediaOptimizationConfigProvider: FakeMediaOptimizationConfigProvider = FakeMediaOptimizationConfigProvider(),
videoCompressionPresetSelector: VideoCompressionPresetSelector = VideoCompressionPresetSelector(),
sendAsFile: Boolean = false,
): DefaultMediaOptimizationSelectorPresenter {
return DefaultMediaOptimizationSelectorPresenter(
localMedia = localMedia,
sendAsFile = sendAsFile,
maxUploadSizeProvider = maxUploadSizeProvider,
featureFlagService = featureFlagService,
mediaExtractorFactory = mediaExtractorFactory,
mediaOptimizationConfigProvider = mediaOptimizationConfigProvider,
videoCompressionPresetSelector = videoCompressionPresetSelector,
)
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (c) 2026 Element Creations Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.messages.impl.attachments.video
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.collections.immutable.persistentListOf
import org.junit.Test
class VideoCompressionPresetSelectorTest {
private val selector = VideoCompressionPresetSelector()
@Test
fun `selectBestVideoPreset - returns expected preset when it can upload`() {
val result = selector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.HIGH,
videoSizeEstimations = AsyncData.Success(
persistentListOf(
VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = true),
VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = true),
VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = true),
)
)
)
assertThat(result.dataOrNull()).isEqualTo(VideoCompressionPreset.HIGH)
}
@Test
fun `selectBestVideoPreset - falls back to the highest fitting preset`() {
val result = selector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.HIGH,
videoSizeEstimations = AsyncData.Success(
persistentListOf(
VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = false),
VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = true),
VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = true),
)
)
)
assertThat(result.dataOrNull()).isEqualTo(VideoCompressionPreset.STANDARD)
}
@Test
fun `selectBestVideoPreset - starts from the expected preset`() {
val result = selector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.STANDARD,
videoSizeEstimations = AsyncData.Success(
persistentListOf(
VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = true),
VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = true),
VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = true),
)
)
)
assertThat(result.dataOrNull()).isEqualTo(VideoCompressionPreset.STANDARD)
}
@Test
fun `selectBestVideoPreset - returns failure when no preset can upload`() {
val result = selector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.HIGH,
videoSizeEstimations = AsyncData.Success(
persistentListOf(
VideoUploadEstimation(VideoCompressionPreset.HIGH, sizeInBytes = 100, canUpload = false),
VideoUploadEstimation(VideoCompressionPreset.STANDARD, sizeInBytes = 50, canUpload = false),
VideoUploadEstimation(VideoCompressionPreset.LOW, sizeInBytes = 25, canUpload = false),
)
)
)
assertThat(result).isInstanceOf(AsyncData.Failure::class.java)
}
@Test
fun `selectBestVideoPreset - returns loading while estimations are missing`() {
val result = selector.selectBestVideoPreset(
expectedVideoPreset = VideoCompressionPreset.HIGH,
videoSizeEstimations = AsyncData.Loading(),
)
assertThat(result).isInstanceOf(AsyncData.Loading::class.java)
}
}

View file

@ -11,6 +11,7 @@ package io.element.android.features.messages.impl.fixtures
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media(
fun aMediaAttachment(localMedia: LocalMedia, sendAsFile: Boolean = false) = Attachment.Media(
localMedia = localMedia,
sendAsFile = sendAsFile,
)

View file

@ -26,7 +26,7 @@ class FakeMediaOptimizationSelectorPresenterFactory(
)
}
) : MediaOptimizationSelectorPresenter.Factory {
override fun create(localMedia: LocalMedia): MediaOptimizationSelectorPresenter {
override fun create(localMedia: LocalMedia, sendAsFile: Boolean): MediaOptimizationSelectorPresenter {
return fakePresenter
}
}

View file

@ -31,6 +31,10 @@ class FakeMediaPreProcessor(
var cleanUpCallCount = 0
private set
/** The [MediaOptimizationConfig] passed to the most recent [process] call, or `null` if it was never called. */
var lastMediaOptimizationConfig: MediaOptimizationConfig? = null
private set
private var result: Result<MediaUploadInfo> = Result.success(
MediaUploadInfo.AnyFile(
File("test"),
@ -51,6 +55,7 @@ class FakeMediaPreProcessor(
): Result<MediaUploadInfo> = simulateLongTask {
processLatch?.await()
processCallCount++
lastMediaOptimizationConfig = mediaOptimizationConfig
result
}