Render images in a conversation true-to-size.

Previously, we were always rendering images as squares. Instead of doing
that, we now render them as close to true-to-size as possible (within
reasonable min/max width/height boundaries).
This commit is contained in:
Greyson Parrelli 2018-03-20 11:27:11 -07:00 committed by Moxie Marlinspike
parent 9f8b4cf892
commit ea374735e1
25 changed files with 376 additions and 55 deletions

View File

@ -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',

View File

@ -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" />
<org.thoughtcrime.securesms.components.AudioView
android:id="@+id/attachment_audio"

View File

@ -78,8 +78,8 @@
<ViewStub
android:id="@+id/image_view_stub"
android:layout="@layout/conversation_item_received_thumbnail"
android:layout_width="@dimen/media_bubble_height"
android:layout_height="@dimen/media_bubble_height"/>
android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens"/>
<ViewStub
android:id="@+id/audio_view_stub"

View File

@ -2,12 +2,17 @@
<org.thoughtcrime.securesms.components.ThumbnailView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image_view"
android:layout_width="@dimen/media_bubble_height"
android:layout_height="@dimen/media_bubble_height"
android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"
app:minWidth="@dimen/media_bubble_min_width"
app:maxWidth="@dimen/media_bubble_max_width"
app:minHeight="@dimen/media_bubble_min_height"
app:maxHeight="@dimen/media_bubble_max_height"
tools:src="@drawable/ic_video_light"
tools:visibility="gone" />

View File

@ -40,8 +40,8 @@
<ViewStub
android:id="@+id/image_view_stub"
android:layout_width="@dimen/media_bubble_height"
android:layout_height="@dimen/media_bubble_height"
android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens"
android:layout="@layout/conversation_item_sent_thumbnail" />
<ViewStub

View File

@ -2,14 +2,19 @@
<org.thoughtcrime.securesms.components.ThumbnailView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/image_view"
android:layout_width="@dimen/media_bubble_height"
android:layout_height="@dimen/media_bubble_height"
android:layout_width="@dimen/media_bubble_default_dimens"
android:layout_height="@dimen/media_bubble_default_dimens"
android:layout_marginBottom="5dp"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:adjustViewBounds="true"
android:contentDescription="@string/conversation_item__mms_image_description"
android:visibility="gone"
app:minWidth="@dimen/media_bubble_min_width"
app:maxWidth="@dimen/media_bubble_max_width"
app:minHeight="@dimen/media_bubble_min_height"
app:maxHeight="@dimen/media_bubble_max_height"
tools:src="@drawable/ic_video_light"
tools:visibility="visible" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="media_bubble_max_width">220dp</dimen>
<dimen name="media_bubble_max_height">300dp</dimen>
</resources>

View File

@ -147,6 +147,10 @@
<declare-styleable name="ThumbnailView">
<attr name="backgroundColorHint" format="color" />
<attr name="minWidth" format="dimension" />
<attr name="maxWidth" format="dimension" />
<attr name="minHeight" format="dimension" />
<attr name="maxHeight" format="dimension" />
</declare-styleable>
<declare-styleable name="DeliveryStatusView">

View File

@ -18,9 +18,13 @@
<dimen name="message_bubble_corner_radius">4dp</dimen>
<dimen name="message_bubble_shadow_distance">1.5dp</dimen>
<dimen name="media_bubble_height">210dp</dimen>
<dimen name="media_bubble_remove_button_size">24dp</dimen>
<dimen name="media_bubble_edit_button_size">24dp</dimen>
<dimen name="media_bubble_default_dimens">210dp</dimen>
<dimen name="media_bubble_min_width">150dp</dimen>
<dimen name="media_bubble_max_width">250dp</dimen>
<dimen name="media_bubble_min_height">100dp</dimen>
<dimen name="media_bubble_max_height">320dp</dimen>
<integer name="media_overview_cols">3</integer>
<dimen name="message_details_table_row_pad">10dp</dimen>

View File

@ -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) {

View File

@ -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);

View File

@ -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;
}

View File

@ -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<TransferControlView> transferControls = Optional.absent();
private SlideClickListener thumbnailClickListener = null;
private SlideClickListener downloadClickListener = null;
@ -64,10 +82,109 @@ 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);
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 {

View File

@ -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;
}

View File

@ -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...");

View File

@ -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<Integer, Integer> 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<Integer, Integer> 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);

View File

@ -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) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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<String> resolvedType = Optional.fromNullable(MediaUtil.getMimeType(context, uri));
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.or(defaultMime), AttachmentDatabase.TRANSFER_PROGRESS_STARTED, size, fileName, fastPreflightId, voiceNote);
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);
}

View File

@ -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) {

View File

@ -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<Integer, Integer> 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<Integer, Integer> getDimensions(InputStream inputStream) throws BitmapDecodingException {
BitmapFactory.Options options = getImageDimensions(inputStream);
return new Pair<>(options.outWidth, options.outHeight);

View File

@ -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<Integer, Integer> getDimensions(@NonNull Context context, @Nullable String contentType, @Nullable Uri uri) {
if (uri == null || !MediaUtil.isImageType(contentType)) {
return new Pair<>(0, 0);
}
Pair<Integer, Integer> 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");
}