From 8d6f1341f1c884cf95906e72703e7d283965a720 Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Wed, 17 Jul 2019 15:05:19 -0400 Subject: [PATCH] Reduce resolution of image editor preview and make memory efficiencies. Fixes #8929 --- .../securesms/imageeditor/model/Bisect.java | 2 +- .../imageeditor/model/EditorModel.java | 7 ++- .../mediasend/MediaSendFragment.java | 53 +++++++++---------- .../securesms/scribbles/UriGlideRenderer.java | 34 +++++++++--- 4 files changed, 59 insertions(+), 37 deletions(-) diff --git a/src/org/thoughtcrime/securesms/imageeditor/model/Bisect.java b/src/org/thoughtcrime/securesms/imageeditor/model/Bisect.java index 22c4559238..35f3115b0a 100644 --- a/src/org/thoughtcrime/securesms/imageeditor/model/Bisect.java +++ b/src/org/thoughtcrime/securesms/imageeditor/model/Bisect.java @@ -83,7 +83,7 @@ final class Bisect { inBoundsValue = nextValueToTry; // if first success or closer to out of bounds than the current closest - if (!haveResult || Math.abs(nextValueToTry) < Math.abs(successValue)) { + if (!haveResult || Math.abs(nextValueToTry - outOfBoundsValue) < Math.abs(successValue - outOfBoundsValue)) { haveResult = true; successValue = nextValueToTry; closestSuccessful.set(elementMatrix); diff --git a/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java index 8cbd9d0a16..d9e8256515 100644 --- a/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java +++ b/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java @@ -9,6 +9,7 @@ import android.graphics.PointF; import android.graphics.RectF; import android.os.Parcel; import android.os.Parcelable; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -524,7 +525,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { * Blocking render of the model. */ @WorkerThread - public Bitmap render(@NonNull Context context) { + public @NonNull Bitmap render(@NonNull Context context) { EditorElement image = editorElementHierarchy.getFlipRotate(); RectF cropRect = editorElementHierarchy.getCropRect(); Point outputSize = getOutputSize(); @@ -573,11 +574,15 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { @Override public void onReady(@NonNull Renderer renderer, @Nullable Matrix cropMatrix, @Nullable Point size) { if (cropMatrix != null && size != null && isRendererOfMainImage(renderer)) { + boolean changedBefore = isChanged(); Matrix imageCropMatrix = editorElementHierarchy.getImageCrop().getLocalMatrix(); this.size.set(size.x, size.y); if (imageCropMatrix.isIdentity()) { imageCropMatrix.set(cropMatrix); editorElementHierarchy.doneCrop(visibleViewPort, null); + if (!changedBefore) { + undoRedoStacks.clear(editorElementHierarchy.getRoot()); + } } } } diff --git a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index e9f75954ac..02e85f564a 100644 --- a/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.mediasend; import android.annotation.SuppressLint; -import androidx.lifecycle.ViewModelProviders; import android.content.Context; import android.graphics.Bitmap; import android.graphics.PorterDuff; @@ -9,14 +8,6 @@ import android.graphics.Rect; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.viewpager.widget.ViewPager; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import android.text.Editable; import android.text.TextWatcher; import android.view.KeyEvent; @@ -28,6 +19,16 @@ import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProviders; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.ViewPager; + import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.TransportOption; import org.thoughtcrime.securesms.components.ComposeText; @@ -425,7 +426,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl @SuppressLint("StaticFieldLeak") private void processMedia(@NonNull List mediaList, @NonNull Map savedState) { - Map> futures = new HashMap<>(); + Map modelsToRender = new HashMap<>(); for (Media media : mediaList) { Object state = savedState.get(media.getUri()); @@ -433,7 +434,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl if (state instanceof ImageEditorFragment.Data) { EditorModel model = ((ImageEditorFragment.Data) state).readModel(); if (model != null && model.isChanged()) { - futures.put(media, render(requireContext(), model)); + modelsToRender.put(media, model); } } } @@ -461,28 +462,32 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl @Override protected List doInBackground(Void... voids) { - Context context = requireContext(); - List updatedMedia = new ArrayList<>(mediaList.size()); + Context context = requireContext(); + List updatedMedia = new ArrayList<>(mediaList.size()); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); for (Media media : mediaList) { - if (futures.containsKey(media)) { + EditorModel modelToRender = modelsToRender.get(media); + if (modelToRender != null) { + Bitmap bitmap = modelToRender.render(context); try { - Bitmap bitmap = futures.get(media).get(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos); + outputStream.reset(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream); Uri uri = BlobProvider.getInstance() - .forData(baos.toByteArray()) + .forData(outputStream.toByteArray()) .withMimeType(MediaUtil.IMAGE_JPEG) .createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e)); - Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), baos.size(), media.getBucketId(), media.getCaption()); + Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption()); updatedMedia.add(updated); renderTimer.split("item"); - } catch (InterruptedException | ExecutionException | IOException e) { + } catch (IOException e) { Log.w(TAG, "Failed to render image. Using base image."); updatedMedia.add(media); + } finally { + bitmap.recycle(); } } else { updatedMedia.add(media); @@ -503,14 +508,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl }.execute(); } - private static ListenableFuture render(@NonNull Context context, @NonNull EditorModel model) { - SettableFuture future = new SettableFuture<>(); - - AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> future.set(model.render(context))); - - return future; - } - public void onRequestFullScreen(boolean fullScreen) { captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE); } diff --git a/src/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java b/src/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java index 598975c18e..429136917f 100644 --- a/src/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java +++ b/src/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java @@ -6,13 +6,15 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Point; import android.graphics.RectF; +import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Parcel; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.bumptech.glide.load.engine.DiskCacheStrategy; -import com.bumptech.glide.request.target.SimpleTarget; +import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import org.thoughtcrime.securesms.imageeditor.Bounds; @@ -31,6 +33,8 @@ import java.util.concurrent.ExecutionException; */ final class UriGlideRenderer implements Renderer { + private static final int PREVIEW_DIMENSION_LIMIT = 2048; + private final Uri imageUri; private final Paint paint = new Paint(); private final Matrix imageProjectionMatrix = new Matrix(); @@ -57,7 +61,7 @@ final class UriGlideRenderer implements Renderer { if (getBitmap() == null) { if (rendererContext.isBlockingLoad()) { try { - Bitmap bitmap = getBitmapGlideRequest(rendererContext.context).submit().get(); + Bitmap bitmap = getBitmapGlideRequest(rendererContext.context, false).submit().get(); setBitmap(rendererContext, bitmap); } catch (ExecutionException e) { throw new RuntimeException(e); @@ -65,11 +69,16 @@ final class UriGlideRenderer implements Renderer { throw new RuntimeException(e); } } else { - getBitmapGlideRequest(rendererContext.context).into(new SimpleTarget() { + getBitmapGlideRequest(rendererContext.context, true).into(new CustomTarget() { @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { setBitmap(rendererContext, resource); } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + bitmap = null; + } }); } } @@ -96,11 +105,19 @@ final class UriGlideRenderer implements Renderer { } } - private GlideRequest getBitmapGlideRequest(@NonNull Context context) { + private GlideRequest getBitmapGlideRequest(@NonNull Context context, boolean preview) { + int width = this.maxWidth; + int height = this.maxHeight; + + if (preview) { + width = Math.min(width, PREVIEW_DIMENSION_LIMIT); + height = Math.min(height, PREVIEW_DIMENSION_LIMIT); + } + return GlideApp.with(context) .asBitmap() .diskCacheStrategy(DiskCacheStrategy.NONE) - .override(maxWidth, maxHeight) + .override(width, height) .centerInside() .load(decryptable ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri); } @@ -130,8 +147,11 @@ final class UriGlideRenderer implements Renderer { } } - @Nullable - private Bitmap getBitmap() { + /** + * Always use this getter, as Bitmap is kept in Glide's LRUCache, so it could have been recycled + * by Glide. If it has, or was never set, this method returns null. + */ + private @Nullable Bitmap getBitmap() { if (bitmap != null && bitmap.isRecycled()) { bitmap = null; }