diff --git a/README.md b/README.md index cbe22cd119..c9aac7dcab 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ FairEmail uses: * [ShortcutBadger](https://github.com/leolin310148/ShortcutBadger). Copyright 2014 Leo Lin. [Apache license](https://github.com/leolin310148/ShortcutBadger/blob/master/LICENSE). * [Bugsnag exception reporter for Android](https://github.com/bugsnag/bugsnag-android). Copyright (c) 2012 Bugsnag. [MIT License](https://github.com/bugsnag/bugsnag-android/blob/master/LICENSE.txt). * [biweekly](https://github.com/mangstadt/biweekly). Copyright (c) 2013-2018, Michael Angstadt. [BSD 2-Clause](https://github.com/mangstadt/biweekly/blob/master/LICENSE). -* Source code snippets from Stack Overflow. [MIT License](https://meta.stackexchange.com/questions/271080/the-mit-license-clarity-on-using-code-on-stack-overflow-and-stack-exchange). +* [PhotoView](https://github.com/chrisbanes/PhotoView). Copyright 2018 Chris Banes. [Apache License](https://github.com/chrisbanes/PhotoView/blob/master/LICENSE). Error reporting is sponsored by: diff --git a/app/build.gradle b/app/build.gradle index c942d350ef..2dab8ad05b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -147,6 +147,7 @@ dependencies { def badge_version = "1.1.22" def bugsnag_version = "4.15.0" def biweekly_version = "0.6.3" + def photoview_version = "2.3.0" // https://developer.android.com/jetpack/androidx/releases/ @@ -228,6 +229,9 @@ dependencies { exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core' } + // https://github.com/chrisbanes/PhotoView + implementation "com.github.chrisbanes:PhotoView:$photoview_version" + // git clone https://android.googlesource.com/platform/frameworks/opt/colorpicker implementation project(path: ':colorpicker') } diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 83e276357b..1b92a17b2f 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -105,6 +105,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.StaggeredGridLayoutManager; +import com.github.chrisbanes.photoview.PhotoView; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.bottomnavigation.LabelVisibilityMode; import com.google.android.material.snackbar.Snackbar; @@ -1930,7 +1931,7 @@ public class AdapterMessage extends RecyclerView.Adapter 0 && image[0].getSource() != null) { - onOpenImage(image[0].getDrawable()); - return true; + if (image.length > 0) { + String source = image[0].getSource(); + if (source != null) { + onOpenImage(message.id, source); + return true; + } } DynamicDrawableSpan[] ddss = buffer.getSpans(off, off, DynamicDrawableSpan.class); @@ -2107,26 +2111,16 @@ public class AdapterMessage extends RecyclerView.Adapter() { + @Override + protected Drawable onExecute(Context context, Bundle args) throws Throwable { + long id = args.getLong("id"); + String source = args.getString("source"); + return HtmlHelper.decodeImage(context, id, source, true, null); + } + + @Override + protected void onExecuted(Bundle args, Drawable drawable) { + pv.setImageDrawable(drawable); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Helper.unexpectedError(getFragmentManager(), ex); + } + }.execute(getContext(), getActivity(), getArguments(), "view:image"); + + // TODO: dialog fragment + final Dialog dialog = new Dialog(getContext(), android.R.style.Theme_Black_NoTitleBar_Fullscreen); + dialog.setContentView(pv); + + return dialog; + } + } + public static class FragmentDialogFull extends DialogFragmentEx { @NonNull @Override diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index f10f2dc6ec..be6d9cbc44 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -2854,7 +2854,7 @@ public class FragmentCompose extends FragmentBase { new Html.ImageGetter() { @Override public Drawable getDrawable(String source) { - Drawable image = HtmlHelper.decodeImage(source, id, show_images, tvReference); + Drawable image = HtmlHelper.decodeImage(context, id, source, show_images, tvReference); ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) tvReference.getLayoutParams(); diff --git a/app/src/main/java/eu/faircode/email/HtmlHelper.java b/app/src/main/java/eu/faircode/email/HtmlHelper.java index 30c851c9ca..342660f745 100644 --- a/app/src/main/java/eu/faircode/email/HtmlHelper.java +++ b/app/src/main/java/eu/faircode/email/HtmlHelper.java @@ -314,16 +314,16 @@ public class HtmlHelper { return (body == null ? "" : body.html()); } - static Drawable decodeImage(final String source, final long id, boolean show, final TextView view) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(view.getContext()); + static Drawable decodeImage(final Context context, final long id, final String source, boolean show, final TextView view) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); boolean compact = prefs.getBoolean("compact", false); int zoom = prefs.getInt("zoom", compact ? 0 : 1); boolean inline = prefs.getBoolean("inline_images", false); - final int px = Helper.dp2pixels(view.getContext(), (zoom + 1) * 24); + final int px = Helper.dp2pixels(context, (zoom + 1) * 24); - final Resources.Theme theme = view.getContext().getTheme(); - final Resources res = view.getContext().getResources(); + final Resources.Theme theme = context.getTheme(); + final Resources res = context.getResources(); if (TextUtils.isEmpty(source)) { Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme); @@ -348,7 +348,7 @@ public class HtmlHelper { // Embedded images if (embedded) { - DB db = DB.getInstance(view.getContext()); + DB db = DB.getInstance(context); String cid = "<" + source.substring(4) + ">"; EntityAttachment attachment = db.attachment().getAttachment(id, cid); if (attachment == null) { @@ -362,7 +362,7 @@ public class HtmlHelper { d.setBounds(0, 0, px, px); return d; } else { - Bitmap bm = Helper.decodeImage(attachment.getFile(view.getContext()), + Bitmap bm = Helper.decodeImage(attachment.getFile(context), res.getDisplayMetrics().widthPixels); if (bm == null) { Log.i("Image not decodable CID=" + cid); @@ -380,7 +380,7 @@ public class HtmlHelper { // Data URI if (data) try { - return getDataDrawable(source, res); + return getDataDrawable(res, source); } catch (IllegalArgumentException ex) { Log.w(ex); Drawable d = res.getDrawable(R.drawable.baseline_broken_image_24, theme); @@ -389,13 +389,13 @@ public class HtmlHelper { } // Get cache file name - File dir = new File(view.getContext().getCacheDir(), "images"); + File dir = new File(context.getCacheDir(), "images"); if (!dir.exists()) dir.mkdir(); final File file = new File(dir, id + "_" + Math.abs(source.hashCode()) + ".png"); - Drawable cached = getCachedImage(view.getContext(), file); - if (cached != null) + Drawable cached = getCachedImage(context, file); + if (cached != null || view == null) return cached; final LevelListDrawable lld = new LevelListDrawable(); @@ -404,7 +404,6 @@ public class HtmlHelper { lld.setBounds(0, 0, px, px); lld.setLevel(1); - final Context context = view.getContext().getApplicationContext(); executor.submit(new Runnable() { @Override public void run() { @@ -493,7 +492,7 @@ public class HtmlHelper { return lld; } - private static Drawable getDataDrawable(String source, Resources res) { + private static Drawable getDataDrawable(Resources res, String source) { // " minScale) { - saveScale = newScale; - float width = getWidth(); - float height = getHeight(); - right = (originalBitmapWidth * saveScale) - width; - bottom = (originalBitmapHeight * saveScale) - height; - - float scaledBitmapWidth = originalBitmapWidth * saveScale; - float scaledBitmapHeight = originalBitmapHeight * saveScale; - - if (scaledBitmapWidth <= width || scaledBitmapHeight <= height) { - matrix.postScale(scaleFactor, scaleFactor, width / 2, height / 2); - } else { - matrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY()); - } - } - return true; - } - - } - - static final int NONE = 0; - static final int DRAG = 1; - static final int ZOOM = 2; - static final int CLICK = 3; - - private int mode = NONE; - - private Matrix matrix = new Matrix(); - - private PointF last = new PointF(); - private PointF start = new PointF(); - private float minScale = 0.5f; - private float maxScale = 4f; - private float[] m; - - private float redundantXSpace, redundantYSpace; - private float saveScale = 1f; - private float right, bottom, originalBitmapWidth, originalBitmapHeight; - - private ScaleGestureDetector mScaleDetector; - - public ZoomableImageView(Context context) { - super(context); - init(context); - } - - public ZoomableImageView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - public ZoomableImageView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(context); - } - - private void init(Context context) { - super.setClickable(true); - mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); - m = new float[9]; - setImageMatrix(matrix); - setScaleType(ScaleType.MATRIX); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - int bmHeight = getBmHeight(); - int bmWidth = getBmWidth(); - - float width = getMeasuredWidth(); - float height = getMeasuredHeight(); - //Fit to screen. - float scale = width > height ? height / bmHeight : width / bmWidth; - - matrix.setScale(scale, scale); - saveScale = 1f; - - originalBitmapWidth = scale * bmWidth; - originalBitmapHeight = scale * bmHeight; - - // Center the image - redundantYSpace = (height - originalBitmapHeight); - redundantXSpace = (width - originalBitmapWidth); - - matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2); - - setImageMatrix(matrix); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - mScaleDetector.onTouchEvent(event); - - matrix.getValues(m); - float x = m[Matrix.MTRANS_X]; - float y = m[Matrix.MTRANS_Y]; - PointF curr = new PointF(event.getX(), event.getY()); - - switch (event.getAction()) { - //when one finger is touching - //set the mode to DRAG - case MotionEvent.ACTION_DOWN: - last.set(event.getX(), event.getY()); - start.set(last); - mode = DRAG; - break; - //when two fingers are touching - //set the mode to ZOOM - case MotionEvent.ACTION_POINTER_DOWN: - last.set(event.getX(), event.getY()); - start.set(last); - mode = ZOOM; - break; - //when a finger moves - //If mode is applicable move image - case MotionEvent.ACTION_MOVE: - //if the mode is ZOOM or - //if the mode is DRAG and already zoomed - if (mode == ZOOM || (mode == DRAG && saveScale > minScale)) { - float deltaX = curr.x - last.x;// x difference - float deltaY = curr.y - last.y;// y difference - float scaleWidth = Math.round(originalBitmapWidth * saveScale);// width after applying current scale - float scaleHeight = Math.round(originalBitmapHeight * saveScale);// height after applying current scale - - boolean limitX = false; - boolean limitY = false; - - //if scaleWidth is smaller than the views width - //in other words if the image width fits in the view - //limit left and right movement - if (scaleWidth < getWidth() && scaleHeight < getHeight()) { - // don't do anything - } else if (scaleWidth < getWidth()) { - deltaX = 0; - limitY = true; - } - //if scaleHeight is smaller than the views height - //in other words if the image height fits in the view - //limit up and down movement - else if (scaleHeight < getHeight()) { - deltaY = 0; - limitX = true; - } - //if the image doesnt fit in the width or height - //limit both up and down and left and right - else { - limitX = true; - limitY = true; - } - - if (limitY) { - if (y + deltaY > 0) { - deltaY = -y; - } else if (y + deltaY < -bottom) { - deltaY = -(y + bottom); - } - - } - - if (limitX) { - if (x + deltaX > 0) { - deltaX = -x; - } else if (x + deltaX < -right) { - deltaX = -(x + right); - } - - } - //move the image with the matrix - matrix.postTranslate(deltaX, deltaY); - //set the last touch location to the current - last.set(curr.x, curr.y); - } - break; - //first finger is lifted - case MotionEvent.ACTION_UP: - mode = NONE; - int xDiff = (int) Math.abs(curr.x - start.x); - int yDiff = (int) Math.abs(curr.y - start.y); - if (xDiff < CLICK && yDiff < CLICK) - performClick(); - break; - // second finger is lifted - case MotionEvent.ACTION_POINTER_UP: - mode = NONE; - break; - } - setImageMatrix(matrix); - invalidate(); - return true; - } - - public void setMaxZoom(float x) { - maxScale = x; - } - - private int getBmWidth() { - Drawable drawable = getDrawable(); - if (drawable != null) { - return drawable.getIntrinsicWidth(); - } - return 0; - } - - private int getBmHeight() { - Drawable drawable = getDrawable(); - if (drawable != null) { - return drawable.getIntrinsicHeight(); - } - return 0; - } -}