From 61f3f6c9280de47016ad61f5b8bc5ecc3ca97440 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 23 Jul 2018 21:55:09 +0200 Subject: [PATCH] Improve media resizing (#722) * improve MediaUtils.getImageThumbnail so it does not load the whole bitmap into memory * load thumbnails in device specific sizes --- .../keylesspalace/tusky/ComposeActivity.java | 14 ++-- .../tusky/EditProfileActivity.kt | 19 +---- .../tusky/util/DownsizeImageTask.java | 20 +---- .../keylesspalace/tusky/util/MediaUtils.java | 81 ++++++++++++++----- app/src/main/res/values/dimens.xml | 2 +- 5 files changed, 73 insertions(+), 63 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index a011a2331..6b09855b8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -154,8 +154,6 @@ public final class ComposeActivity private static final int MEDIA_PICK_RESULT = 1; private static final int MEDIA_TAKE_PHOTO_RESULT = 2; private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1; - @Px - private static final int THUMBNAIL_SIZE = 128; private static final String SAVED_TOOT_UID_EXTRA = "saved_toot_uid"; private static final String SAVED_TOOT_TEXT_EXTRA = "saved_toot_text"; @@ -210,6 +208,7 @@ public final class ComposeActivity private int savedTootUid = 0; private List emojiList; private int maximumTootCharacters = STATUS_CHARACTER_LIMIT; + private @Px int thumbnailViewSize; private SaveTootHelper saveTootHelper; @@ -341,6 +340,8 @@ public final class ComposeActivity actionPhotoTake.setOnClickListener(v -> initiateCameraApp()); actionPhotoPick.setOnClickListener(v -> onMediaPick()); + thumbnailViewSize = getResources().getDimensionPixelSize(R.dimen.compose_media_preview_size); + /* Initialise all the state, or restore it from a previous run, to determine a "starting" * state. */ Status.Visibility startingVisibility = Status.Visibility.UNKNOWN; @@ -527,7 +528,7 @@ public final class ComposeActivity } } else if (savedMediaQueued != null) { for (SavedQueuedMedia item : savedMediaQueued) { - Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, THUMBNAIL_SIZE); + Bitmap preview = MediaUtils.getImageThumbnail(getContentResolver(), item.uri, thumbnailViewSize); addMediaToQueue(item.id, item.type, preview, item.uri, item.mediaSize, item.readyStage, item.description); } } else if (intent != null && savedInstanceState == null) { @@ -1033,11 +1034,10 @@ public final class ComposeActivity item.readyStage = readyStage; ImageView view = item.preview; Resources resources = getResources(); - int side = resources.getDimensionPixelSize(R.dimen.compose_media_preview_side); int margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin); int marginBottom = resources.getDimensionPixelSize( R.dimen.compose_media_preview_margin_bottom); - LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(side, side); + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize); layoutParams.setMargins(margin, 0, margin, marginBottom); view.setLayoutParams(layoutParams); view.setScaleType(ImageView.ScaleType.CENTER_CROP); @@ -1343,7 +1343,7 @@ public final class ComposeActivity displayTransientError(R.string.error_media_upload_image_or_video); return; } - Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, THUMBNAIL_SIZE); + Bitmap bitmap = MediaUtils.getVideoThumbnail(this, uri, thumbnailViewSize); if (bitmap != null) { addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize); } else { @@ -1352,7 +1352,7 @@ public final class ComposeActivity break; } case "image": { - Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, THUMBNAIL_SIZE); + Bitmap bitmap = MediaUtils.getImageThumbnail(contentResolver, uri, thumbnailViewSize); if (bitmap != null) { addMediaToQueue(QueuedMedia.Type.IMAGE, bitmap, uri, mediaSize); } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 162f4e26d..0b7068f43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -36,6 +36,7 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.IOUtils +import com.keylesspalace.tusky.util.MediaUtils import com.squareup.picasso.Picasso import com.theartofdev.edmodo.cropper.CropImage import kotlinx.android.synthetic.main.activity_edit_profile.* @@ -446,23 +447,9 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun doInBackground(vararg uris: Uri): Boolean? { val uri = uris[0] - val inputStream: InputStream? - try { - inputStream = contentResolver.openInputStream(uri) - } catch (e: FileNotFoundException) { - Log.d(TAG, Log.getStackTraceString(e)) - return false - } - val sourceBitmap: Bitmap? - try { - sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null) - } catch (error: OutOfMemoryError) { - Log.d(TAG, Log.getStackTraceString(error)) - return false - } finally { - IOUtils.closeQuietly(inputStream) - } + val sourceBitmap = MediaUtils.getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight) + if (sourceBitmap == null) { return false } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java index 4df258897..be0c989dd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/DownsizeImageTask.java @@ -131,21 +131,6 @@ public class DownsizeImageTask extends AsyncTask { return orientation; } - 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(Uri... uris) { resultList = new ArrayList<>(); @@ -160,8 +145,6 @@ public class DownsizeImageTask extends AsyncTask { 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); // Get EXIF data, for orientation info. int orientation = getOrientation(uri, contentResolver); @@ -180,8 +163,7 @@ public class DownsizeImageTask extends AsyncTask { } catch (FileNotFoundException e) { return false; } - options.inSampleSize = calculateInSampleSize(beforeWidth, beforeHeight, - scaledImageSize); + options.inSampleSize = MediaUtils.calculateInSampleSize(options, scaledImageSize, scaledImageSize); options.inJustDecodeBounds = false; Bitmap scaledBitmap; try { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java index 3341f6d39..5b16e6e73 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.java @@ -27,6 +27,7 @@ import android.provider.OpenableColumns; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.Px; +import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; @@ -34,11 +35,10 @@ import java.io.IOException; import java.io.InputStream; /** - * Class who will have all the code link with Media - *

- * Motivation : try to keep the ComposeActivity "smaller" and make modular method + * Class with helper methods for obtaining and resizing media files */ public class MediaUtils { + private static final String TAG = "MediaUtils"; public static final int MEDIA_SIZE_UNKNOWN = -1; /** @@ -88,30 +88,51 @@ public class MediaUtils { } @Nullable - public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri, - @Px int thumbnailSize) { + public static Bitmap getSampledBitmap(ContentResolver contentResolver, Uri uri, @Px int reqWidth, @Px int reqHeight) { + // First decode with inJustDecodeBounds=true to check dimensions + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; InputStream stream; try { stream = contentResolver.openInputStream(uri); } catch (FileNotFoundException e) { + Log.w(TAG, e); return null; } - Bitmap source = BitmapFactory.decodeStream(stream); - if (source == null) { - IOUtils.closeQuietly(stream); - return null; - } - Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize); - source.recycle(); + + BitmapFactory.decodeStream(stream, null, options); + + IOUtils.closeQuietly(stream); + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false; try { - if (stream != null) { - stream.close(); - } - } catch (IOException e) { - bitmap.recycle(); + stream = contentResolver.openInputStream(uri); + return BitmapFactory.decodeStream(stream, null, options); + } catch (FileNotFoundException e) { + Log.w(TAG, e); + return null; + } catch (OutOfMemoryError e) { + Log.e(TAG, "OutOfMemoryError while trying to get sampled Bitmap", e); + return null; + } finally { + IOUtils.closeQuietly(stream); + } + + } + + @Nullable + public static Bitmap getImageThumbnail(ContentResolver contentResolver, Uri uri, + @Px int thumbnailSize) { + Bitmap source = getSampledBitmap(contentResolver, uri, thumbnailSize, thumbnailSize); + if(source != null) { + return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT); + } else { return null; } - return bitmap; } @Nullable @@ -128,8 +149,7 @@ public class MediaUtils { } public static long getImageSquarePixels(ContentResolver contentResolver, Uri uri) throws FileNotFoundException { - InputStream input; - input = contentResolver.openInputStream(uri); + InputStream input = contentResolver.openInputStream(uri); final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; @@ -139,4 +159,25 @@ public class MediaUtils { return (long) options.outWidth * options.outHeight; } + + public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { + // Raw height and width of image + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + 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) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } } diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 5f52f7ac4..1f2321c55 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -7,7 +7,7 @@ 130dp 8dp 0dp - 120dp + 120dp 8dp 14dp 8dp