2013-02-07 14:01:49 -05:00
|
|
|
package com.fsck.k9.activity.misc;
|
|
|
|
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.io.InputStream;
|
|
|
|
import java.lang.ref.WeakReference;
|
2013-08-21 07:05:42 -04:00
|
|
|
import java.util.Locale;
|
2013-02-07 14:01:49 -05:00
|
|
|
import java.util.concurrent.RejectedExecutionException;
|
2013-07-21 10:58:38 -04:00
|
|
|
import java.util.regex.Matcher;
|
|
|
|
import java.util.regex.Pattern;
|
2013-02-07 14:01:49 -05:00
|
|
|
|
2013-09-25 08:54:27 -04:00
|
|
|
import android.annotation.TargetApi;
|
2013-02-07 14:01:49 -05:00
|
|
|
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;
|
2013-07-21 10:58:38 -04:00
|
|
|
import android.graphics.Canvas;
|
|
|
|
import android.graphics.Paint;
|
|
|
|
import android.graphics.Rect;
|
2013-02-07 14:01:49 -05:00
|
|
|
import android.graphics.drawable.BitmapDrawable;
|
|
|
|
import android.graphics.drawable.Drawable;
|
|
|
|
import android.net.Uri;
|
|
|
|
import android.os.AsyncTask;
|
|
|
|
import android.os.Build;
|
|
|
|
import android.support.v4.util.LruCache;
|
|
|
|
import android.widget.QuickContactBadge;
|
|
|
|
import com.fsck.k9.helper.Contacts;
|
2013-07-21 10:58:38 -04:00
|
|
|
import com.fsck.k9.mail.Address;
|
2013-02-07 14:01:49 -05:00
|
|
|
|
|
|
|
public class ContactPictureLoader {
|
2013-02-08 12:04:38 -05:00
|
|
|
/**
|
|
|
|
* Resize the pictures to the following value (device-independent pixels).
|
|
|
|
*/
|
2013-02-13 13:43:07 -05:00
|
|
|
private static final int PICTURE_SIZE = 40;
|
2013-02-08 12:04:38 -05:00
|
|
|
|
2013-08-21 07:05:42 -04:00
|
|
|
/**
|
|
|
|
* Pattern to extract the letter to be displayed as fallback image.
|
|
|
|
*/
|
|
|
|
private static final Pattern EXTRACT_LETTER_PATTERN = Pattern.compile("[a-zA-Z]");
|
|
|
|
|
|
|
|
|
2013-02-07 14:01:49 -05:00
|
|
|
private ContentResolver mContentResolver;
|
|
|
|
private Resources mResources;
|
|
|
|
private Contacts mContactsHelper;
|
2013-02-08 12:04:38 -05:00
|
|
|
private int mPictureSizeInPx;
|
2013-02-07 14:01:49 -05:00
|
|
|
|
2013-08-16 12:42:13 -04:00
|
|
|
private int mDefaultBackgroundColor;
|
|
|
|
|
2013-02-07 14:01:49 -05:00
|
|
|
/**
|
|
|
|
* LRU cache of contact pictures.
|
|
|
|
*/
|
2013-09-27 02:53:14 -04:00
|
|
|
private final LruCache<Address, Bitmap> mBitmapCache;
|
2013-02-07 14:01:49 -05:00
|
|
|
|
|
|
|
/**
|
2013-07-23 18:49:58 -04:00
|
|
|
* @see <a href="http://developer.android.com/design/style/color.html">Color palette used</a>
|
2013-02-07 14:01:49 -05:00
|
|
|
*/
|
2013-07-23 15:46:03 -04:00
|
|
|
private final static int CONTACT_DUMMY_COLORS_ARGB[] = {
|
2013-07-23 18:49:58 -04:00
|
|
|
0xff33B5E5,
|
|
|
|
0xffAA66CC,
|
|
|
|
0xff99CC00,
|
|
|
|
0xffFFBB33,
|
|
|
|
0xffFF4444,
|
|
|
|
0xff0099CC,
|
|
|
|
0xff9933CC,
|
|
|
|
0xff669900,
|
|
|
|
0xffFF8800,
|
|
|
|
0xffCC0000
|
2013-07-23 15:46:03 -04:00
|
|
|
};
|
2013-02-07 14:01:49 -05:00
|
|
|
|
2013-08-16 12:42:13 -04:00
|
|
|
/**
|
|
|
|
* 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) {
|
2013-02-07 14:01:49 -05:00
|
|
|
Context appContext = context.getApplicationContext();
|
|
|
|
mContentResolver = appContext.getContentResolver();
|
|
|
|
mResources = appContext.getResources();
|
|
|
|
mContactsHelper = Contacts.getInstance(appContext);
|
|
|
|
|
2013-02-08 12:04:38 -05:00
|
|
|
float scale = mResources.getDisplayMetrics().density;
|
|
|
|
mPictureSizeInPx = (int) (PICTURE_SIZE * scale);
|
|
|
|
|
2013-08-16 12:42:13 -04:00
|
|
|
mDefaultBackgroundColor = defaultBackgroundColor;
|
|
|
|
|
2013-02-07 14:01:49 -05:00
|
|
|
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;
|
|
|
|
|
2013-09-27 02:53:14 -04:00
|
|
|
mBitmapCache = new LruCache<Address, Bitmap>(cacheSize) {
|
2013-09-25 08:54:27 -04:00
|
|
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
|
2013-02-07 14:01:49 -05:00
|
|
|
@Override
|
2013-09-27 02:53:14 -04:00
|
|
|
protected int sizeOf(Address key, Bitmap bitmap) {
|
2013-02-07 14:01:49 -05:00
|
|
|
// The cache size will be measured in bytes rather than number of items.
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
|
|
|
|
return bitmap.getByteCount();
|
|
|
|
}
|
|
|
|
|
|
|
|
return bitmap.getRowBytes() * bitmap.getHeight();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load a contact picture and display it using the supplied {@link QuickContactBadge} instance.
|
|
|
|
*
|
|
|
|
* <p>
|
2013-08-15 19:45:23 -04:00
|
|
|
* If a picture is found in the cache, it is displayed in the {@code QuickContactBadge}
|
|
|
|
* 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.
|
2013-02-07 14:01:49 -05:00
|
|
|
* </p>
|
|
|
|
*
|
2013-08-15 19:45:23 -04:00
|
|
|
* @param address
|
|
|
|
* The {@link Address} instance holding the email address that is used to search the
|
|
|
|
* contacts database.
|
2013-02-07 14:01:49 -05:00
|
|
|
* @param badge
|
|
|
|
* The {@code QuickContactBadge} instance to receive the picture.
|
|
|
|
*
|
|
|
|
* @see #mBitmapCache
|
2013-08-15 19:45:23 -04:00
|
|
|
* @see #calculateFallbackBitmap(Address)
|
2013-02-07 14:01:49 -05:00
|
|
|
*/
|
2013-07-21 10:58:38 -04:00
|
|
|
public void loadContactPicture(Address address, QuickContactBadge badge) {
|
2013-09-27 02:53:14 -04:00
|
|
|
Bitmap bitmap = getBitmapFromCache(address);
|
2013-02-07 14:01:49 -05:00
|
|
|
if (bitmap != null) {
|
|
|
|
// The picture was found in the bitmap cache
|
|
|
|
badge.setImageBitmap(bitmap);
|
2013-09-27 02:53:14 -04:00
|
|
|
} else if (cancelPotentialWork(address, badge)) {
|
2013-02-07 14:01:49 -05:00
|
|
|
// Query the contacts database in a background thread and try to load the contact
|
|
|
|
// picture, if there is one.
|
2013-08-21 06:33:27 -04:00
|
|
|
ContactPictureRetrievalTask task = new ContactPictureRetrievalTask(badge, address);
|
2013-08-15 19:45:23 -04:00
|
|
|
AsyncDrawable asyncDrawable = new AsyncDrawable(mResources,
|
|
|
|
calculateFallbackBitmap(address), task);
|
2013-02-07 14:01:49 -05:00
|
|
|
badge.setImageDrawable(asyncDrawable);
|
|
|
|
try {
|
2013-08-21 06:33:27 -04:00
|
|
|
task.exec();
|
2013-02-07 14:01:49 -05:00
|
|
|
} catch (RejectedExecutionException e) {
|
2013-08-15 19:45:23 -04:00
|
|
|
// We flooded the thread pool queue... use a fallback picture
|
2013-07-21 10:58:38 -04:00
|
|
|
badge.setImageBitmap(calculateFallbackBitmap(address));
|
2013-02-07 14:01:49 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-07-21 10:58:38 -04:00
|
|
|
private int calcUnknownContactColor(Address address) {
|
2013-08-16 12:42:13 -04:00
|
|
|
if (mDefaultBackgroundColor != 0) {
|
|
|
|
return mDefaultBackgroundColor;
|
|
|
|
}
|
|
|
|
|
2013-09-25 09:20:43 -04:00
|
|
|
int val = address.getAddress().toLowerCase(Locale.getDefault()).hashCode();
|
2013-07-23 17:27:44 -04:00
|
|
|
int rgb = CONTACT_DUMMY_COLORS_ARGB[Math.abs(val) % CONTACT_DUMMY_COLORS_ARGB.length];
|
2013-08-21 07:23:52 -04:00
|
|
|
return rgb;
|
2013-07-21 10:58:38 -04:00
|
|
|
}
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-07-21 10:58:38 -04:00
|
|
|
private char calcUnknownContactLetter(Address address) {
|
2013-08-21 07:23:52 -04:00
|
|
|
String letter = "";
|
|
|
|
String str = address.getPersonal() != null ? address.getPersonal() : address.getAddress();
|
2013-08-21 07:05:42 -04:00
|
|
|
|
|
|
|
Matcher m = EXTRACT_LETTER_PATTERN.matcher(str);
|
|
|
|
if (m.find()) {
|
|
|
|
letter = m.group(0).toUpperCase(Locale.US);
|
|
|
|
}
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-07-23 19:07:12 -04:00
|
|
|
return letter.length() == 0 ? '?' : letter.charAt(0);
|
2013-07-21 10:58:38 -04:00
|
|
|
}
|
2013-08-15 19:45:23 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculates a bitmap with a color and a capital letter for contacts without picture.
|
|
|
|
*/
|
2013-07-21 10:58:38 -04:00
|
|
|
private Bitmap calculateFallbackBitmap(Address address) {
|
2013-08-21 07:23:52 -04:00
|
|
|
Bitmap result = Bitmap.createBitmap(mPictureSizeInPx, mPictureSizeInPx,
|
2013-08-15 19:45:23 -04:00
|
|
|
Bitmap.Config.ARGB_8888);
|
|
|
|
|
2013-08-21 07:23:52 -04:00
|
|
|
Canvas canvas = new Canvas(result);
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-08-21 07:23:52 -04:00
|
|
|
int rgb = calcUnknownContactColor(address);
|
|
|
|
result.eraseColor(rgb);
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-08-21 07:23:52 -04:00
|
|
|
String letter = Character.toString(calcUnknownContactLetter(address));
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-08-21 07:23:52 -04:00
|
|
|
Paint paint = new Paint();
|
|
|
|
paint.setAntiAlias(true);
|
|
|
|
paint.setStyle(Paint.Style.FILL);
|
|
|
|
paint.setARGB(255, 255, 255, 255);
|
2013-07-23 18:32:27 -04:00
|
|
|
paint.setTextSize(mPictureSizeInPx * 3 / 4); // just scale this down a bit
|
2013-08-21 07:23:52 -04:00
|
|
|
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 result;
|
2013-07-21 10:58:38 -04:00
|
|
|
}
|
|
|
|
|
2013-09-27 02:53:14 -04:00
|
|
|
private void addBitmapToCache(Address key, Bitmap bitmap) {
|
2013-02-07 14:01:49 -05:00
|
|
|
if (getBitmapFromCache(key) == null) {
|
|
|
|
mBitmapCache.put(key, bitmap);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-09-27 02:53:14 -04:00
|
|
|
private Bitmap getBitmapFromCache(Address key) {
|
2013-02-07 14:01:49 -05:00
|
|
|
return mBitmapCache.get(key);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if a {@code ContactPictureRetrievalTask} was already created to load the contact
|
2013-09-30 19:53:21 -04:00
|
|
|
* picture for the supplied {@code Address}.
|
2013-02-07 14:01:49 -05:00
|
|
|
*
|
2013-09-27 02:53:14 -04:00
|
|
|
* @param address
|
2013-09-30 19:53:21 -04:00
|
|
|
* The {@link Address} instance holding the email address that is used to search the
|
|
|
|
* contacts database.
|
2013-02-07 14:01:49 -05:00
|
|
|
* @param badge
|
|
|
|
* The {@code QuickContactBadge} 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.
|
|
|
|
*/
|
2013-09-27 02:53:14 -04:00
|
|
|
private boolean cancelPotentialWork(Address address, QuickContactBadge badge) {
|
2013-02-07 14:01:49 -05:00
|
|
|
final ContactPictureRetrievalTask task = getContactPictureRetrievalTask(badge);
|
|
|
|
|
2013-09-27 02:53:14 -04:00
|
|
|
if (task != null && address != null) {
|
|
|
|
if (!address.equals(task.getAddress())) {
|
2013-02-07 14:01:49 -05:00
|
|
|
// Cancel previous task
|
|
|
|
task.cancel(true);
|
|
|
|
} else {
|
|
|
|
// The same work is already in progress
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// No task associated with the QuickContactBadge, or an existing task was cancelled
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private ContactPictureRetrievalTask getContactPictureRetrievalTask(QuickContactBadge badge) {
|
|
|
|
if (badge != null) {
|
|
|
|
Drawable drawable = badge.getDrawable();
|
|
|
|
if (drawable instanceof AsyncDrawable) {
|
|
|
|
AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
|
|
|
return asyncDrawable.getContactPictureRetrievalTask();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load a contact picture in a background thread.
|
|
|
|
*/
|
2013-08-21 06:33:27 -04:00
|
|
|
class ContactPictureRetrievalTask extends AsyncTask<Void, Void, Bitmap> {
|
2013-02-07 14:01:49 -05:00
|
|
|
private final WeakReference<QuickContactBadge> mQuickContactBadgeReference;
|
2013-07-21 10:58:38 -04:00
|
|
|
private Address mAddress;
|
2013-02-07 14:01:49 -05:00
|
|
|
|
2013-08-21 06:33:27 -04:00
|
|
|
ContactPictureRetrievalTask(QuickContactBadge badge, Address address) {
|
2013-02-07 14:01:49 -05:00
|
|
|
mQuickContactBadgeReference = new WeakReference<QuickContactBadge>(badge);
|
2013-08-21 06:33:27 -04:00
|
|
|
mAddress = new Address(address);
|
2013-02-07 14:01:49 -05:00
|
|
|
}
|
|
|
|
|
2013-09-25 08:54:27 -04:00
|
|
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
2013-08-21 06:33:27 -04:00
|
|
|
public void exec(Void... args) {
|
2013-02-07 14:01:49 -05:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
|
|
|
|
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, args);
|
|
|
|
} else {
|
|
|
|
execute(args);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-07-21 10:58:38 -04:00
|
|
|
public Address getAddress() {
|
|
|
|
return mAddress;
|
2013-02-07 14:01:49 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
2013-08-21 06:33:27 -04:00
|
|
|
protected Bitmap doInBackground(Void... args) {
|
|
|
|
final String email = mAddress.getAddress();
|
2013-02-07 14:01:49 -05:00
|
|
|
final Uri x = mContactsHelper.getPhotoUri(email);
|
|
|
|
Bitmap bitmap = null;
|
|
|
|
if (x != null) {
|
|
|
|
try {
|
|
|
|
InputStream stream = mContentResolver.openInputStream(x);
|
|
|
|
if (stream != null) {
|
|
|
|
try {
|
2013-02-08 12:04:38 -05:00
|
|
|
Bitmap tempBitmap = BitmapFactory.decodeStream(stream);
|
2013-03-18 21:59:05 -04:00
|
|
|
if (tempBitmap != null) {
|
|
|
|
bitmap = Bitmap.createScaledBitmap(tempBitmap, mPictureSizeInPx,
|
|
|
|
mPictureSizeInPx, true);
|
|
|
|
if (tempBitmap != bitmap) {
|
|
|
|
tempBitmap.recycle();
|
|
|
|
}
|
2013-02-08 12:04:38 -05:00
|
|
|
}
|
2013-02-07 14:01:49 -05:00
|
|
|
} finally {
|
|
|
|
try { stream.close(); } catch (IOException e) { /* ignore */ }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (FileNotFoundException e) {
|
|
|
|
/* ignore */
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (bitmap == null) {
|
2013-08-21 07:23:52 -04:00
|
|
|
bitmap = calculateFallbackBitmap(mAddress);
|
2013-02-07 14:01:49 -05:00
|
|
|
}
|
|
|
|
|
2013-08-15 19:45:23 -04:00
|
|
|
// Save the picture of the contact with that email address in the bitmap cache
|
2013-09-27 02:53:14 -04:00
|
|
|
addBitmapToCache(mAddress, bitmap);
|
2013-08-15 19:45:23 -04:00
|
|
|
|
2013-02-07 14:01:49 -05:00
|
|
|
return bitmap;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
protected void onPostExecute(Bitmap bitmap) {
|
|
|
|
if (mQuickContactBadgeReference != null) {
|
|
|
|
QuickContactBadge badge = mQuickContactBadgeReference.get();
|
|
|
|
if (badge != null && getContactPictureRetrievalTask(badge) == this) {
|
|
|
|
badge.setImageBitmap(bitmap);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@code Drawable} subclass that stores a reference to the {@link ContactPictureRetrievalTask}
|
|
|
|
* that is trying to load the contact picture.
|
|
|
|
*
|
|
|
|
* <p>
|
2013-09-30 19:53:21 -04:00
|
|
|
* The reference is used by {@link ContactPictureLoader#cancelPotentialWork(Address,
|
2013-02-07 14:01:49 -05:00
|
|
|
* QuickContactBadge)} 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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|