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) - Can't handle ongoing call events in background (#1992)
- Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034 - Crash / Attachment viewer: Cannot draw a recycled Bitmap #2034
- Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027) - Login with Matrix-Id | Autodiscovery fails if identity server is invalid and Homeserver ok (#2027)
- Support for image compression on Android 10
Translations 🗣: Translations 🗣:
- -

View file

@ -144,7 +144,6 @@ dependencies {
// Image // Image
implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01' implementation 'androidx.exifinterface:exifinterface:1.3.0-alpha01'
implementation 'id.zelory:compressor:3.0.0'
// Database // Database
implementation 'com.github.Zhuinden:realm-monarchy:0.5.1' 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.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.squareup.moshi.JsonClass 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.extensions.tryThis
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event 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 contentUploadStateTracker: DefaultContentUploadStateTracker
@Inject lateinit var fileService: DefaultFileService @Inject lateinit var fileService: DefaultFileService
@Inject lateinit var cancelSendTracker: CancelSendTracker @Inject lateinit var cancelSendTracker: CancelSendTracker
@Inject lateinit var imageCompressor: ImageCompressor
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
val params = WorkerParamsFactory.fromData<Params>(inputData) val params = WorkerParamsFactory.fromData<Params>(inputData)
@ -180,26 +181,20 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
val fileToUplaod: File val fileToUplaod: File
if (attachment.type == ContentAttachmentData.Type.IMAGE && params.compressBeforeSending) { 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 fileToUplaod = imageCompressor.compress(context, workingFile.toUri(), MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
// copy it to a cache folder by using InputStream and OutputStream. .also { compressedUri ->
// https://github.com/zetbaitsu/Compressor/pull/150 context.contentResolver.openInputStream(compressedUri)?.use {
// 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 } val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
BitmapFactory.decodeFile(compressedFile.absolutePath, options) val bitmap = BitmapFactory.decodeStream(it, null, options)
val fileSize = compressedFile.length().toInt() val fileSize = bitmap?.byteCount ?: 0
newImageAttributes = NewImageAttributes( newImageAttributes = NewImageAttributes(
options.outWidth, options.outWidth,
options.outHeight, options.outHeight,
fileSize fileSize
) )
fileToUplaod = compressedFile }
}
.toFile()
} else { } else {
fileToUplaod = workingFile fileToUplaod = workingFile
} }

View file

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