Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b5cf015e authored by Vincent Breitmoser's avatar Vincent Breitmoser Committed by GitHub
Browse files

Merge pull request #2315 from daquexian/issue_2057

Load all contact pictures by Glide
parents 6520f3ac 2cd31048
Loading
Loading
Loading
Loading
+1 −11
Original line number Diff line number Diff line
@@ -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
+124 −197
Original line number Diff line number Diff line
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<Address, Bitmap> mBitmapCache;

    /**
     * @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 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;
    public void loadContactPicture(final Address address, final ImageView imageView) {
        Uri photoUri = mContactsHelper.getPhotoUri(address.getAddress());
        loadContactPicture(photoUri, address, imageView);
    }

        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(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);
    }

    /**
     * Load a contact picture and display it using the supplied {@link ImageView} instance.
     *
     * <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
     * contact picture in a background thread. Depending on the result the contact picture or a
     * fallback picture is then stored in the bitmap cache.
     * </p>
     *
     * @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));
    private void loadContactPicture(Uri photoUri, final Address address, final ImageView imageView) {
        if (photoUri != null) {
            RequestListener<Uri, GlideDrawable> noPhotoListener = new RequestListener<Uri, GlideDrawable>() {
                @Override
                public boolean onException(Exception e, Uri model, Target<GlideDrawable> target,
                        boolean isFirstResource) {
                    loadFallbackPicture(address, imageView);
                    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];
    }

    /**
     * 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);

        Canvas canvas = new Canvas(result);
    private Bitmap drawTextAndBgColorOnBitmap(Bitmap bitmap, FallbackGlideParams params) {
        Canvas canvas = new Canvas(bitmap);

        int rgb = calcUnknownContactColor(address);
        result.eraseColor(rgb);
        int rgb = calcUnknownContactColor(params.address);
        bitmap.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<FallbackGlideParams, Bitmap> {
        private final Context context;

    private Bitmap getBitmapFromCache(Address key) {
        return mBitmapCache.get(key);
        FallbackGlideBitmapDecoder(Context context) {
            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;
            }
        @Override
        public Resource<Bitmap> 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);
            }

        // No task associated with the ContactBadge, or an existing task was cancelled
        return true;
            drawTextAndBgColorOnBitmap(bitmap, source);
            return BitmapResource.obtain(bitmap, pool);
        }

    private ContactPictureRetrievalTask getContactPictureRetrievalTask(ImageView imageView) {
        if (imageView != null) {
           Drawable drawable = imageView.getDrawable();
           if (drawable instanceof AsyncDrawable) {
               AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
               return asyncDrawable.getContactPictureRetrievalTask();
           }
        @Override
        public String getId() {
            return "fallback-photo";
        }

        return null;
    }

    private class FallbackGlideParams {
        final Address address;

    /**
     * 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);
        FallbackGlideParams(Address address) {
            this.address = address;
        }

        public Address getAddress() {
            return mAddress;
        public String getId() {
            return String.format(Locale.ROOT, "%s-%s", address.getAddress(), address.getPersonal());
        }
    }

    private class FallbackGlideModelLoader implements ModelLoader<FallbackGlideParams, FallbackGlideParams> {
        @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 DataFetcher<FallbackGlideParams> getResourceFetcher(final FallbackGlideParams model, int width,
                int height) {

            }
            return new DataFetcher<FallbackGlideParams>() {

            if (bitmap == null) {
                bitmap = calculateFallbackBitmap(mAddress);
                @Override
                public FallbackGlideParams loadData(Priority priority) throws Exception {
                    return model;
                }

            // Save the picture of the contact with that email address in the bitmap cache
            addBitmapToCache(mAddress, bitmap);
                @Override
                public void cleanup() {

            return bitmap;
                }

                @Override
        protected void onPostExecute(Bitmap bitmap) {
            ImageView imageView = mImageViewReference.get();
            if (imageView != null && getContactPictureRetrievalTask(imageView) == this) {
                imageView.setImageBitmap(bitmap);
            }
        }
                public String getId() {
                    return model.getId();
                }

    /**
     * {@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;
                @Override
                public void cancel() {

        public AsyncDrawable(Resources res, Bitmap bitmap, ContactPictureRetrievalTask task) {
            super(res, bitmap);
            mAsyncTaskReference = new WeakReference<ContactPictureRetrievalTask>(task);
                }

        public ContactPictureRetrievalTask getContactPictureRetrievalTask() {
            return mAsyncTaskReference.get();
            };
        }
    }

}
+5 −24
Original line number Diff line number Diff line
@@ -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;