Use Glide for loading part thumbnails

Closes #2885

// FREEBIE
master
Jake McGinty 2015-03-31 15:44:41 -07:00 committed by Moxie Marlinspike
parent 9ba19df2af
commit f42d100f15
26 changed files with 506 additions and 429 deletions

View File

@ -45,6 +45,9 @@
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
<meta-data android:name="org.thoughtcrime.securesms.mms.TextSecureGlideModule"
android:value="GlideModule" />
<activity android:name=".RegistrationProblemsActivity"
android:theme="@style/TextSecure.Light.Dialog"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>

View File

@ -38,6 +38,7 @@ dependencies {
compile 'org.w3c:smil:1.0.0'
compile 'org.apache.httpcomponents:httpclient-android:4.3.5'
compile 'com.github.chrisbanes.photoview:library:1.2.3'
compile 'com.github.bumptech.glide:glide:3.5.2'
compile 'com.makeramen:roundedimageview:1.5.0'
compile ('com.afollestad:material-dialogs:0.6.4.7') {
exclude module: 'appcompat-v7'
@ -88,6 +89,7 @@ dependencyVerification {
'org.w3c:smil:085dc40f2bb249651578bfa07499fd08b16ad0886dbe2c4078586a408da62f9b',
'org.apache.httpcomponents:httpclient-android:6f56466a9bd0d42934b90bfbfe9977a8b654c058bf44a12bdc2877c4e1f033f1',
'com.github.chrisbanes.photoview:library:8b5344e206f125e7ba9d684008f36c4992d03853c57e5814125f88496126e3cc',
'com.github.bumptech.glide:glide:16936352b96aa604b030f2d2263fb0cc656933e6cdda0bf6ac681282d1f7f196',
'com.makeramen:roundedimageview:7dda2e78c406760e5c356ccce59b0df46b5b171cf18abb891998594405021548',
'com.afollestad:material-dialogs:6ed57c1c479219f8ae929c310fc171dbfcbcee8326a6dcd50a91959d69eccdf0',
'com.soundcloud.android:android-crop:ffd4b973cf6e97f7d64118a0dc088df50e9066fd5634fe6911dd0c0c5d346177',
@ -167,6 +169,7 @@ android {
'proguard-square-okio.pro',
'proguard-spongycastle.pro',
'proguard-rounded-image-view.pro',
'proguard-glide.pro',
'proguard.cfg'
}
release {
@ -181,6 +184,7 @@ android {
'proguard-square-okio.pro',
'proguard-spongycastle.pro',
'proguard-rounded-image-view.pro',
'proguard-glide.pro',
'proguard.cfg'
signingConfig signingConfigs.release
}

View File

@ -0,0 +1,4 @@
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}

View File

@ -36,7 +36,7 @@
android:orientation="horizontal"
android:visibility="gone">
<ImageView
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/attachment_thumbnail"
android:layout_width="0dp"
android:layout_height="150dip"

View File

@ -19,14 +19,14 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<org.thoughtcrime.securesms.components.ForegroundImageView
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/media_bubble_height"
android:layout_marginRight="@dimen/message_bubble_end_padding"
android:visibility="gone"
android:layout_gravity="center"
android:scaleType="centerCrop"
android:scaleType="centerInside"
android:adjustViewBounds="true"
android:contentDescription="@string/conversation_item__mms_image_description"
app:riv_corner_radius="@dimen/message_bubble_corner_radius"

View File

@ -17,7 +17,7 @@
android:layout_gravity="center"
android:layout_height="wrap_content">
<org.thoughtcrime.securesms.components.ForegroundImageView
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/image_view"
android:layout_width="wrap_content"
android:layout_height="@dimen/media_bubble_height"

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.ForegroundImageView
<org.thoughtcrime.securesms.components.ThumbnailView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<color name="textsecure_primary">#ff2090ea</color>
<color name="textsecure_primary_dark">#ff145c95</color>
<color name="textsecure_primary_dark">#ff1c7ac5</color>
<color name="white">#ffffffff</color>
<color name="black">#ff000000</color>

View File

@ -22,16 +22,12 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.provider.ContactsContract;
import android.provider.ContactsContract.QuickContact;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
@ -43,7 +39,7 @@ import com.afollestad.materialdialogs.AlertDialogWrapper;
import org.thoughtcrime.securesms.ConversationFragment.SelectionClickListener;
import org.thoughtcrime.securesms.components.BubbleContainer;
import org.thoughtcrime.securesms.components.ForegroundImageView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.contacts.ContactPhotoFactory;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@ -58,12 +54,9 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob;
import org.thoughtcrime.securesms.jobs.SmsSendJob;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.Emoji;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import java.util.Set;
@ -97,16 +90,10 @@ public class ConversationItem extends LinearLayout {
private Set<MessageRecord> batchSelected;
private SelectionClickListener selectionClickListener;
private ForegroundImageView mediaThumbnail;
private ThumbnailView mediaThumbnail;
private Button mmsDownloadButton;
private TextView mmsDownloadingLabel;
private ListenableFutureTask<SlideDeck> slideDeckFuture;
private ListenableFutureTask<Pair<Drawable, Boolean>> thumbnailFuture;
private SlideDeckListener slideDeckListener;
private ThumbnailListener thumbnailListener;
private Handler handler;
private final MmsDownloadClickListener mmsDownloadClickListener = new MmsDownloadClickListener();
private final MmsPreferencesClickListener mmsPreferencesClickListener = new MmsPreferencesClickListener();
private final ClickListener clickListener = new ClickListener();
@ -139,14 +126,14 @@ public class ConversationItem extends LinearLayout {
this.bodyBubble = findViewById(R.id.body_bubble);
this.pendingIndicator = (ImageView)findViewById(R.id.pending_approval_indicator);
this.bubbleContainer = (BubbleContainer)findViewById(R.id.bubble);
this.mediaThumbnail = (ForegroundImageView)findViewById(R.id.image_view);
slideDeckListener = new SlideDeckListener();
handler = new Handler(Looper.getMainLooper());
this.mediaThumbnail = (ThumbnailView)findViewById(R.id.image_view);
setOnClickListener(clickListener);
if (mmsDownloadButton != null) mmsDownloadButton.setOnClickListener(mmsDownloadClickListener);
if (mediaThumbnail != null) mediaThumbnail.setOnLongClickListener(new MultiSelectLongClickListener());
if (mediaThumbnail != null) {
mediaThumbnail.setThumbnailClickListener(new ThumbnailClickListener());
mediaThumbnail.setOnLongClickListener(new MultiSelectLongClickListener());
}
}
public void set(@NonNull MasterSecret masterSecret,
@ -177,13 +164,6 @@ public class ConversationItem extends LinearLayout {
}
public void unbind() {
if (slideDeckFuture != null && slideDeckListener != null) {
slideDeckFuture.removeListener(slideDeckListener);
}
if (thumbnailFuture != null && thumbnailListener != null) {
thumbnailFuture.removeListener(thumbnailListener);
}
}
public MessageRecord getMessageRecord() {
@ -376,8 +356,7 @@ public class ConversationItem extends LinearLayout {
private void resolveMedia(MediaMmsMessageRecord messageRecord) {
if (hasMedia(messageRecord)) {
slideDeckFuture = messageRecord.getSlideDeckFuture();
slideDeckFuture.addListener(slideDeckListener);
mediaThumbnail.setImageResource(messageRecord.getSlideDeckFuture(), masterSecret);
}
}
@ -429,14 +408,8 @@ public class ConversationItem extends LinearLayout {
context.startActivity(intent);
}
private class ThumbnailClickListener implements View.OnClickListener {
private final Slide slide;
public ThumbnailClickListener(Slide slide) {
this.slide = slide;
}
private void fireIntent() {
private class ThumbnailClickListener implements ThumbnailView.ThumbnailClickListener {
private void fireIntent(Slide slide) {
Log.w(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@ -449,7 +422,7 @@ public class ConversationItem extends LinearLayout {
}
}
public void onClick(View v) {
public void onClick(final View v, final Slide slide) {
if (!batchSelected.isEmpty()) {
selectionClickListener.onItemClick(null, ConversationItem.this, -1, -1);
} else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType())) {
@ -468,7 +441,7 @@ public class ConversationItem extends LinearLayout {
builder.setMessage(R.string.ConversationItem_this_media_has_been_stored_in_an_encrypted_database_external_viewer_warning);
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
fireIntent();
fireIntent(slide);
}
});
builder.setNegativeButton(R.string.no, null);
@ -587,73 +560,4 @@ public class ConversationItem extends LinearLayout {
});
builder.show();
}
private class ThumbnailListener implements FutureTaskListener<Pair<Drawable, Boolean>> {
private final Object tag;
public ThumbnailListener(Object tag) {
this.tag = tag;
}
@Override
public void onSuccess(final Pair<Drawable, Boolean> result) {
handler.post(new Runnable() {
@Override
public void run() {
if (mediaThumbnail.getTag() == tag) {
Log.w(TAG, "displaying media thumbnail");
mediaThumbnail.show(result.first, result.second);
}
}
});
}
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
handler.post(new Runnable() {
@Override
public void run() {
mediaThumbnail.hide();
}
});
}
}
private class SlideDeckListener implements FutureTaskListener<SlideDeck> {
@Override
public void onSuccess(final SlideDeck slideDeck) {
if (slideDeck == null) return;
handler.post(new Runnable() {
@Override
public void run() {
Slide slide = slideDeck.getThumbnailSlide(context);
if (slide != null) {
thumbnailFuture = slide.getThumbnail(context);
if (thumbnailFuture != null) {
Object tag = new Object();
mediaThumbnail.setTag(tag);
thumbnailListener = new ThumbnailListener(tag);
thumbnailFuture.addListener(thumbnailListener);
mediaThumbnail.setOnClickListener(new ThumbnailClickListener(slide));
return;
}
}
mediaThumbnail.hide();
}
});
}
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
handler.post(new Runnable() {
@Override
public void run() {
mediaThumbnail.hide();
}
});
}
}
}

View File

@ -19,25 +19,21 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.ImageMediaAdapter.ViewHolder;
import org.thoughtcrime.securesms.components.ForegroundImageView;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.PartDatabase.ImageRecord;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.recipients.RecipientFactory;
import org.thoughtcrime.securesms.recipients.Recipients;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.MediaUtil;
import ws.com.google.android.mms.pdu.PduPart;
@ -46,21 +42,19 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
private static final String TAG = ImageMediaAdapter.class.getSimpleName();
private final MasterSecret masterSecret;
private final int gridSize;
public static class ViewHolder extends RecyclerView.ViewHolder {
public ForegroundImageView imageView;
public ThumbnailView imageView;
public ViewHolder(View v) {
super(v);
imageView = (ForegroundImageView) v.findViewById(R.id.image);
imageView = (ThumbnailView) v.findViewById(R.id.image);
}
}
public ImageMediaAdapter(Context context, MasterSecret masterSecret, Cursor c) {
super(context, c);
this.masterSecret = masterSecret;
this.gridSize = context.getResources().getDimensionPixelSize(R.dimen.thumbnail_max_size);
}
@Override
@ -71,8 +65,8 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
@Override
public void onBindViewHolder(final ViewHolder viewHolder, final Cursor cursor) {
final ForegroundImageView imageView = viewHolder.imageView;
final ImageRecord imageRecord = ImageRecord.from(cursor);
final ThumbnailView imageView = viewHolder.imageView;
final ImageRecord imageRecord = ImageRecord.from(cursor);
PduPart part = new PduPart();
@ -80,25 +74,9 @@ public class ImageMediaAdapter extends CursorRecyclerViewAdapter<ViewHolder> {
part.setContentType(imageRecord.getContentType().getBytes());
part.setId(imageRecord.getPartId());
imageView.setVisibility(View.INVISIBLE);
Slide slide = MediaUtil.getSlideForPart(getContext(), masterSecret, part, imageRecord.getContentType());
if (slide != null) {
slide.getThumbnail(getContext()).addListener(new FutureTaskListener<Pair<Drawable, Boolean>>() {
@Override
public void onSuccess(final Pair<Drawable, Boolean> result) {
imageView.post(new Runnable() {
@Override
public void run() {
imageView.show(result.first, false);
}
});
}
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
}
});
imageView.setImageResource(slide, masterSecret);
}
imageView.setOnClickListener(new OnMediaClickListener(imageRecord));

View File

@ -22,7 +22,6 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION_CODES;
import android.support.annotation.DrawableRes;
import android.support.annotation.IntDef;
import android.util.AttributeSet;
import android.view.View;
@ -51,11 +50,11 @@ public abstract class BubbleContainer extends RelativeLayout {
@IntDef({MEDIA_STATE_NO_MEDIA, MEDIA_STATE_CAPTIONLESS, MEDIA_STATE_CAPTIONED})
public @interface MediaState {}
private View bodyBubble;
private View triangleTick;
private ForegroundImageView media;
private int shadowColor;
private int mmsPendingOverlayColor;
private View bodyBubble;
private View triangleTick;
private ThumbnailView media;
private int shadowColor;
private int mmsPendingOverlayColor;
public BubbleContainer(Context context) {
super(context);
@ -88,7 +87,7 @@ public abstract class BubbleContainer extends RelativeLayout {
onCreateView();
this.bodyBubble = findViewById(R.id.body_bubble );
this.triangleTick = findViewById(R.id.triangle_tick);
this.media = (ForegroundImageView) findViewById(R.id.image_view);
this.media = (ThumbnailView) findViewById(R.id.image_view);
this.shadowColor = ResUtil.getColor(getContext(), R.attr.conversation_item_shadow);
this.mmsPendingOverlayColor = ResUtil.getColor(getContext(), R.attr.conversation_item_mms_pending_mask);

View File

@ -23,12 +23,11 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.animation.AlphaAnimation;
import com.makeramen.RoundedImageView;
@ -46,9 +45,9 @@ public class ForegroundImageView extends RoundedImageView {
private int mForegroundGravity = Gravity.FILL;
protected boolean mForegroundInPadding = true;
private boolean mForegroundInPadding = true;
boolean mForegroundBoundsChanged = false;
private boolean mForegroundBoundsChanged = false;
public ForegroundImageView(Context context) {
super(context);
@ -123,54 +122,15 @@ public class ForegroundImageView extends RoundedImageView {
return ActivityOptions.makeScaleUpAnimation(this, 0, 0, getWidth(), getHeight());
}
public void show(Drawable drawable, boolean instantaneous) {
setImageDrawable(drawable);
if (drawable.getIntrinsicHeight() < (getHeight() * 0.75f) &&
drawable.getIntrinsicWidth() < (getHeight() * 0.75f))
{
setScaleType(ScaleType.CENTER_INSIDE);
} else {
setScaleType(ScaleType.CENTER_CROP);
}
fadeIn(instantaneous ? 0 : 200);
}
public void reset() {
cancelAnimations();
setImageDrawable(null);
setVisibility(View.INVISIBLE);
setVisibility(View.VISIBLE);
}
public void hide() {
setVisibility(View.GONE);
}
private void fadeIn(final long millis) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) fadeInModern(millis);
else fadeInLegacy(millis);
setVisibility(View.VISIBLE);
}
private void fadeInLegacy(final long millis) {
final AlphaAnimation alpha = new AlphaAnimation(0f, 1f);
alpha.setDuration(millis);
alpha.setFillAfter(true);
startAnimation(alpha);
}
@TargetApi(VERSION_CODES.JELLY_BEAN)
private void fadeInModern(final long millis) {
setAlpha(0f);
animate().alpha(1f).setDuration(millis);
}
private void cancelAnimations() {
if (getAnimation() != null) {
getAnimation().cancel();
clearAnimation();
}
}
@Override
protected boolean verifyDrawable(Drawable who) {
return super.verifyDrawable(who) || (who == mForeground);
@ -249,7 +209,7 @@ public class ForegroundImageView extends RoundedImageView {
}
@Override
public void draw(Canvas canvas) {
public void draw(@NonNull Canvas canvas) {
super.draw(canvas);
if (mForeground != null) {
@ -278,5 +238,4 @@ public class ForegroundImageView extends RoundedImageView {
foreground.draw(canvas);
}
}
}

View File

@ -0,0 +1,202 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.bumptech.glide.GenericRequestBuilder;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.ThumbnailTransform;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import ws.com.google.android.mms.pdu.PduPart;
public class ThumbnailView extends ForegroundImageView {
private ListenableFutureTask<SlideDeck> slideDeckFuture = null;
private SlideDeckListener slideDeckListener = null;
private ThumbnailClickListener thumbnailClickListener = null;
private Handler handler = new Handler();
public ThumbnailView(Context context) {
super(context);
}
public ThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ThumbnailView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setImageResource(@NonNull ListenableFutureTask<SlideDeck> slideDeckFuture,
@Nullable MasterSecret masterSecret)
{
if (this.slideDeckFuture != null && this.slideDeckListener != null) {
this.slideDeckFuture.removeListener(this.slideDeckListener);
}
this.slideDeckListener = new SlideDeckListener(masterSecret);
this.slideDeckFuture = slideDeckFuture;
this.slideDeckFuture.addListener(this.slideDeckListener);
}
public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) {
buildGlideRequest(slide, masterSecret).into(ThumbnailView.this);
setOnClickListener(new ThumbnailClickDispatcher(thumbnailClickListener, slide));
}
public void setImageResource(@NonNull Slide slide)
{
setImageResource(slide, null);
}
public void setThumbnailClickListener(ThumbnailClickListener listener) {
this.thumbnailClickListener = listener;
}
private GenericRequestBuilder buildGlideRequest(@NonNull Slide slide,
@Nullable MasterSecret masterSecret)
{
final GenericRequestBuilder builder;
if (slide.getPart().isPendingPush()) {
builder = buildPendingGlideRequest(slide);
} else if (slide.getThumbnailUri() != null) {
builder = buildThumbnailGlideRequest(slide, masterSecret);
} else {
builder = buildPlaceholderGlideRequest(slide);
}
return builder.error(R.drawable.ic_missing_thumbnail_picture);
}
private GenericRequestBuilder buildPendingGlideRequest(Slide slide) {
return Glide.with(getContext()).load(R.drawable.stat_sys_download_anim0)
.dontTransform()
.skipMemoryCache(true)
.crossFade();
}
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
final GenericRequestBuilder builder;
if (slide.isDraft()) builder = buildDraftGlideRequest(slide);
else builder = buildEncryptedPartGlideRequest(slide, masterSecret);
return builder;
}
private GenericRequestBuilder buildDraftGlideRequest(Slide slide) {
return Glide.with(getContext()).load(slide.getThumbnailUri()).asBitmap()
.fitCenter()
.listener(new PduThumbnailSetListener(slide.getPart()));
}
private GenericRequestBuilder buildEncryptedPartGlideRequest(Slide slide, MasterSecret masterSecret) {
if (masterSecret == null) {
throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail");
}
return Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
.crossFade().transform(new ThumbnailTransform(getContext()));
}
private GenericRequestBuilder buildPlaceholderGlideRequest(Slide slide) {
return Glide.with(getContext()).load(slide.getPlaceholderRes(getContext().getTheme()))
.fitCenter()
.crossFade();
}
private class SlideDeckListener implements FutureTaskListener<SlideDeck> {
private final MasterSecret masterSecret;
public SlideDeckListener(MasterSecret masterSecret) {
this.masterSecret = masterSecret;
}
@Override
public void onSuccess(final SlideDeck slideDeck) {
if (slideDeck == null) return;
final Slide slide = slideDeck.getThumbnailSlide(getContext());
if (slide != null) {
handler.post(new Runnable() {
@Override
public void run() {
setImageResource(slide, masterSecret);
}
});
} else {
handler.post(new Runnable() {
@Override
public void run() {
hide();
}
});
}
}
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
handler.post(new Runnable() {
@Override
public void run() {
hide();
}
});
}
}
public interface ThumbnailClickListener {
void onClick(View v, Slide slide);
}
private class ThumbnailClickDispatcher implements View.OnClickListener {
private ThumbnailClickListener listener;
private Slide slide;
public ThumbnailClickDispatcher(ThumbnailClickListener listener, Slide slide) {
this.listener = listener;
this.slide = slide;
}
@Override
public void onClick(View view) {
listener.onClick(view, slide);
}
}
private static class PduThumbnailSetListener implements RequestListener<Uri, Bitmap> {
private PduPart part;
public PduThumbnailSetListener(@NonNull PduPart part) {
this.part = part;
}
@Override
public boolean onException(Exception e, Uri model, Target<Bitmap> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Bitmap resource, Uri model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
part.setThumbnail(resource);
return false;
}
}
}

View File

@ -20,21 +20,17 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import android.provider.ContactsContract;
import android.util.Pair;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import java.io.IOException;
@ -43,14 +39,14 @@ public class AttachmentManager {
private final Context context;
private final View attachmentView;
private final ImageView thumbnail;
private final ThumbnailView thumbnail;
private final Button removeButton;
private final SlideDeck slideDeck;
private final AttachmentListener attachmentListener;
public AttachmentManager(Activity view, AttachmentListener listener) {
this.attachmentView = (View)view.findViewById(R.id.attachment_editor);
this.thumbnail = (ImageView)view.findViewById(R.id.attachment_thumbnail);
this.thumbnail = (ThumbnailView)view.findViewById(R.id.attachment_thumbnail);
this.removeButton = (Button)view.findViewById(R.id.remove_image_button);
this.slideDeck = new SlideDeck();
this.context = view;
@ -66,7 +62,7 @@ public class AttachmentManager {
}
public void setImage(Uri image) throws IOException, BitmapDecodingException {
setMedia(new ImageSlide(context, image), 345, 261);
setMedia(new ImageSlide(context, image));
}
public void setVideo(Uri video) throws IOException, MediaTooLargeException {
@ -77,32 +73,11 @@ public class AttachmentManager {
setMedia(new AudioSlide(context, audio));
}
public void setMedia(final Slide slide, final int thumbnailWidth, final int thumbnailHeight) {
public void setMedia(final Slide slide) {
slideDeck.clear();
slideDeck.addSlide(slide);
slide.getThumbnail(context).addListener(new FutureTaskListener<Pair<Drawable, Boolean>>() {
@Override
public void onSuccess(final Pair<Drawable, Boolean> result) {
thumbnail.post(new Runnable() {
@Override
public void run() {
thumbnail.setImageDrawable(result.first);
attachmentView.setVisibility(View.VISIBLE);
attachmentListener.onAttachmentChanged();
}
});
}
@Override
public void onFailure(Throwable error) {
Log.w(TAG, error);
slideDeck.clear();
}
});
}
public void setMedia(Slide slide) {
setMedia(slide, thumbnail.getWidth(), thumbnail.getHeight());
attachmentView.setVisibility(View.VISIBLE);
thumbnail.setImageResource(slide);
}
public boolean isAttachmentPresent() {

View File

@ -16,20 +16,20 @@
*/
package org.thoughtcrime.securesms.mms;
import java.io.IOException;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore.Audio;
import android.support.annotation.DrawableRes;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.ResUtil;
import java.io.IOException;
import ws.com.google.android.mms.pdu.PduPart;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.MediaStore.Audio;
import android.util.Pair;
public class AudioSlide extends Slide {
@ -52,8 +52,8 @@ public class AudioSlide extends Slide {
}
@Override
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
return new ListenableFutureTask<>(new Pair<>(ResUtil.getDrawable(context, R.attr.conversation_icon_attach_audio), true));
public @DrawableRes int getPlaceholderRes(Theme theme) {
return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_audio);
}
public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {

View File

@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.mms;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import com.bumptech.glide.load.data.StreamLocalUriFetcher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class DecryptableStreamLocalUriFetcher extends StreamLocalUriFetcher {
private static final String TAG = DecryptableStreamLocalUriFetcher.class.getSimpleName();
private Context context;
private MasterSecret masterSecret;
public DecryptableStreamLocalUriFetcher(Context context, MasterSecret masterSecret, Uri uri) {
super(context, uri);
this.context = context;
this.masterSecret = masterSecret;
}
@Override
protected InputStream loadResource(Uri uri, ContentResolver contentResolver) throws FileNotFoundException {
try {
return PartAuthority.getPartStream(context, masterSecret, uri);
} catch (IOException ioe) {
Log.w(TAG, ioe);
throw new FileNotFoundException("PartAuthority couldn't load Uri resource.");
}
}
}

View File

@ -0,0 +1,60 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GenericLoaderFactory;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.stream.StreamModelLoader;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import java.io.InputStream;
/**
* A {@link ModelLoader} for translating uri models into {@link InputStream} data. Capable of handling 'http',
* 'https', 'android.resource', 'content', and 'file' schemes. Unsupported schemes will throw an exception in
* {@link #getResourceFetcher(Uri, int, int)}.
*/
public class DecryptableStreamUriLoader implements StreamModelLoader<DecryptableUri> {
private final Context context;
/**
* THe default factory for {@link com.bumptech.glide.load.model.stream.StreamUriLoader}s.
*/
public static class Factory implements ModelLoaderFactory<DecryptableUri, InputStream> {
@Override
public StreamModelLoader<DecryptableUri> build(Context context, GenericLoaderFactory factories) {
return new DecryptableStreamUriLoader(context);
}
@Override
public void teardown() {
// Do nothing.
}
}
public DecryptableStreamUriLoader(Context context) {
this.context = context;
}
@Override
public DataFetcher<InputStream> getResourceFetcher(DecryptableUri model, int width, int height) {
return new DecryptableStreamLocalUriFetcher(context, model.masterSecret, model.uri);
}
public static class DecryptableUri {
public MasterSecret masterSecret;
public Uri uri;
public DecryptableUri(MasterSecret masterSecret, Uri uri) {
this.masterSecret = masterSecret;
this.uri = uri;
}
}
}

View File

@ -17,27 +17,15 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.content.res.Resources.Theme;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
import android.support.annotation.DrawableRes;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.LRUCache;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.Callable;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart;
@ -45,10 +33,6 @@ import ws.com.google.android.mms.pdu.PduPart;
public class ImageSlide extends Slide {
private static final String TAG = ImageSlide.class.getSimpleName();
private static final int MAX_CACHE_SIZE = 10;
private static final Map<Uri, SoftReference<Drawable>> thumbnailCache =
Collections.synchronizedMap(new LRUCache<Uri, SoftReference<Drawable>>(MAX_CACHE_SIZE));
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
}
@ -58,68 +42,21 @@ public class ImageSlide extends Slide {
}
@Override
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
if (getPart().isPendingPush()) {
return new ListenableFutureTask<>(new Pair<>(context.getResources().getDrawable(R.drawable.stat_sys_download), true));
}
Drawable thumbnail = getCachedThumbnail();
if (thumbnail != null) {
Log.w(TAG, "getThumbnail() returning cached thumbnail");
return new ListenableFutureTask<>(new Pair<>(thumbnail, true));
}
Log.w(TAG, "getThumbnail() resolving thumbnail, as it wasn't cached");
return resolveThumbnail(context);
}
private ListenableFutureTask<Pair<Drawable,Boolean>> resolveThumbnail(Context context) {
final WeakReference<Context> weakContext = new WeakReference<>(context);
Callable<Pair<Drawable,Boolean>> slideCallable = new Callable<Pair<Drawable, Boolean>>() {
@Override
public Pair<Drawable, Boolean> call() throws Exception {
final Context context = weakContext.get();
if (context == null) {
Log.w(TAG, "context SoftReference was null, leaving");
return null;
}
try {
final long startDecode = System.currentTimeMillis();
final Bitmap thumbnailBitmap = MediaUtil.getOrGenerateThumbnail(context, masterSecret, part);
final Drawable thumbnail = new BitmapDrawable(context.getResources(), thumbnailBitmap);
Log.w(TAG, "thumbnail decode/generate time: " + (System.currentTimeMillis() - startDecode) + "ms");
thumbnailCache.put(part.getDataUri(), new SoftReference<>(thumbnail));
return new Pair<>(thumbnail, false);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return new Pair<>(context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture), false);
}
}
};
ListenableFutureTask<Pair<Drawable,Boolean>> futureTask = new ListenableFutureTask<>(slideCallable);
MmsDatabase.slideResolver.execute(futureTask);
return futureTask;
}
private Drawable getCachedThumbnail() {
synchronized (thumbnailCache) {
SoftReference<Drawable> bitmapReference = thumbnailCache.get(part.getDataUri());
Log.w("ImageSlide", "Got soft reference: " + bitmapReference);
if (bitmapReference != null) {
Drawable bitmap = bitmapReference.get();
Log.w("ImageSlide", "Got cached bitmap: " + bitmap);
if (bitmap != null) return bitmap;
else thumbnailCache.remove(part.getDataUri());
}
public Uri getThumbnailUri() {
if (!getPart().isPendingPush() && getPart().getDataUri() != null) {
return isDraft()
? getPart().getDataUri()
: PartAuthority.getThumbnailUri(getPart().getId());
}
return null;
}
@Override
public @DrawableRes int getPlaceholderRes(Theme theme) {
return R.drawable.ic_missing_thumbnail_picture;
}
@Override
public boolean hasImage() {
return true;
@ -137,4 +74,5 @@ public class ImageSlide extends Slide {
return part;
}
}

View File

@ -15,16 +15,20 @@ import java.io.InputStream;
public class PartAuthority {
private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part";
public static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
private static final String PART_URI_STRING = "content://org.thoughtcrime.securesms/part";
private static final String THUMB_URI_STRING = "content://org.thoughtcrime.securesms/thumb";
public static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
public static final Uri THUMB_CONTENT_URI = Uri.parse(THUMB_URI_STRING);
private static final int PART_ROW = 1;
private static final int THUMB_ROW = 2;
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("org.thoughtcrime.securesms", "part/#", PART_ROW);
uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/#", THUMB_ROW);
}
public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
@ -36,6 +40,7 @@ public class PartAuthority {
try {
switch (match) {
case PART_ROW: return partDatabase.getPartStream(masterSecret, ContentUris.parseId(uri));
case THUMB_ROW: return partDatabase.getThumbnailStream(masterSecret, ContentUris.parseId(uri));
default: return context.getContentResolver().openInputStream(uri);
}
} catch (SecurityException se) {
@ -43,19 +48,11 @@ public class PartAuthority {
}
}
public static InputStream getThumbnail(Context context, MasterSecret masterSecret, Uri uri)
throws IOException
{
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
int match = uriMatcher.match(uri);
switch (match) {
case PART_ROW: return partDatabase.getThumbnailStream(masterSecret, ContentUris.parseId(uri));
default: return null;
}
}
public static Uri getPublicPartUri(Uri uri) {
return ContentUris.withAppendedId(PartProvider.CONTENT_URI, ContentUris.parseId(uri));
}
public static Uri getThumbnailUri(long partId) {
return ContentUris.withAppendedId(THUMB_CONTENT_URI, partId);
}
}

View File

@ -16,20 +16,18 @@
*/
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.io.InputStream;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
import ws.com.google.android.mms.pdu.PduPart;
public abstract class Slide {
@ -68,10 +66,6 @@ public abstract class Slide {
return part.getDataUri();
}
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
throw new AssertionError("getThumbnail() called on non-thumbnail producing slide!");
}
public boolean hasImage() {
return false;
}
@ -84,22 +78,22 @@ public abstract class Slide {
return false;
}
public Bitmap getImage() {
throw new AssertionError("getImage() called on non-image slide!");
}
public boolean hasText() {
return false;
}
public String getText() {
throw new AssertionError("getText() called on non-text slide!");
}
public PduPart getPart() {
return part;
}
public Uri getThumbnailUri() {
return null;
}
public @DrawableRes int getPlaceholderRes(Theme theme) {
throw new AssertionError("getPlaceholderRes() called for non-drawable slide");
}
public boolean isDraft() {
return getPart().getId() < 0;
}
protected static void assertMediaSize(Context context, Uri uri)
throws MediaTooLargeException, IOException
{

View File

@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.engine.cache.DiskCache;
import com.bumptech.glide.load.engine.cache.DiskCacheAdapter;
import com.bumptech.glide.module.GlideModule;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import java.io.InputStream;
public class TextSecureGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDiskCache(new NoopDiskCacheFactory());
}
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory());
}
public static class NoopDiskCacheFactory implements DiskCache.Factory {
@Override
public DiskCache build() {
return new DiskCacheAdapter();
}
}
}

View File

@ -19,12 +19,7 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import android.widget.ImageView;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.LRUCache;
@ -51,35 +46,6 @@ public class TextSlide extends Slide {
super(context, getPartForMessage(message));
}
@Override
public boolean hasText() {
return true;
}
@Override
public String getText() {
try {
SoftReference<String> reference = textCache.get(part.getDataUri());
if (reference != null) {
String cachedText = reference.get();
if (cachedText != null) {
return cachedText;
}
}
String text = new String(getPartData(), CharacterSets.getMimeName(part.getCharset()));
textCache.put(part.getDataUri(), new SoftReference<String>(text));
return text;
} catch (UnsupportedEncodingException uee) {
Log.w("TextSlide", uee);
return new String(getPartData());
}
}
private static PduPart getPartForMessage(String message) {
PduPart part = new PduPart();

View File

@ -0,0 +1,48 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
import com.bumptech.glide.load.resource.bitmap.TransformationUtils;
public class ThumbnailTransform extends BitmapTransformation {
private static final String TAG = ThumbnailTransform.class.getSimpleName();
public ThumbnailTransform(Context context) {
super(context);
}
@SuppressWarnings("unused")
public ThumbnailTransform(BitmapPool bitmapPool) {
super(bitmapPool);
}
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
if (toTransform.getWidth() < (outWidth / 2) && toTransform.getHeight() < (outHeight / 2)) {
return toTransform;
}
final float inAspectRatio = (float) toTransform.getWidth() / toTransform.getHeight();
final float outAspectRatio = (float) outWidth / outHeight;
if (inAspectRatio < outAspectRatio) {
outWidth = (int)(outHeight * inAspectRatio);
}
final Bitmap toReuse = pool.get(outWidth, outHeight, toTransform.getConfig() != null
? toTransform.getConfig()
: Bitmap.Config.ARGB_8888);
Bitmap transformed = TransformationUtils.centerCrop(toReuse, toTransform, outWidth, outHeight);
if (toReuse != null && toReuse != transformed && !pool.put(toReuse)) {
toReuse.recycle();
}
return transformed;
}
@Override
public String getId() {
return ThumbnailTransform.class.getCanonicalName();
}
}

View File

@ -16,22 +16,22 @@
*/
package org.thoughtcrime.securesms.mms;
import java.io.IOException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.annotation.DrawableRes;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.ResUtil;
import java.io.IOException;
import ws.com.google.android.mms.pdu.PduPart;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import android.util.Pair;
public class VideoSlide extends Slide {
@ -44,8 +44,8 @@ public class VideoSlide extends Slide {
}
@Override
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
return new ListenableFutureTask<>(new Pair<>(ResUtil.getDrawable(context, R.attr.conversation_icon_attach_video), true));
public @DrawableRes int getPlaceholderRes(Theme theme) {
return ResUtil.getDrawableRes(theme, R.attr.conversation_icon_attach_video);
}
@Override

View File

@ -2,34 +2,23 @@ package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaTooLargeException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.Callable;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.MmsException;
import ws.com.google.android.mms.pdu.PduPart;
import ws.com.google.android.mms.pdu.SendReq;
public class MediaUtil {
private static final String TAG = MediaUtil.class.getSimpleName();
@ -51,22 +40,6 @@ public class MediaUtil {
return data;
}
public static Bitmap getOrGenerateThumbnail(Context context, MasterSecret masterSecret, PduPart part)
throws IOException, BitmapDecodingException
{
if (part.getDataUri() != null && part.getId() > -1) {
return BitmapFactory.decodeStream(DatabaseFactory.getPartDatabase(context)
.getThumbnailStream(masterSecret, part.getId()));
} else if (part.getDataUri() != null) {
Log.w(TAG, "generating thumbnail for new part");
Bitmap bitmap = MediaUtil.generateThumbnail(context, masterSecret, part.getDataUri(), Util.toIsoString(part.getContentType())).getBitmap();
part.setThumbnail(bitmap);
return bitmap;
} else {
throw new FileNotFoundException("no data location specified");
}
}
public static byte[] getPartData(Context context, MasterSecret masterSecret, PduPart part)
throws IOException
{

View File

@ -18,6 +18,7 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.support.annotation.AttrRes;
@ -33,8 +34,12 @@ public class ResUtil {
}
public static int getDrawableRes(Context c, @AttrRes int attr) {
return getDrawableRes(c.getTheme(), attr);
}
public static int getDrawableRes(Theme theme, @AttrRes int attr) {
final TypedValue out = new TypedValue();
c.getTheme().resolveAttribute(attr, out, true);
theme.resolveAttribute(attr, out, true);
return out.resourceId;
}