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.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.net.Uri;
import android.support.annotation.VisibleForTesting;
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 {
/**
* 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 Resources mResources;
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) {
Context appContext = context.getApplicationContext();
mResources = appContext.getResources();
mContactsHelper = Contacts.getInstance(appContext);
float scale = mResources.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);
}
}
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() {
}
};
}
}
}