summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authormindyp <mindyp@google.com>2013-01-03 13:19:15 -0800
committermindyp <mindyp@google.com>2013-01-04 14:24:34 -0800
commit6d11c8fbca5d54a013d78c85d6eb28f590093e3c (patch)
treea5ceda3557296c02c62ab00246d19ec54ebaf163
parent29ff6b7ac29064cee7c74336f485d26e25c2e73b (diff)
downloadandroid_packages_apps_UnifiedEmail-6d11c8fbca5d54a013d78c85d6eb28f590093e3c.tar.gz
android_packages_apps_UnifiedEmail-6d11c8fbca5d54a013d78c85d6eb28f590093e3c.tar.bz2
android_packages_apps_UnifiedEmail-6d11c8fbca5d54a013d78c85d6eb28f590093e3c.zip
Sender emails are now available. Use the senders email to lookup their contact photo.
Creates the DividedImageCanvas, a light weight way of collecting and rendering sender images to a canvas than a standard android view. Also, allows us to get bitmap processing off the main thread in a future perf related cl TODO: perf pass; implement correct visual design for letter tiles once available Change-Id: I67b8f74f40703543609d1011098062c98e3e42cc
-rw-r--r--res/values/dimen.xml3
-rw-r--r--src/com/android/mail/browse/ConversationItemView.java55
-rw-r--r--src/com/android/mail/browse/ConversationItemViewModel.java10
-rw-r--r--src/com/android/mail/browse/SendersView.java17
-rw-r--r--src/com/android/mail/photomanager/ContactPhotoManager.java423
-rw-r--r--src/com/android/mail/photomanager/LetterTileProvider.java101
-rw-r--r--src/com/android/mail/photomanager/MemoryUtils.java1
-rw-r--r--src/com/android/mail/ui/DividedImageCanvas.java270
-rw-r--r--src/com/android/mail/widget/WidgetService.java2
-rw-r--r--tests/src/com/android/mail/browse/SendersFormattingTests.java10
10 files changed, 580 insertions, 312 deletions
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 86497cf51..f6e7e5dad 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -109,4 +109,7 @@
<dimen name="senders_textview_top_padding">-4dp</dimen>
<dimen name="senders_textview_height">30dp</dimen>
<integer name="chips_max_lines">2</integer>
+ <dimen name="tile_letter_font_size">32sp</dimen>
+ <dimen name="tile_letter_padding_bottom">12dp</dimen>
+ <dimen name="tile_letter_padding_left">14dp</dimen>
</resources>
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index e8ba0f23a..7d7b82f74 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -48,7 +48,6 @@ import android.text.TextUtils.TruncateAt;
import android.text.format.DateUtils;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
import android.text.style.TextAppearanceSpan;
import android.util.SparseArray;
import android.view.DragEvent;
@@ -57,13 +56,13 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.DecelerateInterpolator;
-import android.widget.ImageView;
import android.widget.TextView;
import com.android.mail.R;
import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
import com.android.mail.perf.Timer;
import com.android.mail.photomanager.ContactPhotoManager;
+import com.android.mail.photomanager.LetterTileProvider;
import com.android.mail.providers.Conversation;
import com.android.mail.providers.Folder;
import com.android.mail.providers.UIProvider;
@@ -72,6 +71,8 @@ import com.android.mail.ui.AnimatedAdapter;
import com.android.mail.ui.ControllableActivity;
import com.android.mail.ui.ConversationSelectionSet;
import com.android.mail.ui.CustomTypefaceSpan;
+import com.android.mail.ui.DividedImageCanvas;
+import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
import com.android.mail.ui.FolderDisplayer;
import com.android.mail.ui.SwipeableItemView;
import com.android.mail.ui.SwipeableListView;
@@ -85,7 +86,8 @@ import com.google.common.annotations.VisibleForTesting;
import java.util.ArrayList;
-public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem {
+public class ConversationItemView extends View implements SwipeableItemView, ToggleableItem,
+ InvalidateCallback {
// Timer.
private static int sLayoutCount = 0;
private static Timer sTimer; // Create the sTimer here if you need to do
@@ -109,7 +111,6 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private static Bitmap IMPORTANT_ONLY_TO_ME;
private static Bitmap IMPORTANT_TO_ME_AND_OTHERS;
private static Bitmap IMPORTANT_TO_OTHERS;
- private static Bitmap DATE_BACKGROUND;
private static Bitmap STATE_REPLIED;
private static Bitmap STATE_FORWARDED;
private static Bitmap STATE_REPLIED_AND_FORWARDED;
@@ -175,8 +176,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private TextView mSubjectTextView;
private TextView mSendersTextView;
private TextView mDateTextView;
- private ImageView mContactImagesView;
- private static Drawable sDefaultContactDrawable;
+ private DividedImageCanvas mContactImagesHolder;
private static TextAppearanceSpan sDateTextAppearance;
private static CustomTypefaceSpan sSubjectTextUnreadSpan;
private static TextAppearanceSpan sSubjectTextReadSpan;
@@ -190,8 +190,8 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private static HtmlTreeBuilder sHtmlBuilder;
private static HtmlParser sHtmlParser;
private static ContactPhotoManager sContactPhotoManager;
- public static final ContactPhotoManager.DefaultImageProvider DEFAULT_AVATAR =
- new ContactPhotoManager.AvatarDefaultImageProvider();
+ public static final LetterTileProvider DEFAULT_AVATAR_PROVIDER =
+ new LetterTileProvider();
static {
sPaint.setAntiAlias(true);
@@ -361,7 +361,6 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
R.drawable.ic_email_caret_none_important_unread);
ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
MORE_FOLDERS = BitmapFactory.decodeResource(res, R.drawable.ic_folders_more);
- DATE_BACKGROUND = BitmapFactory.decodeResource(res, R.drawable.folder_bg_holo_light);
STATE_REPLIED =
BitmapFactory.decodeResource(res, R.drawable.ic_badge_reply_holo_light);
STATE_FORWARDED =
@@ -400,7 +399,6 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
sDateTextAppearance = new TextAppearanceSpan(mContext, R.style.DateTextAppearance);
sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
- sDefaultContactDrawable = res.getDrawable(R.drawable.ic_contact_picture);
}
mSubjectTextView = new TextView(mContext);
@@ -417,7 +415,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mDateTextView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
- mContactImagesView = new ImageView(context);
+ mContactImagesHolder = new DividedImageCanvas(context, this);
}
public void bind(Cursor cursor, ControllableActivity activity, ConversationSelectionSet set,
@@ -448,6 +446,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mAdapter = adapter;
setContentDescription();
requestLayout();
+ sContactPhotoManager.removePhoto(mContactImagesHolder);
}
/**
@@ -549,16 +548,6 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
mChecked = mSelectedConversationSet.contains(mHeader.conversation);
}
mHeader.checkboxVisible = mCheckboxesEnabled;
- // Show either checkbox OR contact images.
- if (!mHeader.checkboxVisible) {
- mContactImagesView.setLayoutParams(new LayoutParams(mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesHeight));
- mContactImagesView.setImageDrawable(sDefaultContactDrawable);
- mContactImagesView.measure(mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesHeight);
- mContactImagesView.layout(0, 0, mCoordinates.contactImagesWidth,
- mCoordinates.contactImagesHeight);
- }
final boolean isUnread = mHeader.unread;
updateBackground(isUnread);
@@ -578,10 +567,14 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
ConversationItemViewCoordinates.getMode(context, mActivity.getViewMode()),
mHeader.conversation.hasAttachments);
mHeader.displayableSenderEmails = new ArrayList<String>();
+ mHeader.displayableSenderNames = new ArrayList<String>();
mHeader.styledSenders = new ArrayList<SpannableString>();
SendersView.format(context, mHeader.conversation.conversationInfo,
mHeader.messageInfoString.toString(), maxChars, getParser(), getBuilder(),
- mHeader.styledSenders, mHeader.displayableSenderEmails);
+ mHeader.styledSenders, mHeader.displayableSenderNames,
+ mHeader.displayableSenderEmails);
+ // If we have displayable sendres, load their thumbnails
+ loadSenderImages();
} else {
SendersView.formatSenders(mHeader, getContext());
}
@@ -635,6 +628,21 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
pauseTimer(PERF_TAG_CALCULATE_TEXTS_BITMAPS);
}
+ private void loadSenderImages() {
+ if (!mCheckboxesEnabled && mHeader.displayableSenderEmails != null
+ && mHeader.displayableSenderEmails.size() > 0) {
+ mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
+ mCoordinates.contactImagesHeight);
+ mContactImagesHolder.setDivisionIds(mHeader.displayableSenderEmails);
+ int size = mHeader.displayableSenderEmails.size();
+ for (int i = 1; i <= DividedImageCanvas.MAX_DIVISIONS && i <= size; i++) {
+ sContactPhotoManager.loadThumbnail(mContactImagesHolder,
+ mHeader.displayableSenderNames.get(size - i).toString(),
+ mHeader.displayableSenderEmails.get(size - i), DEFAULT_AVATAR_PROVIDER);
+ }
+ }
+ }
+
private void layoutSenders(SpannableStringBuilder sendersText) {
TextView sendersTextView = mSendersTextView;
if (mHeader.styledSendersString != null) {
@@ -1125,7 +1133,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
private void drawContactImages(Canvas canvas) {
canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
- mContactImagesView.draw(canvas);
+ mContactImagesHolder.draw(canvas);
}
private void drawDate(Canvas canvas) {
@@ -1397,6 +1405,7 @@ public class ConversationItemView extends View implements SwipeableItemView, Tog
setAnimatedHeight(-1);
setMinimumHeight(ConversationItemViewCoordinates.getMinHeight(mContext,
mActivity.getViewMode()));
+ sContactPhotoManager.removePhoto(mContactImagesHolder);
}
/**
diff --git a/src/com/android/mail/browse/ConversationItemViewModel.java b/src/com/android/mail/browse/ConversationItemViewModel.java
index d79ec3347..faea5ca8a 100644
--- a/src/com/android/mail/browse/ConversationItemViewModel.java
+++ b/src/com/android/mail/browse/ConversationItemViewModel.java
@@ -120,9 +120,19 @@ public class ConversationItemViewModel {
private String mContentDescription;
+ /**
+ * Email address corresponding to the senders that will be displayed in the
+ * senders field.
+ */
public ArrayList<String> displayableSenderEmails;
/**
+ * Display names corresponding to the email address corresponding to the
+ * senders that will be displayed in the senders field.
+ */
+ public ArrayList<String> displayableSenderNames;
+
+ /**
* Returns the view model for a conversation. If the model doesn't exist for this conversation
* null is returned. Note: this should only be called from the UI thread.
*
diff --git a/src/com/android/mail/browse/SendersView.java b/src/com/android/mail/browse/SendersView.java
index e35b68876..4632f63a2 100644
--- a/src/com/android/mail/browse/SendersView.java
+++ b/src/com/android/mail/browse/SendersView.java
@@ -39,6 +39,7 @@ import com.android.mail.providers.ConversationInfo;
import com.android.mail.providers.MessageInfo;
import com.android.mail.providers.UIProvider;
import com.android.mail.ui.CustomTypefaceSpan;
+import com.android.mail.ui.DividedImageCanvas;
import com.android.mail.utils.ObjectCache;
import com.android.mail.utils.Utils;
import com.google.android.common.html.parser.HtmlParser;
@@ -72,7 +73,6 @@ public class SendersView {
private static String sMessageCountSpacerString;
public static CharSequence sElidedString;
private static BroadcastReceiver sConfigurationChangedReceiver;
- private static int sMaxDisplayableSenderImages;
private static TextAppearanceSpan sMessageInfoReadStyleSpan;
private static TextAppearanceSpan sMessageInfoUnreadStyleSpan;
@@ -131,7 +131,6 @@ public class SendersView {
sReadStyleSpan = new TextAppearanceSpan(context, R.style.SendersReadTextAppearance);
sMessageCountSpacerString = res.getString(R.string.message_count_spacer);
sSendingString = res.getString(R.string.sending);
- sMaxDisplayableSenderImages = res.getInteger(R.integer.max_list_item_images);
}
}
@@ -200,16 +199,17 @@ public class SendersView {
@VisibleForTesting
public static void format(Context context, ConversationInfo conversationInfo,
String messageInfo, int maxChars, HtmlParser parser, HtmlTreeBuilder builder,
- ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderEmails) {
+ ArrayList<SpannableString> styledSenders, ArrayList<String> displayableSenderNames,
+ ArrayList<String> displayableSenderEmails) {
getSenderResources(context);
- handlePriority(context, maxChars, messageInfo,
- conversationInfo, parser, builder, styledSenders, displayableSenderEmails);
+ handlePriority(context, maxChars, messageInfo, conversationInfo, parser, builder,
+ styledSenders, displayableSenderNames, displayableSenderEmails);
}
public static void handlePriority(Context context, int maxChars,
String messageInfoString, ConversationInfo conversationInfo, HtmlParser parser,
HtmlTreeBuilder builder, ArrayList<SpannableString> styledSenders,
- ArrayList<String> displayableSenderEmails) {
+ ArrayList<String> displayableSenderNames, ArrayList<String> displayableSenderEmails) {
boolean shouldAddPhotos = displayableSenderEmails != null;
int maxPriorityToInclude = -1; // inclusive
int numCharsUsed = messageInfoString.length(); // draft, number drafts,
@@ -289,6 +289,7 @@ public class SendersView {
styledSenders.set(oldPos, null);
if (shouldAddPhotos && !TextUtils.isEmpty(currentMessage.senderEmail)) {
displayableSenderEmails.remove(currentMessage.senderEmail);
+ displayableSenderNames.remove(currentMessage.sender);
}
}
displayHash.put(currentMessage.sender, i);
@@ -296,8 +297,10 @@ public class SendersView {
styledSenders.add(spannableDisplay);
if (shouldAddPhotos && !TextUtils.isEmpty(currentMessage.senderEmail)) {
displayableSenderEmails.add(currentMessage.senderEmail);
- if (displayableSenderEmails.size() > sMaxDisplayableSenderImages) {
+ displayableSenderNames.add(currentMessage.sender);
+ if (displayableSenderEmails.size() > DividedImageCanvas.MAX_DIVISIONS) {
displayableSenderEmails.remove(0);
+ displayableSenderNames.remove(0);
}
}
}
diff --git a/src/com/android/mail/photomanager/ContactPhotoManager.java b/src/com/android/mail/photomanager/ContactPhotoManager.java
index 0c9e2b18e..7c1b8362b 100644
--- a/src/com/android/mail/photomanager/ContactPhotoManager.java
+++ b/src/com/android/mail/photomanager/ContactPhotoManager.java
@@ -21,16 +21,12 @@ import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.res.Configuration;
-import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.TransitionDrawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Handler.Callback;
@@ -43,18 +39,14 @@ import android.provider.ContactsContract.Contacts.Photo;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Directory;
import android.text.TextUtils;
-import android.util.Log;
import android.util.LruCache;
-import android.util.TypedValue;
-import android.widget.ImageView;
+import com.android.mail.ui.DividedImageCanvas;
+import com.android.mail.utils.LogUtils;
-import com.android.mail.R;
import com.google.common.base.Objects;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.util.HashMap;
@@ -73,52 +65,20 @@ public abstract class ContactPhotoManager implements ComponentCallbacks2 {
static final boolean DEBUG = false; // Don't submit with true
static final boolean DEBUG_SIZES = false; // Don't submit with true
- /** Caches 180dip in pixel. This is used to detect whether to show the hires or lores version
- * of the default avatar */
- private static int s180DipInPixel = -1;
-
public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
- /**
- * Returns the resource id of the default avatar. Tries to find a resource that is bigger
- * than the given extent (width or height). If extent=-1, a thumbnail avatar is returned
- */
- public static int getDefaultAvatarResId(Context context, int extent, boolean darkTheme) {
- // TODO: Is it worth finding a nicer way to do hires/lores here? In practice, the
- // default avatar doesn't look too different when stretched
- if (s180DipInPixel == -1) {
- Resources r = context.getResources();
- s180DipInPixel = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 180,
- r.getDisplayMetrics());
- }
-
- final boolean hires = (extent != -1) && (extent > s180DipInPixel);
- return getDefaultAvatarResId(hires, darkTheme);
- }
-
- public static int getDefaultAvatarResId(boolean hires, boolean darkTheme) {
- if (hires && darkTheme) return R.drawable.ic_contact_picture_180_holo_dark;
- if (hires) return R.drawable.ic_contact_picture_180_holo_light;
- if (darkTheme) return R.drawable.ic_contact_picture_holo_dark;
- return R.drawable.ic_contact_picture;
- }
-
public static abstract class DefaultImageProvider {
/**
- * Applies the default avatar to the ImageView. Extent is an indicator for the size (width
- * or height). If darkTheme is set, the avatar is one that looks better on dark background
+ * Applies the default avatar to the DividedImageView. Extent is an
+ * indicator for the size (width or height). If darkTheme is set, the
+ * avatar is one that looks better on dark background
+ * @param id
*/
- public abstract void applyDefaultImage(ImageView view, int extent, boolean darkTheme);
+ public abstract void applyDefaultImage(String name, String emailAddress,
+ DividedImageCanvas view, int extent);
}
- public static class AvatarDefaultImageProvider extends DefaultImageProvider {
- @Override
- public void applyDefaultImage(ImageView view, int extent, boolean darkTheme) {
- view.setImageResource(getDefaultAvatarResId(view.getContext(), extent, darkTheme));
- }
- }
-
- public static final DefaultImageProvider DEFAULT_AVATAR = new AvatarDefaultImageProvider();
+ public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileProvider();
/**
* Requests the singleton instance of {@link AccountTypeManager} with data bound from
@@ -130,7 +90,7 @@ public abstract class ContactPhotoManager implements ComponentCallbacks2 {
(ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
if (service == null) {
service = createContactPhotoManager(applicationContext);
- Log.e(TAG, "No contact photo service in context: " + applicationContext);
+ LogUtils.e(TAG, "No contact photo service in context: " + applicationContext);
}
return service;
}
@@ -140,21 +100,21 @@ public abstract class ContactPhotoManager implements ComponentCallbacks2 {
}
/**
- * Calls {@link #loadThumbnail(ImageView, long, boolean, DefaultImageProvider)} with
+ * Calls {@link #loadThumbnail(DividedImageCanvas, long, boolean, DefaultImageProvider)} with
* {@link #DEFAULT_AVATAR}.
*/
- public final void loadThumbnail(ImageView view, String name, boolean darkTheme) {
- loadThumbnail(view, name, darkTheme, DEFAULT_AVATAR);
+ public final void loadThumbnail(DividedImageCanvas view, String name, String emailAddress) {
+ loadThumbnail(view, name, emailAddress, DEFAULT_AVATAR);
}
- public abstract void loadThumbnail(ImageView view, String name, boolean darkTheme,
+ public abstract void loadThumbnail(DividedImageCanvas view, String name, String emailAddress,
DefaultImageProvider defaultProvider);
/**
* Remove photo from the supplied image view. This also cancels current pending load request
* inside this photo manager.
*/
- public abstract void removePhoto(ImageView view);
+ public abstract void removePhoto(DividedImageCanvas view);
/**
* Temporarily stops loading photos from the database.
@@ -174,14 +134,6 @@ public abstract class ContactPhotoManager implements ComponentCallbacks2 {
public abstract void refreshCache();
/**
- * Stores the given bitmap directly in the LRU bitmap cache.
- * @param photoUri The URI of the photo (for future requests).
- * @param bitmap The bitmap.
- * @param photoBytes The bytes that were parsed to create the bitmap.
- */
- public abstract void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes);
-
- /**
* Initiates a background process that over time will fill up cache with
* preload photos.
*/
@@ -206,8 +158,6 @@ public abstract class ContactPhotoManager implements ComponentCallbacks2 {
class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
- private static final int FADE_TRANSITION_DURATION = 200;
-
/**
* Type of message sent by the UI thread to itself to indicate that some photos
* need to be loaded.
@@ -252,6 +202,11 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
/**
+ * An LRU cache for photo ids mapped to contact addresses.
+ */
+ private final LruCache<String, Long> mPhotoIdCache;
+
+ /**
* {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh.
*/
private volatile boolean mBitmapHolderCacheAllUnfresh = true;
@@ -269,11 +224,11 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
private final LruCache<Object, Bitmap> mBitmapCache;
/**
- * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request.
+ * A map from DividedImageView to the corresponding photo ID or uri, encapsulated in a request.
* The request may swapped out before the photo loading request is started.
*/
- private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
- new ConcurrentHashMap<ImageView, Request>();
+ private final ConcurrentHashMap<DividedImageCanvas, Request> mPendingRequests =
+ new ConcurrentHashMap<DividedImageCanvas, Request>();
/**
* Handler for messages sent to the UI thread.
@@ -302,7 +257,8 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
/** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
- private static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
+ /** Cache size for {@link #mPhotoIdCache}. Starting with 500 entries. */
+ private static final int PHOTO_ID_CACHE_SIZE = 500;
/** For debug: How many times we had to reload cached photo for a stale entry */
private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
@@ -314,7 +270,8 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
mContext = context;
final float cacheSizeAdjustment =
- (MemoryUtils.getTotalMemorySize() >= LARGE_RAM_THRESHOLD) ? 1.0f : 0.5f;
+ (MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
+ 1.0f : 0.5f;
final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
mBitmapCache = new LruCache<Object, Bitmap>(bitmapCacheSize) {
@Override protected int sizeOf(Object key, Bitmap value) {
@@ -338,11 +295,12 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
};
mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
- Log.i(TAG, "Cache adj: " + cacheSizeAdjustment);
+ LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment);
if (DEBUG) {
- Log.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
+ LogUtils.d(TAG, "Cache size: " + btk(mBitmapHolderCache.maxSize())
+ " + " + btk(mBitmapCache.maxSize()));
}
+ mPhotoIdCache = new LruCache<String, Long>(PHOTO_ID_CACHE_SIZE);
}
/** Converts bytes to K bytes, rounding up. Used only for debug log. */
@@ -376,12 +334,12 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
bitmapBytes += b.getByteCount();
}
}
- Log.d(TAG,
+ LogUtils.d(TAG,
"L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
+ btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
+ numBitmaps + " bitmaps, avg: " + btk(safeDiv(rawBytes, numHolders)) + ","
+ btk(safeDiv(bitmapBytes, numBitmaps)));
- Log.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString() + ", overwrite: fresh="
+ LogUtils.d(TAG, "L1 Stats: " + mBitmapHolderCache.toString() + ", overwrite: fresh="
+ mFreshCacheOverwrite.get() + " stale=" + mStaleCacheOverwrite.get());
numBitmaps = 0;
@@ -390,14 +348,14 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
numBitmaps++;
bitmapBytes += b.getByteCount();
}
- Log.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" + ", avg: "
+ LogUtils.d(TAG, "L2: " + btk(bitmapBytes) + ", " + numBitmaps + " bitmaps" + ", avg: "
+ btk(safeDiv(bitmapBytes, numBitmaps)));
// We don't get from L2 cache, so L2 stats is meaningless.
}
@Override
public void onTrimMemory(int level) {
- if (DEBUG) Log.d(TAG, "onTrimMemory: " + level);
+ if (DEBUG) LogUtils.d(TAG, "onTrimMemory: " + level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
// Clear the caches. Note all pending requests will be removed too.
clear();
@@ -411,20 +369,21 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
@Override
- public void loadThumbnail(ImageView view, String name, boolean darkTheme,
+ public void loadThumbnail(DividedImageCanvas view, String name, String emailAddress,
DefaultImageProvider defaultProvider) {
if (TextUtils.isEmpty(name)) {
// No photo is needed
- defaultProvider.applyDefaultImage(view, -1, darkTheme);
+ defaultProvider.applyDefaultImage(name, emailAddress, view, -1);
mPendingRequests.remove(view);
} else {
- if (DEBUG) Log.d(TAG, "loadPhoto request: " + name);
- loadPhotoByIdOrUri(view, Request.createFromName(name, darkTheme,
- defaultProvider));
+ if (DEBUG)
+ LogUtils.d(TAG, "loadPhoto request: " + name);
+ loadPhotoByIdOrUri(view,
+ Request.createFromEmailAddress(name, emailAddress, defaultProvider));
}
}
- private void loadPhotoByIdOrUri(ImageView view, Request request) {
+ private void loadPhotoByIdOrUri(DividedImageCanvas view, Request request) {
boolean loaded = loadCachedPhoto(view, request, false);
if (loaded) {
mPendingRequests.remove(view);
@@ -438,18 +397,18 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
@Override
- public void removePhoto(ImageView view) {
- view.setImageDrawable(null);
+ public void removePhoto(DividedImageCanvas view) {
+ view.reset();
mPendingRequests.remove(view);
}
@Override
public void refreshCache() {
if (mBitmapHolderCacheAllUnfresh) {
- if (DEBUG) Log.d(TAG, "refreshCache -- no fresh entries.");
+ if (DEBUG) LogUtils.d(TAG, "refreshCache -- no fresh entries.");
return;
}
- if (DEBUG) Log.d(TAG, "refreshCache");
+ if (DEBUG) LogUtils.d(TAG, "refreshCache");
mBitmapHolderCacheAllUnfresh = true;
for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
holder.fresh = false;
@@ -461,7 +420,7 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
*
* @return false if the photo needs to be (re)loaded from the provider.
*/
- private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
+ private boolean loadCachedPhoto(DividedImageCanvas view, Request request, boolean fadeIn) {
BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
if (holder == null) {
// The bitmap has not been loaded ==> show default avatar
@@ -484,30 +443,13 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
} else {
// This is bigger data. Let's send that back to the Loader so that we can
// inflate this in the background
+ holder.bitmap = null;
request.applyDefaultImage(view);
return false;
}
}
- final Drawable previousDrawable = view.getDrawable();
- if (fadeIn && previousDrawable != null) {
- final Drawable[] layers = new Drawable[2];
- // Prevent cascade of TransitionDrawables.
- if (previousDrawable instanceof TransitionDrawable) {
- final TransitionDrawable previousTransitionDrawable =
- (TransitionDrawable) previousDrawable;
- layers[0] = previousTransitionDrawable.getDrawable(
- previousTransitionDrawable.getNumberOfLayers() - 1);
- } else {
- layers[0] = previousDrawable;
- }
- layers[1] = new BitmapDrawable(mContext.getResources(), cachedBitmap);
- TransitionDrawable drawable = new TransitionDrawable(layers);
- view.setImageDrawable(drawable);
- drawable.startTransition(FADE_TRANSITION_DURATION);
- } else {
- view.setImageBitmap(cachedBitmap);
- }
+ view.addDivisionImage(cachedBitmap, request.getEmailAddress());
// Put the bitmap in the LRU cache. But only do this for images that are small enough
// (we require that at least six of those can be cached at the same time)
@@ -568,7 +510,7 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
holder.bitmap = bitmap;
holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
if (DEBUG) {
- Log.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
+ LogUtils.d(TAG, "inflateBitmap " + btk(bytes.length) + " -> "
+ bitmap.getWidth() + "x" + bitmap.getHeight()
+ ", " + btk(bitmap.getByteCount()));
}
@@ -578,10 +520,11 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
public void clear() {
- if (DEBUG) Log.d(TAG, "clear");
+ if (DEBUG) LogUtils.d(TAG, "clear");
mPendingRequests.clear();
mBitmapHolderCache.evictAll();
mBitmapCache.evictAll();
+ mPhotoIdCache.evictAll();
}
@Override
@@ -649,9 +592,9 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
* photos still haven't been loaded, sends another request for image loading.
*/
private void processLoadedImages() {
- Iterator<ImageView> iterator = mPendingRequests.keySet().iterator();
+ Iterator<DividedImageCanvas> iterator = mPendingRequests.keySet().iterator();
while (iterator.hasNext()) {
- ImageView view = iterator.next();
+ DividedImageCanvas view = iterator.next();
Request key = mPendingRequests.get(view);
boolean loaded = loadCachedPhoto(view, key, true);
if (loaded) {
@@ -659,7 +602,8 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
}
- softenCache();
+ // TODO: this already seems to happen when calling loadCachedPhoto
+ //softenCache();
if (!mPendingRequests.isEmpty()) {
requestLoading();
@@ -667,13 +611,13 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
/**
- * Removes strong references to loaded bitmaps to allow them to be garbage collected
- * if needed. Some of the bitmaps will still be retained by {@link #mBitmapCache}.
+ * Store the supplied photo id to contact address mapping so that we don't
+ * have to lookup the contact again.
+ * @param id Id of the photo matching the contact
+ * @param contactAddress Email address of the contact
*/
- private void softenCache() {
- for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
- holder.bitmap = null;
- }
+ private void cachePhotoId(Long id, String contactAddress) {
+ mPhotoIdCache.put(contactAddress, id);
}
/**
@@ -683,14 +627,14 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
if (DEBUG) {
BitmapHolder prev = mBitmapHolderCache.get(key);
if (prev != null && prev.bytes != null) {
- Log.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
+ LogUtils.d(TAG, "Overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
if (prev.fresh) {
mFreshCacheOverwrite.incrementAndGet();
} else {
mStaleCacheOverwrite.incrementAndGet();
}
}
- Log.d(TAG, "Caching data: key=" + key + ", " +
+ LogUtils.d(TAG, "Caching data: key=" + key + ", " +
(bytes == null ? "<null>" : btk(bytes.length)));
}
BitmapHolder holder = new BitmapHolder(bytes,
@@ -706,19 +650,6 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
mBitmapHolderCacheAllUnfresh = false;
}
- @Override
- public void cacheBitmap(Uri photoUri, Bitmap bitmap, byte[] photoBytes) {
- final int smallerExtent = Math.min(bitmap.getWidth(), bitmap.getHeight());
- // We can pretend here that the extent of the photo was the size that we originally
- // requested
- Request request = Request.createFromUri(photoUri, smallerExtent, false, DEFAULT_AVATAR);
- BitmapHolder holder = new BitmapHolder(photoBytes, smallerExtent);
- holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
- mBitmapHolderCache.put(request.getKey(), holder);
- mBitmapHolderCacheAllUnfresh = false;
- mBitmapCache.put(request.getKey(), bitmap);
- }
-
/**
* Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
* already loaded
@@ -744,21 +675,15 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
while (iterator.hasNext()) {
Request request = iterator.next();
final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
- if (holder != null && holder.bytes != null && holder.fresh &&
- (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
- // This was previously loaded but we don't currently have the inflated Bitmap
+ if (holder != null && holder.bytes != null && holder.fresh
+ && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
+ // This was previously loaded but we don't currently have the
+ // inflated Bitmap
inflateBitmap(holder, request.getRequestedExtent());
jpegsDecoded = true;
} else {
if (holder == null || !holder.fresh) {
- if (request.isUriRequest()) {
- uris.add(request);
- } else if (request.isNameRequest()){
- names.add(request);
- } else {
- photoIds.add(request.getId());
- photoIdsAsStrings.add(String.valueOf(request.mId));
- }
+ names.add(request);
}
}
}
@@ -770,7 +695,6 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
* The thread that performs loading of photos from the database.
*/
private class LoaderThread extends HandlerThread implements Callback {
- private static final int BUFFER_SIZE = 1024*16;
private static final int MESSAGE_PRELOAD_PHOTOS = 0;
private static final int MESSAGE_LOAD_PHOTOS = 1;
@@ -799,7 +723,6 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
private final List<Long> mPreloadPhotoIds = Lists.newArrayList();
private Handler mLoaderThreadHandler;
- private byte mBuffer[];
private static final int PRELOAD_STATUS_NOT_STARTED = 0;
private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
@@ -914,7 +837,7 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
mPreloadStatus = PRELOAD_STATUS_DONE;
}
- Log.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
+ LogUtils.v(TAG, "Preloaded " + count + " photos. Cached bytes: "
+ mBitmapHolderCache.size());
requestPreloading();
@@ -950,8 +873,7 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
private void loadPhotosInBackground() {
obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris, mNames);
loadThumbnails(false);
- loadUriBasedPhotos();
- loadNameBasedPhotos(false);
+ loadEmailAddressBasedPhotos(false);
requestPreloading();
}
@@ -1043,58 +965,74 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
return mStringBuilder.toString();
}
- private void loadNameBasedPhotos(boolean preloading) {
- HashSet<String> names = new HashSet<String>();
+ private void loadEmailAddressBasedPhotos(boolean preloading) {
+ HashSet<String> addresses = new HashSet<String>();
Set<Long> photoIds = new HashSet<Long>();
Set<String> photoIdsAsString = new HashSet<String>();
HashMap<Long, String> photoIdMap = new HashMap<Long, String>();
-
+ Long match;
+ String emailAddress;
for (Request request : mNames) {
- names.add(request.getName());
+ emailAddress = request.getEmailAddress();
+ match = mPhotoIdCache.get(emailAddress);
+ if (match != null) {
+ photoIds.add(match);
+ photoIdsAsString.add(match + "");
+ photoIdMap.put(match, emailAddress);
+ } else {
+ addresses.add(emailAddress);
+ }
}
- String[] selectionArgs = new String[names.size()];
- names.toArray(selectionArgs);
-
- Cursor photoIdsCursor = null;
- try {
- photoIdsCursor = mResolver.query(Email.CONTENT_URI, new String[] {
- Email.PHOTO_ID, Email.ADDRESS
- }, createInQuery(Email.ADDRESS, names.size()), selectionArgs, null);
- Long id;
- if (photoIdsCursor != null) {
- while (photoIdsCursor.moveToNext()) {
- id = photoIdsCursor.getLong(0);
- photoIds.add(id);
- photoIdsAsString.add(id + "");
- photoIdMap.put(id, photoIdsCursor.getString(1));
+ if (addresses.size() > 0) {
+ String[] selectionArgs = new String[addresses.size()];
+ addresses.toArray(selectionArgs);
+ Cursor photoIdsCursor = null;
+ try {
+ photoIdsCursor = mResolver.query(Email.CONTENT_URI, new String[] {
+ Email.PHOTO_ID, Email.ADDRESS
+ }, createInQuery(Email.ADDRESS, addresses.size()), selectionArgs, null);
+ Long id;
+ String contactAddress;
+ if (photoIdsCursor != null) {
+ while (photoIdsCursor.moveToNext()) {
+ id = photoIdsCursor.getLong(0);
+ contactAddress = photoIdsCursor.getString(1);
+ photoIds.add(id);
+ photoIdsAsString.add(id + "");
+ photoIdMap.put(id, contactAddress);
+ cachePhotoId(id, contactAddress);
+ }
+ }
+ } finally {
+ if (photoIdsCursor != null) {
+ photoIdsCursor.close();
}
- }
- } finally {
- if (photoIdsCursor != null) {
- photoIdsCursor.close();
}
}
- Cursor photosCursor = null;
- try {
- photosCursor = loadThumbnails(preloading, photoIdsAsString, photoIds);
-
- if (photosCursor != null) {
- while (photosCursor.moveToNext()) {
- Long id = photosCursor.getLong(0);
- byte[] bytes = photosCursor.getBlob(1);
- cacheBitmap(photoIdMap.get(id), bytes, preloading, -1);
- photoIds.remove(id);
+ if (photoIds != null && photoIds.size() > 0) {
+ Cursor photosCursor = null;
+ try {
+ photosCursor = loadThumbnails(preloading, photoIdsAsString, photoIds);
+
+ if (photosCursor != null) {
+ while (photosCursor.moveToNext()) {
+ Long id = photosCursor.getLong(0);
+ byte[] bytes = photosCursor.getBlob(1);
+ cacheBitmap(photoIdMap.get(id), bytes, preloading, -1);
+ photoIds.remove(id);
+ }
+ }
+ } finally {
+ if (photosCursor != null) {
+ photosCursor.close();
}
- }
- } finally {
- if (photosCursor != null) {
- photosCursor.close();
}
}
-
+ String matchingAddress;
// Remaining photos were not found in the contacts database (but might be in profile).
for (Long id : photoIds) {
+ matchingAddress = photoIdMap.get(id);
if (ContactsContract.isProfileId(id)) {
Cursor profileCursor = null;
try {
@@ -1102,11 +1040,11 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
ContentUris.withAppendedId(Data.CONTENT_URI, id),
COLUMNS, null, null, null);
if (profileCursor != null && profileCursor.moveToFirst()) {
- cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1),
+ cacheBitmap(matchingAddress, profileCursor.getBlob(1),
preloading, -1);
} else {
// Couldn't load a photo this way either.
- cacheBitmap(photoIdMap.get(id), null, preloading, -1);
+ cacheBitmap(matchingAddress, null, preloading, -1);
}
} finally {
if (profileCursor != null) {
@@ -1115,107 +1053,41 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
} else {
// Not a profile photo and not found - mark the cache accordingly
- cacheBitmap(id, null, preloading, -1);
+ cacheBitmap(matchingAddress, null, preloading, -1);
}
}
-
mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
}
-
- /**
- * Loads photos referenced with Uris. Those can be remote thumbnails
- * (from directory searches), display photos etc
- */
- private void loadUriBasedPhotos() {
- for (Request uriRequest : mPhotoUris) {
- Uri uri = uriRequest.getUri();
- if (mBuffer == null) {
- mBuffer = new byte[BUFFER_SIZE];
- }
- try {
- if (DEBUG) Log.d(TAG, "Loading " + uri);
- InputStream is = mResolver.openInputStream(uri);
- if (is != null) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- int size;
- while ((size = is.read(mBuffer)) != -1) {
- baos.write(mBuffer, 0, size);
- }
- } finally {
- is.close();
- }
- cacheBitmap(uri, baos.toByteArray(), false,
- uriRequest.getRequestedExtent());
- mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
- } else {
- Log.v(TAG, "Cannot load photo " + uri);
- cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
- }
- } catch (Exception ex) {
- Log.v(TAG, "Cannot load photo " + uri, ex);
- cacheBitmap(uri, null, false, uriRequest.getRequestedExtent());
- }
- }
- }
}
/**
- * A holder for either a Uri or an id and a flag whether this was requested for the dark or
- * light theme
+ * A holder for a contact photo request.
*/
private static final class Request {
- private final long mId;
- private final Uri mUri;
- private final boolean mDarkTheme;
private final int mRequestedExtent;
private final DefaultImageProvider mDefaultProvider;
- private String mName;
+ private final String mDisplayName;
+ private final String mEmailAddress;
- private Request(long id, Uri uri, String name, int requestedExtent, boolean darkTheme,
+ private Request(String name, String emailAddress, int requestedExtent,
DefaultImageProvider defaultProvider) {
- mId = id;
- mUri = uri;
- mDarkTheme = darkTheme;
mRequestedExtent = requestedExtent;
mDefaultProvider = defaultProvider;
- mName = name;
- }
-
- public String getName() {
- return mName;
- }
-
- public boolean isNameRequest() {
- return !TextUtils.isEmpty(mName);
+ mDisplayName = name;
+ mEmailAddress = emailAddress;
}
- public static Request createFromThumbnailId(long id, boolean darkTheme,
- DefaultImageProvider defaultProvider) {
- return new Request(id, null /* no URI */, null, -1, darkTheme, defaultProvider);
+ public String getDisplayName() {
+ return mDisplayName;
}
- public static Request createFromName(String name, boolean darkTheme,
- DefaultImageProvider defaultProvider) {
- return new Request(0, null /* no URI */, name, -1, darkTheme, defaultProvider);
+ public String getEmailAddress() {
+ return mEmailAddress;
}
- public static Request createFromUri(Uri uri, int requestedExtent, boolean darkTheme,
+ public static Request createFromEmailAddress(String displayName, String emailAddress,
DefaultImageProvider defaultProvider) {
- return new Request(0 /* no ID */, uri, null, requestedExtent, darkTheme,
- defaultProvider);
- }
-
- public boolean isUriRequest() {
- return mUri != null;
- }
-
- public Uri getUri() {
- return mUri;
- }
-
- public long getId() {
- return mId;
+ return new Request(displayName, emailAddress, -1, defaultProvider);
}
public int getRequestedExtent() {
@@ -1226,10 +1098,9 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
public int hashCode() {
final int prime = 31;
int result = 1;
- result = prime * result + (int) (mId ^ (mId >>> 32));
result = prime * result + mRequestedExtent;
- result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
- result = prime * result + ((mName == null) ? 0 : mName.hashCode());
+ result = prime * result + ((mDisplayName == null) ? 0 : mDisplayName.hashCode());
+ result = prime * result + ((mEmailAddress == null) ? 0 : mEmailAddress.hashCode());
return result;
}
@@ -1239,10 +1110,9 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
final Request that = (Request) obj;
- if (mId != that.mId) return false;
if (mRequestedExtent != that.mRequestedExtent) return false;
- if (!Objects.equal(mUri, that.mUri)) return false;
- if (!Objects.equal(mName, that.mName)) return false;
+ if (!Objects.equal(mDisplayName, that.mDisplayName)) return false;
+ if (!Objects.equal(mEmailAddress, that.mEmailAddress)) return false;
// Don't compare equality of mDarkTheme because it is only used in the default contact
// photo case. When the contact does have a photo, the contact photo is the same
// regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
@@ -1251,11 +1121,12 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
}
public Object getKey() {
- return mName == null ? (mUri == null ? mId : mUri) : mName;
+ return mEmailAddress;
}
- public void applyDefaultImage(ImageView view) {
- mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme);
+ public void applyDefaultImage(DividedImageCanvas view) {
+ mDefaultProvider.applyDefaultImage(getDisplayName(), getEmailAddress(), view,
+ mRequestedExtent);
}
}
}
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
new file mode 100644
index 000000000..85febe883
--- /dev/null
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.photomanager;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.util.LruCache;
+import android.util.TypedValue;
+
+import com.android.mail.R;
+import com.android.mail.photomanager.ContactPhotoManager.DefaultImageProvider;
+import com.android.mail.ui.DividedImageCanvas;
+
+/**
+ * LetterTileProvider is an implementation of the DefaultImageProvider. When no
+ * matching contact photo is found, and there is a supplied displayName or email
+ * address whose first letter corresponds to an English alphabet letter (or
+ * number), this method creates a bitmap with the letter in the center of a
+ * tile. If there is no English alphabet character (or digit), it creates a
+ * bitmap with the default contact avatar.
+ */
+public class LetterTileProvider extends DefaultImageProvider {
+ private Bitmap mDefaultBitmap;
+ private final LruCache<String, Bitmap> mTileBitmapCache;
+ private static int sTilePaddingBottom;
+ private static int sTilePaddingLeft;
+ private static int sTileLetterFontSize = -1;
+ private static TextPaint sPaint = new TextPaint();
+ private static int DEFAULT_AVATAR_DRAWABLE = R.drawable.ic_contact_picture;
+
+ public LetterTileProvider() {
+ super();
+ final float cacheSizeAdjustment =
+ (MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
+ 1.0f : 0.5f;
+ final int bitmapCacheSize = (int) (cacheSizeAdjustment * 26);
+ mTileBitmapCache = new LruCache<String, Bitmap>(bitmapCacheSize);
+ }
+
+ @Override
+ public void applyDefaultImage(String displayName, String address, DividedImageCanvas view,
+ int extent) {
+ String display = !TextUtils.isEmpty(displayName) ? displayName : address;
+ String firstChar = display.substring(0, 1).toUpperCase();
+ Bitmap bitmap;
+ byte[] bytes = firstChar.getBytes();
+ // If its a valid ascii character...
+ if (bytes[0] > 31 && bytes[0] < 253) {
+ bitmap = mTileBitmapCache.get(firstChar);
+ if (bitmap == null) {
+ // Create bitmap based on the first char
+ int width = view.getWidth();
+ int height = view.getHeight();
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(bitmap);
+ sPaint.setColor(Color.BLACK);
+ if (sTileLetterFontSize == -1) {
+ final Resources res = view.getContext().getResources();
+ sTileLetterFontSize = res
+ .getDimensionPixelSize(R.dimen.tile_letter_font_size);
+ sTilePaddingBottom = res
+ .getDimensionPixelSize(R.dimen.tile_letter_padding_bottom);
+ sTilePaddingLeft = res
+ .getDimensionPixelSize(R.dimen.tile_letter_padding_left);
+ }
+ sPaint.setTextSize(sTileLetterFontSize);
+ c.drawText(firstChar, sTilePaddingLeft, height - sTilePaddingBottom, sPaint);
+ mTileBitmapCache.put(firstChar, bitmap);
+ }
+ } else {
+ if (mDefaultBitmap == null) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ mDefaultBitmap = BitmapFactory.decodeResource(view.getContext().getResources(),
+ DEFAULT_AVATAR_DRAWABLE, options);
+ }
+ bitmap = mDefaultBitmap;
+ }
+ view.addDivisionImage(bitmap, address);
+ }
+}
diff --git a/src/com/android/mail/photomanager/MemoryUtils.java b/src/com/android/mail/photomanager/MemoryUtils.java
index 6e62030a0..499202637 100644
--- a/src/com/android/mail/photomanager/MemoryUtils.java
+++ b/src/com/android/mail/photomanager/MemoryUtils.java
@@ -20,6 +20,7 @@ public class MemoryUtils {
private MemoryUtils() {
}
+ public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
private static long sTotalMemorySize = -1;
public static long getTotalMemorySize() {
diff --git a/src/com/android/mail/ui/DividedImageCanvas.java b/src/com/android/mail/ui/DividedImageCanvas.java
new file mode 100644
index 000000000..d9afe717e
--- /dev/null
+++ b/src/com/android/mail/ui/DividedImageCanvas.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.mail.ui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import java.util.ArrayList;
+
+/**
+ * DividedImageCanvas creates a canvas that can display into a minimum of 1
+ * and maximum of 4 images. As images are added, they
+ * are laid out according to the following algorithm:
+ * 1 Image: Draw the bitmap filling the entire canvas.
+ * 2 Images: Draw 2 bitmaps split vertically down the middle.
+ * 3 Images: Draw 3 bitmaps: the first takes up all vertical space; the 2nd and 3rd are stacked in
+ * the second vertical position.
+ * 4 Images: Divide the Canvas into 4 equal quadrants and draws 1 bitmap in each.
+ */
+public class DividedImageCanvas {
+ public static final int MAX_DIVISIONS = 4;
+
+ private ArrayList<String> mDivisionIds;
+ private ArrayList<Bitmap> mDivisionImages;
+ private Bitmap mDividedBitmap;
+ private Canvas mCanvas;
+ private int mWidth;
+ private int mHeight;
+
+ private final Context mContext;
+ private final InvalidateCallback mCallback;
+
+ private static final Paint sPaint = new Paint();
+ private static final Rect sSrc = new Rect();
+ private static final Rect sDest = new Rect();
+
+ public DividedImageCanvas(Context context, InvalidateCallback callback) {
+ mContext = context;
+ mCallback = callback;
+ }
+
+ /**
+ * Get application context for this object.
+ */
+ public Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Set the id associated with each quadrant. The quadrants are laid out:
+ * TopLeft, TopRight, Bottom Left, Bottom Right
+ * @param divisionIds
+ */
+ public void setDivisionIds(ArrayList<String> divisionIds) {
+ mDivisionIds = divisionIds;
+ mDivisionImages = new ArrayList<Bitmap>(divisionIds.size());
+ for (int i = 0; i < mDivisionIds.size(); i++) {
+ mDivisionImages.add(null);
+ }
+ }
+
+ private void draw(Bitmap b, Canvas c, int left, int top, int right, int bottom) {
+ if (b != null) {
+ // l t r b
+ sSrc.set(0, 0, b.getWidth(), b.getHeight());
+ sDest.set(left, top, right, bottom);
+ c.drawBitmap(b, sSrc, sDest, sPaint);
+ }
+ }
+
+ /**
+ * Add a bitmap to this view in the quadrant matching its id.
+ * @param b Bitmap
+ * @param id Id to look for that was previously set in setDivisionIds.
+ */
+ public void addDivisionImage(Bitmap b, String id) {
+ int pos = mDivisionIds.indexOf(id);
+ if (pos >= 0 && mDivisionImages.get(pos) == null && b != null) {
+ boolean complete = false;
+ int width = mWidth;
+ int height = mHeight;
+ if (mDividedBitmap == null) {
+ mDividedBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ mCanvas = new Canvas(mDividedBitmap);
+ }
+ // Different layouts depending on count.
+ int size = mDivisionIds.size();
+ switch (size) {
+ case 0:
+ // Do nothing.
+ break;
+ case 1:
+ // Draw the bitmap filling the entire canvas.
+ mDivisionImages.set(pos, b);
+ draw(mDivisionImages.get(0), mCanvas, 0, 0, width, height);
+ complete = true;
+ break;
+ case 2:
+ // Draw 2 bitmaps split vertically down the middle
+ mDivisionImages.set(pos, obtainBitmapWithHalfWidth(b, width, height));
+ switch (pos) {
+ case 0:
+ draw(mDivisionImages.get(0), mCanvas, 0, 0, width / 2, height);
+ break;
+ case 1:
+ draw(mDivisionImages.get(1), mCanvas, width / 2, 0, width, height);
+ break;
+ }
+ complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null;
+ break;
+ case 3:
+ // Draw 3 bitmaps: the first takes up all vertical
+ // space,
+ // the 2nd and 3rd are stacked in the second vertical
+ // position.
+ switch (pos) {
+ case 0:
+ mDivisionImages.set(pos, obtainBitmapWithHalfWidth(b, width, height));
+ draw(mDivisionImages.get(0), mCanvas, 0, 0, width / 2, height);
+ break;
+ case 1:
+ mDivisionImages.set(pos, obtainBitmapWithHalfHeightAndHalfWidth(b));
+ draw(mDivisionImages.get(1), mCanvas, width / 2, 0, width, height / 2);
+ break;
+ case 2:
+ mDivisionImages.set(pos, obtainBitmapWithHalfHeightAndHalfWidth(b));
+ draw(mDivisionImages.get(2), mCanvas, width / 2, height / 2, width,
+ height);
+ break;
+ }
+ complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
+ && mDivisionImages.get(2) != null;
+ break;
+ default:
+ // Draw all 4 bitmaps in a grid
+ mDivisionImages.set(pos, obtainBitmapWithHalfHeightAndHalfWidth(b));
+ switch (pos) {
+ case 0:
+ draw(mDivisionImages.get(0), mCanvas, 0, 0, width / 2, height / 2);
+ break;
+ case 1:
+ draw(mDivisionImages.get(1), mCanvas, width / 2, 0, width, height / 2);
+ break;
+ case 2:
+ draw(mDivisionImages.get(2), mCanvas, 0, height / 2, width / 2, height);
+ break;
+ case 3:
+ draw(mDivisionImages.get(3), mCanvas, width / 2, height / 2, width,
+ height);
+ break;
+ }
+ complete = mDivisionImages.get(0) != null && mDivisionImages.get(1) != null
+ && mDivisionImages.get(2) != null && mDivisionImages.get(3) != null;
+ break;
+ }
+ // Create the new image bitmap.
+ if (complete) {
+ mCallback.invalidate();
+ }
+ }
+ }
+
+ /**
+ * Draw the contents of the DividedImageCanvas to the supplied canvas.
+ */
+ public void draw(Canvas canvas) {
+ if (mDividedBitmap != null) {
+ canvas.drawBitmap(mDividedBitmap, 0, 0, sPaint);
+ }
+ }
+
+ private static Bitmap obtainBitmapWithHalfWidth(Bitmap bitmap, int width, int height) {
+ if (bitmap != null) {
+ final float originalWidth = bitmap.getWidth();
+ final float originalHeight = bitmap.getHeight();
+ final float originalWidthToHeightRate = originalWidth / originalHeight;
+ final float desiredWidth = width / 2;
+ final float desiredWidthToHeightRate = desiredWidth / height;
+
+ // Scale
+ final int dstWidth;
+ final int dstHeight;
+ if (originalWidthToHeightRate > desiredWidthToHeightRate) {
+ dstWidth = (int) (originalWidth * height / originalHeight);
+ dstHeight = (int) height;
+ } else {
+ dstHeight = (int) (originalHeight * desiredWidth / originalWidth);
+ dstWidth = (int) desiredWidth;
+ }
+ Bitmap scaled = Bitmap.createScaledBitmap(bitmap, dstWidth, dstHeight, true);
+
+ // Crop
+ final float extraWidth = scaled.getWidth() - desiredWidth;
+ final float extraHeight = scaled.getHeight() - height;
+ final int x = (int) (extraWidth / 2.0f);
+ final int y = (int) (extraHeight / 2.0f);
+
+ return Bitmap.createBitmap(scaled, x, y, (int) desiredWidth, (int) height);
+ }
+ return null;
+ }
+
+ private static Bitmap obtainBitmapWithHalfHeightAndHalfWidth(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ return Bitmap.createScaledBitmap(bitmap, width / 2, height / 2, false);
+ }
+
+ /**
+ * Reset all state associated with this view so that it can be reused.
+ */
+ public void reset() {
+ mDividedBitmap = null;
+ mDivisionIds = null;
+ mDivisionImages = null;
+ mCanvas = null;
+ }
+
+ /**
+ * Set the width and height of the canvas.
+ * @param width
+ * @param height
+ */
+ public void setDimensions(int width, int height) {
+ mWidth = width;
+ mHeight = height;
+ }
+
+ /**
+ * Get the resulting canvas width.
+ */
+ public int getWidth() {
+ return mWidth;
+ }
+
+ /**
+ * Get the resulting canvas height.
+ */
+ public int getHeight() {
+ return mHeight;
+ }
+
+ /**
+ * The class that will provided the canvas to which the DividedImageCanvas
+ * should render its contents must implement this interface.
+ */
+ public interface InvalidateCallback {
+ public void invalidate();
+ }
+}
diff --git a/src/com/android/mail/widget/WidgetService.java b/src/com/android/mail/widget/WidgetService.java
index f74aa5418..ef3805ef9 100644
--- a/src/com/android/mail/widget/WidgetService.java
+++ b/src/com/android/mail/widget/WidgetService.java
@@ -366,7 +366,7 @@ public class WidgetService extends RemoteViewsService {
ArrayList<SpannableString> senders = new ArrayList<SpannableString>();
SendersView.format(mContext,
conversation.conversationInfo, "", MAX_SENDERS_LENGTH,
- new HtmlParser(), new HtmlTreeBuilder(), senders, null);
+ new HtmlParser(), new HtmlTreeBuilder(), senders, null, null);
senderBuilder = ellipsizeStyledSenders(conversation.conversationInfo,
MAX_SENDERS_LENGTH, senders);
} else {
diff --git a/tests/src/com/android/mail/browse/SendersFormattingTests.java b/tests/src/com/android/mail/browse/SendersFormattingTests.java
index 686655b19..a05f9884f 100644
--- a/tests/src/com/android/mail/browse/SendersFormattingTests.java
+++ b/tests/src/com/android/mail/browse/SendersFormattingTests.java
@@ -45,8 +45,8 @@ public class SendersFormattingTests extends AndroidTestCase {
conv.addMessage(info);
ArrayList<SpannableString> strings = new ArrayList<SpannableString>();
ArrayList<String> emailDisplays = null;
- SendersView.format(getContext(), conv, "", 100,
- new HtmlParser(), new HtmlTreeBuilder(), strings, emailDisplays);
+ SendersView.format(getContext(), conv, "", 100, new HtmlParser(), new HtmlTreeBuilder(),
+ strings, emailDisplays, emailDisplays);
assertEquals(1, strings.size());
assertEquals(strings.get(0).toString(), "me");
@@ -55,7 +55,7 @@ public class SendersFormattingTests extends AndroidTestCase {
strings.clear();
conv2.addMessage(info2);
SendersView.format(getContext(), conv, "", 100, new HtmlParser(),
- new HtmlTreeBuilder(), strings, emailDisplays);
+ new HtmlTreeBuilder(), strings, emailDisplays, emailDisplays);
assertEquals(1, strings.size());
assertEquals(strings.get(0).toString(), "me");
@@ -66,7 +66,7 @@ public class SendersFormattingTests extends AndroidTestCase {
conv3.addMessage(info4);
strings.clear();
SendersView.format(getContext(), conv, "", 100, new HtmlParser(),
- new HtmlTreeBuilder(), strings, emailDisplays);
+ new HtmlTreeBuilder(), strings, emailDisplays, emailDisplays);
assertEquals(1, strings.size());
assertEquals(strings.get(0).toString(), "me");
}
@@ -83,7 +83,7 @@ public class SendersFormattingTests extends AndroidTestCase {
MessageInfo info2 = new MessageInfo(read, starred, sender, -1, null);
conv.addMessage(info2);
SendersView.format(getContext(), conv, "", 100,
- new HtmlParser(), new HtmlTreeBuilder(), strings, emailDisplays);
+ new HtmlParser(), new HtmlTreeBuilder(), strings, emailDisplays, emailDisplays);
// We actually don't remove the item, we just set it to null, so count
// just the non-null items.
int count = 0;