Load all contact pictures by Glide, and fix the wrong getPhotoUri method

This commit is contained in:
daquexian 2017-03-25 07:56:40 +08:00
parent 76a948e811
commit 2cd31048ac
3 changed files with 142 additions and 244 deletions

View file

@ -20,7 +20,6 @@ import android.widget.Filterable;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.bumptech.glide.Glide;
import com.fsck.k9.R; import com.fsck.k9.R;
import com.fsck.k9.helper.ContactPicture; import com.fsck.k9.helper.ContactPicture;
import com.fsck.k9.view.RecipientSelectView.Recipient; import com.fsck.k9.view.RecipientSelectView.Recipient;
@ -126,16 +125,7 @@ public class RecipientAdapter extends BaseAdapter implements Filterable {
} }
public static void setContactPhotoOrPlaceholder(Context context, ImageView imageView, Recipient recipient) { public static void setContactPhotoOrPlaceholder(Context context, ImageView imageView, Recipient recipient) {
// TODO don't use two different mechanisms for loading! ContactPicture.getContactPictureLoader(context).loadContactPicture(recipient, imageView);
if (recipient.photoThumbnailUri != null) {
Glide.with(context).load(recipient.photoThumbnailUri)
// for some reason, this fixes loading issues.
.placeholder(null)
.dontAnimate()
.into(imageView);
} else {
ContactPicture.getContactPictureLoader(context).loadContactPicture(recipient.address, imageView);
}
} }
@Override @Override

View file

@ -1,34 +1,43 @@
package com.fsck.k9.activity.misc; package com.fsck.k9.activity.misc;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.RejectedExecutionException;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.support.annotation.VisibleForTesting; import android.support.annotation.VisibleForTesting;
import android.support.v4.util.LruCache;
import android.text.TextUtils; import android.text.TextUtils;
import android.widget.ImageView; import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
import com.bumptech.glide.load.resource.bitmap.BitmapResource;
import com.bumptech.glide.load.resource.bitmap.StreamBitmapDecoder;
import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.bumptech.glide.load.resource.file.FileToStreamDecoder;
import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.fsck.k9.helper.Contacts; import com.fsck.k9.helper.Contacts;
import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Address;
import com.fsck.k9.view.RecipientSelectView.Recipient;
public class ContactPictureLoader { public class ContactPictureLoader {
/** /**
@ -47,18 +56,12 @@ public class ContactPictureLoader {
private static final String FALLBACK_CONTACT_LETTER = "?"; private static final String FALLBACK_CONTACT_LETTER = "?";
private ContentResolver mContentResolver;
private Resources mResources; private Resources mResources;
private Contacts mContactsHelper; private Contacts mContactsHelper;
private int mPictureSizeInPx; private int mPictureSizeInPx;
private int mDefaultBackgroundColor; private int mDefaultBackgroundColor;
/**
* LRU cache of contact pictures.
*/
private final LruCache<Address, Bitmap> mBitmapCache;
/** /**
* @see <a href="http://developer.android.com/design/style/color.html">Color palette used</a> * @see <a href="http://developer.android.com/design/style/color.html">Color palette used</a>
*/ */
@ -80,7 +83,6 @@ public class ContactPictureLoader {
String letter = null; String letter = null;
String personal = address.getPersonal(); String personal = address.getPersonal();
String str = (personal != null) ? personal : address.getAddress(); String str = (personal != null) ? personal : address.getAddress();
Matcher m = EXTRACT_LETTER_PATTERN.matcher(str); Matcher m = EXTRACT_LETTER_PATTERN.matcher(str);
if (m.find()) { if (m.find()) {
letter = m.group(0).toUpperCase(Locale.US); letter = m.group(0).toUpperCase(Locale.US);
@ -101,7 +103,6 @@ public class ContactPictureLoader {
*/ */
public ContactPictureLoader(Context context, int defaultBackgroundColor) { public ContactPictureLoader(Context context, int defaultBackgroundColor) {
Context appContext = context.getApplicationContext(); Context appContext = context.getApplicationContext();
mContentResolver = appContext.getContentResolver();
mResources = appContext.getResources(); mResources = appContext.getResources();
mContactsHelper = Contacts.getInstance(appContext); mContactsHelper = Contacts.getInstance(appContext);
@ -110,59 +111,64 @@ public class ContactPictureLoader {
mDefaultBackgroundColor = defaultBackgroundColor; mDefaultBackgroundColor = defaultBackgroundColor;
ActivityManager activityManager =
(ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE);
int memClass = activityManager.getMemoryClass();
// Use 1/16th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 16;
mBitmapCache = new LruCache<Address, Bitmap>(cacheSize) {
@Override
protected int sizeOf(Address key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items.
return bitmap.getByteCount();
}
};
} }
/** public void loadContactPicture(final Address address, final ImageView imageView) {
* Load a contact picture and display it using the supplied {@link ImageView} instance. Uri photoUri = mContactsHelper.getPhotoUri(address.getAddress());
* loadContactPicture(photoUri, address, imageView);
* <p> }
* If a picture is found in the cache, it is displayed in the {@code ContactBadge}
* immediately. Otherwise a {@link ContactPictureRetrievalTask} is started to try to load the public void loadContactPicture(Recipient recipient, ImageView imageView) {
* contact picture in a background thread. Depending on the result the contact picture or a loadContactPicture(recipient.photoThumbnailUri, recipient.address, imageView);
* fallback picture is then stored in the bitmap cache. }
* </p>
* private void loadFallbackPicture(Address address, ImageView imageView) {
* @param address Context context = imageView.getContext();
* The {@link Address} instance holding the email address that is used to search the
* contacts database. Glide.with(context)
* @param imageView .using(new FallbackGlideModelLoader(), FallbackGlideParams.class)
* The {@code ContactBadge} instance to receive the picture. .from(FallbackGlideParams.class)
* .as(Bitmap.class)
* @see #mBitmapCache .transcode(new BitmapToGlideDrawableTranscoder(context), GlideDrawable.class)
* @see #calculateFallbackBitmap(Address) .decoder(new FallbackGlideBitmapDecoder(context))
*/ .encoder(new BitmapEncoder(Bitmap.CompressFormat.PNG, 0))
public void loadContactPicture(Address address, ImageView imageView) { .cacheDecoder(new FileToStreamDecoder<>(new StreamBitmapDecoder(context)))
Bitmap bitmap = getBitmapFromCache(address); .diskCacheStrategy(DiskCacheStrategy.NONE)
if (bitmap != null) { .load(new FallbackGlideParams(address))
// The picture was found in the bitmap cache // for some reason, following 2 lines fix loading issues.
imageView.setImageBitmap(bitmap); .dontAnimate()
} else if (cancelPotentialWork(address, imageView)) { .override(mPictureSizeInPx, mPictureSizeInPx)
// Query the contacts database in a background thread and try to load the contact .into(imageView);
// picture, if there is one. }
ContactPictureRetrievalTask task = new ContactPictureRetrievalTask(imageView, address);
AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, private void loadContactPicture(Uri photoUri, final Address address, final ImageView imageView) {
calculateFallbackBitmap(address), task); if (photoUri != null) {
imageView.setImageDrawable(asyncDrawable); RequestListener<Uri, GlideDrawable> noPhotoListener = new RequestListener<Uri, GlideDrawable>() {
try { @Override
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); public boolean onException(Exception e, Uri model, Target<GlideDrawable> target,
} catch (RejectedExecutionException e) { boolean isFirstResource) {
// We flooded the thread pool queue... use a fallback picture loadFallbackPicture(address, imageView);
imageView.setImageBitmap(calculateFallbackBitmap(address)); return true;
} }
@Override
public boolean onResourceReady(GlideDrawable resource, Uri model,
Target<GlideDrawable> target,
boolean isFromMemoryCache, boolean isFirstResource) {
return false;
}
};
Glide.with(imageView.getContext())
.load(photoUri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.listener(noPhotoListener)
// for some reason, following 2 lines fix loading issues.
.dontAnimate()
.override(mPictureSizeInPx, mPictureSizeInPx)
.into(imageView);
} else {
loadFallbackPicture(address, imageView);
} }
} }
@ -176,23 +182,17 @@ public class ContactPictureLoader {
return CONTACT_DUMMY_COLORS_ARGB[colorIndex]; return CONTACT_DUMMY_COLORS_ARGB[colorIndex];
} }
/** private Bitmap drawTextAndBgColorOnBitmap(Bitmap bitmap, FallbackGlideParams params) {
* Calculates a bitmap with a color and a capital letter for contacts without picture. Canvas canvas = new Canvas(bitmap);
*/
private Bitmap calculateFallbackBitmap(Address address) {
Bitmap result = Bitmap.createBitmap(mPictureSizeInPx, mPictureSizeInPx,
Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result); int rgb = calcUnknownContactColor(params.address);
bitmap.eraseColor(rgb);
int rgb = calcUnknownContactColor(address); String letter = calcUnknownContactLetter(params.address);
result.eraseColor(rgb);
String letter = calcUnknownContactLetter(address);
Paint paint = new Paint(); Paint paint = new Paint();
paint.setAntiAlias(true); paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL); paint.setStyle(Style.FILL);
paint.setARGB(255, 255, 255, 255); paint.setARGB(255, 255, 255, 255);
paint.setTextSize(mPictureSizeInPx * 3 / 4); // just scale this down a bit paint.setTextSize(mPictureSizeInPx * 3 / 4); // just scale this down a bit
Rect rect = new Rect(); Rect rect = new Rect();
@ -202,146 +202,73 @@ public class ContactPictureLoader {
(mPictureSizeInPx / 2f) - (width / 2f), (mPictureSizeInPx / 2f) - (width / 2f),
(mPictureSizeInPx / 2f) + (rect.height() / 2f), paint); (mPictureSizeInPx / 2f) + (rect.height() / 2f), paint);
return result; return bitmap;
} }
private void addBitmapToCache(Address key, Bitmap bitmap) { private class FallbackGlideBitmapDecoder implements ResourceDecoder<FallbackGlideParams, Bitmap> {
if (getBitmapFromCache(key) == null) { private final Context context;
mBitmapCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromCache(Address key) { FallbackGlideBitmapDecoder(Context context) {
return mBitmapCache.get(key); this.context = context;
}
/**
* Checks if a {@code ContactPictureRetrievalTask} was already created to load the contact
* picture for the supplied {@code Address}.
*
* @param address
* The {@link Address} instance holding the email address that is used to search the
* contacts database.
* @param imageView
* The {@link ImageView} instance that will receive the picture.
*
* @return {@code true}, if the contact picture should be loaded in a background thread.
* {@code false}, if another {@link ContactPictureRetrievalTask} was already scheduled
* to load that contact picture.
*/
private boolean cancelPotentialWork(Address address, ImageView imageView) {
final ContactPictureRetrievalTask task = getContactPictureRetrievalTask(imageView);
if (task != null && address != null) {
if (!address.equals(task.getAddress())) {
// Cancel previous task
task.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ContactBadge, or an existing task was cancelled
return true;
}
private ContactPictureRetrievalTask getContactPictureRetrievalTask(ImageView imageView) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getContactPictureRetrievalTask();
}
}
return null;
}
/**
* Load a contact picture in a background thread.
*/
class ContactPictureRetrievalTask extends AsyncTask<Void, Void, Bitmap> {
private final WeakReference<ImageView> mImageViewReference;
private final Address mAddress;
ContactPictureRetrievalTask(ImageView imageView, Address address) {
mImageViewReference = new WeakReference<ImageView>(imageView);
mAddress = new Address(address);
}
public Address getAddress() {
return mAddress;
} }
@Override @Override
protected Bitmap doInBackground(Void... args) { public Resource<Bitmap> decode(FallbackGlideParams source, int width, int height) throws IOException {
final String email = mAddress.getAddress(); BitmapPool pool = Glide.get(context).getBitmapPool();
final Uri photoUri = mContactsHelper.getPhotoUri(email); Bitmap bitmap = pool.getDirty(mPictureSizeInPx, mPictureSizeInPx, Bitmap.Config.ARGB_8888);
Bitmap bitmap = null; if (bitmap == null) {
if (photoUri != null) { bitmap = Bitmap.createBitmap(mPictureSizeInPx, mPictureSizeInPx, Bitmap.Config.ARGB_8888);
try { }
InputStream stream = mContentResolver.openInputStream(photoUri); drawTextAndBgColorOnBitmap(bitmap, source);
if (stream != null) { return BitmapResource.obtain(bitmap, pool);
try { }
Bitmap tempBitmap = BitmapFactory.decodeStream(stream);
if (tempBitmap != null) { @Override
bitmap = Bitmap.createScaledBitmap(tempBitmap, mPictureSizeInPx, public String getId() {
mPictureSizeInPx, true); return "fallback-photo";
if (tempBitmap != bitmap) { }
tempBitmap.recycle(); }
}
} private class FallbackGlideParams {
} finally { final Address address;
try { stream.close(); } catch (IOException e) { /* ignore */ }
} FallbackGlideParams(Address address) {
} this.address = address;
} catch (FileNotFoundException e) { }
/* ignore */
public String getId() {
return String.format(Locale.ROOT, "%s-%s", address.getAddress(), address.getPersonal());
}
}
private class FallbackGlideModelLoader implements ModelLoader<FallbackGlideParams, FallbackGlideParams> {
@Override
public DataFetcher<FallbackGlideParams> getResourceFetcher(final FallbackGlideParams model, int width,
int height) {
return new DataFetcher<FallbackGlideParams>() {
@Override
public FallbackGlideParams loadData(Priority priority) throws Exception {
return model;
} }
} @Override
public void cleanup() {
if (bitmap == null) { }
bitmap = calculateFallbackBitmap(mAddress);
}
// Save the picture of the contact with that email address in the bitmap cache @Override
addBitmapToCache(mAddress, bitmap); public String getId() {
return model.getId();
}
return bitmap; @Override
} public void cancel() {
@Override }
protected void onPostExecute(Bitmap bitmap) { };
ImageView imageView = mImageViewReference.get();
if (imageView != null && getContactPictureRetrievalTask(imageView) == this) {
imageView.setImageBitmap(bitmap);
}
} }
} }
/**
* {@code Drawable} subclass that stores a reference to the {@link ContactPictureRetrievalTask}
* that is trying to load the contact picture.
*
* <p>
* The reference is used by {@link ContactPictureLoader#cancelPotentialWork(Address,
* ImageView)} to find out if the contact picture is already being loaded by a
* {@code ContactPictureRetrievalTask}.
* </p>
*/
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<ContactPictureRetrievalTask> mAsyncTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, ContactPictureRetrievalTask task) {
super(res, bitmap);
mAsyncTaskReference = new WeakReference<ContactPictureRetrievalTask>(task);
}
public ContactPictureRetrievalTask getContactPictureRetrievalTask() {
return mAsyncTaskReference.get();
}
}
} }

View file

@ -2,15 +2,14 @@ package com.fsck.k9.helper;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import timber.log.Timber; import timber.log.Timber;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Address; import com.fsck.k9.mail.Address;
/** /**
@ -229,7 +228,6 @@ public class Contacts {
* no such contact could be found or the contact doesn't have a picture. * no such contact could be found or the contact doesn't have a picture.
*/ */
public Uri getPhotoUri(String address) { public Uri getPhotoUri(String address) {
Long contactId;
try { try {
final Cursor c = getContactByAddress(address); final Cursor c = getContactByAddress(address);
if (c == null) { if (c == null) {
@ -240,30 +238,13 @@ public class Contacts {
if (!c.moveToFirst()) { if (!c.moveToFirst()) {
return null; return null;
} }
final String uriString = c.getString(c.getColumnIndex(Photo.PHOTO_URI));
contactId = c.getLong(CONTACT_ID_INDEX); return Uri.parse(uriString);
} catch (IllegalStateException e) {
return null;
} finally { } finally {
c.close(); c.close();
} }
Cursor cur = mContentResolver.query(
ContactsContract.Data.CONTENT_URI,
null,
ContactsContract.Data.CONTACT_ID + "=" + contactId + " AND "
+ ContactsContract.Data.MIMETYPE + "='"
+ ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE + "'", null,
null);
if (cur == null) {
return null;
}
if (!cur.moveToFirst()) {
cur.close();
return null; // no photo
}
// Ok, they have a photo
cur.close();
Uri person = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
return Uri.withAppendedPath(person, ContactsContract.Contacts.Photo.CONTENT_DIRECTORY);
} catch (Exception e) { } catch (Exception e) {
Timber.e(e, "Couldn't fetch photo for contact with email %s", address); Timber.e(e, "Couldn't fetch photo for contact with email %s", address);
return null; return null;