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

Unverified Commit 92e11f0f authored by cketti's avatar cketti Committed by GitHub
Browse files

Merge pull request #3509 from k9mail/refactor_contact_picture_loader

Refactor ContactPictureLoader
parents 7d11244d 4faf17fb
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -21,7 +21,7 @@ import android.support.annotation.Nullable;
import com.fsck.k9.activity.MessageCompose;
import com.fsck.k9.activity.compose.RecipientLoader;
import com.fsck.k9.activity.misc.ContactPicture;
import com.fsck.k9.activity.misc.ContactPictureLoader;
import com.fsck.k9.contacts.ContactPictureLoader;
import com.fsck.k9.mail.Address;
import com.fsck.k9.view.RecipientSelectView.Recipient;

@@ -39,7 +39,7 @@ public class K9ChooserTargetService extends ChooserTargetService {

        Context applicationContext = getApplicationContext();
        recipientLoader = RecipientLoader.getMostContactedRecipientLoader(applicationContext, MAX_TARGETS);
        contactPictureLoader = ContactPicture.getContactPictureLoader(applicationContext);
        contactPictureLoader = ContactPicture.getContactPictureLoader();
    }

    @Override
+3 −1
Original line number Diff line number Diff line
package com.fsck.k9

import com.fsck.k9.activity.activityModule
import com.fsck.k9.contacts.contactsModule
import com.fsck.k9.fragment.fragmentModule
import com.fsck.k9.ui.endtoend.endToEndUiModule
import com.fsck.k9.ui.settings.settingsUiModule
@@ -11,5 +12,6 @@ val uiModules = listOf(
        uiModule,
        settingsUiModule,
        endToEndUiModule,
        fragmentModule
        fragmentModule,
        contactsModule
)
+1 −1
Original line number Diff line number Diff line
@@ -153,7 +153,7 @@ public class RecipientAdapter extends BaseAdapter implements Filterable {
    }

    public static void setContactPhotoOrPlaceholder(Context context, ImageView imageView, Recipient recipient) {
        ContactPicture.getContactPictureLoader(context).loadContactPicture(recipient, imageView);
        ContactPicture.getContactPictureLoader().loadContactPicture(recipient, imageView);
    }

    @Override
+5 −16
Original line number Diff line number Diff line
package com.fsck.k9.activity.misc;

import android.content.Context;
import android.util.TypedValue;

import com.fsck.k9.K9;
import com.fsck.k9.ui.R;
import com.fsck.k9.DI;
import com.fsck.k9.contacts.ContactPictureLoader;

public class ContactPicture {

    public static ContactPictureLoader getContactPictureLoader(Context context) {
        final int defaultBgColor;
        if (!K9.isColorizeMissingContactPictures()) {
            TypedValue outValue = new TypedValue();
            context.getTheme().resolveAttribute(R.attr.contactPictureFallbackDefaultBackgroundColor,
                    outValue, true);
            defaultBgColor = outValue.data;
        } else {
            defaultBgColor = 0;
        }
public class ContactPicture {

        return new ContactPictureLoader(context, defaultBgColor);
    public static ContactPictureLoader getContactPictureLoader() {
        return DI.get(ContactPictureLoader.class);
    }
}
+0 −319
Original line number Diff line number Diff line
package com.fsck.k9.activity.misc;


import java.io.IOException;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.annotation.WorkerThread;
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.FutureTarget;
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 {
    /**
     * Resize the pictures to the following value (device-independent pixels).
     */
    private static final int PICTURE_SIZE = 40;

    /**
     * Pattern to extract the letter to be displayed as fallback image.
     */
    private static final Pattern EXTRACT_LETTER_PATTERN = Pattern.compile("\\p{L}\\p{M}*");

    /**
     * Letter to use when {@link #EXTRACT_LETTER_PATTERN} couldn't find a match.
     */
    private static final String FALLBACK_CONTACT_LETTER = "?";


    private final Context context;
    private Contacts mContactsHelper;
    private int mPictureSizeInPx;

    private int mDefaultBackgroundColor;

    /**
     * @see <a href="http://developer.android.com/design/style/color.html">Color palette used</a>
     */
    private final static int CONTACT_DUMMY_COLORS_ARGB[] = {
        0xff33B5E5,
        0xffAA66CC,
        0xff99CC00,
        0xffFFBB33,
        0xffFF4444,
        0xff0099CC,
        0xff9933CC,
        0xff669900,
        0xffFF8800,
        0xffCC0000
    };

    @VisibleForTesting
    protected static String calcUnknownContactLetter(Address address) {
        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);
        }

        return (TextUtils.isEmpty(letter)) ?
                FALLBACK_CONTACT_LETTER : letter;
    }

    /**
     * Constructor.
     *
     * @param context
     *         A {@link Context} instance.
     * @param defaultBackgroundColor
     *         The ARGB value to be used as background color for the fallback picture. {@code 0} to
     *         use a dynamically calculated background color.
     */
    public ContactPictureLoader(Context context, int defaultBackgroundColor) {
        this.context = context.getApplicationContext();
        mContactsHelper = Contacts.getInstance(this.context);

        Resources resources = context.getResources();
        float scale = resources.getDisplayMetrics().density;
        mPictureSizeInPx = (int) (PICTURE_SIZE * scale);

        mDefaultBackgroundColor = defaultBackgroundColor;

    }

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

    public Bitmap loadContactPictureIcon(Recipient recipient) {
        return loadContactPicture(recipient.photoThumbnailUri, recipient.address);
    }

    @WorkerThread
    private Bitmap loadContactPicture(Uri photoUri, Address address) {
        FutureTarget<Bitmap> bitmapTarget;
        if (photoUri != null) {
            bitmapTarget = Glide.with(context)
                    .load(photoUri)
                    .asBitmap()
                    .diskCacheStrategy(DiskCacheStrategy.NONE)
                    .dontAnimate()
                    .into(mPictureSizeInPx, mPictureSizeInPx);
        } else {
            bitmapTarget = Glide.with(context)
                    .using(new FallbackGlideModelLoader(), FallbackGlideParams.class)
                    .from(FallbackGlideParams.class)
                    .as(Bitmap.class)
                    .decoder(new FallbackGlideBitmapDecoder(context))
                    .encoder(new BitmapEncoder(CompressFormat.PNG, 0))
                    .cacheDecoder(new FileToStreamDecoder<>(new StreamBitmapDecoder(context)))
                    .diskCacheStrategy(DiskCacheStrategy.NONE)
                    .load(new FallbackGlideParams(address))
                    .dontAnimate()
                    .into(mPictureSizeInPx, mPictureSizeInPx);
        }

        return loadIgnoringErors(bitmapTarget);
    }

    private int calcUnknownContactColor(Address address) {
        if (mDefaultBackgroundColor != 0) {
            return mDefaultBackgroundColor;
        }

        int val = address.hashCode();
        int colorIndex = (val & Integer.MAX_VALUE) % CONTACT_DUMMY_COLORS_ARGB.length;
        return CONTACT_DUMMY_COLORS_ARGB[colorIndex];
    }

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

        int rgb = calcUnknownContactColor(params.address);
        bitmap.eraseColor(rgb);

        String letter = calcUnknownContactLetter(params.address);

        Paint paint = new Paint();
        paint.setAntiAlias(true);
        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();
        paint.getTextBounds(letter, 0, 1, rect);
        float width = paint.measureText(letter);
        canvas.drawText(letter,
                (mPictureSizeInPx / 2f) - (width / 2f),
                (mPictureSizeInPx / 2f) + (rect.height() / 2f), paint);

        return bitmap;
    }

    private class FallbackGlideBitmapDecoder implements ResourceDecoder<FallbackGlideParams, Bitmap> {
        private final Context context;

        FallbackGlideBitmapDecoder(Context context) {
            this.context = context;
        }

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

                }

                @Override
                public String getId() {
                    return model.getId();
                }

                @Override
                public void cancel() {

                }
            };
        }
    }

    @WorkerThread
    @Nullable
    private <T> T loadIgnoringErors(FutureTarget<T> target) {
        try {
            return target.get();
        } catch (Exception e) {
            return null;
        }
    }

}
Loading