From 26c722c95097c4c93156598d18ee03de67283faa Mon Sep 17 00:00:00 2001 From: Vavassor Date: Fri, 31 Mar 2017 17:53:35 -0400 Subject: [PATCH] Prevents an out-of-memory crash when uploading images and also downsizing now uses significantly less memory. --- .../keylesspalace/tusky/ComposeActivity.java | 33 ++++---- .../tusky/DownsizeImageTask.java | 77 +++++++++++++++---- 2 files changed, 75 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 1889f0ece..b333646a8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -945,27 +945,20 @@ public class ComposeActivity extends BaseActivity { private void downsizeMedia(final QueuedMedia item) { item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING; - InputStream stream; - try { - stream = getContentResolver().openInputStream(item.uri); - } catch (FileNotFoundException e) { - onMediaDownsizeFailure(item); - return; - } - Bitmap bitmap = BitmapFactory.decodeStream(stream); - IOUtils.closeQuietly(stream); - new DownsizeImageTask(STATUS_MEDIA_SIZE_LIMIT, new DownsizeImageTask.Listener() { - @Override - public void onSuccess(List contentList) { - item.content = contentList.get(0); - uploadMedia(item); - } - @Override - public void onFailure() { - onMediaDownsizeFailure(item); - } - }).execute(bitmap); + new DownsizeImageTask(STATUS_MEDIA_SIZE_LIMIT, getContentResolver(), + new DownsizeImageTask.Listener() { + @Override + public void onSuccess(List contentList) { + item.content = contentList.get(0); + uploadMedia(item); + } + + @Override + public void onFailure() { + onMediaDownsizeFailure(item); + } + }).execute(item.uri); } private void onMediaDownsizeFailure(QueuedMedia item) { diff --git a/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java index ea06edcb2..e6d6ed24a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java +++ b/app/src/main/java/com/keylesspalace/tusky/DownsizeImageTask.java @@ -15,35 +15,63 @@ package com.keylesspalace.tusky; +import android.content.ContentResolver; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; import android.os.AsyncTask; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.util.ArrayList; import java.util.List; -class DownsizeImageTask extends AsyncTask { - private Listener listener; +class DownsizeImageTask extends AsyncTask { private int sizeLimit; + private ContentResolver contentResolver; + private Listener listener; private List resultList; - DownsizeImageTask(int sizeLimit, Listener listener) { - this.listener = listener; + DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, Listener listener) { this.sizeLimit = sizeLimit; + this.contentResolver = contentResolver; + this.listener = listener; } - private static Bitmap scaleDown(Bitmap source, float maxImageSize, boolean filter) { - float ratio = Math.min(maxImageSize / source.getWidth(), maxImageSize / source.getHeight()); - int width = Math.round(ratio * source.getWidth()); - int height = Math.round(ratio * source.getHeight()); - return Bitmap.createScaledBitmap(source, width, height, filter); + private static int calculateInSampleSize(int width, int height, int requiredScale) { + int inSampleSize = 1; + if (height > requiredScale || width > requiredScale) { + final int halfHeight = height / 2; + final int halfWidth = 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 >= requiredScale + && halfWidth / inSampleSize >= requiredScale) { + inSampleSize *= 2; + } + } + return inSampleSize; } @Override - protected Boolean doInBackground(Bitmap... bitmaps) { - final int count = bitmaps.length; - resultList = new ArrayList<>(count); - for (Bitmap bitmap : bitmaps) { + protected Boolean doInBackground(Uri... uris) { + resultList = new ArrayList<>(); + for (Uri uri : uris) { + InputStream inputStream; + try { + inputStream = contentResolver.openInputStream(uri); + } catch (FileNotFoundException e) { + return false; + } + // Initially, just get the image dimensions. + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(inputStream, null, options); + int beforeWidth = options.outWidth; + int beforeHeight = options.outHeight; + IOUtils.closeQuietly(inputStream); + // Then use that information to determine how much to compress. ByteArrayOutputStream stream = new ByteArrayOutputStream(); /* Unfortunately, there isn't a determined worst case compression ratio for image * formats. So, the only way to tell if they're too big is to compress them and @@ -54,7 +82,25 @@ class DownsizeImageTask extends AsyncTask { int scaledImageSize = 4096; do { stream.reset(); - Bitmap scaledBitmap = scaleDown(bitmap, scaledImageSize, true); + try { + inputStream = contentResolver.openInputStream(uri); + } catch (FileNotFoundException e) { + return false; + } + options.inSampleSize = calculateInSampleSize(beforeWidth, beforeHeight, + scaledImageSize); + options.inJustDecodeBounds = false; + Bitmap scaledBitmap; + try { + scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options); + } catch (OutOfMemoryError error) { + return false; + } finally { + IOUtils.closeQuietly(inputStream); + } + if (scaledBitmap == null) { + return false; + } Bitmap.CompressFormat format; /* It's not likely the user will give transparent images over the upload limit, but * if they do, make sure the transparency is retained. */ @@ -64,6 +110,7 @@ class DownsizeImageTask extends AsyncTask { format = Bitmap.CompressFormat.PNG; } scaledBitmap.compress(format, 75, stream); + scaledBitmap.recycle(); scaledImageSize /= 2; iterations++; } while (stream.size() > sizeLimit); @@ -86,7 +133,7 @@ class DownsizeImageTask extends AsyncTask { super.onPostExecute(successful); } - public interface Listener { + interface Listener { void onSuccess(List contentList); void onFailure(); }