Support for image compression on Android 10
This commit is contained in:
parent
f6c7f3eed1
commit
af6a94d08e
|
@ -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 🗣:
|
||||
-
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 */
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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/>
|
||||
|
|
Loading…
Reference in New Issue