From 75a4455d15a33d3e8523e44134b9703c63f003da Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 12:03:54 +0200 Subject: [PATCH 01/32] reorder methods --- .../session/room/send/DefaultSendService.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 26a87557ff..7cc238fcc9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -109,14 +109,6 @@ internal class DefaultSendService @AssistedInject constructor( .let { sendEvent(it) } } - override fun sendMedias(attachments: List, - compressBeforeSending: Boolean, - roomIds: Set): Cancelable { - return attachments.mapTo(CancelableBag()) { - sendMedia(it, compressBeforeSending, roomIds) - } - } - override fun redactEvent(event: Event, reason: String?): Cancelable { // TODO manage media/attachements? val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason) @@ -240,6 +232,14 @@ internal class DefaultSendService @AssistedInject constructor( } } + override fun sendMedias(attachments: List, + compressBeforeSending: Boolean, + roomIds: Set): Cancelable { + return attachments.mapTo(CancelableBag()) { + sendMedia(it, compressBeforeSending, roomIds) + } + } + override fun sendMedia(attachment: ContentAttachmentData, compressBeforeSending: Boolean, roomIds: Set): Cancelable { From a377a595b9dc60ae3770007a7b30f141144c93a0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 12:29:20 +0200 Subject: [PATCH 02/32] Cleanup file, this belongs to the main build.gradle file --- attachment-viewer/build.gradle | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/attachment-viewer/build.gradle b/attachment-viewer/build.gradle index 8db57a59af..0ac8ac4a9a 100644 --- a/attachment-viewer/build.gradle +++ b/attachment-viewer/build.gradle @@ -17,20 +17,6 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -buildscript { - repositories { - maven { - url 'https://jitpack.io' - content { - // PhotoView - includeGroupByRegex 'com\\.github\\.chrisbanes' - } - } - jcenter() - } - -} - android { compileSdkVersion 30 From 5e1c503d2e5be1a4dccde50b2c8a2a28fe01c06b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 14:04:56 +0200 Subject: [PATCH 03/32] Compress video before sending (#442) --- CHANGES.md | 1 + build.gradle | 4 +- matrix-sdk-android/build.gradle | 3 + .../session/content/UploadContentWorker.kt | 13 +++ .../session/content/VideoCompressor.kt | 83 +++++++++++++++++++ .../attachments/ContentAttachmentData.kt | 8 +- .../preview/AttachmentsPreviewFragment.kt | 12 ++- vector/src/main/res/values/strings.xml | 5 ++ 8 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt diff --git a/CHANGES.md b/CHANGES.md index 027b9e8416..553abdda8e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ Features ✨: Improvements 🙌: - Add ability to install APK from directly from Element (#2381) - Delete and react to stickers (#3250) + - Compress video before sending (#442) Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/build.gradle b/build.gradle index 25734e3b09..d9fb1542ec 100644 --- a/build.gradle +++ b/build.gradle @@ -45,9 +45,11 @@ allprojects { // PFLockScreen-Android includeGroupByRegex 'com\\.github\\.vector-im' - //Chat effects + // Chat effects includeGroupByRegex 'com\\.github\\.jetradarmobile' includeGroupByRegex 'nl\\.dionsegijn' + // LightCompressor + includeGroupByRegex 'com\\.github\\.AbedElazizShe' } } maven { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ea94d44346..57436813d5 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -168,6 +168,9 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' + // Video compression + implementation 'com.github.AbedElazizShe:LightCompressor:0.9.0' + // Phone number https://github.com/google/libphonenumber implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.22' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 3b727690bf..660a0376e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -77,6 +77,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var fileService: DefaultFileService @Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var imageCompressor: ImageCompressor + @Inject lateinit var videoCompressor: VideoCompressor @Inject lateinit var localEchoRepository: LocalEchoRepository override fun injectWith(injector: SessionComponent) { @@ -170,6 +171,18 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } .also { filesToDelete.add(it) } + } else if (attachment.type == ContentAttachmentData.Type.VIDEO + // Do not compress gif + && attachment.mimeType != MimeTypes.Gif + && params.compressBeforeSending) { + fileToUpload = videoCompressor.compress(workingFile) + .also { compressedFile -> + // Get new Video size + newAttachmentAttributes = NewAttachmentAttributes( + newFileSize = compressedFile.length() + ) + } + .also { filesToDelete.add(it) } } else { fileToUpload = workingFile // Fix: OpenableColumns.SIZE may return -1 or 0 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt new file mode 100644 index 0000000000..78624bbbf9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import android.content.Context +import com.abedelazizshe.lightcompressorlibrary.CompressionListener +import com.abedelazizshe.lightcompressorlibrary.VideoCompressor +import com.abedelazizshe.lightcompressorlibrary.VideoQuality +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.File +import java.util.UUID +import javax.inject.Inject + +internal class VideoCompressor @Inject constructor(private val context: Context) { + suspend fun compress(videoFile: File, + quality: VideoQuality = VideoQuality.MEDIUM, + isMinBitRateEnabled: Boolean = false, + keepOriginalResolution: Boolean = true): File { + return withContext(Dispatchers.IO) { + val job = Job() + val destinationFile = createDestinationFile() + + // Sadly it does not return the Job, the API is not ideal + VideoCompressor.start( + context = null, + srcUri = null, + srcPath = videoFile.path, + destPath = destinationFile.path, + listener = object : CompressionListener { + override fun onProgress(percent: Float) { + Timber.d("Compressing: $percent%") + } + + override fun onStart() { + Timber.d("Compressing: start") + } + + override fun onSuccess() { + Timber.d("Compressing: success") + job.complete() + } + + override fun onFailure(failureMessage: String) { + Timber.d("Compressing: failure: $failureMessage") + job.completeExceptionally(Exception(failureMessage)) + } + + override fun onCancelled() { + Timber.d("Compressing: cancel") + job.cancel() + } + }, + quality = quality, + isMinBitRateEnabled = isMinBitRateEnabled, + keepOriginalResolution = keepOriginalResolution + ) + + job.join() + destinationFile + } + } + + private fun createDestinationFile(): File { + return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt index e35ab96365..0502f2b0ad 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/ContentAttachmentData.kt @@ -21,15 +21,15 @@ import org.matrix.android.sdk.api.util.MimeTypes private val listOfPreviewableMimeTypes = listOf( MimeTypes.Jpeg, - MimeTypes.BadJpg, MimeTypes.Png, MimeTypes.Gif ) fun ContentAttachmentData.isPreviewable(): Boolean { - // For now the preview only supports still image - return type == ContentAttachmentData.Type.IMAGE - && listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "") + // Preview supports image and video + return (type == ContentAttachmentData.Type.IMAGE + && listOfPreviewableMimeTypes.contains(getSafeMimeType() ?: "")) + || type == ContentAttachmentData.Type.VIDEO } data class GroupedContentAttachmentData( diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt index cdb015e4da..9594f89a0e 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -139,7 +139,17 @@ class AttachmentsPreviewFragment @Inject constructor( attachmentBigPreviewController.setData(state) views.attachmentPreviewerBigList.scrollToPosition(state.currentAttachmentIndex) views.attachmentPreviewerMiniatureList.scrollToPosition(state.currentAttachmentIndex) - views.attachmentPreviewerSendImageOriginalSize.text = resources.getQuantityString(R.plurals.send_images_with_original_size, state.attachments.size) + views.attachmentPreviewerSendImageOriginalSize.text = getCheckboxText(state) + } + } + + private fun getCheckboxText(state: AttachmentsPreviewViewState): CharSequence { + val nbImages = state.attachments.count { it.type == ContentAttachmentData.Type.IMAGE } + val nbVideos = state.attachments.count { it.type == ContentAttachmentData.Type.VIDEO } + return when { + nbVideos == 0 -> resources.getQuantityString(R.plurals.send_images_with_original_size, nbImages) + nbImages == 0 -> resources.getQuantityString(R.plurals.send_videos_with_original_size, nbVideos) + else -> getString(R.string.send_images_and_video_with_original_size) } } diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index c7153df6e4..5dc21601a9 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2785,6 +2785,11 @@ Send image with the original size Send images with the original size + + Send video with the original size + Send videos with the original size + + Send media with the original size Confirm Removal Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change. From 8ff65b381619a99ddfefe15d48d6139d6434b1dc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 14:24:49 +0200 Subject: [PATCH 04/32] Add a video indicator on the miniature (videos are now previewable) --- .../attachments/preview/AttachmentPreviewItems.kt | 3 +++ .../res/layout/item_attachment_miniature_preview.xml | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt index f715d0cb3f..ae18d2561d 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/preview/AttachmentPreviewItems.kt @@ -18,6 +18,7 @@ package im.vector.app.features.attachments.preview import android.view.View import android.widget.ImageView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.bumptech.glide.Glide @@ -65,6 +66,7 @@ abstract class AttachmentMiniaturePreviewItem : AttachmentPreviewItem(R.id.attachmentMiniatureImageView) + val miniatureVideoIndicator by bind(R.id.attachmentMiniatureVideoIndicator) } } diff --git a/vector/src/main/res/layout/item_attachment_miniature_preview.xml b/vector/src/main/res/layout/item_attachment_miniature_preview.xml index c52dc6cead..5f318127e7 100644 --- a/vector/src/main/res/layout/item_attachment_miniature_preview.xml +++ b/vector/src/main/res/layout/item_attachment_miniature_preview.xml @@ -1,5 +1,6 @@ + + \ No newline at end of file From 4a23d3127149bc7ad8b0e1a88ca7b1b3fdcfb548 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 15:27:50 +0200 Subject: [PATCH 05/32] Fix regression on sending error indicator color --- .../java/im/vector/app/core/ui/views/SendStateImageView.kt | 5 +++++ vector/src/main/res/layout/item_timeline_event_base.xml | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/SendStateImageView.kt b/vector/src/main/java/im/vector/app/core/ui/views/SendStateImageView.kt index 9acb82581f..308efe09b2 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/SendStateImageView.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/SendStateImageView.kt @@ -17,11 +17,13 @@ package im.vector.app.core.ui.views import android.content.Context +import android.content.res.ColorStateList import android.util.AttributeSet import androidx.appcompat.widget.AppCompatImageView import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration +import im.vector.app.features.themes.ThemeUtils class SendStateImageView @JvmOverloads constructor( context: Context, @@ -39,16 +41,19 @@ class SendStateImageView @JvmOverloads constructor( isVisible = when (sendState) { SendStateDecoration.SENDING_NON_MEDIA -> { setImageResource(R.drawable.ic_sending_message) + imageTintList = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.riotx_text_tertiary)) contentDescription = context.getString(R.string.event_status_a11y_sending) true } SendStateDecoration.SENT -> { setImageResource(R.drawable.ic_message_sent) + imageTintList = ColorStateList.valueOf(ThemeUtils.getColor(context, R.attr.riotx_text_tertiary)) contentDescription = context.getString(R.string.event_status_a11y_sent) true } SendStateDecoration.FAILED -> { setImageResource(R.drawable.ic_sending_message_failed) + imageTintList = null contentDescription = context.getString(R.string.event_status_a11y_failed) true } diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 0515ace033..282c3f6140 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -145,8 +145,8 @@ android:layout_marginBottom="4dp" android:contentDescription="@string/event_status_a11y_sending" android:src="@drawable/ic_sending_message" - android:tint="?riotx_text_tertiary" android:visibility="gone" + tools:tint="?riotx_text_tertiary" tools:visibility="visible" /> From e108534a2a7a4bcd112fe561fe0b2523019837c5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 15:34:15 +0200 Subject: [PATCH 06/32] Improve file too big error detection and rendering (#3245) --- CHANGES.md | 1 + .../sdk/api/session/events/model/Event.kt | 12 +++++++ .../tasks/SendVerificationMessageTask.kt | 3 +- .../database/RealmSessionStoreMigration.kt | 13 +++++-- .../internal/database/mapper/EventMapper.kt | 1 + .../internal/database/model/EventEntity.kt | 3 ++ .../internal/session/content/FileUploader.kt | 21 +++++++++++ .../session/content/UploadContentWorker.kt | 4 ++- .../session/room/relation/EventEditor.kt | 1 + .../session/room/send/LocalEchoRepository.kt | 4 ++- .../MultipleEventSendingDispatcherWorker.kt | 7 +++- .../session/room/send/SendEventWorker.kt | 15 ++++++-- .../android/sdk/internal/util/FailureExt.kt | 36 +++++++++++++++++++ .../vector/app/core/error/ErrorFormatter.kt | 3 ++ .../home/room/detail/RoomDetailViewEvents.kt | 1 + .../home/room/detail/RoomDetailViewModel.kt | 7 ++-- .../action/MessageActionsEpoxyController.kt | 9 ++++- vector/src/main/res/values/strings.xml | 1 + 18 files changed, 130 insertions(+), 12 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt diff --git a/CHANGES.md b/CHANGES.md index 553abdda8e..d3e17e6980 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Improvements 🙌: - Add ability to install APK from directly from Element (#2381) - Delete and react to stickers (#3250) - Compress video before sending (#442) + - Improve file too big error detection (#3245) Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 89b873febb..6400dd6444 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -28,6 +28,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.di.MoshiProvider import org.json.JSONObject +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.MatrixError import timber.log.Timber typealias Content = JsonDict @@ -90,6 +92,16 @@ data class Event( @Transient var sendState: SendState = SendState.UNKNOWN + @Transient + var sendStateDetails: String? = null + + fun sendStateError(): MatrixError? { + return sendStateDetails?.let { + val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + tryOrNull { matrixErrorAdapter.fromJson(it) } + } + } + /** * The `age` value transcoded in a timestamp based on the device clock when the SDK received * the event from the home server. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt index d8b9d3cd86..7fa48c3da1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendVerificationMessageTask.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.task.Task +import org.matrix.android.sdk.internal.util.toMatrixErrorStr import javax.inject.Inject internal interface SendVerificationMessageTask : Task { @@ -55,7 +56,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor( localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT) return response.eventId } catch (e: Throwable) { - localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr()) throw e } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index ac72592a1e..05213b40e5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -43,7 +43,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 10L + const val SESSION_STORE_SCHEMA_VERSION = 11L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -59,6 +59,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 7) migrateTo8(realm) if (oldVersion <= 8) migrateTo9(realm) if (oldVersion <= 9) migrateTo10(realm) + if (oldVersion <= 10) migrateTo11(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -163,7 +164,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema) } - fun migrateTo9(realm: DynamicRealm) { + private fun migrateTo9(realm: DynamicRealm) { Timber.d("Step 8 -> 9") realm.schema.get("RoomSummaryEntity") @@ -201,7 +202,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { } } - fun migrateTo10(realm: DynamicRealm) { + private fun migrateTo10(realm: DynamicRealm) { Timber.d("Step 9 -> 10") realm.schema.create("SpaceChildSummaryEntity") ?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java) @@ -240,4 +241,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { ?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!) ?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!) } + + private fun migrateTo11(realm: DynamicRealm) { + Timber.d("Step 10 -> 11") + realm.schema.get("EventEntity") + ?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index a4a2fadd21..613b38e340 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -80,6 +80,7 @@ internal object EventMapper { ).also { it.ageLocalTs = eventEntity.ageLocalTs it.sendState = eventEntity.sendState + it.sendStateDetails = eventEntity.sendStateDetails eventEntity.decryptionResultJson?.let { json -> try { it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index fe59f4fceb..fcb171c617 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider import io.realm.RealmObject import io.realm.annotations.Index +import org.matrix.android.sdk.api.failure.MatrixError internal open class EventEntity(@Index var eventId: String = "", @Index var roomId: String = "", @@ -32,6 +33,8 @@ internal open class EventEntity(@Index var eventId: String = "", @Index var stateKey: String? = null, var originServerTs: Long? = null, @Index var sender: String? = null, + // Can contain a serialized MatrixError + var sendStateDetails: String? = null, var age: Long? = 0, var unsignedData: String? = null, var redacts: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index 8fa595db30..f3974487f7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -31,12 +31,16 @@ import okhttp3.RequestBody.Companion.toRequestBody import okio.BufferedSink import okio.source import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.session.content.ContentUrlResolver +import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.ProgressRequestBody import org.matrix.android.sdk.internal.network.awaitResponse import org.matrix.android.sdk.internal.network.toFailure +import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -46,6 +50,7 @@ import javax.inject.Inject internal class FileUploader @Inject constructor(@Authenticated private val okHttpClient: OkHttpClient, private val globalErrorReceiver: GlobalErrorReceiver, + private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService, private val context: Context, contentUrlResolver: ContentUrlResolver, moshi: Moshi) { @@ -57,6 +62,22 @@ internal class FileUploader @Inject constructor(@Authenticated filename: String?, mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { + // Check size limit + // DO NOT COMMIT: 5 Mo + val maxUploadFileSize = 5 * 1024 * 1024L // homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize + + if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN + && file.length() > maxUploadFileSize) { + // Known limitation and file too big for the server, save the pain to upload it + throw Failure.ServerError( + error = MatrixError( + code = MatrixError.M_TOO_LARGE, + message = "Cannot upload files larger than ${maxUploadFileSize / 1048576L}mb" + ), + httpCode = 413 + ) + } + val uploadBody = object : RequestBody() { override fun contentLength() = file.length() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 660a0376e3..bc7d846e55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -41,6 +41,7 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker +import org.matrix.android.sdk.internal.util.toMatrixErrorStr import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import org.matrix.android.sdk.internal.worker.WorkerParamsFactory @@ -129,6 +130,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } + // TODO Send the Thumbnail after the main content, because the main content can fail if too large. val uploadThumbnailResult = dealWithThumbnail(params) val progressListener = object : ProgressRequestBody.Listener { @@ -304,7 +306,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter return Result.success( WorkerParamsFactory.toData( params.copy( - lastFailureMessage = failure.localizedMessage + lastFailureMessage = failure.toMatrixErrorStr() ) ) ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt index 5fe06287d2..a666d40fc3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/EventEditor.kt @@ -99,6 +99,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor: entity.age = editedEventEntity.age entity.originServerTs = editedEventEntity.originServerTs entity.sendState = editedEventEntity.sendState + entity.sendStateDetails = editedEventEntity.sendStateDetails } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt index 70245cbd5e..e98e5646af 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoRepository.kt @@ -87,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } } - fun updateSendState(eventId: String, roomId: String?, sendState: SendState) { + fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) { Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}") timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState) updateEchoAsync(eventId) { realm, sendingEventEntity -> @@ -96,6 +96,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private } else { sendingEventEntity.sendState = sendState } + sendingEventEntity.sendStateDetails = sendStateDetails roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId) } } @@ -161,6 +162,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll() timelineEvents.forEach { it.root?.sendState = sendState + it.root?.sendStateDetails = null } roomSummaryUpdater.updateSendingInformation(realm, roomId) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt index bc307bc74f..e889f1a61b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/MultipleEventSendingDispatcherWorker.kt @@ -55,7 +55,12 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo override fun doOnError(params: Params): Result { params.localEchoIds.forEach { localEchoIds -> - localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState( + eventId = localEchoIds.eventId, + roomId = localEchoIds.roomId, + sendState = SendState.UNDELIVERED, + sendStateDetails = params.lastFailureMessage + ) } return super.doOnError(params) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt index d55dce57af..cd7911910d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/SendEventWorker.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.SessionComponent +import org.matrix.android.sdk.internal.util.toMatrixErrorStr import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker import org.matrix.android.sdk.internal.worker.SessionWorkerParams import timber.log.Timber @@ -77,7 +78,12 @@ internal class SendEventWorker(context: Context, } if (params.lastFailureMessage != null) { - localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState( + eventId = event.eventId, + roomId = event.roomId, + sendState = SendState.UNDELIVERED, + sendStateDetails = params.lastFailureMessage + ) // Transmit the error return Result.success(inputData) .also { Timber.e("Work cancelled due to input error from parent") } @@ -90,7 +96,12 @@ internal class SendEventWorker(context: Context, } catch (exception: Throwable) { if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}") - localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED) + localEchoRepository.updateSendState( + eventId = event.eventId, + roomId = event.roomId, + sendState = SendState.UNDELIVERED, + sendStateDetails = exception.toMatrixErrorStr() + ) Result.success() } else { Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}") diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt new file mode 100644 index 0000000000..8c78feeac3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/util/FailureExt.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.util + +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.failure.Failure +import org.matrix.android.sdk.api.failure.MatrixError +import org.matrix.android.sdk.internal.di.MoshiProvider + +/** + * Try to extract and serialize a MatrixError, or default to localizedMessage + */ +internal fun Throwable.toMatrixErrorStr(): String { + return (this as? Failure.ServerError) + ?.let { + // Serialize the MatrixError in this case + val adapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java) + tryOrNull { adapter.toJson(error) } + } + ?: localizedMessage + ?: "error" +} diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt index d7f003574c..0a724b62c6 100644 --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt @@ -78,6 +78,9 @@ class DefaultErrorFormatter @Inject constructor( throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> { limitExceededError(throwable.error) } + throwable.error.code == MatrixError.M_TOO_LARGE -> { + stringProvider.getString(R.string.error_file_too_big_simple) + } throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> { stringProvider.getString(R.string.login_reset_password_error_not_found) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index b326700d5e..0a9ffd83ab 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -54,6 +54,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object ShowWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents() + // TODO Remove data class FileTooBigError( val filename: String, val fileSizeInBytes: Long, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1dd13bf481..e5e9b2c7d3 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -79,7 +79,6 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.file.FileService -import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership @@ -292,7 +291,6 @@ class RoomDetailViewModel @AssistedInject constructor( is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) is RoomDetailAction.ResendMessage -> handleResendEvent(action) is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.ResendAll -> handleResendAll() is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() is RoomDetailAction.ReportContent -> handleReportContent(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) @@ -1107,6 +1105,10 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleSendMedia(action: RoomDetailAction.SendMedia) { + room.sendMedias(action.attachments, action.compressBeforeSending, emptySet()) + + /* + TODO Cleanup this error is now managed by the SDK val attachments = action.attachments val homeServerCapabilities = session.getHomeServerCapabilities() val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize @@ -1124,6 +1126,7 @@ class RoomDetailViewModel @AssistedInject constructor( )) } } + */ } private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index 30587e6659..d62083de30 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -28,6 +28,7 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem import im.vector.app.core.epoxy.dividerItem +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer @@ -38,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.media.ImageContentRenderer import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.room.send.SendState import javax.inject.Inject @@ -50,6 +52,7 @@ class MessageActionsEpoxyController @Inject constructor( private val fontProvider: EmojiCompatFontProvider, private val imageContentRenderer: ImageContentRenderer, private val dimensionConverter: DimensionConverter, + private val errorFormatter: ErrorFormatter, private val dateFormatter: VectorDateFormatter ) : TypedEpoxyController() { @@ -74,10 +77,14 @@ class MessageActionsEpoxyController @Inject constructor( // Send state val sendState = state.sendState() if (sendState?.hasFailed().orFalse()) { + // Get more details about the error + val errorMessage = state.timelineEvent()?.root?.sendStateError() + ?.let { errorFormatter.toHumanReadable(Failure.ServerError(it, 0)) } + ?: stringProvider.getString(R.string.unable_to_send_message) bottomSheetSendStateItem { id("send_state") showProgress(false) - text(stringProvider.getString(R.string.unable_to_send_message)) + text(errorMessage) drawableStart(R.drawable.ic_warning_badge) } } else if (sendState?.isSending().orFalse()) { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5dc21601a9..eadc0db1f4 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2295,6 +2295,7 @@ %d users read + "The file is too large to upload." "The file '%1$s' (%2$s) is too large to upload. The limit is %3$s." "An error occurred while retrieving the attachment." From e1e4b035321a47af851f0f3a8d6103afb06f2b28 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 17:14:11 +0200 Subject: [PATCH 07/32] Do not serialize "soft_logout" --- .../java/org/matrix/android/sdk/api/failure/MatrixError.kt | 2 +- .../matrix/android/sdk/internal/network/RetrofitExtensions.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt index 3820a442aa..73b0fe0a7c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -41,7 +41,7 @@ data class MatrixError( // For M_LIMIT_EXCEEDED @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null, // For M_UNKNOWN_TOKEN - @Json(name = "soft_logout") val isSoftLogout: Boolean = false, + @Json(name = "soft_logout") val isSoftLogout: Boolean? = null, // For M_INVALID_PEPPER // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} @Json(name = "lookup_pepper") val newLookupPepper: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt index 7132b4ff7a..2116063626 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/RetrofitExtensions.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.internal.di.MoshiProvider import kotlinx.coroutines.suspendCancellableCoroutine import okhttp3.ResponseBody +import org.matrix.android.sdk.api.extensions.orFalse import retrofit2.HttpException import retrofit2.Response import timber.log.Timber @@ -91,7 +92,7 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int, globalErrorReceiv } else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */ && matrixError.code == MatrixError.M_UNKNOWN_TOKEN) { // Also send this error to the globalErrorReceiver, for a global management - globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout)) + globalErrorReceiver?.handleGlobalError(GlobalError.InvalidToken(matrixError.isSoftLogout.orFalse())) } return Failure.ServerError(matrixError, httpCode) From 52133095876e249fc890a1ca14e4d8710ee638d8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 17:22:55 +0200 Subject: [PATCH 08/32] Send the Thumbnail after the main content, for the case the main content fails if too large. --- .../session/content/UploadContentWorker.kt | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index bc7d846e55..f8b58e3ad3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -111,7 +111,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val attachment = params.attachment val filesToDelete = mutableListOf() - try { + return try { val inputStream = context.contentResolver.openInputStream(attachment.queryUri) ?: return Result.success( WorkerParamsFactory.toData( @@ -130,9 +130,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } - // TODO Send the Thumbnail after the main content, because the main content can fail if too large. - val uploadThumbnailResult = dealWithThumbnail(params) - val progressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { notifyTracker(params) { @@ -147,7 +144,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null - return try { + try { val fileToUpload: File var newAttachmentAttributes = NewAttachmentAttributes( params.attachment.width?.toInt(), @@ -232,6 +229,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.e(failure, "## FileService: Failed to update file cache") } + val uploadThumbnailResult = dealWithThumbnail(params) + handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, @@ -244,7 +243,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } catch (e: Exception) { Timber.e(e, "## FileService: ERROR") - return handleFailure(params, e) + handleFailure(params, e) } finally { // Delete all temporary files filesToDelete.forEach { @@ -275,19 +274,23 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.v("Encrypt thumbnail") notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) - val contentUploadResponse = fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${params.attachment.name}", - MimeTypes.OctetStream, - thumbnailProgressListener) + val contentUploadResponse = fileUploader.uploadByteArray( + byteArray = encryptionResult.encryptedByteArray, + filename = "thumb_${params.attachment.name}", + mimeType = MimeTypes.OctetStream, + progressListener = thumbnailProgressListener + ) UploadThumbnailResult( contentUploadResponse.contentUri, encryptionResult.encryptedFileInfo ) } else { - val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes, - "thumb_${params.attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) + val contentUploadResponse = fileUploader.uploadByteArray( + byteArray = thumbnailData.bytes, + filename = "thumb_${params.attachment.name}", + mimeType = thumbnailData.mimeType, + progressListener = thumbnailProgressListener + ) UploadThumbnailResult( contentUploadResponse.contentUri, null From d6b6768f41a41efc588d0467c9d5fb9462c4a844 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 30 Apr 2021 17:34:40 +0200 Subject: [PATCH 09/32] Cleanup --- .../internal/database/model/EventEntity.kt | 1 - .../internal/session/content/FileUploader.kt | 3 +-- .../home/room/detail/RoomDetailFragment.kt | 14 ------------- .../home/room/detail/RoomDetailViewEvents.kt | 7 ------- .../home/room/detail/RoomDetailViewModel.kt | 21 ------------------- 5 files changed, 1 insertion(+), 45 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt index fcb171c617..c9edbcd889 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventEntity.kt @@ -22,7 +22,6 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult import org.matrix.android.sdk.internal.di.MoshiProvider import io.realm.RealmObject import io.realm.annotations.Index -import org.matrix.android.sdk.api.failure.MatrixError internal open class EventEntity(@Index var eventId: String = "", @Index var roomId: String = "", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt index f3974487f7..2626f96708 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/FileUploader.kt @@ -63,8 +63,7 @@ internal class FileUploader @Inject constructor(@Authenticated mimeType: String?, progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse { // Check size limit - // DO NOT COMMIT: 5 Mo - val maxUploadFileSize = 5 * 1024 * 1024L // homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize + val maxUploadFileSize = homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN && file.length() > maxUploadFileSize) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 31a589ce3f..365277b63a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -101,7 +101,6 @@ import im.vector.app.core.utils.Debouncer import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.KeyboardStateUtils import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES -import im.vector.app.core.utils.TextUtils import im.vector.app.core.utils.checkPermissions import im.vector.app.core.utils.colorizeMatchingText import im.vector.app.core.utils.copyToClipboard @@ -381,7 +380,6 @@ class RoomDetailFragment @Inject constructor( is RoomDetailViewEvents.ActionFailure -> displayRoomDetailActionFailure(it) is RoomDetailViewEvents.ShowMessage -> showSnackWithMessage(it.message) is RoomDetailViewEvents.NavigateToEvent -> navigateToEvent(it) - is RoomDetailViewEvents.FileTooBigError -> displayFileTooBigError(it) is RoomDetailViewEvents.DownloadFileState -> handleDownloadFileState(it) is RoomDetailViewEvents.JoinRoomCommandSuccess -> handleJoinedToAnotherRoom(it) is RoomDetailViewEvents.SendMessageResult -> renderSendMessageResult(it) @@ -749,18 +747,6 @@ class RoomDetailFragment @Inject constructor( } } - private fun displayFileTooBigError(action: RoomDetailViewEvents.FileTooBigError) { - AlertDialog.Builder(requireActivity()) - .setTitle(R.string.dialog_title_error) - .setMessage(getString(R.string.error_file_too_big, - action.filename, - TextUtils.formatFileSize(requireContext(), action.fileSizeInBytes), - TextUtils.formatFileSize(requireContext(), action.homeServerLimitInBytes) - )) - .setPositiveButton(R.string.ok, null) - .show() - } - private fun handleDownloadFileState(action: RoomDetailViewEvents.DownloadFileState) { val activity = requireActivity() if (action.throwable != null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index 0a9ffd83ab..4d1e62da7e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -54,13 +54,6 @@ sealed class RoomDetailViewEvents : VectorViewEvents { object ShowWaitingView : RoomDetailViewEvents() object HideWaitingView : RoomDetailViewEvents() - // TODO Remove - data class FileTooBigError( - val filename: String, - val fileSizeInBytes: Long, - val homeServerLimitInBytes: Long - ) : RoomDetailViewEvents() - data class DownloadFileState( val mimeType: String?, val file: File?, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index e5e9b2c7d3..52779c863d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -1106,27 +1106,6 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleSendMedia(action: RoomDetailAction.SendMedia) { room.sendMedias(action.attachments, action.compressBeforeSending, emptySet()) - - /* - TODO Cleanup this error is now managed by the SDK - val attachments = action.attachments - val homeServerCapabilities = session.getHomeServerCapabilities() - val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize - - if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) { - // Unknown limitation - room.sendMedias(attachments, action.compressBeforeSending, emptySet()) - } else { - when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) { - null -> room.sendMedias(attachments, action.compressBeforeSending, emptySet()) - else -> _viewEvents.post(RoomDetailViewEvents.FileTooBigError( - tooBigFile.name ?: tooBigFile.queryUri.toString(), - tooBigFile.size, - maxUploadFileSize - )) - } - } - */ } private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) { From 765380ab95eeb13b968df7e2c58c9748a00e37df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 1 May 2021 11:05:07 +0200 Subject: [PATCH 10/32] Fix potential issue with video message conten --- .../session/content/UploadContentWorker.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index f8b58e3ad3..4dec8a9c78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -176,10 +176,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter && params.compressBeforeSending) { fileToUpload = videoCompressor.compress(workingFile) .also { compressedFile -> - // Get new Video size - newAttachmentAttributes = NewAttachmentAttributes( - newFileSize = compressedFile.length() - ) + // Get new Video file size. For now video dimensions are not updated + newAttachmentAttributes = newAttachmentAttributes.copy(newFileSize = compressedFile.length()) } .also { filesToDelete.add(it) } } else { @@ -346,8 +344,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val messageContent: MessageContent? = event.asDomain().content.toModel() val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) - is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, - newAttachmentAttributes.newFileSize) + is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) is MessageFileContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) else -> messageContent @@ -378,14 +375,16 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter encryptedFileInfo: EncryptedFileInfo?, thumbnailUrl: String?, thumbnailEncryptedFileInfo: EncryptedFileInfo?, - size: Long): MessageVideoContent { + newAttachmentAttributes: NewAttachmentAttributes?): MessageVideoContent { return copy( url = if (encryptedFileInfo == null) url else null, encryptedFileInfo = encryptedFileInfo?.copy(url = url), videoInfo = videoInfo?.copy( thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl), - size = size + width = newAttachmentAttributes?.newWidth ?: videoInfo.width, + height = newAttachmentAttributes?.newHeight ?: videoInfo.height, + size = newAttachmentAttributes?.newFileSize ?: videoInfo.size ) ) } From e510de1cccf8fda19cabdb8e535731b433a1194d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 1 May 2021 13:34:46 +0200 Subject: [PATCH 11/32] Display video/image compression progress --- .../content/ContentUploadStateTracker.kt | 2 + .../DefaultContentUploadStateTracker.kt | 10 +++ .../session/content/UploadContentWorker.kt | 9 ++- .../session/content/VideoCompressor.kt | 5 ++ .../helper/ContentUploadStateTrackerBinder.kt | 65 ++++++++++++------- vector/src/main/res/values/strings.xml | 2 + 6 files changed, 68 insertions(+), 25 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt index 924da6c19b..ec63eb0be2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/content/ContentUploadStateTracker.kt @@ -31,6 +31,8 @@ interface ContentUploadStateTracker { sealed class State { object Idle : State() object EncryptingThumbnail : State() + object CompressingImage : State() + data class CompressingVideo(val percent: Float) : State() data class UploadingThumbnail(val current: Long, val total: Long) : State() data class Encrypting(val current: Long, val total: Long) : State() data class Uploading(val current: Long, val total: Long) : State() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt index 754f12bd68..17e0a930c1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/DefaultContentUploadStateTracker.kt @@ -78,6 +78,16 @@ internal class DefaultContentUploadStateTracker @Inject constructor() : ContentU updateState(key, progressData) } + internal fun setCompressingImage(key: String) { + val progressData = ContentUploadStateTracker.State.CompressingImage + updateState(key, progressData) + } + + internal fun setCompressingVideo(key: String, percent: Float) { + val progressData = ContentUploadStateTracker.State.CompressingVideo(percent) + updateState(key, progressData) + } + internal fun setProgress(key: String, current: Long, total: Long) { val progressData = ContentUploadStateTracker.State.Uploading(current, total) updateState(key, progressData) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 4dec8a9c78..380f8ee9d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -21,6 +21,7 @@ import android.graphics.BitmapFactory import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -156,6 +157,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // Do not compress gif && attachment.mimeType != MimeTypes.Gif && params.compressBeforeSending) { + notifyTracker(params) { contentUploadStateTracker.setCompressingImage(it) } + fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) .also { compressedFile -> // Get new Bitmap size @@ -174,7 +177,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // Do not compress gif && attachment.mimeType != MimeTypes.Gif && params.compressBeforeSending) { - fileToUpload = videoCompressor.compress(workingFile) + fileToUpload = videoCompressor.compress(workingFile, object: ProgressListener { + override fun onProgress(progress: Int, total: Int) { + notifyTracker(params) { contentUploadStateTracker.setCompressingVideo(it, progress.toFloat()) } + } + }) .also { compressedFile -> // Get new Video file size. For now video dimensions are not updated newAttachmentAttributes = newAttachmentAttributes.copy(newFileSize = compressedFile.length()) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt index 78624bbbf9..8d3425d0cf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -23,6 +23,7 @@ import com.abedelazizshe.lightcompressorlibrary.VideoQuality import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext +import org.matrix.android.sdk.api.listeners.ProgressListener import timber.log.Timber import java.io.File import java.util.UUID @@ -30,6 +31,7 @@ import javax.inject.Inject internal class VideoCompressor @Inject constructor(private val context: Context) { suspend fun compress(videoFile: File, + progressListener: ProgressListener?, quality: VideoQuality = VideoQuality.MEDIUM, isMinBitRateEnabled: Boolean = false, keepOriginalResolution: Boolean = true): File { @@ -46,14 +48,17 @@ internal class VideoCompressor @Inject constructor(private val context: Context) listener = object : CompressionListener { override fun onProgress(percent: Float) { Timber.d("Compressing: $percent%") + progressListener?.onProgress(percent.toInt(), 100) } override fun onStart() { Timber.d("Compressing: start") + progressListener?.onProgress(0, 100) } override fun onSuccess() { Timber.d("Compressing: success") + progressListener?.onProgress(100, 100) job.complete() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 8216d36ac9..2dd94ff244 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -25,6 +25,7 @@ import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenScope import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.utils.TextUtils import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import org.matrix.android.sdk.api.session.content.ContentUploadStateTracker @@ -70,6 +71,9 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, private val messageColorProvider: MessageColorProvider, private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener { + private val progressBar: ProgressBar = progressLayout.findViewById(R.id.mediaProgressBar) + private val progressTextView: TextView = progressLayout.findViewById(R.id.mediaProgressTextView) + override fun onUpdate(state: ContentUploadStateTracker.State) { when (state) { is ContentUploadStateTracker.State.Idle -> handleIdle() @@ -79,19 +83,19 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, is ContentUploadStateTracker.State.Uploading -> handleProgress(state) is ContentUploadStateTracker.State.Failure -> handleFailure(/*state*/) is ContentUploadStateTracker.State.Success -> handleSuccess() - } + is ContentUploadStateTracker.State.CompressingImage -> handleCompressingImage() + is ContentUploadStateTracker.State.CompressingVideo -> handleCompressingVideo(state) + }.exhaustive } private fun handleIdle() { if (isLocalFile) { progressLayout.isVisible = true - val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) - val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isVisible = true - progressBar?.isIndeterminate = true - progressBar?.progress = 0 - progressTextView?.text = progressLayout.context.getString(R.string.send_file_step_idle) - progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.UNSENT)) + progressBar.isVisible = true + progressBar.isIndeterminate = true + progressBar.progress = 0 + progressTextView.text = progressLayout.context.getString(R.string.send_file_step_idle) + progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.UNSENT)) } else { progressLayout.isVisible = false } @@ -113,38 +117,51 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, doHandleProgress(R.string.send_file_step_sending_file, state.current, state.total) } + private fun handleCompressingImage() { + progressLayout.visibility = View.VISIBLE + progressBar.isVisible = true + progressBar.isIndeterminate = true + progressTextView.isVisible = true + progressTextView.text = progressLayout.context.getString(R.string.send_file_step_compressing_image) + progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) + } + + private fun handleCompressingVideo(state: ContentUploadStateTracker.State.CompressingVideo) { + progressLayout.visibility = View.VISIBLE + progressBar.isVisible = true + progressBar.isIndeterminate = false + progressBar.progress = state.percent.toInt() + progressTextView.isVisible = true + progressTextView.text = progressLayout.context.getString(R.string.send_file_step_compressing_video, state.percent.toInt()) + progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) + } + private fun doHandleEncrypting(resId: Int, current: Long, total: Long) { progressLayout.visibility = View.VISIBLE val percent = if (total > 0) (100L * (current.toFloat() / total.toFloat())) else 0f - val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) - val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isIndeterminate = false - progressBar?.progress = percent.toInt() + progressBar.isIndeterminate = false + progressBar.progress = percent.toInt() progressTextView.isVisible = true - progressTextView?.text = progressLayout.context.getString(resId) - progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.ENCRYPTING)) + progressTextView.text = progressLayout.context.getString(resId) + progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.ENCRYPTING)) } private fun doHandleProgress(resId: Int, current: Long, total: Long) { progressLayout.visibility = View.VISIBLE val percent = 100L * (current.toFloat() / total.toFloat()) - val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) - val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isVisible = true - progressBar?.isIndeterminate = false - progressBar?.progress = percent.toInt() + progressBar.isVisible = true + progressBar.isIndeterminate = false + progressBar.progress = percent.toInt() progressTextView.isVisible = true - progressTextView?.text = progressLayout.context.getString(resId, + progressTextView.text = progressLayout.context.getString(resId, TextUtils.formatFileSize(progressLayout.context, current, true), TextUtils.formatFileSize(progressLayout.context, total, true)) - progressTextView?.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) + progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) } private fun handleFailure(/*state: ContentUploadStateTracker.State.Failure*/) { progressLayout.visibility = View.VISIBLE - val progressBar = progressLayout.findViewById(R.id.mediaProgressBar) - val progressTextView = progressLayout.findViewById(R.id.mediaProgressTextView) - progressBar?.isVisible = false + progressBar.isVisible = false // Do not show the message it's too technical for users, and unfortunate when upload is cancelled // in the middle by turning airplane mode for example progressTextView.isVisible = false diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index eadc0db1f4..55a46a2d35 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2174,6 +2174,8 @@ Sending thumbnail (%1$s / %2$s) Encrypting file… Sending file (%1$s / %2$s) + Compressing image… + Compressing video %d%% Downloading file %1$s… File %1$s has been downloaded! From d7cbae7c4730917ff2498ffa72a64d43e9f2a219 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 1 May 2021 14:14:59 +0200 Subject: [PATCH 12/32] Fix a regression with video size --- .../sdk/internal/session/content/UploadContentWorker.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 380f8ee9d1..7404485cc8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -389,8 +389,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter videoInfo = videoInfo?.copy( thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl), - width = newAttachmentAttributes?.newWidth ?: videoInfo.width, - height = newAttachmentAttributes?.newHeight ?: videoInfo.height, + // FIXME for now, the video size is not updated, but if it was, this code is not working. + // width = newAttachmentAttributes?.newWidth ?: videoInfo.width, + // height = newAttachmentAttributes?.newHeight ?: videoInfo.height, size = newAttachmentAttributes?.newFileSize ?: videoInfo.size ) ) From 570cffd3ed84eab5bb7b124365c1c0dec7d40492 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 May 2021 13:36:15 +0200 Subject: [PATCH 13/32] Fix small copy paste error --- .../vector/app/features/home/room/detail/RoomDetailFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 365277b63a..09f17c3367 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -972,7 +972,7 @@ class RoomDetailFragment @Inject constructor( private val attachmentFileActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { - attachmentsHelper.onImageResult(it.data) + attachmentsHelper.onFileResult(it.data) } } From 30a54cfdbc342fd165b8337f42eb2cbefd3f9cc3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 May 2021 13:47:42 +0200 Subject: [PATCH 14/32] User can now select video when selecting Gallery to send attachments to a room --- CHANGES.md | 1 + .../im/vector/lib/multipicker/MediaPicker.kt | 124 ++++++++++++++++++ .../im/vector/lib/multipicker/MultiPicker.kt | 2 + .../entity/MultiPickerImageType.kt | 8 +- .../entity/MultiPickerMediaType.kt | 23 ++++ .../entity/MultiPickerVideoType.kt | 8 +- .../features/attachments/AttachmentsHelper.kt | 8 +- .../features/attachments/AttachmentsMapper.kt | 9 ++ .../home/room/detail/RoomDetailFragment.kt | 6 +- 9 files changed, 174 insertions(+), 15 deletions(-) create mode 100644 multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt create mode 100644 multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerMediaType.kt diff --git a/CHANGES.md b/CHANGES.md index d3e17e6980..01f5aa31a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Improvements 🙌: - Delete and react to stickers (#3250) - Compress video before sending (#442) - Improve file too big error detection (#3245) + - User can now select video when selecting Gallery to send attachments to a room Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt new file mode 100644 index 0000000000..76929b52bd --- /dev/null +++ b/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.multipicker + +import android.content.Context +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.provider.MediaStore +import im.vector.lib.multipicker.entity.MultiPickerBaseMediaType +import im.vector.lib.multipicker.entity.MultiPickerImageType +import im.vector.lib.multipicker.entity.MultiPickerVideoType +import im.vector.lib.multipicker.utils.ImageUtils + +/** + * Image/Video Picker implementation + */ +class MediaPicker : Picker() { + + /** + * Call this function from onActivityResult(int, int, Intent). + * Returns selected image/video files or empty list if user did not select any files. + */ + override fun getSelectedFiles(context: Context, data: Intent?): List { + val mediaList = mutableListOf() + + getSelectedUriList(data).forEach { selectedUri -> + val projection = arrayOf( + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media.MIME_TYPE + ) + + context.contentResolver.query( + selectedUri, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) + val mimeTypeColumn = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + val mimeType = cursor.getString(mimeTypeColumn) + + if (mimeType.isMimeTypeVideo()) { + var duration = 0L + var width = 0 + var height = 0 + var orientation = 0 + + context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L + width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 + height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 + orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0 + } + + mediaList.add( + MultiPickerVideoType( + name, + size, + context.contentResolver.getType(selectedUri), + selectedUri, + width, + height, + orientation, + duration + ) + ) + } else { + // Assume it's an image + val bitmap = ImageUtils.getBitmap(context, selectedUri) + val orientation = ImageUtils.getOrientation(context, selectedUri) + + mediaList.add( + MultiPickerImageType( + name, + size, + context.contentResolver.getType(selectedUri), + selectedUri, + bitmap?.width ?: 0, + bitmap?.height ?: 0, + orientation + ) + ) + } + } + } + } + return mediaList + } + + override fun createIntent(): Intent { + return Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !single) + type = "video/*, image/*" + val mimeTypes = arrayOf("image/*", "video/*") + putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + } + } + + private fun String?.isMimeTypeVideo() = this?.startsWith("video/") == true +} diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt index 7e639a9bd3..f7ed4e5cd9 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt @@ -20,6 +20,7 @@ class MultiPicker { companion object Type { val IMAGE by lazy { MultiPicker() } + val MEDIA by lazy { MultiPicker() } val FILE by lazy { MultiPicker() } val VIDEO by lazy { MultiPicker() } val AUDIO by lazy { MultiPicker() } @@ -31,6 +32,7 @@ class MultiPicker { return when (type) { IMAGE -> ImagePicker() as T VIDEO -> VideoPicker() as T + MEDIA -> MediaPicker() as T FILE -> FilePicker() as T AUDIO -> AudioPicker() as T CONTACT -> ContactPicker() as T diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerImageType.kt b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerImageType.kt index a3f30fc0d5..9efae715cd 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerImageType.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerImageType.kt @@ -23,7 +23,7 @@ data class MultiPickerImageType( override val size: Long, override val mimeType: String?, override val contentUri: Uri, - val width: Int, - val height: Int, - val orientation: Int -) : MultiPickerBaseType + override val width: Int, + override val height: Int, + override val orientation: Int +) : MultiPickerBaseMediaType diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerMediaType.kt b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerMediaType.kt new file mode 100644 index 0000000000..9357e22a74 --- /dev/null +++ b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerMediaType.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.multipicker.entity + +interface MultiPickerBaseMediaType : MultiPickerBaseType { + val width: Int + val height: Int + val orientation: Int +} diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerVideoType.kt b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerVideoType.kt index 0015052c7c..20eb844c8a 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerVideoType.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerVideoType.kt @@ -23,8 +23,8 @@ data class MultiPickerVideoType( override val size: Long, override val mimeType: String?, override val contentUri: Uri, - val width: Int, - val height: Int, - val orientation: Int, + override val width: Int, + override val height: Int, + override val orientation: Int, val duration: Long -) : MultiPickerBaseType +) : MultiPickerBaseMediaType diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index d4efb22eb8..a9fcd353db 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -77,10 +77,10 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab } /** - * Starts the process for handling image picking + * Starts the process for handling image/video picking */ fun selectGallery(activityResultLauncher: ActivityResultLauncher) { - MultiPicker.get(MultiPicker.IMAGE).startWith(activityResultLauncher) + MultiPicker.get(MultiPicker.MEDIA).startWith(activityResultLauncher) } /** @@ -133,9 +133,9 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab } } - fun onImageResult(data: Intent?) { + fun onMediaResult(data: Intent?) { callback.onContentAttachmentsReady( - MultiPicker.get(MultiPicker.IMAGE) + MultiPicker.get(MultiPicker.MEDIA) .getSelectedFiles(context, data) .map { it.toContentAttachmentData() } ) diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt index 4e8dcaacb7..7f8021ea72 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt @@ -17,6 +17,7 @@ package im.vector.app.features.attachments import im.vector.lib.multipicker.entity.MultiPickerAudioType +import im.vector.lib.multipicker.entity.MultiPickerBaseMediaType import im.vector.lib.multipicker.entity.MultiPickerBaseType import im.vector.lib.multipicker.entity.MultiPickerContactType import im.vector.lib.multipicker.entity.MultiPickerFileType @@ -69,6 +70,14 @@ private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type { } } +fun MultiPickerBaseMediaType.toContentAttachmentData(): ContentAttachmentData { + return when (this) { + is MultiPickerImageType -> toContentAttachmentData() + is MultiPickerVideoType -> toContentAttachmentData() + else -> throw IllegalStateException("Unknown media type") + } +} + fun MultiPickerImageType.toContentAttachmentData(): ContentAttachmentData { if (mimeType == null) Timber.w("No mimeType") return ContentAttachmentData( diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 09f17c3367..f78dad4e16 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -988,9 +988,9 @@ class RoomDetailFragment @Inject constructor( } } - private val attachmentImageActivityResultLauncher = registerStartForActivityResult { + private val attachmentMediaActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { - attachmentsHelper.onImageResult(it.data) + attachmentsHelper.onMediaResult(it.data) } } @@ -1991,7 +1991,7 @@ class RoomDetailFragment @Inject constructor( when (type) { AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher) AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) - AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentImageActivityResultLauncher) + AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher) AttachmentTypeSelectorView.Type.CONTACT -> attachmentsHelper.selectContact(attachmentContactActivityResultLauncher) AttachmentTypeSelectorView.Type.STICKER -> roomDetailViewModel.handle(RoomDetailAction.SelectStickerAttachment) From d9ffce7e0d1150b6522667cf23cd5faf459ac466 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 May 2021 16:59:32 +0200 Subject: [PATCH 15/32] Add option to record a video from the camera Replace #2411 --- CHANGES.md | 1 + .../lib/multipicker/CameraVideoPicker.kt | 126 ++++++++++++++++++ .../im/vector/lib/multipicker/MultiPicker.kt | 2 + .../app/core/dialogs/PhotoOrVideoDialog.kt | 118 ++++++++++++++++ .../features/attachments/AttachmentsHelper.kt | 34 ++++- .../home/room/detail/RoomDetailFragment.kt | 17 ++- .../features/settings/VectorPreferences.kt | 20 +++ .../VectorSettingsPreferencesFragment.kt | 26 ++++ .../main/res/layout/dialog_photo_or_video.xml | 52 ++++++++ vector/src/main/res/values/strings.xml | 3 + .../res/xml/vector_settings_preferences.xml | 17 ++- 11 files changed, 404 insertions(+), 12 deletions(-) create mode 100644 multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt create mode 100644 vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt create mode 100644 vector/src/main/res/layout/dialog_photo_or_video.xml diff --git a/CHANGES.md b/CHANGES.md index 01f5aa31a1..7a596066aa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,7 @@ Improvements 🙌: - Compress video before sending (#442) - Improve file too big error detection (#3245) - User can now select video when selecting Gallery to send attachments to a room + - Add option to record a video from the camera Bugfix 🐛: - Message states cosmetic changes (#3007) diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt new file mode 100644 index 0000000000..e7faf9e83c --- /dev/null +++ b/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.multipicker + +import android.content.Context +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.provider.MediaStore +import androidx.activity.result.ActivityResultLauncher +import androidx.core.content.FileProvider +import im.vector.lib.multipicker.entity.MultiPickerVideoType +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Implementation of taking a video with Camera + */ +class CameraVideoPicker { + + /** + * Start camera by using a ActivityResultLauncher + * @return Uri of taken photo or null if the operation is cancelled. + */ + fun startWithExpectingFile(context: Context, activityResultLauncher: ActivityResultLauncher): Uri? { + val videoUri = createVideoUri(context) + val intent = createIntent().apply { + putExtra(MediaStore.EXTRA_OUTPUT, videoUri) + } + activityResultLauncher.launch(intent) + return videoUri + } + + /** + * Call this function from onActivityResult(int, int, Intent). + * @return Taken photo or null if request code is wrong + * or result code is not Activity.RESULT_OK + * or user cancelled the operation. + */ + fun getTakenVideo(context: Context, videoUri: Uri): MultiPickerVideoType? { + val projection = arrayOf( + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media.MIME_TYPE + ) + + context.contentResolver.query( + videoUri, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + var duration = 0L + var width = 0 + var height = 0 + var orientation = 0 + + context.contentResolver.openFileDescriptor(videoUri, "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L + width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 + height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 + orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0 + } + + return MultiPickerVideoType( + name, + size, + context.contentResolver.getType(videoUri), + videoUri, + width, + height, + orientation, + duration + ) + } + } + return null + } + + private fun createIntent(): Intent { + return Intent(MediaStore.ACTION_VIDEO_CAPTURE) + } + + companion object { + fun createVideoUri(context: Context): Uri { + val file = createVideoFile(context) + val authority = context.packageName + ".multipicker.fileprovider" + return FileProvider.getUriForFile(context, authority, file) + } + + private fun createVideoFile(context: Context): File { + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + val storageDir: File = context.filesDir + return File.createTempFile( + "${timeStamp}_", /* prefix */ + ".mp4", /* suffix */ + storageDir /* directory */ + ) + } + } +} diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt index f7ed4e5cd9..6ce50f622a 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/MultiPicker.kt @@ -26,6 +26,7 @@ class MultiPicker { val AUDIO by lazy { MultiPicker() } val CONTACT by lazy { MultiPicker() } val CAMERA by lazy { MultiPicker() } + val CAMERA_VIDEO by lazy { MultiPicker() } @Suppress("UNCHECKED_CAST") fun get(type: MultiPicker): T { @@ -37,6 +38,7 @@ class MultiPicker { AUDIO -> AudioPicker() as T CONTACT -> ContactPicker() as T CAMERA -> CameraPicker() as T + CAMERA_VIDEO -> CameraVideoPicker() as T else -> throw IllegalArgumentException("Unsupported type $type") } } diff --git a/vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt b/vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt new file mode 100644 index 0000000000..d9188397d8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/dialogs/PhotoOrVideoDialog.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.dialogs + +import android.app.Activity +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import im.vector.app.R +import im.vector.app.databinding.DialogPhotoOrVideoBinding +import im.vector.app.features.settings.VectorPreferences + +class PhotoOrVideoDialog( + private val activity: Activity, + private val vectorPreferences: VectorPreferences +) { + + interface PhotoOrVideoDialogListener { + fun takePhoto() + fun takeVideo() + } + + interface PhotoOrVideoDialogSettingsListener { + fun onUpdated() + } + + fun show(listener: PhotoOrVideoDialogListener) { + when (vectorPreferences.getTakePhotoVideoMode()) { + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO -> listener.takePhoto() + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO -> listener.takeVideo() + /* VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK */ + else -> { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_photo_or_video, null) + val views = DialogPhotoOrVideoBinding.bind(dialogLayout) + + // Show option to set as default in this case + views.dialogPhotoOrVideoAsDefault.isVisible = true + // Always default to photo + views.dialogPhotoOrVideoPhoto.isChecked = true + + AlertDialog.Builder(activity) + .setTitle(R.string.option_take_photo_video) + .setView(dialogLayout) + .setPositiveButton(R.string._continue) { _, _ -> + submit(views, vectorPreferences, listener) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + } + } + + private fun submit(views: DialogPhotoOrVideoBinding, + vectorPreferences: VectorPreferences, + listener: PhotoOrVideoDialogListener) { + val mode = if (views.dialogPhotoOrVideoPhoto.isChecked) { + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO + } else { + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO + } + + if (views.dialogPhotoOrVideoAsDefault.isChecked) { + vectorPreferences.setTakePhotoVideoMode(mode) + } + + when (mode) { + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO -> listener.takePhoto() + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO -> listener.takeVideo() + } + } + + fun showForSettings(listener: PhotoOrVideoDialogSettingsListener) { + val currentMode = vectorPreferences.getTakePhotoVideoMode() + + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_photo_or_video, null) + val views = DialogPhotoOrVideoBinding.bind(dialogLayout) + + // Show option for always ask in this case + views.dialogPhotoOrVideoAlwaysAsk.isVisible = true + // Always default to photo + views.dialogPhotoOrVideoPhoto.isChecked = currentMode == VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO + views.dialogPhotoOrVideoVideo.isChecked = currentMode == VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO + views.dialogPhotoOrVideoAlwaysAsk.isChecked = currentMode == VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK + + AlertDialog.Builder(activity) + .setTitle(R.string.option_take_photo_video) + .setView(dialogLayout) + .setPositiveButton(R.string.save) { _, _ -> + submitSettings(views) + listener.onUpdated() + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + private fun submitSettings(views: DialogPhotoOrVideoBinding) { + vectorPreferences.setTakePhotoVideoMode( + when { + views.dialogPhotoOrVideoPhoto.isChecked -> VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO + views.dialogPhotoOrVideoVideo.isChecked -> VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO + else -> VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK + } + ) + } +} diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt index a9fcd353db..28760bf52f 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsHelper.kt @@ -15,12 +15,15 @@ */ package im.vector.app.features.attachments +import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle import androidx.activity.result.ActivityResultLauncher +import im.vector.app.core.dialogs.PhotoOrVideoDialog import im.vector.app.core.platform.Restorable +import im.vector.app.features.settings.VectorPreferences import im.vector.lib.multipicker.MultiPicker import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.content.ContentAttachmentData @@ -91,10 +94,21 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab } /** - * Starts the process for handling capture image picking + * Starts the process for handling image/video capture. Can open a dialog */ - fun openCamera(context: Context, activityResultLauncher: ActivityResultLauncher) { - captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(context, activityResultLauncher) + fun openCamera(activity: Activity, + vectorPreferences: VectorPreferences, + cameraActivityResultLauncher: ActivityResultLauncher, + cameraVideoActivityResultLauncher: ActivityResultLauncher) { + PhotoOrVideoDialog(activity, vectorPreferences).show(object : PhotoOrVideoDialog.PhotoOrVideoDialogListener { + override fun takePhoto() { + captureUri = MultiPicker.get(MultiPicker.CAMERA).startWithExpectingFile(context, cameraActivityResultLauncher) + } + + override fun takeVideo() { + captureUri = MultiPicker.get(MultiPicker.CAMERA_VIDEO).startWithExpectingFile(context, cameraVideoActivityResultLauncher) + } + }) } /** @@ -141,7 +155,7 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab ) } - fun onPhotoResult() { + fun onCameraResult() { captureUri?.let { captureUri -> MultiPicker.get(MultiPicker.CAMERA) .getTakenPhoto(context, captureUri) @@ -153,6 +167,18 @@ class AttachmentsHelper(val context: Context, val callback: Callback) : Restorab } } + fun onCameraVideoResult() { + captureUri?.let { captureUri -> + MultiPicker.get(MultiPicker.CAMERA_VIDEO) + .getTakenVideo(context, captureUri) + ?.let { + callback.onContentAttachmentsReady( + listOf(it).map { it.toContentAttachmentData() } + ) + } + } + } + fun onVideoResult(data: Intent?) { callback.onContentAttachmentsReady( MultiPicker.get(MultiPicker.VIDEO) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index f78dad4e16..cabd69ecf9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -994,9 +994,15 @@ class RoomDetailFragment @Inject constructor( } } - private val attachmentPhotoActivityResultLauncher = registerStartForActivityResult { + private val attachmentCameraActivityResultLauncher = registerStartForActivityResult { if (it.resultCode == Activity.RESULT_OK) { - attachmentsHelper.onPhotoResult() + attachmentsHelper.onCameraResult() + } + } + + private val attachmentCameraVideoActivityResultLauncher = registerStartForActivityResult { + if (it.resultCode == Activity.RESULT_OK) { + attachmentsHelper.onCameraVideoResult() } } @@ -1989,7 +1995,12 @@ class RoomDetailFragment @Inject constructor( private fun launchAttachmentProcess(type: AttachmentTypeSelectorView.Type) { when (type) { - AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera(requireContext(), attachmentPhotoActivityResultLauncher) + AttachmentTypeSelectorView.Type.CAMERA -> attachmentsHelper.openCamera( + activity = requireActivity(), + vectorPreferences = vectorPreferences, + cameraActivityResultLauncher = attachmentCameraActivityResultLauncher, + cameraVideoActivityResultLauncher = attachmentCameraVideoActivityResultLauncher + ) AttachmentTypeSelectorView.Type.FILE -> attachmentsHelper.selectFile(attachmentFileActivityResultLauncher) AttachmentTypeSelectorView.Type.GALLERY -> attachmentsHelper.selectGallery(attachmentMediaActivityResultLauncher) AttachmentTypeSelectorView.Type.AUDIO -> attachmentsHelper.selectAudio(attachmentAudioActivityResultLauncher) diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index 8c6edccda8..b44d44aba0 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -193,6 +193,13 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_UNKNOWN_DEVICE_DISMISSED_LIST = "SETTINGS_UNKNWON_DEVICE_DISMISSED_LIST" + private const val TAKE_PHOTO_VIDEO_MODE = "TAKE_PHOTO_VIDEO_MODE" + + // Possible values for TAKE_PHOTO_VIDEO_MODE + const val TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK = 0 + const val TAKE_PHOTO_VIDEO_MODE_PHOTO = 1 + const val TAKE_PHOTO_VIDEO_MODE_VIDEO = 2 + // Background sync modes // some preferences keys must be kept after a logout @@ -948,4 +955,17 @@ class VectorPreferences @Inject constructor(private val context: Context) { fun labsUseExperimentalRestricted(): Boolean { return defaultPrefs.getBoolean(SETTINGS_LABS_USE_RESTRICTED_JOIN_RULE, false) } + + /* + * Photo / video picker + */ + fun getTakePhotoVideoMode(): Int { + return defaultPrefs.getInt(TAKE_PHOTO_VIDEO_MODE, TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK) + } + + fun setTakePhotoVideoMode(mode: Int) { + return defaultPrefs.edit { + putInt(TAKE_PHOTO_VIDEO_MODE, mode) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt index e895e54a20..6f7e9c27ca 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsPreferencesFragment.kt @@ -23,6 +23,7 @@ import androidx.appcompat.app.AlertDialog import androidx.core.view.children import androidx.preference.Preference import im.vector.app.R +import im.vector.app.core.dialogs.PhotoOrVideoDialog import im.vector.app.core.extensions.restart import im.vector.app.core.preference.VectorListPreference import im.vector.app.core.preference.VectorPreference @@ -45,6 +46,9 @@ class VectorSettingsPreferencesFragment @Inject constructor( private val textSizePreference by lazy { findPreference(VectorPreferences.SETTINGS_INTERFACE_TEXT_SIZE_KEY)!! } + private val takePhotoOrVideoPreference by lazy { + findPreference("SETTINGS_INTERFACE_TAKE_PHOTO_VIDEO")!! + } override fun bindPref() { // user interface preferences @@ -123,6 +127,28 @@ class VectorSettingsPreferencesFragment @Inject constructor( false } } + + // Take photo or video + updateTakePhotoOrVideoPreferenceSummary() + takePhotoOrVideoPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { + PhotoOrVideoDialog(requireActivity(), vectorPreferences).showForSettings(object: PhotoOrVideoDialog.PhotoOrVideoDialogSettingsListener { + override fun onUpdated() { + updateTakePhotoOrVideoPreferenceSummary() + } + }) + true + } + } + + private fun updateTakePhotoOrVideoPreferenceSummary() { + takePhotoOrVideoPreference.summary = getString( + when (vectorPreferences.getTakePhotoVideoMode()) { + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_PHOTO -> R.string.option_take_photo + VectorPreferences.TAKE_PHOTO_VIDEO_MODE_VIDEO -> R.string.option_take_video + /* VectorPreferences.TAKE_PHOTO_VIDEO_MODE_ALWAYS_ASK */ + else -> R.string.option_always_ask + } + ) } // ============================================================================================================== diff --git a/vector/src/main/res/layout/dialog_photo_or_video.xml b/vector/src/main/res/layout/dialog_photo_or_video.xml new file mode 100644 index 0000000000..bc6839738e --- /dev/null +++ b/vector/src/main/res/layout/dialog_photo_or_video.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 55a46a2d35..bfd91a6707 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -575,6 +575,9 @@ Take photo or video Take photo Take video + Always ask + + Use as default and do not ask again You don’t currently have any stickerpacks enabled.\n\nAdd some now? diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 1d39791ad8..1be192e0f5 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> + + @@ -185,10 +192,10 @@ - + \ No newline at end of file From f7949100a74c40c56b47089636442a80aff9daaa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 May 2021 19:44:11 +0200 Subject: [PATCH 16/32] Sent video does not contains duration (#3272) When using the file picker (and not the media picker). Now when using the file picker, we detect the mime type and we send the correct event Also some code duplication --- CHANGES.md | 1 + .../im/vector/lib/multipicker/AudioPicker.kt | 46 +----- .../im/vector/lib/multipicker/CameraPicker.kt | 37 +---- .../lib/multipicker/CameraVideoPicker.kt | 49 +----- .../im/vector/lib/multipicker/FilePicker.kt | 46 ++++-- .../im/vector/lib/multipicker/ImagePicker.kt | 44 +---- .../im/vector/lib/multipicker/MediaPicker.kt | 87 ++-------- .../im/vector/lib/multipicker/VideoPicker.kt | 55 +------ .../multipicker/utils/ContentResolverUtil.kt | 152 ++++++++++++++++++ .../lib/multipicker/utils/MimeTypeUtil.kt | 21 +++ .../features/attachments/AttachmentsMapper.kt | 10 ++ 11 files changed, 237 insertions(+), 311 deletions(-) create mode 100644 multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt create mode 100644 multipicker/src/main/java/im/vector/lib/multipicker/utils/MimeTypeUtil.kt diff --git a/CHANGES.md b/CHANGES.md index 7a596066aa..a48156ac4e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -22,6 +22,7 @@ Bugfix 🐛: - Fix wording issue (#3242) - Fix missing sender information after edits (#3184) - Fix read marker not updating automatically (#3267) + - Sent video does not contains duration (#3272) Translations 🗣: - diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt index e8970d72ef..739bda7004 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/AudioPicker.kt @@ -18,9 +18,8 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent -import android.media.MediaMetadataRetriever -import android.provider.MediaStore import im.vector.lib.multipicker.entity.MultiPickerAudioType +import im.vector.lib.multipicker.utils.toMultiPickerAudioType /** * Audio file picker implementation @@ -32,48 +31,9 @@ class AudioPicker : Picker() { * Returns selected audio files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - val audioList = mutableListOf() - - getSelectedUriList(data).forEach { selectedUri -> - val projection = arrayOf( - MediaStore.Audio.Media.DISPLAY_NAME, - MediaStore.Audio.Media.SIZE - ) - - context.contentResolver.query( - selectedUri, - projection, - null, - null, - null - )?.use { cursor -> - val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE) - - if (cursor.moveToNext()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) - var duration = 0L - - context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) - duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L - } - - audioList.add( - MultiPickerAudioType( - name, - size, - context.contentResolver.getType(selectedUri), - selectedUri, - duration - ) - ) - } - } + return getSelectedUriList(data).mapNotNull { selectedUri -> + selectedUri.toMultiPickerAudioType(context) } - return audioList } override fun createIntent(): Intent { diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt index 64df788e53..b1442a56e1 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/CameraPicker.kt @@ -23,7 +23,7 @@ import android.provider.MediaStore import androidx.activity.result.ActivityResultLauncher import androidx.core.content.FileProvider import im.vector.lib.multipicker.entity.MultiPickerImageType -import im.vector.lib.multipicker.utils.ImageUtils +import im.vector.lib.multipicker.utils.toMultiPickerImageType import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -54,40 +54,7 @@ class CameraPicker { * or user cancelled the operation. */ fun getTakenPhoto(context: Context, photoUri: Uri): MultiPickerImageType? { - val projection = arrayOf( - MediaStore.Images.Media.DISPLAY_NAME, - MediaStore.Images.Media.SIZE - ) - - context.contentResolver.query( - photoUri, - projection, - null, - null, - null - )?.use { cursor -> - val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) - - if (cursor.moveToNext()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) - - val bitmap = ImageUtils.getBitmap(context, photoUri) - val orientation = ImageUtils.getOrientation(context, photoUri) - - return MultiPickerImageType( - name, - size, - context.contentResolver.getType(photoUri), - photoUri, - bitmap?.width ?: 0, - bitmap?.height ?: 0, - orientation - ) - } - } - return null + return photoUri.toMultiPickerImageType(context) } private fun createIntent(): Intent { diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt index e7faf9e83c..76342b6e2e 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/CameraVideoPicker.kt @@ -18,12 +18,12 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent -import android.media.MediaMetadataRetriever import android.net.Uri import android.provider.MediaStore import androidx.activity.result.ActivityResultLauncher import androidx.core.content.FileProvider import im.vector.lib.multipicker.entity.MultiPickerVideoType +import im.vector.lib.multipicker.utils.toMultiPickerVideoType import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -54,52 +54,7 @@ class CameraVideoPicker { * or user cancelled the operation. */ fun getTakenVideo(context: Context, videoUri: Uri): MultiPickerVideoType? { - val projection = arrayOf( - MediaStore.Images.Media.DISPLAY_NAME, - MediaStore.Images.Media.SIZE, - MediaStore.Images.Media.MIME_TYPE - ) - - context.contentResolver.query( - videoUri, - projection, - null, - null, - null - )?.use { cursor -> - val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) - - if (cursor.moveToNext()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) - var duration = 0L - var width = 0 - var height = 0 - var orientation = 0 - - context.contentResolver.openFileDescriptor(videoUri, "r")?.use { pfd -> - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) - duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L - width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 - height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 - orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0 - } - - return MultiPickerVideoType( - name, - size, - context.contentResolver.getType(videoUri), - videoUri, - width, - height, - orientation, - duration - ) - } - } - return null + return videoUri.toMultiPickerVideoType(context) } private fun createIntent(): Intent { diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt index 39bd93d03e..ec98152aa7 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/FilePicker.kt @@ -19,41 +19,55 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent import android.provider.OpenableColumns +import im.vector.lib.multipicker.entity.MultiPickerBaseType import im.vector.lib.multipicker.entity.MultiPickerFileType +import im.vector.lib.multipicker.utils.isMimeTypeAudio +import im.vector.lib.multipicker.utils.isMimeTypeImage +import im.vector.lib.multipicker.utils.isMimeTypeVideo +import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import im.vector.lib.multipicker.utils.toMultiPickerImageType +import im.vector.lib.multipicker.utils.toMultiPickerVideoType /** * Implementation of selecting any type of files */ -class FilePicker : Picker() { +class FilePicker : Picker() { /** * Call this function from onActivityResult(int, int, Intent). * Returns selected files or empty list if user did not select any files. */ - override fun getSelectedFiles(context: Context, data: Intent?): List { - val fileList = mutableListOf() + override fun getSelectedFiles(context: Context, data: Intent?): List { + return getSelectedUriList(data).mapNotNull { selectedUri -> + val type = context.contentResolver.getType(selectedUri) - getSelectedUriList(data).forEach { selectedUri -> - context.contentResolver.query(selectedUri, null, null, null, null) - ?.use { cursor -> - val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE) - if (cursor.moveToFirst()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) + when { + type.isMimeTypeVideo() -> selectedUri.toMultiPickerVideoType(context) + type.isMimeTypeImage() -> selectedUri.toMultiPickerImageType(context) + type.isMimeTypeAudio() -> selectedUri.toMultiPickerAudioType(context) + else -> { + // Other files + context.contentResolver.query(selectedUri, null, null, null, null) + ?.use { cursor -> + val nameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(OpenableColumns.SIZE) + if (cursor.moveToFirst()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) - fileList.add( MultiPickerFileType( name, size, context.contentResolver.getType(selectedUri), selectedUri ) - ) - } - } + } else { + null + } + } + } + } } - return fileList } override fun createIntent(): Intent { diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt index ce73058039..4cc2352109 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/ImagePicker.kt @@ -18,9 +18,8 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent -import android.provider.MediaStore import im.vector.lib.multipicker.entity.MultiPickerImageType -import im.vector.lib.multipicker.utils.ImageUtils +import im.vector.lib.multipicker.utils.toMultiPickerImageType /** * Image Picker implementation @@ -32,46 +31,9 @@ class ImagePicker : Picker() { * Returns selected image files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - val imageList = mutableListOf() - - getSelectedUriList(data).forEach { selectedUri -> - val projection = arrayOf( - MediaStore.Images.Media.DISPLAY_NAME, - MediaStore.Images.Media.SIZE - ) - - context.contentResolver.query( - selectedUri, - projection, - null, - null, - null - )?.use { cursor -> - val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) - - if (cursor.moveToNext()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) - - val bitmap = ImageUtils.getBitmap(context, selectedUri) - val orientation = ImageUtils.getOrientation(context, selectedUri) - - imageList.add( - MultiPickerImageType( - name, - size, - context.contentResolver.getType(selectedUri), - selectedUri, - bitmap?.width ?: 0, - bitmap?.height ?: 0, - orientation - ) - ) - } - } + return getSelectedUriList(data).mapNotNull { selectedUri -> + selectedUri.toMultiPickerImageType(context) } - return imageList } override fun createIntent(): Intent { diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt index 76929b52bd..c58abde694 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/MediaPicker.kt @@ -18,12 +18,10 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent -import android.media.MediaMetadataRetriever -import android.provider.MediaStore import im.vector.lib.multipicker.entity.MultiPickerBaseMediaType -import im.vector.lib.multipicker.entity.MultiPickerImageType -import im.vector.lib.multipicker.entity.MultiPickerVideoType -import im.vector.lib.multipicker.utils.ImageUtils +import im.vector.lib.multipicker.utils.isMimeTypeVideo +import im.vector.lib.multipicker.utils.toMultiPickerImageType +import im.vector.lib.multipicker.utils.toMultiPickerVideoType /** * Image/Video Picker implementation @@ -35,79 +33,16 @@ class MediaPicker : Picker() { * Returns selected image/video files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - val mediaList = mutableListOf() + return getSelectedUriList(data).mapNotNull { selectedUri -> + val mimeType = context.contentResolver.getType(selectedUri) - getSelectedUriList(data).forEach { selectedUri -> - val projection = arrayOf( - MediaStore.Images.Media.DISPLAY_NAME, - MediaStore.Images.Media.SIZE, - MediaStore.Images.Media.MIME_TYPE - ) - - context.contentResolver.query( - selectedUri, - projection, - null, - null, - null - )?.use { cursor -> - val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) - val mimeTypeColumn = cursor.getColumnIndex(MediaStore.Images.Media.MIME_TYPE) - - if (cursor.moveToNext()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) - val mimeType = cursor.getString(mimeTypeColumn) - - if (mimeType.isMimeTypeVideo()) { - var duration = 0L - var width = 0 - var height = 0 - var orientation = 0 - - context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) - duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L - width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 - height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 - orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0 - } - - mediaList.add( - MultiPickerVideoType( - name, - size, - context.contentResolver.getType(selectedUri), - selectedUri, - width, - height, - orientation, - duration - ) - ) - } else { - // Assume it's an image - val bitmap = ImageUtils.getBitmap(context, selectedUri) - val orientation = ImageUtils.getOrientation(context, selectedUri) - - mediaList.add( - MultiPickerImageType( - name, - size, - context.contentResolver.getType(selectedUri), - selectedUri, - bitmap?.width ?: 0, - bitmap?.height ?: 0, - orientation - ) - ) - } - } + if (mimeType.isMimeTypeVideo()) { + selectedUri.toMultiPickerVideoType(context) + } else { + // Assume it's an image + selectedUri.toMultiPickerImageType(context) } } - return mediaList } override fun createIntent(): Intent { @@ -119,6 +54,4 @@ class MediaPicker : Picker() { putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) } } - - private fun String?.isMimeTypeVideo() = this?.startsWith("video/") == true } diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt b/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt index dada9ac5bd..6b6bc52c1b 100644 --- a/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt +++ b/multipicker/src/main/java/im/vector/lib/multipicker/VideoPicker.kt @@ -18,9 +18,8 @@ package im.vector.lib.multipicker import android.content.Context import android.content.Intent -import android.media.MediaMetadataRetriever -import android.provider.MediaStore import im.vector.lib.multipicker.entity.MultiPickerVideoType +import im.vector.lib.multipicker.utils.toMultiPickerVideoType /** * Video Picker implementation @@ -32,57 +31,9 @@ class VideoPicker : Picker() { * Returns selected video files or empty list if user did not select any files. */ override fun getSelectedFiles(context: Context, data: Intent?): List { - val videoList = mutableListOf() - - getSelectedUriList(data).forEach { selectedUri -> - val projection = arrayOf( - MediaStore.Video.Media.DISPLAY_NAME, - MediaStore.Video.Media.SIZE - ) - - context.contentResolver.query( - selectedUri, - projection, - null, - null, - null - )?.use { cursor -> - val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME) - val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE) - - if (cursor.moveToNext()) { - val name = cursor.getString(nameColumn) - val size = cursor.getLong(sizeColumn) - var duration = 0L - var width = 0 - var height = 0 - var orientation = 0 - - context.contentResolver.openFileDescriptor(selectedUri, "r")?.use { pfd -> - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) - duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L - width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 - height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 - orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0 - } - - videoList.add( - MultiPickerVideoType( - name, - size, - context.contentResolver.getType(selectedUri), - selectedUri, - width, - height, - orientation, - duration - ) - ) - } - } + return getSelectedUriList(data).mapNotNull { selectedUri -> + selectedUri.toMultiPickerVideoType(context) } - return videoList } override fun createIntent(): Intent { diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt new file mode 100644 index 0000000000..a1982b0bbc --- /dev/null +++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/ContentResolverUtil.kt @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.multipicker.utils + +import android.content.Context +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.provider.MediaStore +import im.vector.lib.multipicker.entity.MultiPickerAudioType +import im.vector.lib.multipicker.entity.MultiPickerImageType +import im.vector.lib.multipicker.entity.MultiPickerVideoType + +internal fun Uri.toMultiPickerImageType(context: Context): MultiPickerImageType? { + val projection = arrayOf( + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.SIZE + ) + + return context.contentResolver.query( + this, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Images.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + + val bitmap = ImageUtils.getBitmap(context, this) + val orientation = ImageUtils.getOrientation(context, this) + + MultiPickerImageType( + name, + size, + context.contentResolver.getType(this), + this, + bitmap?.width ?: 0, + bitmap?.height ?: 0, + orientation + ) + } else { + null + } + } +} + +internal fun Uri.toMultiPickerVideoType(context: Context): MultiPickerVideoType? { + val projection = arrayOf( + MediaStore.Video.Media.DISPLAY_NAME, + MediaStore.Video.Media.SIZE + ) + + return context.contentResolver.query( + this, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Video.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + var duration = 0L + var width = 0 + var height = 0 + var orientation = 0 + + context.contentResolver.openFileDescriptor(this, "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L + width = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 + height = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0 + orientation = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0 + } + + MultiPickerVideoType( + name, + size, + context.contentResolver.getType(this), + this, + width, + height, + orientation, + duration + ) + } else { + null + } + } +} + +internal fun Uri.toMultiPickerAudioType(context: Context): MultiPickerAudioType? { + val projection = arrayOf( + MediaStore.Audio.Media.DISPLAY_NAME, + MediaStore.Audio.Media.SIZE + ) + + return context.contentResolver.query( + this, + projection, + null, + null, + null + )?.use { cursor -> + val nameColumn = cursor.getColumnIndex(MediaStore.Audio.Media.DISPLAY_NAME) + val sizeColumn = cursor.getColumnIndex(MediaStore.Audio.Media.SIZE) + + if (cursor.moveToNext()) { + val name = cursor.getString(nameColumn) + val size = cursor.getLong(sizeColumn) + var duration = 0L + + context.contentResolver.openFileDescriptor(this, "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L + } + + MultiPickerAudioType( + name, + size, + context.contentResolver.getType(this), + this, + duration + ) + } else { + null + } + } +} diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/utils/MimeTypeUtil.kt b/multipicker/src/main/java/im/vector/lib/multipicker/utils/MimeTypeUtil.kt new file mode 100644 index 0000000000..fc82d03dc5 --- /dev/null +++ b/multipicker/src/main/java/im/vector/lib/multipicker/utils/MimeTypeUtil.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.lib.multipicker.utils + +internal fun String?.isMimeTypeImage() = this?.startsWith("image/") == true +internal fun String?.isMimeTypeVideo() = this?.startsWith("video/") == true +internal fun String?.isMimeTypeAudio() = this?.startsWith("audio/") == true diff --git a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt index 7f8021ea72..2229455dfe 100644 --- a/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt +++ b/vector/src/main/java/im/vector/app/features/attachments/AttachmentsMapper.kt @@ -70,6 +70,16 @@ private fun MultiPickerBaseType.mapType(): ContentAttachmentData.Type { } } +fun MultiPickerBaseType.toContentAttachmentData(): ContentAttachmentData { + return when (this) { + is MultiPickerImageType -> toContentAttachmentData() + is MultiPickerVideoType -> toContentAttachmentData() + is MultiPickerAudioType -> toContentAttachmentData() + is MultiPickerFileType -> toContentAttachmentData() + else -> throw IllegalStateException("Unknown file type") + } +} + fun MultiPickerBaseMediaType.toContentAttachmentData(): ContentAttachmentData { return when (this) { is MultiPickerImageType -> toContentAttachmentData() From 12c4f3c6f77faaeca456dd15ec4c44a10623ffcd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 May 2021 20:25:49 +0200 Subject: [PATCH 17/32] Fix video thumbnail not displayed in when replying and in bottom sheet Also create some extensions for future use --- .../sdk/api/session/room/model/message/FileInfo.kt | 7 +++++++ .../sdk/api/session/room/model/message/ImageInfo.kt | 7 +++++++ .../api/session/room/model/message/LocationInfo.kt | 7 +++++++ .../sdk/api/session/room/model/message/VideoInfo.kt | 7 +++++++ .../detail/timeline/factory/MessageItemFactory.kt | 4 ++-- .../timeline/image/ImageContentRendererFactory.kt | 12 +++++++----- .../features/media/RoomEventsAttachmentProvider.kt | 7 +++---- .../uploads/media/RoomUploadsMediaFragment.kt | 4 ++-- .../uploads/media/UploadsMediaController.kt | 3 ++- 9 files changed, 44 insertions(+), 14 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt index e85bb0800a..f21074096e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/FileInfo.kt @@ -47,3 +47,10 @@ data class FileInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun FileInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt index 048febec39..deadcc3f34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt @@ -57,3 +57,10 @@ data class ImageInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun ImageInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt index a6908dce5b..a76c3c5b64 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -37,3 +37,10 @@ data class LocationInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun LocationInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt index 8379ee9338..8a36c26313 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/VideoInfo.kt @@ -62,3 +62,10 @@ data class VideoInfo( */ @Json(name = "thumbnail_file") val thumbnailFile: EncryptedFileInfo? = null ) + +/** + * Get the url of the encrypted thumbnail or of the thumbnail + */ +fun VideoInfo.getThumbnailUrl(): String? { + return thumbnailFile?.url ?: thumbnailUrl +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 0f214ffb13..63770e4538 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -85,6 +85,7 @@ import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt @@ -337,8 +338,7 @@ class MessageItemFactory @Inject constructor( eventId = informationData.eventId, filename = messageContent.body, mimeType = messageContent.mimeType, - url = messageContent.videoInfo?.thumbnailFile?.url - ?: messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.getThumbnailUrl(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt index 7ff184f664..2ad58df3b8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/image/ImageContentRendererFactory.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt @@ -45,15 +46,16 @@ fun TimelineEvent.buildImageContentRendererData(maxHeight: Int): ImageContentRen } root.isVideoMessage() -> root.getClearContent().toModel() ?.let { messageVideoContent -> + val videoInfo = messageVideoContent.videoInfo ImageContentRenderer.Data( eventId = eventId, filename = messageVideoContent.body, - mimeType = messageVideoContent.mimeType, - url = messageVideoContent.getFileUrl(), - elementToDecrypt = messageVideoContent.encryptedFileInfo?.toElementToDecrypt(), - height = messageVideoContent.videoInfo?.height, + mimeType = videoInfo?.thumbnailInfo?.mimeType, + url = videoInfo?.getThumbnailUrl(), + elementToDecrypt = videoInfo?.thumbnailFile?.toElementToDecrypt(), + height = videoInfo?.thumbnailInfo?.height, maxHeight = maxHeight, - width = messageVideoContent.videoInfo?.width, + width = videoInfo?.thumbnailInfo?.width, maxWidth = maxHeight * 2, allowNonMxcUrls = false ) diff --git a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt index 976f13df58..fc6d5a1f22 100644 --- a/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt +++ b/vector/src/main/java/im/vector/app/features/media/RoomEventsAttachmentProvider.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageWithAttachmentContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt @@ -78,8 +79,7 @@ class RoomEventsAttachmentProvider( eventId = it.eventId, filename = content.body, mimeType = content.mimeType, - url = content.videoInfo?.thumbnailFile?.url - ?: content.videoInfo?.thumbnailUrl, + url = content.videoInfo?.getThumbnailUrl(), elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = content.videoInfo?.height, maxHeight = -1, @@ -102,8 +102,7 @@ class RoomEventsAttachmentProvider( data = data, thumbnail = AttachmentInfo.Image( uid = it.eventId, - url = content.videoInfo?.thumbnailFile?.url - ?: content.videoInfo?.thumbnailUrl ?: "", + url = content.videoInfo?.getThumbnailUrl() ?: "", data = thumbnailData ) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt index fe6dc86165..a7f7dbfe98 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/RoomUploadsMediaFragment.kt @@ -50,6 +50,7 @@ import im.vector.app.features.roomprofile.uploads.RoomUploadsViewState import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import javax.inject.Inject @@ -141,8 +142,7 @@ class RoomUploadsMediaFragment @Inject constructor( eventId = it.eventId, filename = content.body, mimeType = content.mimeType, - url = content.videoInfo?.thumbnailFile?.url - ?: content.videoInfo?.thumbnailUrl, + url = content.videoInfo?.getThumbnailUrl(), elementToDecrypt = content.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = content.videoInfo?.height, maxHeight = -1, diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt index 2a77e65b9e..f8dff345bb 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/uploads/media/UploadsMediaController.kt @@ -30,6 +30,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.getFileUrl +import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.uploads.UploadEvent import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt import javax.inject.Inject @@ -131,7 +132,7 @@ class UploadsMediaController @Inject constructor( eventId = eventId, filename = messageContent.body, mimeType = messageContent.mimeType, - url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.getThumbnailUrl(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = itemSize, From 9d225b78269e86196e3ad96a9c6cdb8671a8fa31 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 3 May 2021 21:12:17 +0200 Subject: [PATCH 18/32] Fixed! --- .../sdk/internal/session/content/UploadContentWorker.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 7404485cc8..380f8ee9d1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -389,9 +389,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter videoInfo = videoInfo?.copy( thumbnailUrl = if (thumbnailEncryptedFileInfo == null) thumbnailUrl else null, thumbnailFile = thumbnailEncryptedFileInfo?.copy(url = thumbnailUrl), - // FIXME for now, the video size is not updated, but if it was, this code is not working. - // width = newAttachmentAttributes?.newWidth ?: videoInfo.width, - // height = newAttachmentAttributes?.newHeight ?: videoInfo.height, + width = newAttachmentAttributes?.newWidth ?: videoInfo.width, + height = newAttachmentAttributes?.newHeight ?: videoInfo.height, size = newAttachmentAttributes?.newFileSize ?: videoInfo.size ) ) From 6347564365a686ee8a2575b7a11202e75b376918 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 12:17:43 +0200 Subject: [PATCH 19/32] Change the library to compress video for licensing reason --- build.gradle | 2 - matrix-sdk-android/build.gradle | 2 +- .../session/content/VideoCompressor.kt | 55 +++++++------------ 3 files changed, 22 insertions(+), 37 deletions(-) diff --git a/build.gradle b/build.gradle index d9fb1542ec..b5be9be768 100644 --- a/build.gradle +++ b/build.gradle @@ -48,8 +48,6 @@ allprojects { // Chat effects includeGroupByRegex 'com\\.github\\.jetradarmobile' includeGroupByRegex 'nl\\.dionsegijn' - // LightCompressor - includeGroupByRegex 'com\\.github\\.AbedElazizShe' } } maven { diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 57436813d5..be1bbe85cb 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -169,7 +169,7 @@ dependencies { implementation 'com.facebook.stetho:stetho-okhttp3:1.6.0' // Video compression - implementation 'com.github.AbedElazizShe:LightCompressor:0.9.0' + implementation 'com.otaliastudios:transcoder:0.10.3' // Phone number https://github.com/google/libphonenumber implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.22' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt index 8d3425d0cf..8eb3e96fc9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -17,9 +17,8 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context -import com.abedelazizshe.lightcompressorlibrary.CompressionListener -import com.abedelazizshe.lightcompressorlibrary.VideoCompressor -import com.abedelazizshe.lightcompressorlibrary.VideoQuality +import com.otaliastudios.transcoder.Transcoder +import com.otaliastudios.transcoder.TranscoderListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext @@ -31,51 +30,39 @@ import javax.inject.Inject internal class VideoCompressor @Inject constructor(private val context: Context) { suspend fun compress(videoFile: File, - progressListener: ProgressListener?, - quality: VideoQuality = VideoQuality.MEDIUM, - isMinBitRateEnabled: Boolean = false, - keepOriginalResolution: Boolean = true): File { + progressListener: ProgressListener?): File { return withContext(Dispatchers.IO) { val job = Job() val destinationFile = createDestinationFile() - // Sadly it does not return the Job, the API is not ideal - VideoCompressor.start( - context = null, - srcUri = null, - srcPath = videoFile.path, - destPath = destinationFile.path, - listener = object : CompressionListener { - override fun onProgress(percent: Float) { - Timber.d("Compressing: $percent%") - progressListener?.onProgress(percent.toInt(), 100) + Timber.d("Compressing: start") + progressListener?.onProgress(0, 100) + + Transcoder.into(destinationFile.path) + .addDataSource(videoFile.path) + .setListener(object: TranscoderListener { + override fun onTranscodeProgress(progress: Double) { + Timber.d("Compressing: $progress%") + progressListener?.onProgress((progress * 100).toInt(), 100) } - override fun onStart() { - Timber.d("Compressing: start") - progressListener?.onProgress(0, 100) - } - - override fun onSuccess() { + override fun onTranscodeCompleted(successCode: Int) { Timber.d("Compressing: success") progressListener?.onProgress(100, 100) job.complete() } - override fun onFailure(failureMessage: String) { - Timber.d("Compressing: failure: $failureMessage") - job.completeExceptionally(Exception(failureMessage)) - } - - override fun onCancelled() { + override fun onTranscodeCanceled() { Timber.d("Compressing: cancel") job.cancel() } - }, - quality = quality, - isMinBitRateEnabled = isMinBitRateEnabled, - keepOriginalResolution = keepOriginalResolution - ) + + override fun onTranscodeFailed(exception: Throwable) { + Timber.d("Compressing: failure: ${exception.localizedMessage}") + job.completeExceptionally(exception) + } + }) + .transcode() job.join() destinationFile From bcf6dcbf7022e29e16e10c2c3aa2dcfb9d78c93f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 12:47:21 +0200 Subject: [PATCH 20/32] Compute new video dimension after compression --- .../session/content/UploadContentWorker.kt | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 380f8ee9d1..4b84b9f9c5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context import android.graphics.BitmapFactory +import android.media.MediaMetadataRetriever +import androidx.core.net.toUri import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass import org.matrix.android.sdk.api.extensions.tryOrNull @@ -183,8 +185,25 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } }) .also { compressedFile -> - // Get new Video file size. For now video dimensions are not updated - newAttachmentAttributes = newAttachmentAttributes.copy(newFileSize = compressedFile.length()) + var compressedWidth = 0 + var compressedHeight = 0 + + tryOrNull { + context.contentResolver.openFileDescriptor(compressedFile.toUri(), "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + compressedWidth = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 + compressedHeight = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() + ?: 0 + } + } + + // Get new Video file size and dimensions + newAttachmentAttributes = newAttachmentAttributes.copy( + newFileSize = compressedFile.length(), + newWidth = compressedWidth.takeIf { it != 0 } ?: newAttachmentAttributes.newWidth, + newHeight = compressedHeight.takeIf { it != 0 } ?: newAttachmentAttributes.newHeight + ) } .also { filesToDelete.add(it) } } else { From c502e971a12e7f833bfdb2076887ba0040846db2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 13:35:03 +0200 Subject: [PATCH 21/32] Use Long for size like for the other models (FileInfo, AudioInfo, VideoInfo) --- .../android/sdk/api/session/room/model/message/ImageInfo.kt | 2 +- .../android/sdk/internal/session/content/UploadContentWorker.kt | 2 +- .../sdk/internal/session/room/send/DefaultSendService.kt | 2 +- .../sdk/internal/session/room/send/LocalEchoEventFactory.kt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt index deadcc3f34..c38ef5bc27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/ImageInfo.kt @@ -40,7 +40,7 @@ data class ImageInfo( /** * Size of the image in bytes. */ - @Json(name = "size") val size: Int = 0, + @Json(name = "size") val size: Long = 0, /** * Metadata about the image referred to in thumbnail_url. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 4b84b9f9c5..305d7b9e70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -392,7 +392,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter info = info?.copy( width = newAttachmentAttributes?.newWidth ?: info.width, height = newAttachmentAttributes?.newHeight ?: info.height, - size = newAttachmentAttributes?.newFileSize?.toInt() ?: info.size + size = newAttachmentAttributes?.newFileSize ?: info.size ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt index 7cc238fcc9..449189e6b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt @@ -141,7 +141,7 @@ internal class DefaultSendService @AssistedInject constructor( is MessageImageContent -> { // The image has not yet been sent val attachmentData = ContentAttachmentData( - size = messageContent.info!!.size.toLong(), + size = messageContent.info!!.size, mimeType = messageContent.info.mimeType!!, width = messageContent.info.width.toLong(), height = messageContent.info.height.toLong(), diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 432a4af062..c1ad6205c3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -244,7 +244,7 @@ internal class LocalEchoEventFactory @Inject constructor( mimeType = attachment.getSafeMimeType(), width = width?.toInt() ?: 0, height = height?.toInt() ?: 0, - size = attachment.size.toInt() + size = attachment.size ), url = attachment.queryUri.toString() ) From edbfc2e2e923ab20db3425ad0090d7b7318825cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 13:50:49 +0200 Subject: [PATCH 22/32] Add details about events with attachment in the bottomsheet --- .../BottomSheetMessagePreviewItem.kt | 5 + .../im/vector/app/core/utils/TextUtils.kt | 12 +++ .../app/features/call/webrtc/WebRtcCall.kt | 12 +-- .../action/MessageActionsEpoxyController.kt | 3 + .../timeline/format/EventDetailsFormatter.kt | 92 +++++++++++++++++++ .../item_bottom_sheet_message_preview.xml | 23 ++++- 6 files changed, 134 insertions(+), 13 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt diff --git a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt index 5ff7a07e3c..f05e0843e8 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/bottomsheet/BottomSheetMessagePreviewItem.kt @@ -46,6 +46,9 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel(R.id.bottom_sheet_message_preview_avatar) val sender by bind(R.id.bottom_sheet_message_preview_sender) val body by bind(R.id.bottom_sheet_message_preview_body) + val bodyDetails by bind(R.id.bottom_sheet_message_preview_body_details) val timestamp by bind(R.id.bottom_sheet_message_preview_timestamp) val imagePreview by bind(R.id.bottom_sheet_message_preview_image) } diff --git a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt index 84dd793172..992a85679c 100644 --- a/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt +++ b/vector/src/main/java/im/vector/app/core/utils/TextUtils.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.content.Context import android.os.Build import android.text.format.Formatter +import org.threeten.bp.Duration import java.util.TreeMap object TextUtils { @@ -68,4 +69,15 @@ object TextUtils { Formatter.formatFileSize(context, normalizedSize) } } + + fun formatDuration(duration: Duration): String { + val hours = duration.seconds / 3600 + val minutes = (duration.seconds % 3600) / 60 + val seconds = duration.seconds % 60 + return if (hours > 0) { + String.format("%d:%02d:%02d", hours, minutes, seconds) + } else { + String.format("%02d:%02d", minutes, seconds) + } + } } diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 469fba4d5e..fcd6bf0a77 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -21,6 +21,7 @@ import android.hardware.camera2.CameraManager import androidx.core.content.getSystemService import im.vector.app.core.services.CallService import im.vector.app.core.utils.CountUpTimer +import im.vector.app.core.utils.TextUtils.formatDuration import im.vector.app.features.call.CameraEventsHandlerAdapter import im.vector.app.features.call.CameraProxy import im.vector.app.features.call.CameraType @@ -829,17 +830,6 @@ class WebRtcCall(val mxCall: MxCall, } } - private fun formatDuration(duration: Duration): String { - val hours = duration.seconds / 3600 - val minutes = (duration.seconds % 3600) / 60 - val seconds = duration.seconds % 60 - return if (hours > 0) { - String.format("%d:%02d:%02d", hours, minutes, seconds) - } else { - String.format("%02d:%02d", minutes, seconds) - } - } - // MxCall.StateListener override fun onStateUpdate(call: MxCall) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index d62083de30..dcbc2c3293 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -33,6 +33,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.format.EventDetailsFormatter import im.vector.app.features.home.room.detail.timeline.image.buildImageContentRendererData import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod @@ -53,6 +54,7 @@ class MessageActionsEpoxyController @Inject constructor( private val imageContentRenderer: ImageContentRenderer, private val dimensionConverter: DimensionConverter, private val errorFormatter: ErrorFormatter, + private val eventDetailsFormatter: EventDetailsFormatter, private val dateFormatter: VectorDateFormatter ) : TypedEpoxyController() { @@ -71,6 +73,7 @@ class MessageActionsEpoxyController @Inject constructor( data(state.timelineEvent()?.buildImageContentRendererData(dimensionConverter.dpToPx(66))) userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) } body(state.messageBody.linkify(listener)) + bodyDetails(eventDetailsFormatter.format(state.timelineEvent()?.root)) time(formattedDate) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt new file mode 100644 index 0000000000..d5b2367c15 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/EventDetailsFormatter.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2021 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.detail.timeline.format + +import android.content.Context +import im.vector.app.core.utils.TextUtils +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.isAudioMessage +import org.matrix.android.sdk.api.session.events.model.isFileMessage +import org.matrix.android.sdk.api.session.events.model.isImageMessage +import org.matrix.android.sdk.api.session.events.model.isVideoMessage +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent +import org.matrix.android.sdk.api.session.room.model.message.MessageFileContent +import org.matrix.android.sdk.api.session.room.model.message.MessageImageContent +import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent +import org.threeten.bp.Duration +import javax.inject.Inject + +class EventDetailsFormatter @Inject constructor( + private val context: Context +) { + + fun format(event: Event?): CharSequence? { + event ?: return null + + if (event.isRedacted()) { + return null + } + + if (event.isEncrypted() && event.mxDecryptionResult == null) { + return null + } + + return when { + event.isImageMessage() -> formatForImageMessage(event) + event.isVideoMessage() -> formatForVideoMessage(event) + event.isAudioMessage() -> formatForAudioMessage(event) + event.isFileMessage() -> formatForFileMessage(event) + else -> null + } + } + + /** + * Example: "1024 x 720 - 670 kB" + */ + private fun formatForImageMessage(event: Event): CharSequence? { + return event.getClearContent().toModel()?.info + ?.let { "${it.width} x ${it.height} - ${it.size.asFileSize()}" } + } + + /** + * Example: "02:45 - 1024 x 720 - 670 kB" + */ + private fun formatForVideoMessage(event: Event): CharSequence? { + return event.getClearContent().toModel()?.videoInfo + ?.let { "${it.duration.asDuration()} - ${it.width} x ${it.height} - ${it.size.asFileSize()}" } + } + + /** + * Example: "02:45 - 670 kB" + */ + private fun formatForAudioMessage(event: Event): CharSequence? { + return event.getClearContent().toModel()?.audioInfo + ?.let { "${it.duration.asDuration()} - ${it.size.asFileSize()}" } + } + + /** + * Example: "670 kB - application/pdf" + */ + private fun formatForFileMessage(event: Event): CharSequence? { + return event.getClearContent().toModel()?.info + ?.let { "${it.size.asFileSize()} - ${it.mimeType}" } + } + + private fun Long.asFileSize() = TextUtils.formatFileSize(context, this) + private fun Int.asDuration() = TextUtils.formatDuration(Duration.ofMillis(toLong())) +} diff --git a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml index afd96d5690..c2175d495e 100644 --- a/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml +++ b/vector/src/main/res/layout/item_bottom_sheet_message_preview.xml @@ -76,10 +76,29 @@ android:textColor="?riotx_text_secondary" android:textIsSelectable="false" android:textSize="14sp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/bottom_sheet_message_preview_body_details" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/bottom_sheet_message_preview_avatar" app:layout_constraintTop_toBottomOf="@id/bottom_sheet_message_preview_image" - tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. " /> + app:layout_goneMarginBottom="4dp" + tools:text="Quis harum id autem cumque consequatur laboriosam aliquam sed. Sint accusamus dignissimos nobis ullam earum debitis aspernatur. Sint accusamus dignissimos nobis ullam earum debitis aspernatur." /> + + From d8bdf8e625d62126b6c5eac838b02ed054359311 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 17:24:48 +0200 Subject: [PATCH 23/32] Rename file (ktlint) --- .../{MultiPickerMediaType.kt => MultiPickerBaseMediaType.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename multipicker/src/main/java/im/vector/lib/multipicker/entity/{MultiPickerMediaType.kt => MultiPickerBaseMediaType.kt} (100%) diff --git a/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerMediaType.kt b/multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerBaseMediaType.kt similarity index 100% rename from multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerMediaType.kt rename to multipicker/src/main/java/im/vector/lib/multipicker/entity/MultiPickerBaseMediaType.kt From fb42b869ddbfb1913ee9f8404ab270276986d8e7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 17:31:04 +0200 Subject: [PATCH 24/32] Fix false positive with lint --- .../detail/timeline/helper/ContentUploadStateTrackerBinder.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt index 2dd94ff244..75570a67a0 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/ContentUploadStateTrackerBinder.kt @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.helper +import android.annotation.SuppressLint import android.view.View import android.view.ViewGroup import android.widget.ProgressBar @@ -126,12 +127,15 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup, progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) } + // Add SuppressLint to fix a false positive + @SuppressLint("StringFormatMatches") private fun handleCompressingVideo(state: ContentUploadStateTracker.State.CompressingVideo) { progressLayout.visibility = View.VISIBLE progressBar.isVisible = true progressBar.isIndeterminate = false progressBar.progress = state.percent.toInt() progressTextView.isVisible = true + // False positive is here... progressTextView.text = progressLayout.context.getString(R.string.send_file_step_compressing_video, state.percent.toInt()) progressTextView.setTextColor(messageColorProvider.getMessageTextColor(SendState.SENDING)) } From efc08b376bff6d8bba7c2f11801172f4fc0e7277 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 22:50:42 +0200 Subject: [PATCH 25/32] Transcoder.transcode() already operated on a background thread --- .../session/content/VideoCompressor.kt | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt index 8eb3e96fc9..712ee94f2f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -31,42 +31,43 @@ import javax.inject.Inject internal class VideoCompressor @Inject constructor(private val context: Context) { suspend fun compress(videoFile: File, progressListener: ProgressListener?): File { - return withContext(Dispatchers.IO) { - val job = Job() - val destinationFile = createDestinationFile() - - Timber.d("Compressing: start") - progressListener?.onProgress(0, 100) - - Transcoder.into(destinationFile.path) - .addDataSource(videoFile.path) - .setListener(object: TranscoderListener { - override fun onTranscodeProgress(progress: Double) { - Timber.d("Compressing: $progress%") - progressListener?.onProgress((progress * 100).toInt(), 100) - } - - override fun onTranscodeCompleted(successCode: Int) { - Timber.d("Compressing: success") - progressListener?.onProgress(100, 100) - job.complete() - } - - override fun onTranscodeCanceled() { - Timber.d("Compressing: cancel") - job.cancel() - } - - override fun onTranscodeFailed(exception: Throwable) { - Timber.d("Compressing: failure: ${exception.localizedMessage}") - job.completeExceptionally(exception) - } - }) - .transcode() - - job.join() - destinationFile + val destinationFile = withContext(Dispatchers.IO) { + createDestinationFile() } + + val job = Job() + + Timber.d("Compressing: start") + progressListener?.onProgress(0, 100) + + Transcoder.into(destinationFile.path) + .addDataSource(videoFile.path) + .setListener(object : TranscoderListener { + override fun onTranscodeProgress(progress: Double) { + Timber.d("Compressing: $progress%") + progressListener?.onProgress((progress * 100).toInt(), 100) + } + + override fun onTranscodeCompleted(successCode: Int) { + Timber.d("Compressing: success") + progressListener?.onProgress(100, 100) + job.complete() + } + + override fun onTranscodeCanceled() { + Timber.d("Compressing: cancel") + job.cancel() + } + + override fun onTranscodeFailed(exception: Throwable) { + Timber.d("Compressing: failure: ${exception.localizedMessage}") + job.completeExceptionally(exception) + } + }) + .transcode() + + job.join() + return destinationFile } private fun createDestinationFile(): File { From 2a411ccf6c746fa3a46162a038eb0b9f528012b6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 23:12:58 +0200 Subject: [PATCH 26/32] Handle properly the case where the video compression is not necessary --- .../session/content/UploadContentWorker.kt | 48 +++++++++++-------- .../session/content/VideoCompressionResult.kt | 24 ++++++++++ .../session/content/VideoCompressor.kt | 28 +++++++++-- 3 files changed, 75 insertions(+), 25 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 305d7b9e70..15c6d243d3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -179,33 +179,41 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // Do not compress gif && attachment.mimeType != MimeTypes.Gif && params.compressBeforeSending) { - fileToUpload = videoCompressor.compress(workingFile, object: ProgressListener { + fileToUpload = videoCompressor.compress(workingFile, object : ProgressListener { override fun onProgress(progress: Int, total: Int) { notifyTracker(params) { contentUploadStateTracker.setCompressingVideo(it, progress.toFloat()) } } }) - .also { compressedFile -> - var compressedWidth = 0 - var compressedHeight = 0 + .let { videoCompressionResult -> + when (videoCompressionResult) { + is VideoCompressionResult.Success -> { + val compressedFile = videoCompressionResult.compressedFile + var compressedWidth: Int? = null + var compressedHeight: Int? = null - tryOrNull { - context.contentResolver.openFileDescriptor(compressedFile.toUri(), "r")?.use { pfd -> - val mediaMetadataRetriever = MediaMetadataRetriever() - mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) - compressedWidth = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0 - compressedHeight = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() - ?: 0 + tryOrNull { + context.contentResolver.openFileDescriptor(compressedFile.toUri(), "r")?.use { pfd -> + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(pfd.fileDescriptor) + compressedWidth = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() + compressedHeight = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() + } + } + + // Get new Video file size and dimensions + newAttachmentAttributes = newAttachmentAttributes.copy( + newFileSize = compressedFile.length(), + newWidth = compressedWidth ?: newAttachmentAttributes.newWidth, + newHeight = compressedHeight ?: newAttachmentAttributes.newHeight + ) + compressedFile + .also { filesToDelete.add(it) } + } + is VideoCompressionResult.CompressionNotNeeded -> { + workingFile } } - - // Get new Video file size and dimensions - newAttachmentAttributes = newAttachmentAttributes.copy( - newFileSize = compressedFile.length(), - newWidth = compressedWidth.takeIf { it != 0 } ?: newAttachmentAttributes.newWidth, - newHeight = compressedHeight.takeIf { it != 0 } ?: newAttachmentAttributes.newHeight - ) } - .also { filesToDelete.add(it) } } else { fileToUpload = workingFile // Fix: OpenableColumns.SIZE may return -1 or 0 @@ -371,7 +379,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val updatedContent = when (messageContent) { is MessageImageContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes) is MessageVideoContent -> messageContent.update(url, encryptedFileInfo, thumbnailUrl, thumbnailEncryptedFileInfo, newAttachmentAttributes) - is MessageFileContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) + is MessageFileContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) is MessageAudioContent -> messageContent.update(url, encryptedFileInfo, newAttachmentAttributes.newFileSize) else -> messageContent } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt new file mode 100644 index 0000000000..b1362317ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressionResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.content + +import java.io.File + +internal sealed class VideoCompressionResult { + data class Success(val compressedFile: File) : VideoCompressionResult() + object CompressionNotNeeded : VideoCompressionResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt index 712ee94f2f..53113e2d86 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/VideoCompressor.kt @@ -29,8 +29,9 @@ import java.util.UUID import javax.inject.Inject internal class VideoCompressor @Inject constructor(private val context: Context) { + suspend fun compress(videoFile: File, - progressListener: ProgressListener?): File { + progressListener: ProgressListener?): VideoCompressionResult { val destinationFile = withContext(Dispatchers.IO) { createDestinationFile() } @@ -40,6 +41,7 @@ internal class VideoCompressor @Inject constructor(private val context: Context) Timber.d("Compressing: start") progressListener?.onProgress(0, 100) + var result: Int = -1 Transcoder.into(destinationFile.path) .addDataSource(videoFile.path) .setListener(object : TranscoderListener { @@ -49,8 +51,8 @@ internal class VideoCompressor @Inject constructor(private val context: Context) } override fun onTranscodeCompleted(successCode: Int) { - Timber.d("Compressing: success") - progressListener?.onProgress(100, 100) + Timber.d("Compressing: success: $successCode") + result = successCode job.complete() } @@ -60,14 +62,30 @@ internal class VideoCompressor @Inject constructor(private val context: Context) } override fun onTranscodeFailed(exception: Throwable) { - Timber.d("Compressing: failure: ${exception.localizedMessage}") + Timber.w(exception, "Compressing: failure") job.completeExceptionally(exception) } }) .transcode() job.join() - return destinationFile + + progressListener?.onProgress(100, 100) + + return when (result) { + Transcoder.SUCCESS_TRANSCODED -> { + VideoCompressionResult.Success(destinationFile) + } + Transcoder.SUCCESS_NOT_NEEDED -> { + // Delete now the temporary file + withContext(Dispatchers.IO) { + destinationFile.delete() + } + VideoCompressionResult.CompressionNotNeeded + } + else -> + throw IllegalStateException("Unknown result: $result") + } } private fun createDestinationFile(): File { From 9e4f1147f437e23173fd5cbb85279ee7c985a86b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 4 May 2021 23:26:06 +0200 Subject: [PATCH 27/32] Fix an issue with the layout dialog_photo_or_video.xml on Dark theme. Light theme does not use .Bridge, so no reason to use it for Dark theme. I hope there will be no regression though... --- vector/src/main/res/values/theme_dark.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index 5d22683b82..d5b8a40cbb 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -3,7 +3,7 @@ -