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