diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt index d09a6aa7e..43a7aa081 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/fetchers/SvgRegionFetcher.kt @@ -12,6 +12,7 @@ import com.caverock.androidsvg.SVG import com.caverock.androidsvg.SVGParseException import deckers.thibault.aves.metadata.SvgHelper.normalizeSize import deckers.thibault.aves.utils.BitmapUtils.getBytes +import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.StorageUtils import io.flutter.plugin.common.MethodChannel import kotlin.math.ceil @@ -29,13 +30,10 @@ class SvgRegionFetcher internal constructor( imageHeight: Int, result: MethodChannel.Result, ) { - if (sizeBytes != null && sizeBytes > FILE_SIZE_DANGER_THRESHOLD) { - val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) } - if (sizeBytes > availableHeapSize) { - // opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser` - result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, with only $availableHeapSize free bytes, for uri=$uri regionRect=$regionRect", null) - return - } + if (!MemoryUtils.canAllocate(sizeBytes)) { + // opening an SVG that large would yield an OOM during parsing from `com.caverock.androidsvg.SVGParser` + result.error("fetch-read-large", "SVG too large at $sizeBytes bytes, for uri=$uri regionRect=$regionRect", null) + return } var currentSvgRef = lastSvgRef @@ -113,9 +111,4 @@ class SvgRegionFetcher internal constructor( val uri: Uri, val svg: SVG, ) - - companion object { - // arbitrary size to detect files that may yield an OOM - private const val FILE_SIZE_DANGER_THRESHOLD = 10 * (1 shl 20) // MB - } } 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 2066e5035..2cf3654cc 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 @@ -15,6 +15,7 @@ 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.LogUtils +import deckers.thibault.aves.utils.MemoryUtils import deckers.thibault.aves.utils.MimeTypes import deckers.thibault.aves.utils.MimeTypes.canDecodeWithFlutter import deckers.thibault.aves.utils.MimeTypes.isHeic @@ -97,18 +98,23 @@ class ImageByteStreamHandler(private val context: Context, private val arguments } if (isVideo(mimeType)) { - streamVideoByGlide(uri, mimeType) + streamVideoByGlide(uri, mimeType, sizeBytes) } else if (!canDecodeWithFlutter(mimeType, rotationDegrees, isFlipped)) { // decode exotic format on platform side, then encode it in portable format for Flutter streamImageByGlide(uri, pageId, mimeType, sizeBytes, rotationDegrees, isFlipped) } else { // to be decoded by Flutter - streamImageAsIs(uri, mimeType) + streamImageAsIs(uri, mimeType, sizeBytes) } endOfStream() } - private fun streamImageAsIs(uri: Uri, mimeType: String) { + private fun streamImageAsIs(uri: Uri, mimeType: String, sizeBytes: Long?) { + if (!MemoryUtils.canAllocate(sizeBytes)) { + error("streamImage-image-read-large", "original image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null) + return + } + try { StorageUtils.openInputStream(context, uri)?.use { input -> streamBytes(input) } } catch (e: Exception) { @@ -144,7 +150,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments bitmap = applyExifOrientation(context, bitmap, rotationDegrees, isFlipped) } if (bitmap != null) { - success(bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false)) + val bytes = bitmap.getBytes(MimeTypes.canHaveAlpha(mimeType), recycle = false) + if (MemoryUtils.canAllocate(sizeBytes)) { + success(bytes) + } else { + error("streamImage-image-decode-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null) + } } else { error("streamImage-image-decode-null", "failed to get image for mimeType=$mimeType uri=$uri", null) } @@ -155,7 +166,7 @@ class ImageByteStreamHandler(private val context: Context, private val arguments } } - private suspend fun streamVideoByGlide(uri: Uri, mimeType: String) { + private suspend fun streamVideoByGlide(uri: Uri, mimeType: String, sizeBytes: Long?) { val target = Glide.with(context) .asBitmap() .apply(glideOptions) @@ -165,7 +176,12 @@ class ImageByteStreamHandler(private val context: Context, private val arguments @Suppress("BlockingMethodInNonBlockingContext") val bitmap = target.get() if (bitmap != null) { - success(bitmap.getBytes(canHaveAlpha = false, recycle = false)) + val bytes = bitmap.getBytes(canHaveAlpha = false, recycle = false) + if (MemoryUtils.canAllocate(sizeBytes)) { + success(bytes) + } else { + error("streamImage-video-large", "decoded image too large at $sizeBytes bytes, for mimeType=$mimeType uri=$uri", null) + } } else { error("streamImage-video-null", "failed to get image for mimeType=$mimeType uri=$uri", null) } 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 d4a7dfd2d..9d51d4008 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 @@ -15,7 +15,7 @@ object BitmapUtils { private const val INITIAL_BUFFER_SIZE = 2 shl 17 // 256kB // arbitrary size to detect buffer that may yield an OOM - private const val BUFFER_SIZE_DANGER_THRESHOLD = 10 * (1 shl 20) // MB + private const val BUFFER_SIZE_DANGER_THRESHOLD = 3 * (1 shl 20) // MB private val freeBaos = ArrayList() private val mutex = Mutex() @@ -44,11 +44,8 @@ object BitmapUtils { if (recycle) this.recycle() val bufferSize = stream.size() - if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD) { - val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) } - if (bufferSize > availableHeapSize) { - throw Exception("compressed bitmap to $bufferSize bytes, which cannot be allocated to a new byte array, with only $availableHeapSize free bytes") - } + if (bufferSize > BUFFER_SIZE_DANGER_THRESHOLD && !MemoryUtils.canAllocate(bufferSize)) { + throw Exception("bitmap compressed to $bufferSize bytes, which cannot be allocated to a new byte array") } val byteArray = stream.toByteArray() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MemoryUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MemoryUtils.kt new file mode 100644 index 000000000..dc2a74859 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MemoryUtils.kt @@ -0,0 +1,17 @@ +package deckers.thibault.aves.utils + +import android.util.Log + +object MemoryUtils { + private val LOG_TAG = LogUtils.createTag() + + fun canAllocate(byteSize: Number?): Boolean { + byteSize ?: return true + val availableHeapSize = Runtime.getRuntime().let { it.maxMemory() - (it.totalMemory() - it.freeMemory()) } + val danger = byteSize.toLong() > availableHeapSize + if (danger) { + Log.e(LOG_TAG, "trying to handle $byteSize bytes, with only $availableHeapSize free bytes") + } + return !danger + } +} \ No newline at end of file