Add media upload setting.

Compress media regarding the settings.
Image compression change quality to 78%
Video compression change size to 720 x 48
This commit is contained in:
Benoit Marty 2024-10-28 10:56:37 +01:00
parent 01e7986347
commit 846dbc2b18
22 changed files with 162 additions and 29 deletions

View file

@ -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)

View file

@ -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(
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(
}
.handleSendResult()
}
suspend fun sendVoiceMessage(
uri: Uri,
mimeType: String,
@ -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

View file

@ -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 {
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 {
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 {
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 {
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 {
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 {
private fun aMediaSender(
preProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
room: MatrixRoom = FakeMatrixRoom(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
) = MediaSender(
preProcessor,
room,
preProcessor = preProcessor,
room = room,
sessionPreferencesStore = sessionPreferencesStore,
)
}

View file

@ -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()

View file

@ -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)

View file

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

View file

@ -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(
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()
}

View file

@ -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(
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(
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)