Support for image compression on Android 10

This commit is contained in:
Benoit Marty 2020-09-02 18:25:29 +02:00 committed by Benoit Marty
parent f6c7f3eed1
commit af6a94d08e
5 changed files with 165 additions and 28 deletions

View file

@ -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 🗣:
-

View file

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

View file

@ -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 */
)
}
}

View file

@ -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<Params>(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
)
}
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 }
BitmapFactory.decodeFile(compressedFile.absolutePath, options)
val fileSize = compressedFile.length().toInt()
val bitmap = BitmapFactory.decodeStream(it, null, options)
val fileSize = bitmap?.byteCount ?: 0
newImageAttributes = NewImageAttributes(
options.outWidth,
options.outHeight,
fileSize
)
fileToUplaod = compressedFile
}
}
.toFile()
} else {
fileToUplaod = workingFile
}

View file

@ -313,11 +313,6 @@ SOFTWARE.
<br/>
Copyright (c) 2012-2016 Dan Wheeler and Dropbox, Inc.
</li>
<li>
<b>Compressor</b>
<br/>
Copyright (c) 2016 Zetra.
</li>
<li>
<b>com.otaliastudios:autocomplete</b>
<br/>