add borderless thumbnails, "bubble" refactor

Closes #2430

// FREEBIE
This commit is contained in:
Jake McGinty
2015-01-29 20:37:01 -10:00
committed by Moxie Marlinspike
parent a4e18c515c
commit 4185006147
75 changed files with 1384 additions and 1001 deletions

View File

@@ -26,14 +26,15 @@ import android.os.AsyncTask;
import android.os.Build;
import android.util.Log;
import android.provider.ContactsContract;
import android.util.Pair;
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.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.FutureTaskListener;
import java.io.IOException;
@@ -79,20 +80,25 @@ public class AttachmentManager {
public void setMedia(final Slide slide, final int thumbnailWidth, final int thumbnailHeight) {
slideDeck.clear();
slideDeck.addSlide(slide);
new AsyncTask<Void,Void,Drawable>() {
slide.getThumbnail(context).addListener(new FutureTaskListener<Pair<Drawable, Boolean>>() {
@Override
protected Drawable doInBackground(Void... params) {
return slide.getThumbnail(context, thumbnailWidth, thumbnailHeight);
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
protected void onPostExecute(Drawable drawable) {
thumbnail.setImageDrawable(drawable);
attachmentView.setVisibility(View.VISIBLE);
attachmentListener.onAttachmentChanged();
public void onFailure(Throwable error) {
Log.w(TAG, error);
slideDeck.clear();
}
}.execute();
});
}
public void setMedia(Slide slide) {

View File

@@ -20,20 +20,20 @@ import java.io.IOException;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
import org.w3c.dom.smil.SMILRegionMediaElement;
import ws.com.google.android.mms.pdu.PduPart;
import android.content.Context;
import android.content.res.TypedArray;
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 {
@@ -66,8 +66,8 @@ public class AudioSlide extends Slide {
}
@Override
public Drawable getThumbnail(Context context, int maxWidth, int maxHeight) {
return ThemeUtil.resolveIcon(context, R.attr.conversation_icon_attach_audio);
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
return new ListenableFutureTask<>(new Pair<>(ResUtil.getDrawable(context, R.attr.conversation_icon_attach_audio), true));
}
public static PduPart constructPartFromUri(Context context, Uri uri) throws IOException, MediaTooLargeException {

View File

@@ -18,38 +18,32 @@ package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.TransitionDrawable;
import android.net.Uri;
import android.os.Handler;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
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.util.MediaUtil.ThumbnailData;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.util.Util;
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 java.io.FileNotFoundException;
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;
@@ -70,106 +64,50 @@ public class ImageSlide extends Slide {
}
@Override
public Drawable getThumbnail(Context context, int maxWidth, int maxHeight) {
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) {
return thumbnail;
Log.w(TAG, "getThumbnail() returning cached thumbnail");
return new ListenableFutureTask<>(new Pair<>(thumbnail, true));
}
if (part.isPendingPush()) {
return context.getResources().getDrawable(R.drawable.stat_sys_download);
}
try {
Bitmap thumbnailBitmap;
long startDecode = System.currentTimeMillis();
if (part.getDataUri() != null && part.getId() > -1) {
thumbnailBitmap = BitmapFactory.decodeStream(DatabaseFactory.getPartDatabase(context)
.getThumbnailStream(masterSecret, part.getId()));
} else if (part.getDataUri() != null) {
Log.w(TAG, "generating thumbnail for new part");
ThumbnailData thumbnailData = MediaUtil.generateThumbnail(context, masterSecret,
part.getDataUri(), Util.toIsoString(part.getContentType()));
thumbnailBitmap = thumbnailData.getBitmap();
part.setThumbnail(thumbnailBitmap);
} else {
throw new FileNotFoundException("no data location specified");
}
Log.w(TAG, "thumbnail decode/generate time: " + (System.currentTimeMillis() - startDecode) + "ms");
thumbnail = new BitmapDrawable(context.getResources(), thumbnailBitmap);
thumbnailCache.put(part.getDataUri(), new SoftReference<>(thumbnail));
return thumbnail;
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return context.getResources().getDrawable(R.drawable.ic_missing_thumbnail_picture);
}
Log.w(TAG, "getThumbnail() resolving thumbnail, as it wasn't cached");
return resolveThumbnail(context);
}
@Override
public void setThumbnailOn(Context context, ImageView imageView) {
setThumbnailOn(context, imageView, imageView.getWidth(), imageView.getHeight(), new ColorDrawable(Color.TRANSPARENT));
}
private ListenableFutureTask<Pair<Drawable,Boolean>> resolveThumbnail(Context context) {
final WeakReference<Context> weakContext = new WeakReference<>(context);
@Override
public void setThumbnailOn(Context context, ImageView imageView, final int width, final int height, final Drawable placeholder) {
Drawable thumbnail = getCachedThumbnail();
if (thumbnail != null) {
Log.w("ImageSlide", "Setting cached thumbnail...");
setThumbnailOn(imageView, thumbnail, true);
return;
}
final WeakReference<Context> weakContext = new WeakReference<>(context);
final WeakReference<ImageView> weakImageView = new WeakReference<>(imageView);
final Handler handler = new Handler();
imageView.setImageDrawable(placeholder);
if (width == 0 || height == 0)
return;
MmsDatabase.slideResolver.execute(new Runnable() {
Callable<Pair<Drawable,Boolean>> slideCallable = new Callable<Pair<Drawable, Boolean>>() {
@Override
public void run() {
public Pair<Drawable, Boolean> call() throws Exception {
final Context context = weakContext.get();
if (context == null) {
Log.w(TAG, "context SoftReference was null, leaving");
return;
return null;
}
final Drawable bitmap = getThumbnail(context, width, height);
final ImageView destination = weakImageView.get();
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");
Log.w(TAG, "slide resolved, destination available? " + (destination == null));
if (destination != null && destination.getDrawable() == placeholder) {
handler.post(new Runnable() {
@Override
public void run() {
setThumbnailOn(destination, bitmap, false);
}
});
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);
}
}
});
}
private void setThumbnailOn(ImageView imageView, Drawable thumbnail, boolean fromMemory) {
if (fromMemory) {
imageView.setImageDrawable(thumbnail);
} else if (thumbnail instanceof AnimationDrawable) {
imageView.setImageDrawable(thumbnail);
((AnimationDrawable)imageView.getDrawable()).start();
} else {
TransitionDrawable fadingResult = new TransitionDrawable(new Drawable[]{imageView.getDrawable(), thumbnail});
imageView.setImageDrawable(fadingResult);
fadingResult.startTransition(300);
}
};
ListenableFutureTask<Pair<Drawable,Boolean>> futureTask = new ListenableFutureTask<>(slideCallable);
MmsDatabase.slideResolver.execute(futureTask);
return futureTask;
}
private Drawable getCachedThumbnail() {

View File

@@ -43,6 +43,18 @@ 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));
}

View File

@@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.mms;
import java.io.IOException;
import java.io.InputStream;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Util;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
@@ -30,8 +31,7 @@ import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.Log;
import android.util.TypedValue;
import android.widget.ImageView;
import android.util.Pair;
import ws.com.google.android.mms.pdu.PduPart;
@@ -71,20 +71,10 @@ public abstract class Slide {
return part.getDataUri();
}
public Drawable getThumbnail(Context context, int maxWidth, int maxHeight) {
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
throw new AssertionError("getThumbnail() called on non-thumbnail producing slide!");
}
public void setThumbnailOn(Context context, ImageView imageView) {
imageView.setImageDrawable(getThumbnail(context, imageView.getWidth(), imageView.getHeight()));
}
public void setThumbnailOn(Context context, ImageView imageView, int height, int width, Drawable placeholder) {
imageView.setImageDrawable(getThumbnail(context, width, height));
}
public Bitmap getGeneratedThumbnail() { return null; }
public boolean hasImage() {
return false;
}

View File

@@ -17,9 +17,13 @@
package org.thoughtcrime.securesms.mms;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Pair;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.dom.smil.parser.SmilXmlSerializer;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -90,8 +94,15 @@ public class SlideDeck {
return true;
}
}
return false;
}
public Slide getThumbnailSlide(Context context) {
for (Slide slide : slides) {
if (slide.hasImage()) {
return slide;
}
}
return null;
}
}

View File

@@ -20,8 +20,9 @@ import java.io.IOException;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.ResUtil;
import org.thoughtcrime.securesms.util.SmilUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.w3c.dom.smil.SMILDocument;
import org.w3c.dom.smil.SMILMediaElement;
import org.w3c.dom.smil.SMILRegionElement;
@@ -29,12 +30,12 @@ import org.w3c.dom.smil.SMILRegionElement;
import ws.com.google.android.mms.pdu.PduPart;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.TypedArray;
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 {
@@ -47,8 +48,8 @@ public class VideoSlide extends Slide {
}
@Override
public Drawable getThumbnail(Context context, int width, int height) {
return ThemeUtil.resolveIcon(context, R.attr.conversation_icon_attach_video);
public ListenableFutureTask<Pair<Drawable,Boolean>> getThumbnail(Context context) {
return new ListenableFutureTask<>(new Pair<>(ResUtil.getDrawable(context, R.attr.conversation_icon_attach_video), true));
}
@Override