From 2cd31048ac45f812873f9b8a305ae718d6f14b3f Mon Sep 17 00:00:00 2001 From: daquexian Date: Sat, 25 Mar 2017 07:56:40 +0800 Subject: [PATCH] Load all contact pictures by Glide, and fix the wrong getPhotoUri method --- .../k9/activity/compose/RecipientAdapter.java | 12 +- .../activity/misc/ContactPictureLoader.java | 345 +++++++----------- .../java/com/fsck/k9/helper/Contacts.java | 29 +- 3 files changed, 142 insertions(+), 244 deletions(-) diff --git a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientAdapter.java b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientAdapter.java index 4ba12e306..75f8f6de2 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientAdapter.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/compose/RecipientAdapter.java @@ -20,7 +20,6 @@ import android.widget.Filterable; import android.widget.ImageView; import android.widget.TextView; -import com.bumptech.glide.Glide; import com.fsck.k9.R; import com.fsck.k9.helper.ContactPicture; 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) { - // TODO don't use two different mechanisms for loading! - 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); - } + ContactPicture.getContactPictureLoader(context).loadContactPicture(recipient, imageView); } @Override diff --git a/k9mail/src/main/java/com/fsck/k9/activity/misc/ContactPictureLoader.java b/k9mail/src/main/java/com/fsck/k9/activity/misc/ContactPictureLoader.java index 46ca7f895..53494e78b 100644 --- a/k9mail/src/main/java/com/fsck/k9/activity/misc/ContactPictureLoader.java +++ b/k9mail/src/main/java/com/fsck/k9/activity/misc/ContactPictureLoader.java @@ -1,34 +1,43 @@ package com.fsck.k9.activity.misc; -import java.io.FileNotFoundException; + import java.io.IOException; -import java.io.InputStream; -import java.lang.ref.WeakReference; import java.util.Locale; -import java.util.concurrent.RejectedExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; -import android.app.ActivityManager; -import android.content.ContentResolver; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; +import android.graphics.Paint.Style; import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.AsyncTask; import android.support.annotation.VisibleForTesting; -import android.support.v4.util.LruCache; import android.text.TextUtils; 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.mail.Address; +import com.fsck.k9.view.RecipientSelectView.Recipient; + public class ContactPictureLoader { /** @@ -47,18 +56,12 @@ public class ContactPictureLoader { private static final String FALLBACK_CONTACT_LETTER = "?"; - private ContentResolver mContentResolver; private Resources mResources; private Contacts mContactsHelper; private int mPictureSizeInPx; private int mDefaultBackgroundColor; - /** - * LRU cache of contact pictures. - */ - private final LruCache mBitmapCache; - /** * @see Color palette used */ @@ -80,7 +83,6 @@ public class ContactPictureLoader { String letter = null; String personal = address.getPersonal(); String str = (personal != null) ? personal : address.getAddress(); - Matcher m = EXTRACT_LETTER_PATTERN.matcher(str); if (m.find()) { letter = m.group(0).toUpperCase(Locale.US); @@ -101,7 +103,6 @@ public class ContactPictureLoader { */ public ContactPictureLoader(Context context, int defaultBackgroundColor) { Context appContext = context.getApplicationContext(); - mContentResolver = appContext.getContentResolver(); mResources = appContext.getResources(); mContactsHelper = Contacts.getInstance(appContext); @@ -110,59 +111,64 @@ public class ContactPictureLoader { 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(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(); - } - }; } - /** - * Load a contact picture and display it using the supplied {@link ImageView} instance. - * - *

- * 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 - * contact picture in a background thread. Depending on the result the contact picture or a - * fallback picture is then stored in the bitmap cache. - *

- * - * @param address - * The {@link Address} instance holding the email address that is used to search the - * contacts database. - * @param imageView - * The {@code ContactBadge} instance to receive the picture. - * - * @see #mBitmapCache - * @see #calculateFallbackBitmap(Address) - */ - public void loadContactPicture(Address address, ImageView imageView) { - Bitmap bitmap = getBitmapFromCache(address); - if (bitmap != null) { - // The picture was found in the bitmap cache - imageView.setImageBitmap(bitmap); - } else if (cancelPotentialWork(address, imageView)) { - // Query the contacts database in a background thread and try to load the contact - // picture, if there is one. - ContactPictureRetrievalTask task = new ContactPictureRetrievalTask(imageView, address); - AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, - calculateFallbackBitmap(address), task); - imageView.setImageDrawable(asyncDrawable); - try { - task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } catch (RejectedExecutionException e) { - // We flooded the thread pool queue... use a fallback picture - imageView.setImageBitmap(calculateFallbackBitmap(address)); - } + public void loadContactPicture(final Address address, final ImageView imageView) { + Uri photoUri = mContactsHelper.getPhotoUri(address.getAddress()); + loadContactPicture(photoUri, address, imageView); + } + + public void loadContactPicture(Recipient recipient, ImageView imageView) { + loadContactPicture(recipient.photoThumbnailUri, recipient.address, imageView); + } + + private void loadFallbackPicture(Address address, ImageView imageView) { + Context context = imageView.getContext(); + + Glide.with(context) + .using(new FallbackGlideModelLoader(), FallbackGlideParams.class) + .from(FallbackGlideParams.class) + .as(Bitmap.class) + .transcode(new BitmapToGlideDrawableTranscoder(context), GlideDrawable.class) + .decoder(new FallbackGlideBitmapDecoder(context)) + .encoder(new BitmapEncoder(Bitmap.CompressFormat.PNG, 0)) + .cacheDecoder(new FileToStreamDecoder<>(new StreamBitmapDecoder(context))) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .load(new FallbackGlideParams(address)) + // for some reason, following 2 lines fix loading issues. + .dontAnimate() + .override(mPictureSizeInPx, mPictureSizeInPx) + .into(imageView); + } + + private void loadContactPicture(Uri photoUri, final Address address, final ImageView imageView) { + if (photoUri != null) { + RequestListener noPhotoListener = new RequestListener() { + @Override + public boolean onException(Exception e, Uri model, Target target, + boolean isFirstResource) { + loadFallbackPicture(address, imageView); + return true; + } + + @Override + public boolean onResourceReady(GlideDrawable resource, Uri model, + Target 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]; } - /** - * Calculates a bitmap with a color and a capital letter for contacts without picture. - */ - private Bitmap calculateFallbackBitmap(Address address) { - Bitmap result = Bitmap.createBitmap(mPictureSizeInPx, mPictureSizeInPx, - Bitmap.Config.ARGB_8888); + private Bitmap drawTextAndBgColorOnBitmap(Bitmap bitmap, FallbackGlideParams params) { + Canvas canvas = new Canvas(bitmap); - Canvas canvas = new Canvas(result); + int rgb = calcUnknownContactColor(params.address); + bitmap.eraseColor(rgb); - int rgb = calcUnknownContactColor(address); - result.eraseColor(rgb); - - String letter = calcUnknownContactLetter(address); + String letter = calcUnknownContactLetter(params.address); Paint paint = new Paint(); paint.setAntiAlias(true); - paint.setStyle(Paint.Style.FILL); + paint.setStyle(Style.FILL); paint.setARGB(255, 255, 255, 255); paint.setTextSize(mPictureSizeInPx * 3 / 4); // just scale this down a bit Rect rect = new Rect(); @@ -202,146 +202,73 @@ public class ContactPictureLoader { (mPictureSizeInPx / 2f) - (width / 2f), (mPictureSizeInPx / 2f) + (rect.height() / 2f), paint); - return result; + return bitmap; } - private void addBitmapToCache(Address key, Bitmap bitmap) { - if (getBitmapFromCache(key) == null) { - mBitmapCache.put(key, bitmap); - } - } + private class FallbackGlideBitmapDecoder implements ResourceDecoder { + private final Context context; - private Bitmap getBitmapFromCache(Address key) { - return mBitmapCache.get(key); - } - - /** - * 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 { - private final WeakReference mImageViewReference; - private final Address mAddress; - - ContactPictureRetrievalTask(ImageView imageView, Address address) { - mImageViewReference = new WeakReference(imageView); - mAddress = new Address(address); - } - - public Address getAddress() { - return mAddress; + FallbackGlideBitmapDecoder(Context context) { + this.context = context; } @Override - protected Bitmap doInBackground(Void... args) { - final String email = mAddress.getAddress(); - final Uri photoUri = mContactsHelper.getPhotoUri(email); - Bitmap bitmap = null; - if (photoUri != null) { - try { - InputStream stream = mContentResolver.openInputStream(photoUri); - if (stream != null) { - try { - Bitmap tempBitmap = BitmapFactory.decodeStream(stream); - if (tempBitmap != null) { - bitmap = Bitmap.createScaledBitmap(tempBitmap, mPictureSizeInPx, - mPictureSizeInPx, true); - if (tempBitmap != bitmap) { - tempBitmap.recycle(); - } - } - } finally { - try { stream.close(); } catch (IOException e) { /* ignore */ } - } - } - } catch (FileNotFoundException e) { - /* ignore */ + public Resource decode(FallbackGlideParams source, int width, int height) throws IOException { + BitmapPool pool = Glide.get(context).getBitmapPool(); + Bitmap bitmap = pool.getDirty(mPictureSizeInPx, mPictureSizeInPx, Bitmap.Config.ARGB_8888); + if (bitmap == null) { + bitmap = Bitmap.createBitmap(mPictureSizeInPx, mPictureSizeInPx, Bitmap.Config.ARGB_8888); + } + drawTextAndBgColorOnBitmap(bitmap, source); + return BitmapResource.obtain(bitmap, pool); + } + + @Override + public String getId() { + return "fallback-photo"; + } + } + + private class FallbackGlideParams { + final Address address; + + FallbackGlideParams(Address address) { + this.address = address; + } + + public String getId() { + return String.format(Locale.ROOT, "%s-%s", address.getAddress(), address.getPersonal()); + } + } + + private class FallbackGlideModelLoader implements ModelLoader { + @Override + public DataFetcher getResourceFetcher(final FallbackGlideParams model, int width, + int height) { + + return new DataFetcher() { + + @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 - addBitmapToCache(mAddress, bitmap); + @Override + 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. - * - *

- * 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}. - *

- */ - static class AsyncDrawable extends BitmapDrawable { - private final WeakReference mAsyncTaskReference; - - public AsyncDrawable(Resources res, Bitmap bitmap, ContactPictureRetrievalTask task) { - super(res, bitmap); - mAsyncTaskReference = new WeakReference(task); - } - - public ContactPictureRetrievalTask getContactPictureRetrievalTask() { - return mAsyncTaskReference.get(); - } - } } diff --git a/k9mail/src/main/java/com/fsck/k9/helper/Contacts.java b/k9mail/src/main/java/com/fsck/k9/helper/Contacts.java index 0759e9ec5..066c59c77 100644 --- a/k9mail/src/main/java/com/fsck/k9/helper/Contacts.java +++ b/k9mail/src/main/java/com/fsck/k9/helper/Contacts.java @@ -2,15 +2,14 @@ package com.fsck.k9.helper; import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import timber.log.Timber; +import android.provider.ContactsContract.CommonDataKinds.Photo; -import com.fsck.k9.K9; 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. */ public Uri getPhotoUri(String address) { - Long contactId; try { final Cursor c = getContactByAddress(address); if (c == null) { @@ -240,30 +238,13 @@ public class Contacts { if (!c.moveToFirst()) { return null; } - - contactId = c.getLong(CONTACT_ID_INDEX); + final String uriString = c.getString(c.getColumnIndex(Photo.PHOTO_URI)); + return Uri.parse(uriString); + } catch (IllegalStateException e) { + return null; } finally { 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) { Timber.e(e, "Couldn't fetch photo for contact with email %s", address); return null;