Use Glide for loading part thumbnails

Closes #2885

// FREEBIE
This commit is contained in:
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

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