From af6a94d08eb5a714939c68e0a809a9252d2a2e84 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 18:25:29 +0200 Subject: [PATCH 01/11] Support for image compression on Android 10 --- CHANGES.md | 1 + matrix-sdk-android/build.gradle | 1 - .../session/content/ImageCompressor.kt | 147 ++++++++++++++++++ .../session/content/UploadContentWorker.kt | 39 ++--- .../src/main/assets/open_source_licenses.html | 5 - 5 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt diff --git a/CHANGES.md b/CHANGES.md index db98189499..3d1b23231c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -23,6 +23,7 @@ Bugfix 🐛: - Can't handle ongoing call events in background (#1992) - Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034 - Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027) + - Support for image compression on Android 10 Translations 🗣: - diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index 5be3330ed8..2c20137647 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -144,7 +144,6 @@ dependencies { // Image implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' - implementation 'id.zelory:compressor:3.0.0' // Database implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt new file mode 100644 index 0000000000..ac6ab3050e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 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 android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import androidx.core.content.FileProvider +import androidx.exifinterface.media.ExifInterface +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory +import timber.log.Timber +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +class ImageCompressor @Inject constructor( + @SessionDownloadsDirectory + private val sessionCacheDirectory: File +) { + + private val cacheFolder = File(sessionCacheDirectory, "MF") + + suspend fun compress( + context: Context, + imageUri: Uri, + desiredWidth: Int = 612, + desiredHeight: Int = 816, + desiredQuality: Int = 80, + coroutineContext: CoroutineContext = Dispatchers.IO + ): Uri = withContext(coroutineContext) { + val compressedBitmap = BitmapFactory.Options().run { + inJustDecodeBounds = true + decodeBitmap(context, imageUri, this) + inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight) + inJustDecodeBounds = false + decodeBitmap(context, imageUri, this)?.let { + rotateBitmap(context, imageUri, it) + } + } ?: return@withContext imageUri + + val destinationUri = createDestinationUri(context) + + context.contentResolver.openOutputStream(destinationUri).use { + compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) + } + + return@withContext destinationUri + } + + private fun rotateBitmap(context: Context, uri: Uri, bitmap: Bitmap): Bitmap { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + try { + ExifInterface(inputStream).let { exifInfo -> + val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.preScale(-1f, 1f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.preScale(1f, -1f) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.preRotate(-90f) + matrix.preScale(-1f, 1f) + } + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.preRotate(90f) + matrix.preScale(-1f, 1f) + } + else -> return bitmap + } + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + } + } catch (e: Exception) { + Timber.e(e, "Cannot read orientation: %s", uri.toString()) + } + } + return bitmap + } + + // https://developer.android.com/topic/performance/graphics/load-bitmap + private fun calculateInSampleSize(width: Int, height: Int, desiredWidth: Int, desiredHeight: Int): Int { + var inSampleSize = 1 + + if (width > desiredWidth || height > desiredHeight) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize >= desiredHeight && halfWidth / inSampleSize >= desiredWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize + } + + private fun decodeBitmap(context: Context, uri: Uri, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? { + return try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + BitmapFactory.decodeStream(inputStream, null, options) + } + } catch (e: Exception) { + Timber.e(e, "Cannot decode Bitmap: %s", uri.toString()) + null + } + } + + private fun createDestinationUri(context: Context): Uri { + val file = createTempFile() + val authority = "${context.packageName}.mx-sdk.fileprovider" + return FileProvider.getUriForFile(context, authority, file) + } + + private fun createTempFile(): File { + if (!cacheFolder.exists()) cacheFolder.mkdirs() + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + return File.createTempFile( + "${timeStamp}_", /* prefix */ + ".jpg", /* suffix */ + cacheFolder /* directory */ + ) + } +} 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 015ad3a1e4..5c00a7b628 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 @@ -19,11 +19,11 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context import android.graphics.BitmapFactory +import androidx.core.net.toFile +import androidx.core.net.toUri 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 org.matrix.android.sdk.api.extensions.tryThis import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.events.model.Event @@ -74,6 +74,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter @Inject lateinit var contentUploadStateTracker: DefaultContentUploadStateTracker @Inject lateinit var fileService: DefaultFileService @Inject lateinit var cancelSendTracker: CancelSendTracker + @Inject lateinit var imageCompressor: ImageCompressor override suspend fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) @@ -180,26 +181,20 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val fileToUplaod: File if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { - // 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. - val compressedFile = Compressor.compress(context, workingFile) { - default( - width = MAX_IMAGE_SIZE, - height = MAX_IMAGE_SIZE - ) - } - - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeFile(compressedFile.absolutePath, options) - val fileSize = compressedFile.length().toInt() - newImageAttributes = NewImageAttributes( - options.outWidth, - options.outHeight, - fileSize - ) - fileToUplaod = compressedFile + fileToUplaod = imageCompressor.compress(context, workingFile.toUri(), MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) + .also { compressedUri -> + context.contentResolver.openInputStream(compressedUri)?.use { + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + val bitmap = BitmapFactory.decodeStream(it, null, options) + val fileSize = bitmap?.byteCount ?: 0 + newImageAttributes = NewImageAttributes( + options.outWidth, + options.outHeight, + fileSize + ) + } + } + .toFile() } else { fileToUplaod = workingFile } diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 17557b7eb3..376745e6f7 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -313,11 +313,6 @@ SOFTWARE.
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc. -
  • - Compressor -
    - Copyright (c) 2016 Zetra. -
  • com.otaliastudios:autocomplete
    From 71aa315f2a489d68de7d931c69a9b359d34d5467 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 18:37:47 +0200 Subject: [PATCH 02/11] Cleanup --- .../sdk/internal/session/content/ImageCompressor.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index ac6ab3050e..f66561cfee 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -35,7 +35,7 @@ import java.util.Locale import javax.inject.Inject import kotlin.coroutines.CoroutineContext -class ImageCompressor @Inject constructor( +internal class ImageCompressor @Inject constructor( @SessionDownloadsDirectory private val sessionCacheDirectory: File ) { @@ -62,8 +62,10 @@ class ImageCompressor @Inject constructor( val destinationUri = createDestinationUri(context) - context.contentResolver.openOutputStream(destinationUri).use { - compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) + runCatching { + context.contentResolver.openOutputStream(destinationUri).use { + compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) + } } return@withContext destinationUri @@ -124,7 +126,7 @@ class ImageCompressor @Inject constructor( BitmapFactory.decodeStream(inputStream, null, options) } } catch (e: Exception) { - Timber.e(e, "Cannot decode Bitmap: %s", uri.toString()) + Timber.e(e, "Cannot decode Bitmap") null } } From 6246fd98c3d8da23356f6c4707a8994a0893539f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 2 Sep 2020 18:51:09 +0200 Subject: [PATCH 03/11] Cleanup --- .../session/content/ImageCompressor.kt | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index f66561cfee..20a588c13c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -33,42 +33,40 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import javax.inject.Inject -import kotlin.coroutines.CoroutineContext internal class ImageCompressor @Inject constructor( @SessionDownloadsDirectory private val sessionCacheDirectory: File ) { - private val cacheFolder = File(sessionCacheDirectory, "MF") suspend fun compress( context: Context, imageUri: Uri, - desiredWidth: Int = 612, - desiredHeight: Int = 816, - desiredQuality: Int = 80, - coroutineContext: CoroutineContext = Dispatchers.IO - ): Uri = withContext(coroutineContext) { - val compressedBitmap = BitmapFactory.Options().run { - inJustDecodeBounds = true - decodeBitmap(context, imageUri, this) - inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight) - inJustDecodeBounds = false - decodeBitmap(context, imageUri, this)?.let { - rotateBitmap(context, imageUri, it) - } - } ?: return@withContext imageUri + desiredWidth: Int, + desiredHeight: Int, + desiredQuality: Int = 80): Uri { + return withContext(Dispatchers.IO) { + val compressedBitmap = BitmapFactory.Options().run { + inJustDecodeBounds = true + decodeBitmap(context, imageUri, this) + inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight) + inJustDecodeBounds = false + decodeBitmap(context, imageUri, this)?.let { + rotateBitmap(context, imageUri, it) + } + } ?: return@withContext imageUri - val destinationUri = createDestinationUri(context) + val destinationUri = createDestinationUri(context) - runCatching { - context.contentResolver.openOutputStream(destinationUri).use { - compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) + runCatching { + context.contentResolver.openOutputStream(destinationUri).use { + compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) + } } + + return@withContext destinationUri } - - return@withContext destinationUri } private fun rotateBitmap(context: Context, uri: Uri, bitmap: Bitmap): Bitmap { From 38631eb70e7a57786a01305f3d6ad24aac513da9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 09:37:10 +0200 Subject: [PATCH 04/11] Format --- .../session/content/UploadContentWorker.kt | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 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 5c00a7b628..1860f07c6f 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 @@ -140,22 +140,22 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } - try { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt thumbnail") - notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${attachment.name}", - "application/octet-stream", - thumbnailProgressListener) - } else { - fileUploader.uploadByteArray(thumbnailData.bytes, - "thumb_${attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) - } + try { + val contentUploadResponse = if (params.isEncrypted) { + Timber.v("Encrypt thumbnail") + notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } + val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) + uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo + fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, + "thumb_${attachment.name}", + "application/octet-stream", + thumbnailProgressListener) + } else { + fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + } uploadedThumbnailUrl = contentUploadResponse.contentUri } catch (t: Throwable) { From b31178683ca06f80e5f83b226c042f05a09515fc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 09:37:49 +0200 Subject: [PATCH 05/11] typo --- .../internal/session/content/UploadContentWorker.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 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 1860f07c6f..870c1b2dc6 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 @@ -178,10 +178,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null return try { - val fileToUplaod: File + val fileToUpload: File if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { - fileToUplaod = imageCompressor.compress(context, workingFile.toUri(), MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) + fileToUpload = imageCompressor.compress(context, workingFile.toUri(), MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) .also { compressedUri -> context.contentResolver.openInputStream(compressedUri)?.use { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } @@ -196,7 +196,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } .toFile() } else { - fileToUplaod = workingFile + fileToUpload = workingFile } val contentUploadResponse = if (params.isEncrypted) { @@ -205,7 +205,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) uploadedFileEncryptedFileInfo = - MXEncryptedAttachments.encrypt(fileToUplaod.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> + MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> notifyTracker(params) { contentUploadStateTracker.setEncrypting(it, read.toLong(), total.toLong()) } @@ -222,7 +222,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } else { Timber.v("## FileService: Clear file") fileUploader - .uploadFile(fileToUplaod, attachment.name, attachment.getSafeMimeType(), progressListener) + .uploadFile(fileToUpload, attachment.name, attachment.getSafeMimeType(), progressListener) } Timber.v("## FileService: Update cache storage for ${contentUploadResponse.contentUri}") From 544bff9f4f99264ca0d2313d54ab4e1036fa5743 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 11:07:06 +0200 Subject: [PATCH 06/11] For only with Files now --- .../session/content/ImageCompressor.kt | 58 ++++++------------- .../session/content/UploadContentWorker.kt | 16 ++--- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index 20a588c13c..a125c0aea4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -21,56 +21,46 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix -import android.net.Uri -import androidx.core.content.FileProvider import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.matrix.android.sdk.internal.di.SessionDownloadsDirectory import timber.log.Timber import java.io.File -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale +import java.util.UUID import javax.inject.Inject -internal class ImageCompressor @Inject constructor( - @SessionDownloadsDirectory - private val sessionCacheDirectory: File -) { - private val cacheFolder = File(sessionCacheDirectory, "MF") - +internal class ImageCompressor @Inject constructor() { suspend fun compress( context: Context, - imageUri: Uri, + imageFile: File, desiredWidth: Int, desiredHeight: Int, - desiredQuality: Int = 80): Uri { + desiredQuality: Int = 80): File { return withContext(Dispatchers.IO) { val compressedBitmap = BitmapFactory.Options().run { inJustDecodeBounds = true - decodeBitmap(context, imageUri, this) + decodeBitmap(imageFile, this) inSampleSize = calculateInSampleSize(outWidth, outHeight, desiredWidth, desiredHeight) inJustDecodeBounds = false - decodeBitmap(context, imageUri, this)?.let { - rotateBitmap(context, imageUri, it) + decodeBitmap(imageFile, this)?.let { + rotateBitmap(imageFile, it) } - } ?: return@withContext imageUri + } ?: return@withContext imageFile - val destinationUri = createDestinationUri(context) + val destinationFile = createDestinationFile(context) runCatching { - context.contentResolver.openOutputStream(destinationUri).use { + destinationFile.outputStream().use { compressedBitmap.compress(Bitmap.CompressFormat.JPEG, desiredQuality, it) } } - return@withContext destinationUri + return@withContext destinationFile } } - private fun rotateBitmap(context: Context, uri: Uri, bitmap: Bitmap): Bitmap { - context.contentResolver.openInputStream(uri)?.use { inputStream -> + private fun rotateBitmap(file: File, bitmap: Bitmap): Bitmap { + file.inputStream().use { inputStream -> try { ExifInterface(inputStream).let { exifInfo -> val orientation = exifInfo.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) @@ -94,7 +84,7 @@ internal class ImageCompressor @Inject constructor( return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) } } catch (e: Exception) { - Timber.e(e, "Cannot read orientation: %s", uri.toString()) + Timber.e(e, "Cannot read orientation") } } return bitmap @@ -118,9 +108,9 @@ internal class ImageCompressor @Inject constructor( return inSampleSize } - private fun decodeBitmap(context: Context, uri: Uri, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? { + private fun decodeBitmap(file: File, options: BitmapFactory.Options = BitmapFactory.Options()): Bitmap? { return try { - context.contentResolver.openInputStream(uri)?.use { inputStream -> + file.inputStream().use { inputStream -> BitmapFactory.decodeStream(inputStream, null, options) } } catch (e: Exception) { @@ -129,19 +119,7 @@ internal class ImageCompressor @Inject constructor( } } - private fun createDestinationUri(context: Context): Uri { - val file = createTempFile() - val authority = "${context.packageName}.mx-sdk.fileprovider" - return FileProvider.getUriForFile(context, authority, file) - } - - private fun createTempFile(): File { - if (!cacheFolder.exists()) cacheFolder.mkdirs() - val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - return File.createTempFile( - "${timeStamp}_", /* prefix */ - ".jpg", /* suffix */ - cacheFolder /* directory */ - ) + private fun createDestinationFile(context: Context): File { + return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) } } 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 870c1b2dc6..d24d98d7a2 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 @@ -19,8 +19,6 @@ package org.matrix.android.sdk.internal.session.content import android.content.Context import android.graphics.BitmapFactory -import androidx.core.net.toFile -import androidx.core.net.toUri import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass @@ -129,7 +127,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter inputStream.copyTo(it) } -// inputStream.use { var uploadedThumbnailUrl: String? = null var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null @@ -181,9 +178,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val fileToUpload: File if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { - fileToUpload = imageCompressor.compress(context, workingFile.toUri(), MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) - .also { compressedUri -> - context.contentResolver.openInputStream(compressedUri)?.use { + fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) + .also { compressedFile -> + // Get new Bitmap size + compressedFile.inputStream().use { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } val bitmap = BitmapFactory.decodeStream(it, null, options) val fileSize = bitmap?.byteCount ?: 0 @@ -194,7 +192,9 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) } } - .toFile() + + // we can delete workingFile + tryThis { workingFile.delete() } } else { fileToUpload = workingFile } @@ -232,7 +232,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } Timber.v("## FileService: cache storage updated") } catch (failure: Throwable) { - Timber.e(failure, "## FileService: Failed to update fileservice cache") + Timber.e(failure, "## FileService: Failed to update file cache") } handleSuccess(params, From 0217e7932425af4aad3c1e710eea6a7759bc4410 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 11:08:44 +0200 Subject: [PATCH 07/11] Ensure input stream is closed --- .../sdk/internal/session/content/UploadContentWorker.kt | 6 ++++-- 1 file changed, 4 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 d24d98d7a2..b472d7f411 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 @@ -123,8 +123,10 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) - workingFile.outputStream().use { - inputStream.copyTo(it) + workingFile.outputStream().use { outputStream -> + inputStream.use { inputStream -> + inputStream.copyTo(outputStream) + } } var uploadedThumbnailUrl: String? = null From 3a659a9f3b7d2b7ffac0c0946b87f2442209ce6a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 12:03:20 +0200 Subject: [PATCH 08/11] Ensure temporary files are deleted even in case of Exception --- .../session/content/UploadContentWorker.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 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 b472d7f411..4d23649519 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,6 +111,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter .also { Timber.e("## Send: Work cancelled by user") } } + val filesToDelete = mutableListOf() + try { val inputStream = context.contentResolver.openInputStream(attachment.queryUri) ?: return Result.success( @@ -123,6 +125,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // always use a temporary file, it guaranties that we could report progress on upload and simplifies the flows val workingFile = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + .also { filesToDelete.add(it) } workingFile.outputStream().use { outputStream -> inputStream.use { inputStream -> inputStream.copyTo(outputStream) @@ -194,9 +197,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) } } - - // we can delete workingFile - tryThis { workingFile.delete() } + .also { filesToDelete.add(it) } } else { fileToUpload = workingFile } @@ -205,6 +206,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter Timber.v("## FileService: Encrypt file") val tmpEncrypted = File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) + .also { filesToDelete.add(it) } uploadedFileEncryptedFileInfo = MXEncryptedAttachments.encrypt(fileToUpload.inputStream(), attachment.getSafeMimeType(), tmpEncrypted) { read, total -> @@ -217,10 +219,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter fileUploader .uploadFile(tmpEncrypted, attachment.name, "application/octet-stream", progressListener) - .also { - // we can delete? - tryThis { tmpEncrypted.delete() } - } } else { Timber.v("## FileService: Clear file") fileUploader @@ -257,6 +255,11 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter ) ) ) + } finally { + // Delete all temporary files + filesToDelete.forEach { + tryThis { it.delete() } + } } } From ad984b26fbc26f284dcc344b5ab8faca29665f09 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 12:22:38 +0200 Subject: [PATCH 09/11] Move variable declaration --- .../sdk/internal/session/content/UploadContentWorker.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 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 4d23649519..e4dcc8cf23 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 @@ -100,10 +100,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter val sessionComponent = getSessionComponent(params.sessionId) ?: return Result.success() sessionComponent.inject(this) - val attachment = params.attachment - - var newImageAttributes: NewImageAttributes? = null - val allCancelled = params.events.all { cancelSendTracker.isCancelRequestedFor(it.eventId, it.roomId) } if (allCancelled) { // there is no point in uploading the image! @@ -111,6 +107,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter .also { Timber.e("## Send: Work cancelled by user") } } + val attachment = params.attachment val filesToDelete = mutableListOf() try { @@ -181,6 +178,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter return try { val fileToUpload: File + var newImageAttributes: NewImageAttributes? = null if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) From 0bd7e40a224a184546a0c0510a4dbfee87f6ad9e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 12:33:10 +0200 Subject: [PATCH 10/11] Rework: split long method, extract thumbnail management --- .../session/content/UploadContentWorker.kt | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 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 e4dcc8cf23..d4b8716408 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 @@ -129,38 +129,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } - var uploadedThumbnailUrl: String? = null - var uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? = null - - ThumbnailExtractor.extractThumbnail(context, params.attachment)?.let { thumbnailData -> - val thumbnailProgressListener = object : ProgressRequestBody.Listener { - override fun onProgress(current: Long, total: Long) { - notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } - } - } - - try { - val contentUploadResponse = if (params.isEncrypted) { - Timber.v("Encrypt thumbnail") - notifyTracker(params) { contentUploadStateTracker.setEncryptingThumbnail(it) } - val encryptionResult = MXEncryptedAttachments.encryptAttachment(thumbnailData.bytes.inputStream(), thumbnailData.mimeType) - uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo - fileUploader.uploadByteArray(encryptionResult.encryptedByteArray, - "thumb_${attachment.name}", - "application/octet-stream", - thumbnailProgressListener) - } else { - fileUploader.uploadByteArray(thumbnailData.bytes, - "thumb_${attachment.name}", - thumbnailData.mimeType, - thumbnailProgressListener) - } - - uploadedThumbnailUrl = contentUploadResponse.contentUri - } catch (t: Throwable) { - Timber.e(t, "Thumbnail update failed") - } - } + val uploadThumbnailResult = dealWithThumbnail(params) val progressListener = object : ProgressRequestBody.Listener { override fun onProgress(current: Long, total: Long) { @@ -236,8 +205,8 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, - uploadedThumbnailUrl, - uploadedThumbnailEncryptedFileInfo, + uploadThumbnailResult?.uploadedThumbnailUrl, + uploadThumbnailResult?.uploadedThumbnailEncryptedFileInfo, newImageAttributes) } catch (t: Throwable) { Timber.e(t, "## FileService: ERROR ${t.localizedMessage}") @@ -261,6 +230,54 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter } } + private data class UploadThumbnailResult( + val uploadedThumbnailUrl: String, + val uploadedThumbnailEncryptedFileInfo: EncryptedFileInfo? + ) + + /** + * If appropriate, it will create and upload a thumbnail + */ + private suspend fun dealWithThumbnail(params: Params): UploadThumbnailResult? { + return ThumbnailExtractor.extractThumbnail(context, params.attachment) + ?.let { thumbnailData -> + val thumbnailProgressListener = object : ProgressRequestBody.Listener { + override fun onProgress(current: Long, total: Long) { + notifyTracker(params) { contentUploadStateTracker.setProgressThumbnail(it, current, total) } + } + } + + try { + if (params.isEncrypted) { + 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}", + "application/octet-stream", + thumbnailProgressListener) + UploadThumbnailResult( + contentUploadResponse.contentUri, + encryptionResult.encryptedFileInfo + ) + } else { + val contentUploadResponse = fileUploader.uploadByteArray(thumbnailData.bytes, + "thumb_${params.attachment.name}", + thumbnailData.mimeType, + thumbnailProgressListener) + UploadThumbnailResult( + contentUploadResponse.contentUri, + null + ) + } + } catch (t: Throwable) { + Timber.e(t, "Thumbnail upload failed") + null + } + } + ?: null + } + private fun handleFailure(params: Params, failure: Throwable): Result { notifyTracker(params) { contentUploadStateTracker.setFailure(it, failure) } From 89fa2ece4322c14ec3fa4738e9baeac26d1a22df Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 4 Sep 2020 15:46:16 +0200 Subject: [PATCH 11/11] Fix compilation issue (I'm tired) --- .../android/sdk/internal/session/content/UploadContentWorker.kt | 1 - 1 file changed, 1 deletion(-) 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 d4b8716408..6e70906d13 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 @@ -275,7 +275,6 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter null } } - ?: null } private fun handleFailure(params: Params, failure: Throwable): Result {