diff --git a/build.gradle b/build.gradle index a3d682549a..bb8b0f295e 100644 --- a/build.gradle +++ b/build.gradle @@ -63,6 +63,7 @@ dependencies { compile 'com.android.support:preference-v14:27.0.2' compile 'com.android.support:gridlayout-v7:27.0.2' compile 'com.android.support:multidex:1.0.2' + compile "com.android.support:exifinterface:27.0.2" compile 'com.google.android.gms:play-services-gcm:9.6.1' compile 'com.google.android.gms:play-services-maps:9.6.1' @@ -156,6 +157,7 @@ dependencyVerification { 'com.android.support:cardview-v7:57f867a3c8f33e2d4dc0a03e2dfa03cad6267a908179f04a725a68ea9f0b8ccf', 'com.android.support:gridlayout-v7:227b5fdffa20f53bd562503aab6d2293d52cf64b5a6ab1116d2150f87bff9e88', 'com.android.support:multidex:7cd48755c7cfdb6dd2d21cbb02236ec390f6ac91cde87eb62f475b259ab5301d', + 'com.android.support:exifinterface:0e7cd526c4468895cd8549def46b3d33c8bcfb1ae4830569898d8c7326b15bb2', 'com.google.android.gms:play-services-gcm:312e61253a236f2d9b750b9c04fc92fd190d23b0b2755c99de6ce4a28b259dae', 'com.google.android.gms:play-services-places:abf3a4a3b146ec7e6e753be62775e512868cf37d6f88ffe2d81167b33b57132b', 'com.google.android.gms:play-services-maps:45e8021e7ddac4a44a82a0e9698991389ded3023d35c58f38dbd86d54211ec0e', diff --git a/res/layout/conversation_activity_attachment_editor_stub.xml b/res/layout/conversation_activity_attachment_editor_stub.xml index c7825b9205..a47de48b99 100644 --- a/res/layout/conversation_activity_attachment_editor_stub.xml +++ b/res/layout/conversation_activity_attachment_editor_stub.xml @@ -27,7 +27,11 @@ android:layout_gravity="center_horizontal" android:visibility="gone" android:contentDescription="@string/conversation_activity__attachment_thumbnail" - app:backgroundColorHint="?conversation_background" /> + app:backgroundColorHint="?conversation_background" + app:minWidth="100dp" + app:maxWidth="300dp" + app:minHeight="100dp" + app:maxHeight="300dp" /> + android:layout_width="@dimen/media_bubble_default_dimens" + android:layout_height="@dimen/media_bubble_default_dimens"/> diff --git a/res/layout/conversation_item_sent.xml b/res/layout/conversation_item_sent.xml index 2686e3ec8a..7e4ed9421b 100644 --- a/res/layout/conversation_item_sent.xml +++ b/res/layout/conversation_item_sent.xml @@ -40,9 +40,9 @@ + android:layout_width="@dimen/media_bubble_default_dimens" + android:layout_height="@dimen/media_bubble_default_dimens" + android:layout="@layout/conversation_item_sent_thumbnail" /> diff --git a/res/values-sw320dp/dimens.xml b/res/values-sw320dp/dimens.xml new file mode 100644 index 0000000000..e10ac512b8 --- /dev/null +++ b/res/values-sw320dp/dimens.xml @@ -0,0 +1,5 @@ + + + 220dp + 300dp + \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 50c793a27a..bbea8b9264 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -147,6 +147,10 @@ + + + + diff --git a/res/values/dimens.xml b/res/values/dimens.xml index cc0c00d123..9a66bf4c7c 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -18,9 +18,13 @@ 4dp 1.5dp - 210dp 24dp 24dp + 210dp + 150dp + 250dp + 100dp + 320dp 3 10dp diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 0451c06519..525575675d 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -112,6 +112,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.identity.IdentityRecordList; import org.thoughtcrime.securesms.events.ReminderUpdateEvent; +import org.thoughtcrime.securesms.giph.ui.GiphyActivity; import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.mms.AttachmentManager; @@ -406,6 +407,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity else mediaType = MediaType.IMAGE; setMedia(data.getData(), mediaType); + break; case PICK_DOCUMENT: setMedia(data.getData(), MediaType.DOCUMENT); @@ -438,7 +440,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity attachmentManager.setLocation(place, getCurrentMediaConstraints()); break; case PICK_GIF: - setMedia(data.getData(), MediaType.GIF); + setMedia(data.getData(), + MediaType.GIF, + data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0), + data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0)); break; case ScribbleActivity.SCRIBBLE_REQUEST_CODE: setMedia(data.getData(), MediaType.IMAGE); @@ -1377,8 +1382,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { + setMedia(uri, mediaType, 0, 0); + } + + private void setMedia(@Nullable Uri uri, @NonNull MediaType mediaType, int width, int height) { if (uri == null) return; - attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints()); + attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height); } private void addAttachmentContactInfo(Uri contactUri) { diff --git a/src/org/thoughtcrime/securesms/ConversationItem.java b/src/org/thoughtcrime/securesms/ConversationItem.java index ecfd17feba..f0c64daace 100644 --- a/src/org/thoughtcrime/securesms/ConversationItem.java +++ b/src/org/thoughtcrime/securesms/ConversationItem.java @@ -45,6 +45,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.components.AlertView; import org.thoughtcrime.securesms.components.AudioView; @@ -385,9 +386,14 @@ public class ConversationItem extends LinearLayout if (documentViewStub.resolved()) documentViewStub.get().setVisibility(View.GONE); //noinspection ConstantConditions + Slide thumbnailSlide = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlide(); + Attachment attachment = thumbnailSlide.asAttachment(); mediaThumbnailStub.get().setImageResource(glideRequests, - ((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide(), - showControls, false); + thumbnailSlide, + showControls, + false, + attachment.getWidth(), + attachment.getHeight()); mediaThumbnailStub.get().setThumbnailClickListener(new ThumbnailClickListener()); mediaThumbnailStub.get().setDownloadClickListener(downloadClickListener); mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener); diff --git a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java index 4511563303..d1cd989c40 100644 --- a/src/org/thoughtcrime/securesms/attachments/UriAttachment.java +++ b/src/org/thoughtcrime/securesms/attachments/UriAttachment.java @@ -12,15 +12,15 @@ public class UriAttachment extends Attachment { public UriAttachment(@NonNull Uri uri, @NonNull String contentType, int transferState, long size, @Nullable String fileName, boolean voiceNote) { - this(uri, uri, contentType, transferState, size, fileName, null, voiceNote); + this(uri, uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote); } public UriAttachment(@NonNull Uri dataUri, @Nullable Uri thumbnailUri, - @NonNull String contentType, int transferState, long size, + @NonNull String contentType, int transferState, long size, int width, int height, @Nullable String fileName, @Nullable String fastPreflightId, boolean voiceNote) { - super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, 0, 0); + super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height); this.dataUri = dataUri; this.thumbnailUri = thumbnailUri; } diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index bc35c1ae81..3c25aab8b7 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -5,20 +5,26 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.net.Uri; import android.support.annotation.NonNull; +import android.support.annotation.UiThread; 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.RequestBuilder; import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.FitCenter; import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; +import org.thoughtcrime.securesms.mms.GlideRequest; import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideClickListener; @@ -26,11 +32,19 @@ import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.ViewUtil; import org.whispersystems.libsignal.util.guava.Optional; +import java.util.Locale; + import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; public class ThumbnailView extends FrameLayout { private static final String TAG = ThumbnailView.class.getSimpleName(); + private static final int WIDTH = 0; + private static final int HEIGHT = 1; + private static final int MIN_WIDTH = 0; + private static final int MAX_WIDTH = 1; + private static final int MIN_HEIGHT = 2; + private static final int MAX_HEIGHT = 3; private ImageView image; private ImageView playOverlay; @@ -38,6 +52,10 @@ public class ThumbnailView extends FrameLayout { private int radius; private OnClickListener parentClickListener; + private final int[] dimens = new int[2]; + private final int[] bounds = new int[4]; + private final int[] measureDimens = new int[2]; + private Optional transferControls = Optional.absent(); private SlideClickListener thumbnailClickListener = null; private SlideClickListener downloadClickListener = null; @@ -63,11 +81,110 @@ public class ThumbnailView extends FrameLayout { if (attrs != null) { TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ThumbnailView, 0, 0); - backgroundColorHint = typedArray.getColor(R.styleable.ThumbnailView_backgroundColorHint, Color.BLACK); + backgroundColorHint = typedArray.getColor(R.styleable.ThumbnailView_backgroundColorHint, Color.BLACK); + bounds[MIN_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minWidth, 0); + bounds[MAX_WIDTH] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxWidth, 0); + bounds[MIN_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_minHeight, 0); + bounds[MAX_HEIGHT] = typedArray.getDimensionPixelSize(R.styleable.ThumbnailView_maxHeight, 0); typedArray.recycle(); } } + @Override + protected void onMeasure(int originalWidthMeasureSpec, int originalHeightMeasureSpec) { + fillTargetDimensions(measureDimens, dimens, bounds); + if (measureDimens[WIDTH] == 0 && measureDimens[HEIGHT] == 0) { + super.onMeasure(originalWidthMeasureSpec, originalHeightMeasureSpec); + return; + } + + int finalWidth = measureDimens[WIDTH] + getPaddingLeft() + getPaddingRight(); + int finalHeight = measureDimens[HEIGHT] + getPaddingTop() + getPaddingBottom(); + + super.onMeasure(MeasureSpec.makeMeasureSpec(finalWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY)); + } + + @SuppressWarnings("SuspiciousNameCombination") + private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) { + int dimensFilledCount = getNonZeroCount(dimens); + int boundsFilledCount = getNonZeroCount(bounds); + + if (dimensFilledCount == 0 || boundsFilledCount == 0) { + targetDimens[WIDTH] = 0; + targetDimens[HEIGHT] = 0; + return; + } + + double naturalWidth = dimens[WIDTH]; + double naturalHeight = dimens[HEIGHT]; + + int minWidth = bounds[MIN_WIDTH]; + int maxWidth = bounds[MAX_WIDTH]; + int minHeight = bounds[MIN_HEIGHT]; + int maxHeight = bounds[MAX_HEIGHT]; + + if (dimensFilledCount > 0 && dimensFilledCount < dimens.length) { + throw new IllegalStateException(String.format(Locale.ENGLISH, "Width or height has been specified, but not both. Dimens: %f x %f", + naturalWidth, naturalHeight)); + } + if (boundsFilledCount > 0 && boundsFilledCount < bounds.length) { + throw new IllegalStateException(String.format(Locale.ENGLISH, "One or more min/max dimensions have been specified, but not all. Bounds: [%d, %d, %d, %d]", + minWidth, maxWidth, minHeight, maxHeight)); + } + + double measuredWidth = naturalWidth; + double measuredHeight = naturalHeight; + + boolean widthInBounds = measuredWidth >= minWidth && measuredWidth <= maxWidth; + boolean heightInBounds = measuredHeight >= minHeight && measuredHeight <= maxHeight; + + if (!widthInBounds || !heightInBounds) { + double minWidthRatio = naturalWidth / minWidth; + double maxWidthRatio = naturalWidth / maxWidth; + double minHeightRatio = naturalHeight / minHeight; + double maxHeightRatio = naturalHeight / maxHeight; + + if (maxWidthRatio > 1 || maxHeightRatio > 1) { + if (maxWidthRatio >= maxHeightRatio) { + measuredWidth /= maxWidthRatio; + measuredHeight /= maxWidthRatio; + } else { + measuredWidth /= maxHeightRatio; + measuredHeight /= maxHeightRatio; + } + + measuredWidth = Math.max(measuredWidth, minWidth); + measuredHeight = Math.max(measuredHeight, minHeight); + + } else if (minWidthRatio < 1 || minHeightRatio < 1) { + if (minWidthRatio <= minHeightRatio) { + measuredWidth /= minWidthRatio; + measuredHeight /= minWidthRatio; + } else { + measuredWidth /= minHeightRatio; + measuredHeight /= minHeightRatio; + } + + measuredWidth = Math.min(measuredWidth, maxWidth); + measuredHeight = Math.min(measuredHeight, maxHeight); + } + } + + targetDimens[WIDTH] = (int) measuredWidth; + targetDimens[HEIGHT] = (int) measuredHeight; + } + + private int getNonZeroCount(int[] vals) { + int count = 0; + for (int val : vals) { + if (val > 0) { + count++; + } + } + return count; + } + @Override public void setOnClickListener(OnClickListener l) { parentClickListener = l; @@ -96,9 +213,22 @@ public class ThumbnailView extends FrameLayout { this.backgroundColorHint = color; } + @UiThread public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, boolean showControls, boolean isPreview) { + setImageResource(glideRequests, slide, showControls, isPreview, 0, 0); + } + + @UiThread + public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, + boolean showControls, boolean isPreview, int naturalWidth, + int naturalHeight) + { + dimens[WIDTH] = naturalWidth; + dimens[HEIGHT] = naturalHeight; + invalidate(); + if (showControls) { getTransferControls().setSlide(slide); getTransferControls().setDownloadClickListener(new DownloadClickDispatcher()); @@ -136,11 +266,11 @@ public class ThumbnailView extends FrameLayout { if (slide.getThumbnailUri() != null) buildThumbnailGlideRequest(glideRequests, slide).into(image); else if (slide.hasPlaceholder()) buildPlaceholderGlideRequest(glideRequests, slide).into(image); else glideRequests.clear(image); + } public void setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); - glideRequests.load(new DecryptableUri(uri)) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) .transform(new RoundedCorners(radius)) @@ -171,22 +301,29 @@ public class ThumbnailView extends FrameLayout { getTransferControls().showProgressSpinner(); } - private RequestBuilder buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { - RequestBuilder builder = glideRequests.load(new DecryptableUri(slide.getThumbnailUri())) + private GlideRequest buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { + GlideRequest request = applySizing(glideRequests.load(new DecryptableUri(slide.getThumbnailUri())) .diskCacheStrategy(DiskCacheStrategy.RESOURCE) - .transform(new RoundedCorners(radius)) - .centerCrop() - .transition(withCrossFade()); + .transition(withCrossFade()), new CenterCrop()); - if (slide.isInProgress()) return builder; - else return builder.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)); + if (slide.isInProgress()) return request; + else return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)); } private RequestBuilder buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { - return glideRequests.asBitmap() + return applySizing(glideRequests.asBitmap() .load(slide.getPlaceholderRes(getContext().getTheme())) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .fitCenter(); + .diskCacheStrategy(DiskCacheStrategy.NONE), new FitCenter()); + } + + private GlideRequest applySizing(@NonNull GlideRequest request, @NonNull BitmapTransformation unavailableDimensSizing) { + int[] size = new int[2]; + fillTargetDimensions(size, dimens, bounds); + if (size[WIDTH] == 0 && size[HEIGHT] == 0) { + return request.transforms(unavailableDimensSizing, new RoundedCorners(radius)); + } + return request.override(size[WIDTH], size[HEIGHT]) + .transforms(new CenterCrop(), new RoundedCorners(radius)); } private class ThumbnailClickDispatcher implements View.OnClickListener { diff --git a/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java b/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java index ace6e16053..9a03884c36 100644 --- a/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java +++ b/src/org/thoughtcrime/securesms/giph/model/GiphyImage.java @@ -28,6 +28,14 @@ public class GiphyImage { return (float)images.downsized.width / (float)images.downsized.height; } + public int getGifWidth() { + return images.downsized.width; + } + + public int getGifHeight() { + return images.downsized.height; + } + public String getStillUrl() { return images.downsized_still.url; } diff --git a/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java index ded8206104..f056fbdddc 100644 --- a/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java +++ b/src/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java @@ -37,6 +37,8 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity private static final String TAG = GiphyActivity.class.getSimpleName(); public static final String EXTRA_IS_MMS = "extra_is_mms"; + public static final String EXTRA_WIDTH = "extra_width"; + public static final String EXTRA_HEIGHT = "extra_height"; private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicLanguage dynamicLanguage = new DynamicLanguage(); @@ -124,7 +126,11 @@ public class GiphyActivity extends PassphraseRequiredActionBarActivity if (uri == null) { Toast.makeText(GiphyActivity.this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show(); } else if (viewHolder == finishingImage) { - setResult(RESULT_OK, new Intent().setData(uri)); + Intent intent = new Intent(); + intent.setData(uri); + intent.putExtra(EXTRA_WIDTH, viewHolder.image.getGifWidth()); + intent.putExtra(EXTRA_HEIGHT, viewHolder.image.getGifHeight()); + setResult(RESULT_OK, intent); finish(); } else { Log.w(TAG, "Resolved Uri is no longer the selected element..."); diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index 4810fffb13..addc7c7386 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -34,6 +34,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.view.View; import android.widget.Toast; @@ -43,6 +44,7 @@ import com.google.android.gms.location.places.ui.PlacePicker; import org.thoughtcrime.securesms.MediaPreviewActivity; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.components.AudioView; import org.thoughtcrime.securesms.components.DocumentView; import org.thoughtcrime.securesms.components.RemovableEditableMediaView; @@ -204,7 +206,9 @@ public class AttachmentManager { public void setMedia(@NonNull final GlideRequests glideRequests, @NonNull final Uri uri, @NonNull final MediaType mediaType, - @NonNull final MediaConstraints constraints) + @NonNull final MediaConstraints constraints, + final int width, + final int height) { inflateStub(); @@ -220,11 +224,11 @@ public class AttachmentManager { protected @Nullable Slide doInBackground(Void... params) { try { if (PartAuthority.isLocalUri(uri)) { - return getManuallyCalculatedSlideInfo(uri); + return getManuallyCalculatedSlideInfo(uri, width, height); } else { - Slide result = getContentResolverSlideInfo(uri); + Slide result = getContentResolverSlideInfo(uri, width, height); - if (result == null) return getManuallyCalculatedSlideInfo(uri); + if (result == null) return getManuallyCalculatedSlideInfo(uri, width, height); else return result; } } catch (IOException e) { @@ -256,7 +260,8 @@ public class AttachmentManager { documentView.setDocument((DocumentSlide) slide, false); removableMediaView.display(documentView, false); } else { - thumbnail.setImageResource(glideRequests, slide, false, true); + Attachment attachment = slide.asAttachment(); + thumbnail.setImageResource(glideRequests, slide, false, true, attachment.getWidth(), attachment.getHeight()); removableMediaView.display(thumbnail, mediaType == MediaType.IMAGE); } @@ -264,7 +269,7 @@ public class AttachmentManager { } } - private @Nullable Slide getContentResolverSlideInfo(Uri uri) { + private @Nullable Slide getContentResolverSlideInfo(Uri uri, int width, int height) { Cursor cursor = null; long start = System.currentTimeMillis(); @@ -276,8 +281,14 @@ public class AttachmentManager { long fileSize = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)); String mimeType = context.getContentResolver().getType(uri); + if (width == 0 || height == 0) { + Pair dimens = MediaUtil.getDimensions(context, mimeType, uri); + width = dimens.first; + height = dimens.second; + } + Log.w(TAG, "remote slide with size " + fileSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, fileSize); + return mediaType.createSlide(context, uri, fileName, mimeType, fileSize, width, height); } } finally { if (cursor != null) cursor.close(); @@ -286,7 +297,7 @@ public class AttachmentManager { return null; } - private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri) throws IOException { + private @NonNull Slide getManuallyCalculatedSlideInfo(Uri uri, int width, int height) throws IOException { long start = System.currentTimeMillis(); Long mediaSize = null; String fileName = null; @@ -302,8 +313,18 @@ public class AttachmentManager { mediaSize = MediaUtil.getMediaSize(context, uri); } + if (mimeType == null) { + mimeType = MediaUtil.getMimeType(context, uri); + } + + if (width == 0 || height == 0) { + Pair dimens = MediaUtil.getDimensions(context, mimeType, uri); + width = dimens.first; + height = dimens.second; + } + Log.w(TAG, "local slide with size " + mediaSize + " took " + (System.currentTimeMillis() - start) + "ms"); - return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize); + return mediaType.createSlide(context, uri, fileName, mimeType, mediaSize, width, height); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @@ -493,15 +514,17 @@ public class AttachmentManager { @NonNull Uri uri, @Nullable String fileName, @Nullable String mimeType, - long dataSize) + long dataSize, + int width, + int height) { if (mimeType == null) { mimeType = "application/octet-stream"; } switch (this) { - case IMAGE: return new ImageSlide(context, uri, dataSize); - case GIF: return new GifSlide(context, uri, dataSize); + case IMAGE: return new ImageSlide(context, uri, dataSize, width, height); + case GIF: return new GifSlide(context, uri, dataSize, width, height); case AUDIO: return new AudioSlide(context, uri, dataSize, false); case VIDEO: return new VideoSlide(context, uri, dataSize); case DOCUMENT: return new DocumentSlide(context, uri, mimeType, dataSize, fileName); diff --git a/src/org/thoughtcrime/securesms/mms/AudioSlide.java b/src/org/thoughtcrime/securesms/mms/AudioSlide.java index 443bf0383f..5010e63624 100644 --- a/src/org/thoughtcrime/securesms/mms/AudioSlide.java +++ b/src/org/thoughtcrime/securesms/mms/AudioSlide.java @@ -34,11 +34,11 @@ import org.thoughtcrime.securesms.util.ResUtil; public class AudioSlide extends Slide { public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, false, null, voiceNote)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, voiceNote)); } public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) { - super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, null, null, voiceNote)); + super(context, new UriAttachment(uri, null, contentType, AttachmentDatabase.TRANSFER_PROGRESS_STARTED, dataSize, 0, 0, null, null, voiceNote)); } public AudioSlide(Context context, Attachment attachment) { diff --git a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java index 3d7e8d37b8..72d19a1a5e 100644 --- a/src/org/thoughtcrime/securesms/mms/DocumentSlide.java +++ b/src/org/thoughtcrime/securesms/mms/DocumentSlide.java @@ -19,7 +19,7 @@ public class DocumentSlide extends Slide { @NonNull String contentType, long size, @Nullable String fileName) { - super(context, constructAttachmentFromUri(context, uri, contentType, size, true, StorageUtil.getCleanFileName(fileName), false)); + super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/GifSlide.java b/src/org/thoughtcrime/securesms/mms/GifSlide.java index f6443180f7..2bdc688a31 100644 --- a/src/org/thoughtcrime/securesms/mms/GifSlide.java +++ b/src/org/thoughtcrime/securesms/mms/GifSlide.java @@ -13,8 +13,8 @@ public class GifSlide extends ImageSlide { super(context, attachment); } - public GifSlide(Context context, Uri uri, long size) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, true, null, false)); + public GifSlide(Context context, Uri uri, long size, int width, int height) { + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 795b651f0c..be15b339d7 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -36,8 +36,8 @@ public class ImageSlide extends Slide { super(context, attachment); } - public ImageSlide(Context context, Uri uri, long size) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, true, null, false)); + public ImageSlide(Context context, Uri uri, long size, int width, int height) { + super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, false)); } @Override diff --git a/src/org/thoughtcrime/securesms/mms/LocationSlide.java b/src/org/thoughtcrime/securesms/mms/LocationSlide.java index f679b8bf82..e5f613d2f9 100644 --- a/src/org/thoughtcrime/securesms/mms/LocationSlide.java +++ b/src/org/thoughtcrime/securesms/mms/LocationSlide.java @@ -14,7 +14,7 @@ public class LocationSlide extends ImageSlide { public LocationSlide(@NonNull Context context, @NonNull Uri uri, long size, @NonNull SignalPlace place) { - super(context, uri, size); + super(context, uri, size, 0, 0); this.place = place; } diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index 623cd3c847..7e49b6484c 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -22,6 +22,7 @@ import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.util.Pair; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.UriAttachment; @@ -132,14 +133,25 @@ public abstract class Slide { @NonNull Uri uri, @NonNull String defaultMime, long size, + int width, + int height, boolean hasThumbnail, @Nullable String fileName, boolean voiceNote) { try { - Optional resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)); - String fastPreflightId = String.valueOf(SecureRandom.getInstance("SHA1PRNG").nextLong()); - return new UriAttachment(uri, hasThumbnail ? uri : null, resolvedType.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size, fileName, fastPreflightId, voiceNote); + String resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri)).or(defaultMime); + String fastPreflightId = String.valueOf(SecureRandom.getInstance("SHA1PRNG").nextLong()); + return new UriAttachment(uri, + hasThumbnail ? uri : null, + resolvedType, + AttachmentDatabase.TRANSFER_PROGRESS_STARTED, + size, + width, + height, + fileName, + fastPreflightId, + voiceNote); } catch (NoSuchAlgorithmException e) { throw new AssertionError(e); } diff --git a/src/org/thoughtcrime/securesms/mms/VideoSlide.java b/src/org/thoughtcrime/securesms/mms/VideoSlide.java index 146cb7bdef..278934f8c6 100644 --- a/src/org/thoughtcrime/securesms/mms/VideoSlide.java +++ b/src/org/thoughtcrime/securesms/mms/VideoSlide.java @@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.ResUtil; public class VideoSlide extends Slide { public VideoSlide(Context context, Uri uri, long dataSize) { - super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, MediaUtil.hasVideoThumbnail(uri), null, false)); + super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, false)); } public VideoSlide(Context context, Attachment attachment) { diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index dc8f391ceb..90ccb50479 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -12,6 +12,7 @@ import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.support.annotation.*; import android.support.annotation.WorkerThread; +import android.support.media.ExifInterface; import android.util.Log; import android.util.Pair; @@ -131,6 +132,26 @@ public class BitmapUtil { return options; } + @Nullable + public static Pair getExifDimensions(InputStream inputStream) throws IOException { + ExifInterface exif = new ExifInterface(inputStream); + int width = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0); + int height = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0); + if (width == 0 && height == 0) { + return null; + } + + int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0); + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || + orientation == ExifInterface.ORIENTATION_ROTATE_270 || + orientation == ExifInterface.ORIENTATION_TRANSVERSE || + orientation == ExifInterface.ORIENTATION_TRANSPOSE) + { + return new Pair<>(height, width); + } + return new Pair<>(width, height); + } + public static Pair getDimensions(InputStream inputStream) throws BitmapDecodingException { BitmapFactory.Options options = getImageDimensions(inputStream); return new Pair<>(options.outWidth, options.outHeight); diff --git a/src/org/thoughtcrime/securesms/util/MediaUtil.java b/src/org/thoughtcrime/securesms/util/MediaUtil.java index 2f023e1c77..b0f5441db9 100644 --- a/src/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/src/org/thoughtcrime/securesms/util/MediaUtil.java @@ -3,18 +3,27 @@ package org.thoughtcrime.securesms.util; import android.content.ContentResolver; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.support.media.ExifInterface; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.webkit.MimeTypeMap; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.resource.gif.GifDrawable; + import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.mms.AudioSlide; +import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DocumentSlide; import org.thoughtcrime.securesms.mms.GifSlide; +import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.MmsSlide; import org.thoughtcrime.securesms.mms.PartAuthority; @@ -22,8 +31,10 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.VideoSlide; import org.thoughtcrime.securesms.providers.PersistentBlobProvider; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.util.concurrent.ExecutionException; public class MediaUtil { @@ -100,6 +111,65 @@ public class MediaUtil { return size; } + @WorkerThread + public static Pair getDimensions(@NonNull Context context, @Nullable String contentType, @Nullable Uri uri) { + if (uri == null || !MediaUtil.isImageType(contentType)) { + return new Pair<>(0, 0); + } + + Pair dimens = null; + + if (MediaUtil.isGif(contentType)) { + try { + GifDrawable drawable = GlideApp.with(context) + .asGif() + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .load(new DecryptableUri(uri)) + .submit() + .get(); + dimens = new Pair<>(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } catch (InterruptedException e) { + Log.w(TAG, "Was unable to complete work for GIF dimensions.", e); + } catch (ExecutionException e) { + Log.w(TAG, "Glide experienced an exception while trying to get GIF dimensions.", e); + } + } else { + InputStream attachmentStream = null; + try { + if (MediaUtil.isJpegType(contentType)) { + attachmentStream = PartAuthority.getAttachmentStream(context, uri); + dimens = BitmapUtil.getExifDimensions(attachmentStream); + attachmentStream.close(); + attachmentStream = null; + } + if (dimens == null) { + attachmentStream = PartAuthority.getAttachmentStream(context, uri); + dimens = BitmapUtil.getDimensions(attachmentStream); + } + } catch (FileNotFoundException e) { + Log.w(TAG, "Failed to find file when retrieving media dimensions.", e); + } catch (IOException e) { + Log.w(TAG, "Experienced a read error when retrieving media dimensions.", e); + } catch (BitmapDecodingException e) { + Log.w(TAG, "Bitmap decoding error when retrieving dimensions.", e); + } finally { + if (attachmentStream != null) { + try { + attachmentStream.close(); + } catch (IOException e) { + Log.w(TAG, "Failed to close stream after retrieving dimensions.", e); + } + } + } + } + if (dimens == null) { + dimens = new Pair<>(0, 0); + } + Log.d(TAG, "Dimensions for [" + uri + "] are " + dimens.first + " x " + dimens.second); + return dimens; + } + public static boolean isMms(String contentType) { return !TextUtils.isEmpty(contentType) && contentType.trim().equals("application/mms"); }