diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java b/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java index 2d06c314..3359d1a2 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java @@ -43,7 +43,36 @@ public class Attachment implements Serializable { public long size; @SerializedName("local_path") public String local_path; + @SerializedName("meta") + public Meta meta; public String peertubeHost = null; public String peertubeId = null; + + public static class Meta implements Serializable { + @SerializedName("focus") + public Focus focus; + @SerializedName("original") + public MediaData original; + @SerializedName("small") + public MediaData small; + } + + public static class Focus implements Serializable { + @SerializedName("x") + public float x; + @SerializedName("y") + public float y; + } + + public static class MediaData implements Serializable { + @SerializedName("width") + public int width; + @SerializedName("height") + public int height; + @SerializedName("size") + public String size; + @SerializedName("aspect") + public float aspect; + } } diff --git a/app/src/main/java/app/fedilab/android/helper/GlideFocus.java b/app/src/main/java/app/fedilab/android/helper/GlideFocus.java new file mode 100644 index 00000000..688cc897 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/GlideFocus.java @@ -0,0 +1,209 @@ +package app.fedilab.android.helper; +/* Copyright 2021 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import static com.bumptech.glide.load.resource.bitmap.TransformationUtils.PAINT_FLAGS; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.os.Build; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.TransformationUtils; +import com.bumptech.glide.util.Synthetic; + +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jp.wasabeef.glide.transformations.BitmapTransformation; + +public class GlideFocus extends BitmapTransformation { + + + private static final int VERSION = 1; + private static final String ID = "app.fedilab.android.GlideFocus." + VERSION; + private static final Set MODELS_REQUIRING_BITMAP_LOCK = + new HashSet<>( + Arrays.asList( + // Moto X gen 2 + "XT1085", + "XT1092", + "XT1093", + "XT1094", + "XT1095", + "XT1096", + "XT1097", + "XT1098", + // Moto G gen 1 + "XT1031", + "XT1028", + "XT937C", + "XT1032", + "XT1008", + "XT1033", + "XT1035", + "XT1034", + "XT939G", + "XT1039", + "XT1040", + "XT1042", + "XT1045", + // Moto G gen 2 + "XT1063", + "XT1064", + "XT1068", + "XT1069", + "XT1072", + "XT1077", + "XT1078", + "XT1079")); + private static final Lock BITMAP_DRAWABLE_LOCK = + MODELS_REQUIRING_BITMAP_LOCK.contains(Build.MODEL) ? new ReentrantLock() : new NoLock(); + private static final Paint DEFAULT_PAINT = new Paint(PAINT_FLAGS); + private final float focalX; + private final float focalY; + + public GlideFocus(float focalX, float focalY) { + this.focalX = focalX; + this.focalY = focalY; + } + + private static void applyMatrix( + @NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap, Matrix matrix) { + BITMAP_DRAWABLE_LOCK.lock(); + try { + Canvas canvas = new Canvas(targetBitmap); + canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT); + clear(canvas); + } finally { + BITMAP_DRAWABLE_LOCK.unlock(); + } + } + + @NonNull + private static Bitmap.Config getNonNullConfig(@NonNull Bitmap bitmap) { + return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888; + } + + // Avoids warnings in M+. + private static void clear(Canvas canvas) { + canvas.setBitmap(null); + } + + @Override + protected Bitmap transform(@NonNull Context context, @NonNull BitmapPool pool, + @NonNull Bitmap inBitmap, int width, int height) { + + if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) { + return inBitmap; + } + // From ImageView/Bitmap.createScaledBitmap. + final float scale; + final float dx; + final float dy; + Matrix m = new Matrix(); + if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) { + scale = (float) height / (float) inBitmap.getHeight(); + dx = (width - inBitmap.getWidth() * scale) * 0.5f * (1 + focalX); + dy = 0; + } else { + scale = (float) width / (float) inBitmap.getWidth(); + dx = 0; + dy = (height - inBitmap.getHeight() * scale) * 0.5f * (1 + focalY); + } + + m.setScale(scale, scale); + m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); + + Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap)); + // We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given. + TransformationUtils.setAlpha(inBitmap, result); + + applyMatrix(inBitmap, result, m); + return result; + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update((ID + focalX + focalY).getBytes(CHARSET)); + } + + @Override + public boolean equals(Object o) { + return o instanceof GlideFocus && + ((GlideFocus) o).focalX == focalX && + ((GlideFocus) o).focalY == focalY; + } + + @Override + public int hashCode() { + return (int) (ID.hashCode() + focalX * 100000 + focalY * 1000); + } + + + @Override + public String toString() { + return "CropTransformation(width=" + focalX + ", height=" + focalY + ")"; + } + + private static final class NoLock implements Lock { + + @Synthetic + NoLock() { + } + + @Override + public void lock() { + // do nothing + } + + @Override + public void lockInterruptibly() throws InterruptedException { + // do nothing + } + + @Override + public boolean tryLock() { + return true; + } + + @Override + public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public void unlock() { + // do nothing + } + + @NonNull + @Override + public Condition newCondition() { + throw new UnsupportedOperationException("Should not be called"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java index 90a4f7f0..907460ab 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/StatusAdapter.java @@ -117,6 +117,7 @@ import app.fedilab.android.databinding.LayoutMediaBinding; import app.fedilab.android.databinding.LayoutPollItemBinding; import app.fedilab.android.exception.DBException; import app.fedilab.android.helper.CrossActionHelper; +import app.fedilab.android.helper.GlideFocus; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.LongClickLinkMovementMethod; import app.fedilab.android.helper.MastodonHelper; @@ -984,10 +985,18 @@ public class StatusAdapter extends RecyclerView.Adapter } else { layoutMediaBinding.playMusic.setVisibility(View.GONE); } + float focusX = 0.f; + float focusY = 0.f; + if (statusToDeal.media_attachments.get(0).meta != null && statusToDeal.media_attachments.get(0).meta.focus != null) { + focusX = statusToDeal.media_attachments.get(0).meta.focus.x; + focusY = statusToDeal.media_attachments.get(0).meta.focus.y; + } + if (!mediaObfuscated(statusToDeal) || expand_media) { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); Glide.with(layoutMediaBinding.media.getContext()) .load(statusToDeal.media_attachments.get(0).preview_url) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); @@ -995,6 +1004,7 @@ public class StatusAdapter extends RecyclerView.Adapter .load(statusToDeal.media_attachments.get(0).preview_url) .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } layoutMediaBinding.viewHide.setOnClickListener(v -> { @@ -1009,6 +1019,13 @@ public class StatusAdapter extends RecyclerView.Adapter for (Attachment attachment : statusToDeal.media_attachments) { LayoutMediaBinding layoutMediaBinding = LayoutMediaBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); RelativeLayout.LayoutParams lp; + float focusX = 0.f; + float focusY = 0.f; + if (statusToDeal.media_attachments.get(0).meta != null && statusToDeal.media_attachments.get(0).meta.focus != null) { + focusX = statusToDeal.media_attachments.get(0).meta.focus.x; + focusY = statusToDeal.media_attachments.get(0).meta.focus.y; + } + if (fullAttachement) { lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); layoutMediaBinding.media.setScaleType(ImageView.ScaleType.FIT_CENTER); @@ -1032,12 +1049,14 @@ public class StatusAdapter extends RecyclerView.Adapter Glide.with(layoutMediaBinding.media.getContext()) .load(attachment.preview_url) .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); Glide.with(layoutMediaBinding.media.getContext()) .load(attachment.preview_url) .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) .into(layoutMediaBinding.media); }