diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9ecede95d9..ce18289bf6 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -5,7 +5,7 @@
android:versionCode="222"
android:versionName="3.25.4">
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
index 9036d4a673..a451e82bcb 100644
--- a/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
+++ b/src/org/thoughtcrime/securesms/MediaPreviewActivity.java
@@ -61,14 +61,15 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
private MasterSecret masterSecret;
- private ZoomingImageView image;
- private VideoPlayer video;
- private Uri mediaUri;
- private String mediaType;
- private Recipient recipient;
- private long threadId;
- private long date;
- private long size;
+ private ZoomingImageView image;
+ private VideoPlayer video;
+
+ private Uri mediaUri;
+ private String mediaType;
+ private Recipient recipient;
+ private long threadId;
+ private long date;
+ private long size;
@Override
protected void onCreate(Bundle bundle, @NonNull MasterSecret masterSecret) {
@@ -137,8 +138,8 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
private void initializeViews() {
- image = (ZoomingImageView)findViewById(R.id.image);
- video = (VideoPlayer)findViewById(R.id.video_player);
+ image = (ZoomingImageView) findViewById(R.id.image);
+ video = (VideoPlayer) findViewById(R.id.video_player);
}
private void initializeResources() {
@@ -171,7 +172,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
if (mediaType != null && mediaType.startsWith("image/")) {
image.setVisibility(View.VISIBLE);
video.setVisibility(View.GONE);
- image.setImageUri(masterSecret, mediaUri);
+ image.setImageUri(masterSecret, mediaUri, mediaType);
} else if (mediaType != null && mediaType.startsWith("video/")) {
image.setVisibility(View.GONE);
video.setVisibility(View.VISIBLE);
@@ -185,7 +186,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
}
private void cleanupMedia() {
- image.setImageDrawable(null);
+ image.cleanup();
video.cleanup();
}
diff --git a/src/org/thoughtcrime/securesms/components/ZoomingImageView.java b/src/org/thoughtcrime/securesms/components/ZoomingImageView.java
index 03947b42e0..37e06ed982 100644
--- a/src/org/thoughtcrime/securesms/components/ZoomingImageView.java
+++ b/src/org/thoughtcrime/securesms/components/ZoomingImageView.java
@@ -1,48 +1,131 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
-import android.graphics.Bitmap;
+import android.graphics.Canvas;
import android.net.Uri;
+import android.os.AsyncTask;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
+import android.widget.FrameLayout;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
-import com.bumptech.glide.request.target.BitmapImageViewTarget;
+import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.GlideDrawableImageViewTarget;
+import com.bumptech.glide.request.target.Target;
+import com.davemorrissey.labs.subscaleview.ImageSource;
+import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder;
+import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
+import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.util.BitmapDecodingException;
+import org.thoughtcrime.securesms.util.BitmapUtil;
+
+import java.io.IOException;
+import java.io.InputStream;
import uk.co.senab.photoview.PhotoViewAttacher;
-public class ZoomingImageView extends ImageView {
- private PhotoViewAttacher attacher = new PhotoViewAttacher(this);
+public class ZoomingImageView extends FrameLayout {
+
+ private static final String TAG = ZoomingImageView.class.getName();
+
+ private final ImageView imageView;
+ private final PhotoViewAttacher imageViewAttacher;
+ private final SubsamplingScaleImageView subsamplingImageView;
public ZoomingImageView(Context context) {
- super(context);
+ this(context, null);
}
public ZoomingImageView(Context context, AttributeSet attrs) {
- super(context, attrs);
+ this(context, attrs, 0);
}
public ZoomingImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
+
+ inflate(context, R.layout.zooming_image_view, this);
+
+ this.imageView = (ImageView) findViewById(R.id.image_view);
+ this.subsamplingImageView = (SubsamplingScaleImageView) findViewById(R.id.subsampling_image_view);
+ this.imageViewAttacher = new PhotoViewAttacher(imageView);
+
+ this.subsamplingImageView.setBitmapDecoderClass(AttachmentBitmapDecoder.class);
+ this.subsamplingImageView.setRegionDecoderClass(AttachmentRegionDecoder.class);
+ this.subsamplingImageView.setOrientation(SubsamplingScaleImageView.ORIENTATION_USE_EXIF);
+ }
+
+ public void setImageUri(final MasterSecret masterSecret, final Uri uri, final String contentType) {
+ final Context context = getContext();
+ final int maxTextureSize = BitmapUtil.getMaxTextureSize();
+
+ Log.w(TAG, "Max texture size: " + maxTextureSize);
+
+ new AsyncTask>() {
+ @Override
+ protected @Nullable Pair doInBackground(Void... params) {
+ if (contentType.equals("image/gif")) return null;
+
+ try {
+ InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, uri);
+ return BitmapUtil.getDimensions(inputStream);
+ } catch (IOException | BitmapDecodingException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ }
+
+ protected void onPostExecute(@Nullable Pair dimensions) {
+ Log.w(TAG, "Dimensions: " + dimensions.first + ", " + dimensions.second);
+
+ if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
+ Log.w(TAG, "Loading in standard image view...");
+ setImageViewUri(masterSecret, uri);
+ } else {
+ Log.w(TAG, "Loading in subsampling image view...");
+ setSubsamplingImageViewUri(uri);
+ }
+ }
+ }.execute();
}
- public void setImageUri(MasterSecret masterSecret, Uri uri) {
+ private void setImageViewUri(MasterSecret masterSecret, Uri uri) {
+ subsamplingImageView.setVisibility(View.GONE);
+ imageView.setVisibility(View.VISIBLE);
+
Glide.with(getContext())
.load(new DecryptableUri(masterSecret, uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontTransform()
.dontAnimate()
- .into(new GlideDrawableImageViewTarget(this) {
+ .into(new GlideDrawableImageViewTarget(imageView) {
@Override protected void setResource(GlideDrawable resource) {
super.setResource(resource);
- attacher.update();
+ imageViewAttacher.update();
}
});
}
+
+ private void setSubsamplingImageViewUri(Uri uri) {
+ subsamplingImageView.setVisibility(View.VISIBLE);
+ imageView.setVisibility(View.GONE);
+
+ subsamplingImageView.setImage(ImageSource.uri(uri));
+ }
+
+
+ public void cleanup() {
+ imageView.setImageDrawable(null);
+ subsamplingImageView.recycle();
+ }
}
diff --git a/src/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java b/src/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java
new file mode 100644
index 0000000000..b4b906138d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/subsampling/AttachmentBitmapDecoder.java
@@ -0,0 +1,52 @@
+package org.thoughtcrime.securesms.components.subsampling;
+
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.net.Uri;
+
+import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder;
+import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder;
+
+import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.service.KeyCachingService;
+
+import java.io.InputStream;
+
+public class AttachmentBitmapDecoder implements ImageDecoder{
+
+ @Override
+ public Bitmap decode(Context context, Uri uri) throws Exception {
+ if (!PartAuthority.isLocalUri(uri)) {
+ return new SkiaImageDecoder().decode(context, uri);
+ }
+
+ MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
+
+ if (masterSecret == null) {
+ throw new IllegalStateException("Can't decode without secret");
+ }
+
+ InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, uri);
+
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
+
+ if (bitmap == null) {
+ throw new RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported");
+ }
+
+ return bitmap;
+ } finally {
+ if (inputStream != null) inputStream.close();
+ }
+ }
+
+
+}
diff --git a/src/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java b/src/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java
new file mode 100644
index 0000000000..8b7e610614
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/subsampling/AttachmentRegionDecoder.java
@@ -0,0 +1,95 @@
+package org.thoughtcrime.securesms.components.subsampling;
+
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+
+import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder;
+import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder;
+
+import org.thoughtcrime.securesms.crypto.MasterSecret;
+import org.thoughtcrime.securesms.mms.PartAuthority;
+import org.thoughtcrime.securesms.service.KeyCachingService;
+
+import java.io.InputStream;
+
+public class AttachmentRegionDecoder implements ImageRegionDecoder {
+
+ private static final String TAG = AttachmentRegionDecoder.class.getName();
+
+ private SkiaImageRegionDecoder passthrough;
+
+ private BitmapRegionDecoder bitmapRegionDecoder;
+
+ @RequiresApi(api = Build.VERSION_CODES.GINGERBREAD_MR1)
+ @Override
+ public Point init(Context context, Uri uri) throws Exception {
+ Log.w(TAG, "Init!");
+ if (!PartAuthority.isLocalUri(uri)) {
+ passthrough = new SkiaImageRegionDecoder();
+ return passthrough.init(context, uri);
+ }
+
+ MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
+
+ if (masterSecret == null) {
+ throw new IllegalStateException("No master secret available...");
+ }
+
+ InputStream inputStream = PartAuthority.getAttachmentStream(context, masterSecret, uri);
+
+ this.bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
+ inputStream.close();
+
+ return new Point(bitmapRegionDecoder.getWidth(), bitmapRegionDecoder.getHeight());
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.GINGERBREAD_MR1)
+ @Override
+ public Bitmap decodeRegion(Rect rect, int sampleSize) {
+ Log.w(TAG, "Decode region: " + rect);
+
+ if (passthrough != null) {
+ return passthrough.decodeRegion(rect, sampleSize);
+ }
+
+ synchronized(this) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inSampleSize = sampleSize;
+ options.inPreferredConfig = Bitmap.Config.RGB_565;
+
+ Bitmap bitmap = bitmapRegionDecoder.decodeRegion(rect, options);
+
+ if (bitmap == null) {
+ throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported");
+ }
+
+ return bitmap;
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.GINGERBREAD_MR1)
+ public boolean isReady() {
+ Log.w(TAG, "isReady");
+ return (passthrough != null && passthrough.isReady()) ||
+ (bitmapRegionDecoder != null && !bitmapRegionDecoder.isRecycled());
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.GINGERBREAD_MR1)
+ public void recycle() {
+ if (passthrough != null) {
+ passthrough.recycle();
+ passthrough = null;
+ } else {
+ bitmapRegionDecoder.recycle();
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
index 848cca09dc..e82db45dfd 100644
--- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -332,6 +332,7 @@ public class AttachmentManager {
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
intent.setDataAndType(slide.getUri(), slide.getContentType());
context.startActivity(intent);
diff --git a/src/org/thoughtcrime/securesms/mms/MediaConstraints.java b/src/org/thoughtcrime/securesms/mms/MediaConstraints.java
index 69e5db275f..37bfd2b1af 100644
--- a/src/org/thoughtcrime/securesms/mms/MediaConstraints.java
+++ b/src/org/thoughtcrime/securesms/mms/MediaConstraints.java
@@ -49,7 +49,7 @@ public abstract class MediaConstraints {
}
}
- public boolean isWithinBounds(Context context, MasterSecret masterSecret, Uri uri) throws IOException {
+ private boolean isWithinBounds(Context context, MasterSecret masterSecret, Uri uri) throws IOException {
try {
InputStream is = PartAuthority.getAttachmentStream(context, masterSecret, uri);
Pair dimensions = BitmapUtil.getDimensions(is);
diff --git a/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java b/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
index 33f22f80e9..f90c27f983 100644
--- a/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
+++ b/src/org/thoughtcrime/securesms/mms/PushMediaConstraints.java
@@ -6,7 +6,7 @@ import org.thoughtcrime.securesms.util.Util;
public class PushMediaConstraints extends MediaConstraints {
private static final int MAX_IMAGE_DIMEN_LOWMEM = 768;
- private static final int MAX_IMAGE_DIMEN = 2048;
+ private static final int MAX_IMAGE_DIMEN = 4096;
private static final int KB = 1024;
private static final int MB = 1024 * KB;
@@ -22,12 +22,12 @@ public class PushMediaConstraints extends MediaConstraints {
@Override
public int getImageMaxSize() {
- return 4 * MB;
+ return 6 * MB;
}
@Override
public int getGifMaxSize() {
- return 5 * MB;
+ return 6 * MB;
}
@Override
diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java
index 1b5d92d1cb..8a35ca0972 100644
--- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java
+++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java
@@ -32,6 +32,11 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicBoolean;
+import javax.microedition.khronos.egl.EGL10;
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.egl.EGLContext;
+import javax.microedition.khronos.egl.EGLDisplay;
+
public class BitmapUtil {
private static final String TAG = BitmapUtil.class.getSimpleName();
@@ -47,10 +52,13 @@ public class BitmapUtil {
int quality = MAX_COMPRESSION_QUALITY;
int attempts = 0;
byte[] bytes;
- Bitmap scaledBitmap = createScaledBitmap(context,
- model,
- constraints.getImageMaxWidth(context),
- constraints.getImageMaxHeight(context));
+
+ Bitmap scaledBitmap = Downsampler.AT_MOST.decode(getInputStreamForModel(context, model),
+ Glide.get(context).getBitmapPool(),
+ constraints.getImageMaxWidth(context),
+ constraints.getImageMaxHeight(context),
+ DecodeFormat.PREFER_RGB_565);
+
try {
do {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@@ -307,4 +315,34 @@ public class BitmapUtil {
return result[0];
}
}
+
+ public static int getMaxTextureSize() {
+ final int IMAGE_MAX_BITMAP_DIMENSION = 2048;
+
+ EGL10 egl = (EGL10) EGLContext.getEGL();
+ EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
+
+ int[] version = new int[2];
+ egl.eglInitialize(display, version);
+
+ int[] totalConfigurations = new int[1];
+ egl.eglGetConfigs(display, null, 0, totalConfigurations);
+
+ EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
+ egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
+
+ int[] textureSize = new int[1];
+ int maximumTextureSize = 0;
+
+ for (int i = 0; i < totalConfigurations[0]; i++) {
+ egl.eglGetConfigAttrib(display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
+
+ if (maximumTextureSize < textureSize[0])
+ maximumTextureSize = textureSize[0];
+ }
+
+ egl.eglTerminate(display);
+
+ return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java
index a9adbec6f3..d27f241af1 100644
--- a/src/org/thoughtcrime/securesms/util/MediaUtil.java
+++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java
@@ -9,6 +9,8 @@ import android.text.TextUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
+import com.bumptech.glide.Glide;
+
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.crypto.MasterSecret;
@@ -23,6 +25,7 @@ import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import java.io.IOException;
import java.io.InputStream;
+import java.util.concurrent.ExecutionException;
import ws.com.google.android.mms.ContentType;
@@ -52,8 +55,18 @@ public class MediaUtil {
private static Bitmap generateImageThumbnail(Context context, MasterSecret masterSecret, Uri uri)
throws BitmapDecodingException
{
- int maxSize = context.getResources().getDimensionPixelSize(R.dimen.media_bubble_height);
- return BitmapUtil.createScaledBitmap(context, new DecryptableUri(masterSecret, uri), maxSize, maxSize);
+ try {
+ int maxSize = context.getResources().getDimensionPixelSize(R.dimen.media_bubble_height);
+ return Glide.with(context)
+ .load(new DecryptableUri(masterSecret, uri))
+ .asBitmap()
+ .centerCrop()
+ .into(maxSize, maxSize)
+ .get();
+ } catch (InterruptedException | ExecutionException e) {
+ Log.w(TAG, e);
+ throw new BitmapDecodingException(e);
+ }
}
public static Slide getSlideForAttachment(Context context, Attachment attachment) {