mBitmapCache;
+
+ /**
+ * LRU cache of email addresses that don't belong to a contact we have a picture for.
+ *
+ *
+ * We don't store the default picture for unknown contacts or contacts without a picture in
+ * {@link #mBitmapCache}, because that would lead to an unnecessarily complex implementation of
+ * the {@code LruCache.sizeOf()} method. Instead, we save the email addresses we know don't
+ * belong to one of our contacts with a picture. Knowing this, we can avoid querying the
+ * contacts database for those addresses and immediately return the default picture.
+ *
+ */
+ private final LruCache mUnknownContactsCache;
+
+
+ public ContactPictureLoader(Context context, int defaultPictureResource) {
+ Context appContext = context.getApplicationContext();
+ mContentResolver = appContext.getContentResolver();
+ mResources = appContext.getResources();
+ mContactsHelper = Contacts.getInstance(appContext);
+ mDefaultPicture = BitmapFactory.decodeResource(mResources, defaultPictureResource);
+
+ 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(String key, Bitmap bitmap) {
+ // 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();
+ }
+ };
+
+ mUnknownContactsCache = new LruCache(MAX_UNKNOWN_CONTACTS);
+ }
+
+ /**
+ * Load a contact picture and display it using the supplied {@link QuickContactBadge} instance.
+ *
+ *
+ * If the supplied email address doesn't belong to any of our contacts, the default picture is
+ * returned. If the 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. The picture is then
+ * stored in the bitmap cache or the email address is stored in the "unknown contacts cache" if
+ * it doesn't belong to one of our contacts.
+ *
+ *
+ * @param email
+ * The email address that is used to search the contacts database.
+ * @param badge
+ * The {@code QuickContactBadge} instance to receive the picture.
+ *
+ * @see #mBitmapCache
+ * @see #mUnknownContactsCache
+ */
+ public void loadContactPicture(String email, QuickContactBadge badge) {
+ Bitmap bitmap = getBitmapFromCache(email);
+ if (bitmap != null) {
+ // The picture was found in the bitmap cache
+ badge.setImageBitmap(bitmap);
+ } else if (isEmailInUnknownContactsCache(email)) {
+ // This email address doesn't belong to a contact we have a picture for. Use the
+ // default picture.
+ badge.setImageBitmap(mDefaultPicture);
+ } else if (cancelPotentialWork(email, badge)) {
+ // Query the contacts database in a background thread and try to load the contact
+ // picture, if there is one.
+ ContactPictureRetrievalTask task = new ContactPictureRetrievalTask(badge);
+ AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mDefaultPicture, task);
+ badge.setImageDrawable(asyncDrawable);
+ try {
+ task.exec(email);
+ } catch (RejectedExecutionException e) {
+ // We flooded the thread pool queue... fall back to using the default picture
+ badge.setImageBitmap(mDefaultPicture);
+ }
+ }
+ }
+
+ private void addBitmapToCache(String key, Bitmap bitmap) {
+ if (getBitmapFromCache(key) == null) {
+ mBitmapCache.put(key, bitmap);
+ }
+ }
+
+ private Bitmap getBitmapFromCache(String key) {
+ return mBitmapCache.get(key);
+ }
+
+ private void addEmailToUnknownContactsCache(String key) {
+ if (!isEmailInUnknownContactsCache(key)) {
+ mUnknownContactsCache.put(key, DUMMY_INT_ARRAY);
+ }
+ }
+
+ private boolean isEmailInUnknownContactsCache(String key) {
+ return mUnknownContactsCache.get(key) != null;
+ }
+
+ /**
+ * Checks if a {@code ContactPictureRetrievalTask} was already created to load the contact
+ * picture for the supplied email address.
+ *
+ * @param email
+ * The email address to check the contacts database for.
+ * @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.
+ */
+ private boolean cancelPotentialWork(String email, QuickContactBadge badge) {
+ final ContactPictureRetrievalTask task = getContactPictureRetrievalTask(badge);
+
+ if (task != null && email != null) {
+ String emailFromTask = task.getEmail();
+ if (!email.equals(emailFromTask)) {
+ // 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.
+ */
+ class ContactPictureRetrievalTask extends AsyncTask {
+ private final WeakReference mQuickContactBadgeReference;
+ private String mEmail;
+
+ ContactPictureRetrievalTask(QuickContactBadge badge) {
+ mQuickContactBadgeReference = new WeakReference(badge);
+ }
+
+ public void exec(String... args) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+ executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, args);
+ } else {
+ execute(args);
+ }
+ }
+
+ public String getEmail() {
+ return mEmail;
+ }
+
+ @Override
+ protected Bitmap doInBackground(String... args) {
+ String email = args[0];
+ mEmail = email;
+ final Uri x = mContactsHelper.getPhotoUri(email);
+ Bitmap bitmap = null;
+ if (x != null) {
+ try {
+ InputStream stream = mContentResolver.openInputStream(x);
+ if (stream != null) {
+ try {
+ bitmap = BitmapFactory.decodeStream(stream);
+ } finally {
+ try { stream.close(); } catch (IOException e) { /* ignore */ }
+ }
+ }
+ } catch (FileNotFoundException e) {
+ /* ignore */
+ }
+
+ }
+
+ if (bitmap == null) {
+ bitmap = mDefaultPicture;
+
+ // Remember that we don't have a contact picture for this email address
+ addEmailToUnknownContactsCache(email);
+ } else {
+ // Save the picture of the contact with that email address in the memory cache
+ addBitmapToCache(email, bitmap);
+ }
+
+ 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.
+ *
+ *
+ * The reference is used by {@link ContactPictureLoader#cancelPotentialWork(String,
+ * QuickContactBadge)} 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/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index ba2123334..a4f1c649c 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -54,6 +54,7 @@ import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ListView;
+import android.widget.QuickContactBadge;
import android.widget.TextView;
import android.widget.Toast;
@@ -74,6 +75,7 @@ import com.fsck.k9.activity.ActivityListener;
import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.FolderInfoHolder;
import com.fsck.k9.activity.MessageReference;
+import com.fsck.k9.activity.misc.ContactPictureLoader;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
@@ -428,6 +430,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
*/
private boolean mInitialized = false;
+ private ContactPictureLoader mContactsPictureLoader;
+
/**
* This class is used to run operations that modify UI elements in the UI thread.
*
@@ -746,6 +750,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mPreviewLines = K9.messageListPreviewLines();
mCheckboxes = K9.messageListCheckboxes();
+ mContactsPictureLoader = new ContactPictureLoader(getActivity(),
+ R.drawable.ic_contact_picture);
+
restoreInstanceState(savedInstanceState);
decodeArguments();
@@ -1726,6 +1733,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
holder.date = (TextView) view.findViewById(R.id.date);
holder.chip = view.findViewById(R.id.chip);
holder.preview = (TextView) view.findViewById(R.id.preview);
+ holder.contactBadge = (QuickContactBadge) view.findViewById(R.id.contact_badge);
if (mSenderAboveSubject) {
holder.from = (TextView) view.findViewById(R.id.subject);
@@ -1772,6 +1780,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
CharSequence displayName = mMessageHelper.getDisplayName(account, fromAddrs, toAddrs);
+ String counterpartyAddress = null;
+ if (fromMe) {
+ if (toAddrs.length > 0) {
+ counterpartyAddress = toAddrs[0].getAddress();
+ } else if (ccAddrs.length > 0) {
+ counterpartyAddress = ccAddrs[0].getAddress();
+ }
+ } else if (fromAddrs.length > 0) {
+ counterpartyAddress = fromAddrs[0].getAddress();
+ }
+
Date sentDate = new Date(cursor.getLong(DATE_COLUMN));
String displayDate = mMessageHelper.formatDate(sentDate);
@@ -1819,6 +1838,15 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
holder.position = cursor.getPosition();
}
+ if (holder.contactBadge != null) {
+ holder.contactBadge.assignContactFromEmail(counterpartyAddress, true);
+ if (counterpartyAddress != null) {
+ mContactsPictureLoader.loadContactPicture(counterpartyAddress, holder.contactBadge);
+ } else {
+ holder.contactBadge.setImageResource(R.drawable.ic_contact_picture);
+ }
+ }
+
// Background indicator
if (K9.useBackgroundAsUnreadIndicator()) {
int res = (read) ? R.attr.messageListReadItemBackgroundColor :
@@ -1939,6 +1967,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public TextView threadCount;
public CheckBox selected;
public int position = -1;
+ public QuickContactBadge contactBadge;
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
diff --git a/src/com/fsck/k9/helper/Contacts.java b/src/com/fsck/k9/helper/Contacts.java
index 0923c1446..6e8f9fbb4 100644
--- a/src/com/fsck/k9/helper/Contacts.java
+++ b/src/com/fsck/k9/helper/Contacts.java
@@ -3,6 +3,7 @@ package com.fsck.k9.helper;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
+import android.net.Uri;
import android.os.Build;
import android.content.Intent;
import com.fsck.k9.mail.Address;
@@ -157,6 +158,18 @@ public abstract class Contacts {
*/
public abstract ContactItem extractInfoFromContactPickerIntent(final Intent intent);
+ /**
+ * Get URI to the picture of the contact with the supplied email address.
+ *
+ * @param address
+ * An email address. The contact database is searched for a contact with this email
+ * address.
+ *
+ * @return URI to the picture of the contact with the supplied email address. {@code null} if
+ * no such contact could be found or the contact doesn't have a picture.
+ */
+ public abstract Uri getPhotoUri(String address);
+
/**
* Does the device actually have a Contacts application suitable for
* picking a contact. As hard as it is to believe, some vendors ship
diff --git a/src/com/fsck/k9/helper/ContactsSdk5.java b/src/com/fsck/k9/helper/ContactsSdk5.java
index d9dd03c22..e9efef10e 100644
--- a/src/com/fsck/k9/helper/ContactsSdk5.java
+++ b/src/com/fsck/k9/helper/ContactsSdk5.java
@@ -2,6 +2,7 @@ package com.fsck.k9.helper;
import java.util.ArrayList;
+import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -258,4 +259,47 @@ public class ContactsSdk5 extends com.fsck.k9.helper.Contacts {
SORT_ORDER);
return c;
}
+
+ @Override
+ public Uri getPhotoUri(String address) {
+ Long contactId;
+ try {
+ final Cursor c = getContactByAddress(address);
+ if (c == null) {
+ return null;
+ }
+
+ try {
+ if (!c.moveToFirst()) {
+ return null;
+ }
+
+ contactId = c.getLong(CONTACT_ID_INDEX);
+ } 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) {
+ Log.e(K9.LOG_TAG, "Couldn't fetch photo for contact with email " + address, e);
+ return null;
+ }
+ }
}