mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-23 18:02:15 -05:00
Add contact pictures to message list
This commit is contained in:
parent
ae5e1af54d
commit
04ce0a9d3f
BIN
res/drawable-hdpi/ic_contact_picture.png
Normal file
BIN
res/drawable-hdpi/ic_contact_picture.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
@ -33,6 +33,22 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<QuickContactBadge
|
||||
android:id="@+id/contact_badge"
|
||||
android:layout_marginRight="8dip"
|
||||
android:layout_marginTop="4dip"
|
||||
android:layout_marginBottom="3dip"
|
||||
android:layout_height="48dip"
|
||||
android:layout_width="48dip"
|
||||
android:layout_toRightOf="@id/chip_wrapper"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
android:maxWidth="48dp"
|
||||
android:maxHeight="48dp"
|
||||
android:src="@drawable/ic_contact_picture"
|
||||
style="?android:attr/quickContactBadgeStyleWindowLarge"
|
||||
android:background="@android:color/transparent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subject"
|
||||
android:layout_width="match_parent"
|
||||
@ -41,7 +57,7 @@
|
||||
android:layout_marginBottom="1dip"
|
||||
android:layout_marginLeft="1dip"
|
||||
android:layout_toLeftOf="@+id/date"
|
||||
android:layout_toRightOf="@+id/chip_wrapper"
|
||||
android:layout_toRightOf="@id/contact_badge"
|
||||
android:ellipsize="marquee"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
@ -51,7 +67,7 @@
|
||||
android:id="@+id/preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/chip_wrapper"
|
||||
android:layout_toRightOf="@+id/contact_badge"
|
||||
android:layout_below="@+id/subject"
|
||||
android:layout_toLeftOf="@+id/thread_count"
|
||||
android:layout_marginLeft="1dip"
|
||||
|
288
src/com/fsck/k9/activity/misc/ContactPictureLoader.java
Normal file
288
src/com/fsck/k9/activity/misc/ContactPictureLoader.java
Normal file
@ -0,0 +1,288 @@
|
||||
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.concurrent.RejectedExecutionException;
|
||||
|
||||
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.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;
|
||||
|
||||
public class ContactPictureLoader {
|
||||
/**
|
||||
* Maximum number of email addresses to store in {@link #mUnknownContactsCache}.
|
||||
*/
|
||||
private static final int MAX_UNKNOWN_CONTACTS = 1000;
|
||||
|
||||
/**
|
||||
* Used as lightweight dummy value for entries in {@link #mUnknownContactsCache}.
|
||||
*/
|
||||
private static final int[] DUMMY_INT_ARRAY = new int[0];
|
||||
|
||||
|
||||
private ContentResolver mContentResolver;
|
||||
private Resources mResources;
|
||||
private Contacts mContactsHelper;
|
||||
private Bitmap mDefaultPicture;
|
||||
|
||||
/**
|
||||
* LRU cache of contact pictures.
|
||||
*/
|
||||
private final LruCache<String, Bitmap> mBitmapCache;
|
||||
|
||||
/**
|
||||
* LRU cache of email addresses that don't belong to a contact we have a picture for.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*/
|
||||
private final LruCache<String, int[]> 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<String, Bitmap>(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<String, int[]>(MAX_UNKNOWN_CONTACTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a contact picture and display it using the supplied {@link QuickContactBadge} instance.
|
||||
*
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
*
|
||||
* @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<String, Void, Bitmap> {
|
||||
private final WeakReference<QuickContactBadge> mQuickContactBadgeReference;
|
||||
private String mEmail;
|
||||
|
||||
ContactPictureRetrievalTask(QuickContactBadge badge) {
|
||||
mQuickContactBadgeReference = new WeakReference<QuickContactBadge>(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.
|
||||
*
|
||||
* <p>
|
||||
* 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}.
|
||||
* </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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user