From 96d6a72b970150f9277e29a510e42f9cad564204 Mon Sep 17 00:00:00 2001 From: onurays Date: Thu, 25 Jun 2020 10:09:08 +0300 Subject: [PATCH 1/5] Compress images before sending (for devices below Android 10). Fixes #1333 --- CHANGES.md | 1 + .../session/content/UploadContentWorker.kt | 83 +++++++++++++++---- .../preview/AttachmentsPreviewFragment.kt | 7 ++ 3 files changed, 75 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5ad458878d..b1ac5f4bb5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Bugfix 🐛: - Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519) - User could not redact message that they have sent (#1543) - Use vendor prefix for non merged MSC (#1537) + - Compress images before sending (for devices below Android 10) (#1333) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 03ae366ed5..27872a9bc1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -17,9 +17,13 @@ package im.vector.matrix.android.internal.session.content import android.content.Context +import android.graphics.BitmapFactory +import android.os.Build import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import id.zelory.compressor.Compressor +import id.zelory.compressor.constraint.default import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toContent @@ -38,6 +42,9 @@ import im.vector.matrix.android.internal.worker.WorkerParamsFactory import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileOutputStream +import java.util.UUID import javax.inject.Inject private data class NewImageAttributes( @@ -154,26 +161,70 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt file") - notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } + // Temporary disable compressing for Android 10 and above. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt file") + notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType()) - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType()) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener) + } + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) } else { - fileUploader - .uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener) - } + val cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) + cacheFile.parentFile?.mkdirs() + if (cacheFile.exists()) { + cacheFile.delete() + } + cacheFile.createNewFile() + cacheFile.deleteOnExit() - handleSuccess(params, - contentUploadResponse.contentUri, - uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, - newImageAttributes) + val outputStream = FileOutputStream(cacheFile) + outputStream.use { + inputStream.copyTo(outputStream) + } + + val contentUploadResponse = if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { + Compressor.compress(context, cacheFile) { + default( + width = MAX_IMAGE_SIZE, + height = MAX_IMAGE_SIZE + ) + }.also { compressedFile -> + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(compressedFile.absolutePath, options) + val fileSize = compressedFile.length().toInt() + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + }.let { compressedFile -> + fileUploader.uploadFile(compressedFile, attachment.name, attachment.getSafeMimeType(), progressListener) + } + } else { + fileUploader.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) + } + + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } } catch (t: Throwable) { Timber.e(t) handleFailure(params, t) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index 3b1972ffbc..c1ec6444a5 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -21,6 +21,7 @@ import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.content.Intent import android.graphics.Color +import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.Menu @@ -143,6 +144,12 @@ class AttachmentsPreviewFragment @Inject constructor( attachmentPreviewerBigList.scrollToPosition(state.currentAttachmentIndex) attachmentPreviewerMiniatureList.scrollToPosition(state.currentAttachmentIndex) attachmentPreviewerSendImageOriginalSize.text = resources.getQuantityString(R.plurals.send_images_with_original_size, state.attachments.size) + + // Temporary disable compressing for Android 10 and above. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + attachmentPreviewerSendImageOriginalSize.isChecked = true + attachmentPreviewerSendImageOriginalSize.isEnabled = false + } } } From 4349331ee77326b0886409b26e01fd43993e7390 Mon Sep 17 00:00:00 2001 From: onurays Date: Thu, 25 Jun 2020 16:14:54 +0300 Subject: [PATCH 2/5] Support compressing images on Android 10 and above. --- CHANGES.md | 2 +- .../session/content/UploadContentWorker.kt | 116 ++++++++---------- .../preview/AttachmentsPreviewFragment.kt | 7 -- 3 files changed, 53 insertions(+), 72 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b1ac5f4bb5..9a440f9084 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,7 +13,7 @@ Bugfix 🐛: - Incomplete predicate in RealmCryptoStore#getOutgoingRoomKeyRequest (#1519) - User could not redact message that they have sent (#1543) - Use vendor prefix for non merged MSC (#1537) - - Compress images before sending (for devices below Android 10) (#1333) + - Compress images before sending (#1333) Translations 🗣: - diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 27872a9bc1..1ce805bb66 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.session.content import android.content.Context import android.graphics.BitmapFactory -import android.os.Build import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass @@ -43,6 +42,7 @@ import im.vector.matrix.android.internal.worker.getSessionComponent import timber.log.Timber import java.io.ByteArrayInputStream import java.io.File +import java.io.FileInputStream import java.io.FileOutputStream import java.util.UUID import javax.inject.Inject @@ -161,70 +161,58 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - // Temporary disable compressing for Android 10 and above. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt file") - notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } - - val encryptionResult = MXEncryptedAttachments.encryptAttachment(inputStream, attachment.getSafeMimeType()) - uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo - - fileUploader - .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) - } else { - fileUploader - .uploadByteArray(inputStream.readBytes(), attachment.name, attachment.getSafeMimeType(), progressListener) - } - handleSuccess(params, - contentUploadResponse.contentUri, - uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, - newImageAttributes) - } else { - val cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) - cacheFile.parentFile?.mkdirs() - if (cacheFile.exists()) { - cacheFile.delete() - } - cacheFile.createNewFile() - cacheFile.deleteOnExit() - - val outputStream = FileOutputStream(cacheFile) - outputStream.use { - inputStream.copyTo(outputStream) - } - - val contentUploadResponse = if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { - Compressor.compress(context, cacheFile) { - default( - width = MAX_IMAGE_SIZE, - height = MAX_IMAGE_SIZE - ) - }.also { compressedFile -> - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(compressedFile.absolutePath, options) - val fileSize = compressedFile.length().toInt() - newImageAttributes = NewImageAttributes( - options.outWidth, - options.outHeight, - fileSize - ) - }.let { compressedFile -> - fileUploader.uploadFile(compressedFile, attachment.name, attachment.getSafeMimeType(), progressListener) - } - } else { - fileUploader.uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) - } - - handleSuccess(params, - contentUploadResponse.contentUri, - uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, - newImageAttributes) + var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) + cacheFile.parentFile?.mkdirs() + if (cacheFile.exists()) { + cacheFile.delete() } + cacheFile.createNewFile() + cacheFile.deleteOnExit() + + val outputStream = FileOutputStream(cacheFile) + outputStream.use { + inputStream.copyTo(outputStream) + } + + if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { + cacheFile = Compressor.compress(context, cacheFile) { + default( + width = MAX_IMAGE_SIZE, + height = MAX_IMAGE_SIZE + ) + }.also { compressedFile -> + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeFile(compressedFile.absolutePath, options) + val fileSize = compressedFile.length().toInt() + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + } + } + + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt file") + notifyTracker(params) { contentUploadStateTracker.setEncrypting(it) } + + val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(cacheFile), attachment.getSafeMimeType()) + uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo + + fileUploader + .uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener) + } else { + fileUploader + .uploadFile(cacheFile, attachment.name, attachment.getSafeMimeType(), progressListener) + } + + handleSuccess(params, + contentUploadResponse.contentUri, + uploadedFileEncryptedFileInfo, + uploadedThumbnailUrl, + uploadedThumbnailEncryptedFileInfo, + newImageAttributes) + } catch (t: Throwable) { Timber.e(t) handleFailure(params, t) diff --git a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt index c1ec6444a5..3b1972ffbc 100644 --- a/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/attachments/preview/AttachmentsPreviewFragment.kt @@ -21,7 +21,6 @@ import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.content.Intent import android.graphics.Color -import android.os.Build import android.os.Bundle import android.os.Parcelable import android.view.Menu @@ -144,12 +143,6 @@ class AttachmentsPreviewFragment @Inject constructor( attachmentPreviewerBigList.scrollToPosition(state.currentAttachmentIndex) attachmentPreviewerMiniatureList.scrollToPosition(state.currentAttachmentIndex) attachmentPreviewerSendImageOriginalSize.text = resources.getQuantityString(R.plurals.send_images_with_original_size, state.attachments.size) - - // Temporary disable compressing for Android 10 and above. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - attachmentPreviewerSendImageOriginalSize.isChecked = true - attachmentPreviewerSendImageOriginalSize.isEnabled = false - } } } From c5d2a34ebd447d98b556b8432ef114301b9e5f1c Mon Sep 17 00:00:00 2001 From: onurays Date: Fri, 26 Jun 2020 12:58:41 +0300 Subject: [PATCH 3/5] Documentation added to explain creating a cache file. --- .../android/internal/session/content/UploadContentWorker.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index 1ce805bb66..c5a91cbd9e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -161,6 +161,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { + // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should copy + // it to a cache folder by using InputStream and OutputStream. + // https://github.com/zetbaitsu/Compressor/pull/150 + // As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) cacheFile.parentFile?.mkdirs() if (cacheFile.exists()) { From 3fcf77e2143ed1d97278814f3b5465cf40adae55 Mon Sep 17 00:00:00 2001 From: onurays Date: Fri, 26 Jun 2020 14:10:49 +0300 Subject: [PATCH 4/5] Lint fix. --- .../android/internal/session/content/UploadContentWorker.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index c5a91cbd9e..bc6a0ee66b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -161,8 +161,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should copy - // it to a cache folder by using InputStream and OutputStream. + // Compressor library works with File instead of Uri for now. Since Scoped Storage doesn't allow us to access files directly, we should + // copy it to a cache folder by using InputStream and OutputStream. // https://github.com/zetbaitsu/Compressor/pull/150 // As soon as the above PR is merged, we can use attachment.queryUri instead of creating a cacheFile. var cacheFile = File.createTempFile(attachment.name ?: UUID.randomUUID().toString(), ".jpg", context.cacheDir) From 824dafb52578f3a681fae4633d00a3f0d3ab6772 Mon Sep 17 00:00:00 2001 From: onurays Date: Fri, 26 Jun 2020 14:48:10 +0300 Subject: [PATCH 5/5] ktlint fix. --- .../android/internal/session/content/UploadContentWorker.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt index bc6a0ee66b..75885755e1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/content/UploadContentWorker.kt @@ -216,7 +216,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo, newImageAttributes) - } catch (t: Throwable) { Timber.e(t) handleFailure(params, t)