Browse Source

Merge pull request #3744 from element-hq/feature/bma/resizeMedia

Add setting to compress image and video
pull/3702/merge
Benoit Marty 18 hours ago committed by GitHub
parent
commit
870ae5fb54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt
  2. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt
  3. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt
  4. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  5. 6
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt
  6. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt
  7. 2
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt
  8. 3
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt
  9. 1
      features/preferences/impl/build.gradle.kts
  10. 1
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt
  11. 7
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt
  12. 1
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt
  13. 9
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt
  14. 26
      features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt
  15. 16
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt
  16. 62
      features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt
  17. 1
      features/share/impl/build.gradle.kts
  18. 9
      features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt
  19. 4
      features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt
  20. 2
      gradle/libs.versions.toml
  21. 2
      libraries/mediaupload/api/build.gradle.kts
  22. 8
      libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt
  23. 18
      libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt
  24. 2
      libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt
  25. 2
      libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt
  26. 6
      libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt
  27. 3
      libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt
  28. 4
      libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt
  29. 6
      libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt
  30. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png
  31. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png
  32. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png
  33. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png
  34. 3
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png
  35. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png
  36. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png
  37. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png
  38. 4
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png
  39. 3
      tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png

2
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/Attachment.kt

@ -15,5 +15,5 @@ import kotlinx.parcelize.Parcelize @@ -15,5 +15,5 @@ import kotlinx.parcelize.Parcelize
@Immutable
sealed interface Attachment : Parcelable {
@Parcelize
data class Media(val localMedia: LocalMedia, val compressIfPossible: Boolean) : Attachment
data class Media(val localMedia: LocalMedia) : Attachment
}

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewPresenter.kt

@ -96,7 +96,6 @@ class AttachmentsPreviewPresenter @AssistedInject constructor( @@ -96,7 +96,6 @@ class AttachmentsPreviewPresenter @AssistedInject constructor(
mediaSender.sendMedia(
uri = mediaAttachment.localMedia.uri,
mimeType = mediaAttachment.localMedia.info.mimeType,
compressIfPossible = mediaAttachment.compressIfPossible,
progressCallback = progressCallback
).getOrThrow()
}.fold(

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/attachments/preview/AttachmentsPreviewStateProvider.kt

@ -31,7 +31,6 @@ fun anAttachmentsPreviewState( @@ -31,7 +31,6 @@ fun anAttachmentsPreviewState(
) = AttachmentsPreviewState(
attachment = Attachment.Media(
localMedia = LocalMedia("file://path".toUri(), mediaInfo),
compressIfPossible = true
),
sendActionState = sendActionState,
eventSink = {}

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -169,7 +169,7 @@ class MessageComposerPresenter @Inject constructor( @@ -169,7 +169,7 @@ class MessageComposerPresenter @Inject constructor(
handlePickedMedia(attachmentsState, uri, mimeType)
}
val filesPicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri ->
handlePickedMedia(attachmentsState, uri, compressIfPossible = false)
handlePickedMedia(attachmentsState, uri)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
handlePickedMedia(attachmentsState, uri, MimeTypes.IMAGE_JPEG)
@ -294,7 +294,6 @@ class MessageComposerPresenter @Inject constructor( @@ -294,7 +294,6 @@ class MessageComposerPresenter @Inject constructor(
name = null,
formattedFileSize = null
),
compressIfPossible = true
),
attachmentState = attachmentsState,
)
@ -493,7 +492,6 @@ class MessageComposerPresenter @Inject constructor( @@ -493,7 +492,6 @@ class MessageComposerPresenter @Inject constructor(
attachmentsState: MutableState<AttachmentsState>,
uri: Uri?,
mimeType: String? = null,
compressIfPossible: Boolean = true,
) {
if (uri == null) {
attachmentsState.value = AttachmentsState.None
@ -505,7 +503,7 @@ class MessageComposerPresenter @Inject constructor( @@ -505,7 +503,7 @@ class MessageComposerPresenter @Inject constructor(
name = null,
formattedFileSize = null
)
val mediaAttachment = Attachment.Media(localMedia, compressIfPossible)
val mediaAttachment = Attachment.Media(localMedia)
val isPreviewable = when {
MimeTypes.isImage(localMedia.info.mimeType) -> true
MimeTypes.isVideo(localMedia.info.mimeType) -> true
@ -535,7 +533,6 @@ class MessageComposerPresenter @Inject constructor( @@ -535,7 +533,6 @@ class MessageComposerPresenter @Inject constructor(
mediaSender.sendMedia(
uri = uri,
mimeType = mimeType,
compressIfPossible = false,
progressCallback = progressCallback
).getOrThrow()
}

6
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/attachments/AttachmentsPreviewPresenterTest.kt

@ -17,6 +17,7 @@ import com.google.common.truth.Truth.assertThat @@ -17,6 +17,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewEvents
import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewPresenter
import io.element.android.features.messages.impl.attachments.preview.SendActionState
import io.element.android.features.messages.impl.fixtures.aMediaAttachment
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
@ -26,6 +27,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender @@ -26,6 +27,7 @@ import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.test.viewer.aLocalMedia
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.mockk.mockk
@ -120,8 +122,8 @@ class AttachmentsPreviewPresenterTest { @@ -120,8 +122,8 @@ class AttachmentsPreviewPresenterTest {
room: MatrixRoom = FakeMatrixRoom()
): AttachmentsPreviewPresenter {
return AttachmentsPreviewPresenter(
attachment = Attachment.Media(localMedia, compressIfPossible = false),
mediaSender = MediaSender(mediaPreProcessor, room)
attachment = aMediaAttachment(localMedia),
mediaSender = MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore())
)
}
}

3
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/MediaAttachmentFixtures.kt

@ -10,7 +10,6 @@ package io.element.android.features.messages.impl.fixtures @@ -10,7 +10,6 @@ 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, compressIfPossible: Boolean = true) = Attachment.Media(
fun aMediaAttachment(localMedia: LocalMedia) = Attachment.Media(
localMedia = localMedia,
compressIfPossible = compressIfPossible,
)

2
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenterTest.kt

@ -1489,7 +1489,7 @@ class MessageComposerPresenterTest { @@ -1489,7 +1489,7 @@ class MessageComposerPresenterTest {
featureFlagService,
sessionPreferencesStore,
localMediaFactory,
MediaSender(mediaPreProcessor, room),
MediaSender(mediaPreProcessor, room, InMemorySessionPreferencesStore()),
snackbarDispatcher,
analyticsService,
DefaultMessageComposerContext(),

3
features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/voicemessages/composer/VoiceMessageComposerPresenterTest.kt

@ -30,6 +30,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -30,6 +30,7 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.api.aPermissionsState
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.libraries.textcomposer.model.MessageComposerMode
import io.element.android.libraries.textcomposer.model.VoiceMessagePlayerEvent
import io.element.android.libraries.textcomposer.model.VoiceMessageRecorderEvent
@ -61,7 +62,7 @@ class VoiceMessageComposerPresenterTest { @@ -61,7 +62,7 @@ class VoiceMessageComposerPresenterTest {
sendMediaResult = sendMediaResult
)
private val mediaPreProcessor = FakeMediaPreProcessor().apply { givenAudioResult() }
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom)
private val mediaSender = MediaSender(mediaPreProcessor, matrixRoom, InMemorySessionPreferencesStore())
private val messageComposerContext = FakeMessageComposerContext()
companion object {

1
features/preferences/impl/build.gradle.kts

@ -55,6 +55,7 @@ dependencies { @@ -55,6 +55,7 @@ dependencies {
implementation(projects.features.deactivation.api)
implementation(projects.features.roomlist.api)
implementation(projects.services.analytics.api)
implementation(projects.services.analytics.compose)
implementation(projects.services.toolbox.api)
implementation(libs.datetime)
implementation(libs.coil.compose)

1
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsEvents.kt

@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme @@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
sealed interface AdvancedSettingsEvents {
data class SetDeveloperModeEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetSharePresenceEnabled(val enabled: Boolean) : AdvancedSettingsEvents
data class SetCompressMedia(val compress: Boolean) : AdvancedSettingsEvents
data object ChangeTheme : AdvancedSettingsEvents
data object CancelChangeTheme : AdvancedSettingsEvents
data class SetTheme(val theme: Theme) : AdvancedSettingsEvents

7
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenter.kt

@ -35,6 +35,9 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -35,6 +35,9 @@ class AdvancedSettingsPresenter @Inject constructor(
val isSharePresenceEnabled by sessionPreferencesStore
.isSharePresenceEnabled()
.collectAsState(initial = true)
val doesCompressMedia by sessionPreferencesStore
.doesCompressMedia()
.collectAsState(initial = false)
val theme by remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}
@ -49,6 +52,9 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -49,6 +52,9 @@ class AdvancedSettingsPresenter @Inject constructor(
is AdvancedSettingsEvents.SetSharePresenceEnabled -> localCoroutineScope.launch {
sessionPreferencesStore.setSharePresence(event.enabled)
}
is AdvancedSettingsEvents.SetCompressMedia -> localCoroutineScope.launch {
sessionPreferencesStore.setCompressMedia(event.compress)
}
AdvancedSettingsEvents.CancelChangeTheme -> showChangeThemeDialog = false
AdvancedSettingsEvents.ChangeTheme -> showChangeThemeDialog = true
is AdvancedSettingsEvents.SetTheme -> localCoroutineScope.launch {
@ -61,6 +67,7 @@ class AdvancedSettingsPresenter @Inject constructor( @@ -61,6 +67,7 @@ class AdvancedSettingsPresenter @Inject constructor(
return AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
doesCompressMedia = doesCompressMedia,
theme = theme,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = { handleEvents(it) }

1
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsState.kt

@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme @@ -12,6 +12,7 @@ import io.element.android.compound.theme.Theme
data class AdvancedSettingsState(
val isDeveloperModeEnabled: Boolean,
val isSharePresenceEnabled: Boolean,
val doesCompressMedia: Boolean,
val theme: Theme,
val showChangeThemeDialog: Boolean,
val eventSink: (AdvancedSettingsEvents) -> Unit

9
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsStateProvider.kt

@ -16,18 +16,21 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett @@ -16,18 +16,21 @@ open class AdvancedSettingsStateProvider : PreviewParameterProvider<AdvancedSett
aAdvancedSettingsState(),
aAdvancedSettingsState(isDeveloperModeEnabled = true),
aAdvancedSettingsState(showChangeThemeDialog = true),
aAdvancedSettingsState(isSendPublicReadReceiptsEnabled = true),
aAdvancedSettingsState(isSharePresenceEnabled = true),
aAdvancedSettingsState(doesCompressMedia = true),
)
}
fun aAdvancedSettingsState(
isDeveloperModeEnabled: Boolean = false,
isSendPublicReadReceiptsEnabled: Boolean = false,
isSharePresenceEnabled: Boolean = false,
doesCompressMedia: Boolean = false,
showChangeThemeDialog: Boolean = false,
eventSink: (AdvancedSettingsEvents) -> Unit = {},
) = AdvancedSettingsState(
isDeveloperModeEnabled = isDeveloperModeEnabled,
isSharePresenceEnabled = isSendPublicReadReceiptsEnabled,
isSharePresenceEnabled = isSharePresenceEnabled,
doesCompressMedia = doesCompressMedia,
theme = Theme.System,
showChangeThemeDialog = showChangeThemeDialog,
eventSink = eventSink

26
features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsView.kt

@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.themes
import io.element.android.features.preferences.impl.R
@ -23,6 +24,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight @@ -23,6 +24,8 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analyticsproviders.api.trackers.captureInteraction
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -32,6 +35,7 @@ fun AdvancedSettingsView( @@ -32,6 +35,7 @@ fun AdvancedSettingsView(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val analyticsService = LocalAnalyticsService.current
PreferencePage(
modifier = modifier,
onBackClick = onBackClick,
@ -72,6 +76,28 @@ fun AdvancedSettingsView( @@ -72,6 +76,28 @@ fun AdvancedSettingsView(
),
onClick = { state.eventSink(AdvancedSettingsEvents.SetSharePresenceEnabled(!state.isSharePresenceEnabled)) }
)
ListItem(
headlineContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_title))
},
supportingContent = {
Text(text = stringResource(id = R.string.screen_advanced_settings_media_compression_description))
},
trailingContent = ListItemContent.Switch(
checked = state.doesCompressMedia,
),
onClick = {
val newValue = !state.doesCompressMedia
analyticsService.captureInteraction(
if (newValue) {
Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
} else {
Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
}
)
state.eventSink(AdvancedSettingsEvents.SetCompressMedia(newValue))
}
)
}
if (state.showChangeThemeDialog) {

16
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsPresenterTest.kt

@ -34,6 +34,7 @@ class AdvancedSettingsPresenterTest { @@ -34,6 +34,7 @@ class AdvancedSettingsPresenterTest {
assertThat(initialState.isDeveloperModeEnabled).isFalse()
assertThat(initialState.showChangeThemeDialog).isFalse()
assertThat(initialState.isSharePresenceEnabled).isTrue()
assertThat(initialState.doesCompressMedia).isFalse()
assertThat(initialState.theme).isEqualTo(Theme.System)
}
}
@ -68,6 +69,21 @@ class AdvancedSettingsPresenterTest { @@ -68,6 +69,21 @@ class AdvancedSettingsPresenterTest {
}
}
@Test
fun `present - compress media off on`() = runTest {
val presenter = createAdvancedSettingsPresenter()
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitLastSequentialItem()
assertThat(initialState.doesCompressMedia).isFalse()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(true))
assertThat(awaitItem().doesCompressMedia).isTrue()
initialState.eventSink.invoke(AdvancedSettingsEvents.SetCompressMedia(false))
assertThat(awaitItem().doesCompressMedia).isFalse()
}
}
@Test
fun `present - change theme`() = runTest {
val presenter = createAdvancedSettingsPresenter()

62
features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/advanced/AdvancedSettingsViewTest.kt

@ -8,12 +8,18 @@ @@ -8,12 +8,18 @@
package io.element.android.features.preferences.impl.advanced
import androidx.activity.ComponentActivity
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.compound.theme.Theme
import io.element.android.features.preferences.impl.R
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.compose.LocalAnalyticsService
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
@ -91,16 +97,64 @@ class AdvancedSettingsViewTest { @@ -91,16 +97,64 @@ class AdvancedSettingsViewTest {
rule.clickOn(R.string.screen_advanced_settings_share_presence)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetSharePresenceEnabled(true))
}
@Test
fun `clicking on media to enable compression emits the expected event`() {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
val analyticsService = FakeAnalyticsService()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
eventSink = eventsRecorder,
),
analyticsService = analyticsService
)
rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(true))
assertThat(analyticsService.capturedEvents).isEqualTo(
listOf(
Interaction(
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsEnabled
)
)
)
}
@Test
fun `clicking on media to disable compression emits the expected event`() {
val eventsRecorder = EventsRecorder<AdvancedSettingsEvents>()
val analyticsService = FakeAnalyticsService()
rule.setAdvancedSettingsView(
state = aAdvancedSettingsState(
doesCompressMedia = true,
eventSink = eventsRecorder,
),
analyticsService = analyticsService
)
rule.clickOn(R.string.screen_advanced_settings_media_compression_description)
eventsRecorder.assertSingle(AdvancedSettingsEvents.SetCompressMedia(false))
assertThat(analyticsService.capturedEvents).isEqualTo(
listOf(
Interaction(
name = Interaction.Name.MobileSettingsOptimizeMediaUploadsDisabled
)
)
)
}
}
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAdvancedSettingsView(
state: AdvancedSettingsState,
analyticsService: AnalyticsService = FakeAnalyticsService(),
onBackClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
AdvancedSettingsView(
state = state,
onBackClick = onBackClick,
)
CompositionLocalProvider(
LocalAnalyticsService provides analyticsService,
) {
AdvancedSettingsView(
state = state,
onBackClick = onBackClick,
)
}
}
}

1
features/share/impl/build.gradle.kts

@ -49,6 +49,7 @@ dependencies { @@ -49,6 +49,7 @@ dependencies {
testImplementation(libs.androidx.compose.ui.test.junit)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.tests.testutils)
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
}

9
features/share/impl/src/main/kotlin/io/element/android/features/share/impl/SharePresenter.kt

@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.MatrixClient @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -31,6 +32,7 @@ class SharePresenter @AssistedInject constructor( @@ -31,6 +32,7 @@ class SharePresenter @AssistedInject constructor(
private val shareIntentHandler: ShareIntentHandler,
private val matrixClient: MatrixClient,
private val mediaPreProcessor: MediaPreProcessor,
private val sessionPreferencesStore: SessionPreferencesStore,
) : Presenter<ShareState> {
@AssistedFactory
interface Factory {
@ -71,13 +73,16 @@ class SharePresenter @AssistedInject constructor( @@ -71,13 +73,16 @@ class SharePresenter @AssistedInject constructor(
roomIds
.map { roomId ->
val room = matrixClient.getRoom(roomId) ?: return@map false
val mediaSender = MediaSender(preProcessor = mediaPreProcessor, room = room)
val mediaSender = MediaSender(
preProcessor = mediaPreProcessor,
room = room,
sessionPreferencesStore = sessionPreferencesStore,
)
filesToShare
.map { fileToShare ->
mediaSender.sendMedia(
uri = fileToShare.uri,
mimeType = fileToShare.mimeType,
compressIfPossible = true,
).isSuccess
}
.all { it }

4
features/share/impl/src/test/kotlin/io/element/android/features/share/impl/SharePresenterTest.kt

@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
@ -154,7 +155,8 @@ class SharePresenterTest { @@ -154,7 +155,8 @@ class SharePresenterTest {
appCoroutineScope = this,
shareIntentHandler = shareIntentHandler,
matrixClient = matrixClient,
mediaPreProcessor = mediaPreProcessor
mediaPreProcessor = mediaPreProcessor,
InMemorySessionPreferencesStore(),
)
}
}

2
gradle/libs.versions.toml

@ -193,7 +193,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0" @@ -193,7 +193,7 @@ zxing_cpp = "io.github.zxing-cpp:android:2.2.0"
posthog = "com.posthog:posthog-android:3.8.2"
sentry = "io.sentry:sentry-android:7.16.0"
# main branch can be tested replacing the version with main-SNAPSHOT
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.27.0"
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
# Emojibase
matrix_emojibase_bindings = "io.element.android:emojibase-bindings:1.3.3"

2
libraries/mediaupload/api/build.gradle.kts

@ -23,10 +23,12 @@ dependencies { @@ -23,10 +23,12 @@ dependencies {
implementation(projects.libraries.core)
implementation(projects.libraries.di)
api(projects.libraries.matrix.api)
api(projects.libraries.preferences.api)
implementation(libs.inject)
implementation(libs.coroutines.core)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.libraries.mediaupload.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.test.junit)

8
libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt

@ -12,14 +12,17 @@ import io.element.android.libraries.core.extensions.flatMapCatching @@ -12,14 +12,17 @@ import io.element.android.libraries.core.extensions.flatMapCatching
import io.element.android.libraries.matrix.api.core.ProgressCallback
import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
class MediaSender @Inject constructor(
private val preProcessor: MediaPreProcessor,
private val room: MatrixRoom,
private val sessionPreferencesStore: SessionPreferencesStore,
) {
private val ongoingUploadJobs = ConcurrentHashMap<Job.Key, MediaUploadHandler>()
val hasOngoingMediaUploads get() = ongoingUploadJobs.isNotEmpty()
@ -27,11 +30,11 @@ class MediaSender @Inject constructor( @@ -27,11 +30,11 @@ class MediaSender @Inject constructor(
suspend fun sendMedia(
uri: Uri,
mimeType: String,
compressIfPossible: Boolean,
caption: String? = null,
formattedCaption: String? = null,
progressCallback: ProgressCallback? = null
): Result<Unit> {
val compressIfPossible = sessionPreferencesStore.doesCompressMedia().first()
return preProcessor
.process(
uri = uri,
@ -49,6 +52,7 @@ class MediaSender @Inject constructor( @@ -49,6 +52,7 @@ class MediaSender @Inject constructor(
}
.handleSendResult()
}
suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
@ -60,7 +64,7 @@ class MediaSender @Inject constructor( @@ -60,7 +64,7 @@ class MediaSender @Inject constructor(
uri = uri,
mimeType = mimeType,
deleteOriginal = true,
compressIfPossible = false
compressIfPossible = false,
)
.flatMapCatching { info ->
val audioInfo = (info as MediaUploadInfo.Audio).audioInfo

18
libraries/mediaupload/api/src/test/kotlin/io/element/android/libraries/mediaupload/api/MediaSenderTest.kt

@ -15,6 +15,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -15,6 +15,8 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.test.media.FakeMediaUploadHandler
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
@ -33,7 +35,7 @@ class MediaSenderTest { @@ -33,7 +35,7 @@ class MediaSenderTest {
val sender = aMediaSender(preProcessor)
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
assertThat(preProcessor.processCallCount).isEqualTo(1)
}
@ -49,7 +51,7 @@ class MediaSenderTest { @@ -49,7 +51,7 @@ class MediaSenderTest {
val sender = aMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
sendMediaResult.assertions().isCalledOnce()
}
@ -61,7 +63,7 @@ class MediaSenderTest { @@ -61,7 +63,7 @@ class MediaSenderTest {
val sender = aMediaSender(preProcessor)
val uri = Uri.parse("content://image.jpg")
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
assertThat(result.exceptionOrNull()).isNotNull()
}
@ -74,7 +76,7 @@ class MediaSenderTest { @@ -74,7 +76,7 @@ class MediaSenderTest {
val sender = aMediaSender(room = room)
val uri = Uri.parse("content://image.jpg")
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
val result = sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
assertThat(result.exceptionOrNull()).isNotNull()
}
@ -88,7 +90,7 @@ class MediaSenderTest { @@ -88,7 +90,7 @@ class MediaSenderTest {
val sender = aMediaSender(room = room)
val sendJob = launch {
val uri = Uri.parse("content://image.jpg")
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg, compressIfPossible = true)
sender.sendMedia(uri = uri, mimeType = MimeTypes.Jpeg)
}
// Wait until several internal tasks run and the file is being uploaded
advanceTimeBy(3L)
@ -109,8 +111,10 @@ class MediaSenderTest { @@ -109,8 +111,10 @@ class MediaSenderTest {
private fun aMediaSender(
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
room: MatrixRoom = FakeMatrixRoom(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
) = MediaSender(
preProcessor,
room,
preProcessor = preProcessor,
room = room,
sessionPreferencesStore = sessionPreferencesStore,
)
}

2
libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/ImageCompressor.kt

@ -36,7 +36,7 @@ class ImageCompressor @Inject constructor( @@ -36,7 +36,7 @@ class ImageCompressor @Inject constructor(
resizeMode: ResizeMode,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
orientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
desiredQuality: Int = 80,
desiredQuality: Int = 78,
): Result<ImageCompressionResult> = withContext(dispatchers.io) {
runCatching {
val compressedBitmap = compressToBitmap(inputStreamProvider, resizeMode, orientation).getOrThrow()

2
libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/impl/VideoCompressor.kt

@ -29,7 +29,7 @@ class VideoCompressor @Inject constructor( @@ -29,7 +29,7 @@ class VideoCompressor @Inject constructor(
val future = Transcoder.into(tmpFile.path)
.setVideoTrackStrategy(
DefaultVideoStrategy.Builder()
.addResizer(AtMostResizer(1920, 1080))
.addResizer(AtMostResizer(720, 480))
.build()
)
.addDataSource(context, uri)

6
libraries/mediaupload/impl/src/test/kotlin/io/element/android/libraries/mediaupload/impl/AndroidMediaPreProcessorTest.kt

@ -55,8 +55,8 @@ class AndroidMediaPreProcessorTest { @@ -55,8 +55,8 @@ class AndroidMediaPreProcessorTest {
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 114_867,
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4567),
size = 109_908,
ThumbnailInfo(height = 294, width = 454, mimetype = "image/jpeg", size = 4484),
thumbnailSource = null,
blurhash = "K13]7q%zWC00R4of%\$baad"
)
@ -84,7 +84,7 @@ class AndroidMediaPreProcessorTest { @@ -84,7 +84,7 @@ class AndroidMediaPreProcessorTest {
height = 1_178,
width = 1_818,
mimetype = MimeTypes.Png,
size = 114_867,
size = 109_908,
thumbnailInfo = null,
thumbnailSource = null,
blurhash = null,

3
libraries/preferences/api/src/main/kotlin/io/element/android/libraries/preferences/api/store/SessionPreferencesStore.kt

@ -28,5 +28,8 @@ interface SessionPreferencesStore { @@ -28,5 +28,8 @@ interface SessionPreferencesStore {
suspend fun setSkipSessionVerification(skip: Boolean)
fun isSessionVerificationSkipped(): Flow<Boolean>
suspend fun setCompressMedia(compress: Boolean)
fun doesCompressMedia(): Flow<Boolean>
suspend fun clear()
}

4
libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt

@ -41,6 +41,7 @@ class DefaultSessionPreferencesStore( @@ -41,6 +41,7 @@ class DefaultSessionPreferencesStore(
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
private val compressMedia = booleanPreferencesKey("compressMedia")
private val dataStoreFile = storeFile(context, sessionId)
private val store = PreferenceDataStoreFactory.create(
@ -81,6 +82,9 @@ class DefaultSessionPreferencesStore( @@ -81,6 +82,9 @@ class DefaultSessionPreferencesStore(
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
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 suspend fun clear() {
dataStoreFile.safeDelete()
}

6
libraries/preferences/test/src/main/kotlin/io/element/android/libraries/preferences/test/InMemorySessionPreferencesStore.kt

@ -18,6 +18,7 @@ class InMemorySessionPreferencesStore( @@ -18,6 +18,7 @@ class InMemorySessionPreferencesStore(
isSendTypingNotificationsEnabled: Boolean = true,
isRenderTypingNotificationsEnabled: Boolean = true,
isSessionVerificationSkipped: Boolean = false,
doesCompressMedia: Boolean = false,
) : SessionPreferencesStore {
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
@ -25,6 +26,7 @@ class InMemorySessionPreferencesStore( @@ -25,6 +26,7 @@ class InMemorySessionPreferencesStore(
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
var clearCallCount = 0
private set
@ -66,6 +68,10 @@ class InMemorySessionPreferencesStore( @@ -66,6 +68,10 @@ class InMemorySessionPreferencesStore(
return isSessionVerificationSkipped
}
override suspend fun setCompressMedia(compress: Boolean) = doesCompressMedia.emit(compress)
override fun doesCompressMedia(): Flow<Boolean> = doesCompressMedia
override suspend fun clear() {
clearCallCount++
isSendPublicReadReceiptsEnabled.tryEmit(true)

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_0_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0c6ec8d30f2fd12c78bf62c9913a68c7a1f6328f0ac9e577317d0abf72e1131c
size 41788
oid sha256:13e7793d8dd6d08e182b128a9b3dac2877557e8bdd220561d36c2ce1650b94ff
size 48107

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_1_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e37410e8e25d6908bbd88db983be7f1c50b4f848763b66c80bf18de97d7f4916
size 41548
oid sha256:1bac5247c3a4990eb9155a21f72a49059cbaa93288380ba1ab6be2def8b3a6a9
size 47876

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_2_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4d64cb8ce98dfdf2345e12596e4eb3777ea4cde2bd36303d0af6b47614e28f3f
size 31044
oid sha256:11c969c1d04150cef68da64865bade2ea3bfc1aa5f0d790262315131b57d6233
size 31702

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_3_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:69308b3b8b6a7b8a0c032ba6b023a49fcfbb4f05317cbe96e690b4aa3c6992b8
size 41621
oid sha256:dc1aa9348e470e9d7e7e1e838e18a3403159c2effb49fa359b2b2db97dd81961
size 47901

3
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Day_4_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:90d1184879c5a34dc27cc942ec3110e14ccd9a90c152b10a1844fde0566d54fe
size 47841

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_0_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:702a4067afb752978fef59ca45b6d4c871d084953ab19a3d845f0018a894ac6a
size 40510
oid sha256:c2beb4f4f190f6aca2d4da338d980e8611283ffb9bfd2f402482f8ecc629cf22
size 46759

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_1_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1904587ad63098e18e17663a3bac34f2ad24d79967ab44789a1bc1b994805eeb
size 40205
oid sha256:b65acfd3126efc5cd4fe1a929a786468cc18ee371cb4462ef0cf9f6e8963fcea
size 46456

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_2_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:14098a8dc6d7c9d99b6a04710e1385b39839bf18be3c9ddae7ce437a0c1bf64b
size 28729
oid sha256:8e90bbde9aac7e710703e836f293a00b2a5f35447d4d63c9732de21a0f291449
size 29336

4
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_3_en.png

@ -1,3 +1,3 @@ @@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a6151f04f2dca8124597c6d144cafe15a51f7fe6638cfed4380a9b6e125bed4
size 40259
oid sha256:6da0cf1729162fa1745bcb4dd86be06f90d3bcf6bf034e4a64e1ee9119b6cdd5
size 46501

3
tests/uitests/src/test/snapshots/images/features.preferences.impl.advanced_AdvancedSettingsView_Night_4_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:66109868b1e893bc569d70110e3e587d7a17d777838cfc3e5c3d3189338d924e
size 46423
Loading…
Cancel
Save