From 6d11c8fbca5d54a013d78c85d6eb28f590093e3c Mon Sep 17 00:00:00 2001 From: mindyp Date: Thu, 3 Jan 2013 13:19:15 -0800 Subject: 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 --- res/values/dimen.xml | 3 + .../android/mail/browse/ConversationItemView.java | 55 +-- .../mail/browse/ConversationItemViewModel.java | 10 + src/com/android/mail/browse/SendersView.java | 17 +- .../mail/photomanager/ContactPhotoManager.java | 423 +++++++-------------- .../mail/photomanager/LetterTileProvider.java | 101 +++++ src/com/android/mail/photomanager/MemoryUtils.java | 1 + src/com/android/mail/ui/DividedImageCanvas.java | 270 +++++++++++++ src/com/android/mail/widget/WidgetService.java | 2 +- .../mail/browse/SendersFormattingTests.java | 10 +- 10 files changed, 580 insertions(+), 312 deletions(-) create mode 100644 src/com/android/mail/photomanager/LetterTileProvider.java create mode 100644 src/com/android/mail/ui/DividedImageCanvas.java 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 @@ -4dp 30dp 2 + 32sp + 12dp + 14dp 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(); + mHeader.displayableSenderNames = new ArrayList(); mHeader.styledSenders = new ArrayList(); 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,8 +120,18 @@ public class ConversationItemViewModel { private String mContentDescription; + /** + * Email address corresponding to the senders that will be displayed in the + * senders field. + */ public ArrayList displayableSenderEmails; + /** + * Display names corresponding to the email address corresponding to the + * senders that will be displayed in the senders field. + */ + public ArrayList 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 styledSenders, ArrayList displayableSenderEmails) { + ArrayList styledSenders, ArrayList displayableSenderNames, + ArrayList 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 styledSenders, - ArrayList displayableSenderEmails) { + ArrayList displayableSenderNames, ArrayList 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. @@ -173,14 +133,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. @@ -251,6 +201,11 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { */ private final LruCache mBitmapHolderCache; + /** + * An LRU cache for photo ids mapped to contact addresses. + */ + private final LruCache mPhotoIdCache; + /** * {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */ @@ -269,11 +224,11 @@ class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback { private final LruCache 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 mPendingRequests = - new ConcurrentHashMap(); + private final ConcurrentHashMap mPendingRequests = + new ConcurrentHashMap(); /** * 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(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(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); 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 iterator = mPendingRequests.keySet().iterator(); + Iterator 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 ? "" : 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); - 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 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 names = new HashSet(); + private void loadEmailAddressBasedPhotos(boolean preloading) { + HashSet addresses = new HashSet(); Set photoIds = new HashSet(); Set photoIdsAsString = new HashSet(); HashMap photoIdMap = new HashMap(); - + 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 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(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 mDivisionIds; + private ArrayList 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 divisionIds) { + mDivisionIds = divisionIds; + mDivisionImages = new ArrayList(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 senders = new ArrayList(); 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 strings = new ArrayList(); ArrayList 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; -- cgit v1.2.3