From 1b3bd32805dc6aafc9244f904914e55b6b9ec40a Mon Sep 17 00:00:00 2001 From: Jake McGinty Date: Wed, 3 Sep 2014 15:08:02 -0500 Subject: [PATCH] fallback to rough scaling if low memory // FREEBIE --- .../securesms/util/BitmapUtil.java | 76 ++++++++++++++----- .../securesms/util/FlushedInputStream.java | 33 ++++++++ 2 files changed, 88 insertions(+), 21 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/util/FlushedInputStream.java diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index a3e8db9fa0..a8111d6e37 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -11,12 +11,12 @@ import android.graphics.Rect; import android.net.Uri; import android.util.Log; -import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; public class BitmapUtil { + private static final String TAG = BitmapUtil.class.getSimpleName(); private static final int MAX_COMPRESSION_QUALITY = 95; private static final int MIN_COMPRESSION_QUALITY = 50; @@ -26,9 +26,17 @@ public class BitmapUtil { int maxHeight, int maxSize) throws IOException, BitmapDecodingException { - InputStream measure = context.getContentResolver().openInputStream(uri); - InputStream data = context.getContentResolver().openInputStream(uri); - Bitmap bitmap = createScaledBitmap(measure, data, maxWidth, maxHeight); + Bitmap bitmap; + try { + bitmap = createScaledBitmap(context.getContentResolver().openInputStream(uri), + context.getContentResolver().openInputStream(uri), + maxWidth, maxHeight, false); + } catch(OutOfMemoryError oome) { + Log.w(TAG, "OutOfMemoryError when scaling precisely, doing rough scale to save memory instead"); + bitmap = createScaledBitmap(context.getContentResolver().openInputStream(uri), + context.getContentResolver().openInputStream(uri), + maxWidth, maxHeight, true); + } int quality = MAX_COMPRESSION_QUALITY; int attempts = 0; @@ -53,39 +61,56 @@ public class BitmapUtil { final BitmapFactory.Options options = getImageDimensions(measure); final int outWidth = (int)(options.outWidth * scale); final int outHeight = (int)(options.outHeight * scale); - Log.w("BitmapUtil", "creating scaled bitmap with scale " + scale + " => " + outWidth + "x" + outHeight); - return createScaledBitmap(data, outWidth, outHeight, options); + Log.w(TAG, "creating scaled bitmap with scale " + scale + " => " + outWidth + "x" + outHeight); + return createScaledBitmap(data, outWidth, outHeight, options, false); } public static Bitmap createScaledBitmap(InputStream measure, InputStream data, - int maxWidth, int maxHeight) + int maxWidth, int maxHeight) + throws BitmapDecodingException + { + return createScaledBitmap(measure, data, maxWidth, maxHeight, false); + } + + public static Bitmap createScaledBitmap(InputStream measure, InputStream data, + int maxWidth, int maxHeight, boolean constrainedMemory) throws BitmapDecodingException { final BitmapFactory.Options options = getImageDimensions(measure); - return createScaledBitmap(data, maxWidth, maxHeight, options); + return createScaledBitmap(data, maxWidth, maxHeight, options, constrainedMemory); } - private static Bitmap createScaledBitmap(InputStream data, - int maxWidth, int maxHeight, BitmapFactory.Options options) + private static Bitmap createScaledBitmap(InputStream data, int maxWidth, int maxHeight, + BitmapFactory.Options options, boolean constrainedMemory) throws BitmapDecodingException { final int imageWidth = options.outWidth; final int imageHeight = options.outHeight; int scaler = 1; - - while ((imageWidth / scaler / 2 >= maxWidth) && (imageHeight / scaler / 2 >= maxHeight)) + int scaleFactor = (constrainedMemory ? 1 : 2); + while ((imageWidth / scaler / scaleFactor >= maxWidth) && (imageHeight / scaler / scaleFactor >= maxHeight)) { scaler *= 2; + } options.inSampleSize = scaler; options.inJustDecodeBounds = false; - Bitmap roughThumbnail = BitmapFactory.decodeStream(new BufferedInputStream(data), null, options); - Log.w("BitmapUtil", "rough scale " + (imageWidth) + "x" + (imageHeight) + - " => " + (options.outWidth) + "x" + (options.outHeight)); + FlushedInputStream is = new FlushedInputStream(data); + Bitmap roughThumbnail = BitmapFactory.decodeStream(is, null, options); + try { + is.close(); + } catch (IOException ioe) { + Log.w(TAG, "IOException thrown when closing an images InputStream", ioe); + } + Log.w(TAG, "rough scale " + (imageWidth) + "x" + (imageHeight) + + " => " + (options.outWidth) + "x" + (options.outHeight)); if (roughThumbnail == null) { throw new BitmapDecodingException("Decoded stream was null."); } + if (constrainedMemory) { + return roughThumbnail; + } if (options.outWidth > maxWidth || options.outHeight > maxHeight) { final float aspectWidth, aspectHeight; @@ -101,10 +126,14 @@ public class BitmapUtil { aspectWidth = (aspectHeight / options.outHeight) * options.outWidth; } - Log.w("BitmapUtil", "fine scale " + options.outWidth + "x" + options.outHeight + - " => " + aspectWidth + "x" + aspectHeight); - Bitmap scaledThumbnail = Bitmap.createScaledBitmap(roughThumbnail, (int)aspectWidth, (int)aspectHeight, true); - if (roughThumbnail != scaledThumbnail) roughThumbnail.recycle(); + Log.w(TAG, "fine scale " + options.outWidth + "x" + options.outHeight + + " => " + aspectWidth + "x" + aspectHeight); + Bitmap scaledThumbnail = null; + try { + scaledThumbnail = Bitmap.createScaledBitmap(roughThumbnail, (int) aspectWidth, (int) aspectHeight, true); + } finally { + if (roughThumbnail != scaledThumbnail) roughThumbnail.recycle(); + } return scaledThumbnail; } else { return roughThumbnail; @@ -114,8 +143,13 @@ public class BitmapUtil { private static BitmapFactory.Options getImageDimensions(InputStream inputStream) { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(inputStream, null, options); - + FlushedInputStream fis = new FlushedInputStream(inputStream); + BitmapFactory.decodeStream(fis, null, options); + try { + fis.close(); + } catch (IOException ioe) { + Log.w(TAG, "failed to close the InputStream after reading image dimensions"); + } return options; } diff --git a/src/org/thoughtcrime/securesms/util/FlushedInputStream.java b/src/org/thoughtcrime/securesms/util/FlushedInputStream.java new file mode 100644 index 0000000000..3f6636a59f --- /dev/null +++ b/src/org/thoughtcrime/securesms/util/FlushedInputStream.java @@ -0,0 +1,33 @@ +package org.thoughtcrime.securesms.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Memory-friendly workaround found from https://code.google.com/p/android/issues/detail?id=6066#c23 + * to solve decoding problems in bitmaps from InputStreams that don't skip if no more stream is available. + */ +class FlushedInputStream extends FilterInputStream { + public FlushedInputStream(InputStream inputStream) { + super(inputStream); + } + + @Override + public long skip(long n) throws IOException { + long totalBytesSkipped = 0L; + while (totalBytesSkipped < n) { + long bytesSkipped = in.skip(n - totalBytesSkipped); + if (bytesSkipped == 0L) { + int inByte = read(); + if (inByte < 0) { + break; + } else { + bytesSkipped = 1; + } + } + totalBytesSkipped += bytesSkipped; + } + return totalBytesSkipped; + } +}