diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 59595f304..181d0955d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -6,20 +6,19 @@ import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.content.res.Configuration -import android.graphics.Bitmap import android.net.Uri import android.util.Log import androidx.core.content.FileProvider import com.bumptech.glide.Glide import com.bumptech.glide.load.DecodeFormat import com.bumptech.glide.request.RequestOptions +import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils.createTag import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.io.ByteArrayOutputStream import java.io.File import java.util.* import kotlin.collections.ArrayList @@ -134,12 +133,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { .submit(size, size) try { - val bitmap = target.get() - if (bitmap != null) { - val stream = ByteArrayOutputStream() - bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream) - data = stream.toByteArray() - } + data = target.get()?.getBytes(canHaveAlpha = true, recycle = false) } catch (e: Exception) { Log.w(LOG_TAG, "failed to decode app icon for packageName=$packageName", e) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 5f0231b2f..b21a75ec0 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -4,7 +4,6 @@ import android.content.ContentResolver import android.content.ContentUris import android.content.Context import android.database.Cursor -import android.graphics.Bitmap import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri @@ -46,6 +45,7 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText import deckers.thibault.aves.utils.BitmapUtils +import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.LogUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isImage @@ -58,7 +58,6 @@ import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.io.ByteArrayOutputStream import java.io.IOException import java.util.* import kotlin.math.roundToLong @@ -548,14 +547,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { StorageUtils.openInputStream(context, uri)?.use { input -> val exif = ExifInterface(input) val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) - exif.thumbnailBitmap?.let { - val bitmap = TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), it, orientation) - if (bitmap != null) { - val stream = ByteArrayOutputStream() - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) - thumbnails.add(stream.toByteArray()) + exif.thumbnailBitmap?.let { bitmap -> + TransformationUtils.rotateImageExif(BitmapUtils.getBitmapPool(context), bitmap, orientation)?.let { + thumbnails.add(it.getBytes(canHaveAlpha = false, recycle = false)) } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt index bc35b47f3..3d0212160 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/RegionFetcher.kt @@ -1,16 +1,15 @@ package deckers.thibault.aves.channel.calls import android.content.Context -import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.BitmapRegionDecoder import android.graphics.Rect import android.net.Uri import android.util.Size +import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodChannel -import java.io.ByteArrayOutputStream import kotlin.math.roundToInt class RegionFetcher internal constructor( @@ -61,19 +60,9 @@ class RegionFetcher internal constructor( regionRect } - val data = decoder.decodeRegion(effectiveRect, options)?.let { - val stream = ByteArrayOutputStream() - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency - if (MimeTypes.canHaveAlpha(mimeType)) { - it.compress(Bitmap.CompressFormat.PNG, 0, stream) - } else { - it.compress(Bitmap.CompressFormat.JPEG, 100, stream) - } - stream.toByteArray() - } - if (data != null) { - result.success(data) + val bitmap = decoder.decodeRegion(effectiveRect, options) + if (bitmap != null) { + result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = true)) } else { result.error("getRegion-null", "failed to decode region for uri=$uri regionRect=$regionRect", null) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt index 80da74a90..f0e8fd4c2 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/ThumbnailFetcher.kt @@ -15,11 +15,12 @@ import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.signature.ObjectKey import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation +import deckers.thibault.aves.utils.BitmapUtils.getBytes +import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterContentResolverThumbnail import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide import io.flutter.plugin.common.MethodChannel -import java.io.ByteArrayOutputStream class ThumbnailFetcher internal constructor( private val activity: Activity, @@ -39,6 +40,7 @@ class ThumbnailFetcher internal constructor( fun fetch() { var bitmap: Bitmap? = null + var recycle = true var exception: Exception? = null // fetch low quality thumbnails when size is not specified @@ -58,25 +60,21 @@ class ThumbnailFetcher internal constructor( if (bitmap == null) { try { bitmap = getByGlide() + recycle = false } catch (e: Exception) { exception = e } } if (bitmap != null) { - val stream = ByteArrayOutputStream() - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream) - result.success(stream.toByteArray()) - return + result.success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = recycle)) + } else { + var errorDetails: String? = exception?.message + if (errorDetails?.isNotEmpty() == true) { + errorDetails = errorDetails.split("\n".toRegex(), 2).first() + } + result.error("getThumbnail-null", "failed to get thumbnail for uri=$uri", errorDetails) } - - var errorDetails: String? = exception?.message - if (errorDetails?.isNotEmpty() == true) { - errorDetails = errorDetails.split("\n".toRegex(), 2).first() - } - result.error("getThumbnail-null", "failed to get thumbnail for uri=$uri", errorDetails) } @RequiresApi(api = Build.VERSION_CODES.Q) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt index 1a4da7f1c..ca99a84c1 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/streams/ImageByteStreamHandler.kt @@ -1,7 +1,6 @@ package deckers.thibault.aves.channel.streams import android.app.Activity -import android.graphics.Bitmap import android.net.Uri import android.os.Handler import android.os.Looper @@ -11,7 +10,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.RequestOptions import deckers.thibault.aves.decoder.VideoThumbnail import deckers.thibault.aves.utils.BitmapUtils.applyExifOrientation -import deckers.thibault.aves.utils.MimeTypes.canHaveAlpha +import deckers.thibault.aves.utils.BitmapUtils.getBytes +import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.isSupportedByFlutter import deckers.thibault.aves.utils.MimeTypes.isVideo import deckers.thibault.aves.utils.MimeTypes.needRotationAfterGlide @@ -20,7 +20,6 @@ import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel.EventSink import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import java.io.ByteArrayOutputStream import java.io.IOException import java.io.InputStream @@ -102,15 +101,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen bitmap = applyExifOrientation(activity, bitmap, rotationDegrees, isFlipped) } if (bitmap != null) { - val stream = ByteArrayOutputStream() - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG, but it allows transparency - if (canHaveAlpha(mimeType)) { - bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream) - } else { - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) - } - success(stream.toByteArray()) + success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false)) } else { error("streamImage-image-decode-null", "failed to get image from uri=$uri", null) } @@ -134,11 +125,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen try { val bitmap = target.get() if (bitmap != null) { - val stream = ByteArrayOutputStream() - // we compress the bitmap because Dart Image.memory cannot decode the raw bytes - // Bitmap.CompressFormat.PNG is slower than JPEG - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) - success(stream.toByteArray()) + success(bitmap.getBytes(canHaveAlpha = false, recycle = false)) } else { error("streamImage-video-null", "failed to get image from uri=$uri", null) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt index 8b88d2f85..ac2ccaaed 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/decoder/VideoThumbnailGlideModule.kt @@ -1,7 +1,6 @@ package deckers.thibault.aves.decoder import android.content.Context -import android.graphics.Bitmap import android.net.Uri import com.bumptech.glide.Glide import com.bumptech.glide.Priority @@ -16,9 +15,9 @@ import com.bumptech.glide.load.model.ModelLoaderFactory import com.bumptech.glide.load.model.MultiModelLoaderFactory import com.bumptech.glide.module.LibraryGlideModule import com.bumptech.glide.signature.ObjectKey +import deckers.thibault.aves.utils.BitmapUtils.getBytes import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.InputStream @GlideModule @@ -49,16 +48,12 @@ internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFe val retriever = openMetadataRetriever(model.context, model.uri) if (retriever != null) { try { - var picture = retriever.embeddedPicture - if (picture == null) { - // not ideal: bitmap -> byte[] -> bitmap - // but simple fallback and we cache result - val stream = ByteArrayOutputStream() - val bitmap = retriever.frameAtTime - bitmap?.compress(Bitmap.CompressFormat.PNG, 0, stream) - picture = stream.toByteArray() + val picture = retriever.embeddedPicture ?: retriever.frameAtTime?.getBytes(canHaveAlpha = false, recycle = false) + if (picture != null) { + callback.onDataReady(ByteArrayInputStream(picture)) + } else { + callback.onLoadFailed(Exception("failed to get embedded picture or any frame")) } - callback.onDataReady(ByteArrayInputStream(picture)) } catch (e: Exception) { callback.onLoadFailed(e) } finally { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt index edda55bf4..c8ef015e5 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/BitmapUtils.kt @@ -5,8 +5,22 @@ import android.graphics.Bitmap import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.TransformationUtils import deckers.thibault.aves.metadata.Metadata.getExifCode +import java.io.ByteArrayOutputStream object BitmapUtils { + fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean = true): ByteArray { + val stream = ByteArrayOutputStream() + // we compress the bitmap because Flutter cannot decode the raw bytes + // `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency + if (canHaveAlpha) { + this.compress(Bitmap.CompressFormat.PNG, quality, stream) + } else { + this.compress(Bitmap.CompressFormat.JPEG, quality, stream) + } + if (recycle) this.recycle() + return stream.toByteArray() + } + fun applyExifOrientation(context: Context, bitmap: Bitmap?, rotationDegrees: Int?, isFlipped: Boolean?): Bitmap? { if (bitmap == null || rotationDegrees == null || isFlipped == null) return bitmap if (rotationDegrees == 0 && !isFlipped) return bitmap