am 3d99bb36: Disable RTL support

* commit '3d99bb36823013aba84de6917a4a64c6a264f5e9':
  Disable RTL support
diff --git a/proguard.flags b/proguard.flags
index e0b2c23..7bab921 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -60,7 +60,6 @@
 
 -keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
   *** setAnimatedHeightFraction(...);
-  *** setPhotoFlipFraction(...);
 }
 
 -keepclasseswithmembers class com.android.mail.ui.MailActivity {
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 2693db7..0cfb9b2 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -93,10 +93,10 @@
         <item>#ad62a7</item>
     </array>
     <color name="letter_tile_default_color">#d66161</color>
-
     <color name="letter_tile_font_color">#ffffff</color>
-
     <color name="tile_divider_color">#ffffff</color>
+    <!-- Color.GRAY -->
+    <color name="checkmark_tile_background_color">#ff888888</color>
 
     <!-- Teaser colors -->
 
diff --git a/res/values/constants.xml b/res/values/constants.xml
index c238bf7..2f6cfc0 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -119,5 +119,5 @@
     <integer name="ap_overflow_max_count">99</integer>
 
     <!-- Duration of the animations for entering/exiting CAB mode -->
-    <integer name="conv_item_view_cab_anim_duration">350</integer>
+    <integer name="conv_item_view_cab_anim_duration">250</integer>
 </resources>
diff --git a/src/com/android/bitmap/AltBitmapCache.java b/src/com/android/bitmap/AltBitmapCache.java
index fb8e915..f519c7b 100644
--- a/src/com/android/bitmap/AltBitmapCache.java
+++ b/src/com/android/bitmap/AltBitmapCache.java
@@ -16,25 +16,40 @@
 
 package com.android.bitmap;
 
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap.NullReusableBitmap;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.LruCache;
 
 /**
  * This subclass provides custom pool behavior. The pool can be set to block on {@link #poll()} if
  * nothing can be returned. This is useful if you know you will incur high costs upon receiving
  * nothing from the pool, and you do not want to incur those costs at the critical moment when the
  * UI is animating.
+ *
+ * This subclass provides custom cache behavior. Null values can be cached. Later,
+ * when the same key is used to retrieve the value, a {@link NullReusableBitmap} singleton will
+ * be returned.
  */
-public class AltBitmapCache extends AltPooledCache<DecodeTask.Request, ReusableBitmap>
+public class AltBitmapCache extends AltPooledCache<Request, ReusableBitmap>
         implements BitmapCache {
     private boolean mBlocking = false;
     private final Object mLock = new Object();
 
+    private final LruCache<Request, Void> mNullRequests;
+
     private final static boolean DEBUG = false;
     private final static String TAG = LogTag.getLogTag();
 
-    public AltBitmapCache(final int targetSizeBytes, final float nonPooledFraction) {
+    private int mDecodeWidth;
+    private int mDecodeHeight;
+
+    public AltBitmapCache(final int targetSizeBytes, final float nonPooledFraction,
+            final int nullCapacity) {
         super(targetSizeBytes, nonPooledFraction);
+
+        mNullRequests = new LruCache<Request, Void>(nullCapacity);
     }
 
     /**
@@ -78,7 +93,7 @@
                         LogUtils.d(TAG, "AltBitmapCache: %s notified",
                                 Thread.currentThread().getName());
                     }
-                } catch (InterruptedException e) {
+                } catch (InterruptedException ignored) {
                 }
                 Trace.endSection();
             }
@@ -95,4 +110,50 @@
             mLock.notify();
         }
     }
+
+    @Override
+    public ReusableBitmap get(final Request key, final boolean incrementRefCount) {
+        if (mNullRequests.containsKey(key)) {
+            return NullReusableBitmap.getInstance();
+        }
+        return super.get(key, incrementRefCount);
+    }
+
+    @Override
+    public ReusableBitmap put(final Request key, final ReusableBitmap value) {
+        if (value == null || value == NullReusableBitmap.getInstance()) {
+            mNullRequests.put(key, null);
+            return null;
+        }
+
+        // Do not allow the pool to be filled with bitmaps that are of the wrong dimensions.
+        if (mDecodeWidth > value.bmp.getWidth() || mDecodeHeight > value.bmp.getHeight()) {
+            if (DEBUG) {
+                LogUtils.d(TAG, "Discarding ReusableBitmap size %d x %d for cache size %d x %d.",
+                        value.bmp.getWidth(), value.bmp.getHeight(), mDecodeWidth, mDecodeHeight);
+            }
+            return null;
+        }
+
+        return super.put(key, value);
+    }
+
+    @Override
+    public void setPoolDimensions(final int decodeWidth, final int decodeHeight) {
+        if (mDecodeWidth < decodeWidth || mDecodeHeight < decodeHeight) {
+            clear();
+            mDecodeWidth = decodeWidth;
+            mDecodeHeight = decodeHeight;
+        }
+    }
+
+    @Override
+    public int getDecodeWidth() {
+        return mDecodeWidth;
+    }
+
+    @Override
+    public int getDecodeHeight() {
+        return mDecodeHeight;
+    }
 }
diff --git a/src/com/android/bitmap/AltPooledCache.java b/src/com/android/bitmap/AltPooledCache.java
index 0a7d7b4..03a6c50 100644
--- a/src/com/android/bitmap/AltPooledCache.java
+++ b/src/com/android/bitmap/AltPooledCache.java
@@ -67,6 +67,7 @@
 
     @Override
     public V get(K key, boolean incrementRefCount) {
+        Trace.beginSection("cache get");
         synchronized (mCache) {
             V result = mCache.get(key);
             if (result == null && mNonPooledCache != null) {
@@ -75,12 +76,14 @@
             if (incrementRefCount && result != null) {
                 result.acquireReference();
             }
+            Trace.endSection();
             return result;
         }
     }
 
     @Override
     public V put(K key, V value) {
+        Trace.beginSection("cache put");
         synchronized (mCache) {
             final V prev;
             if (value.isEligibleForPooling()) {
@@ -90,22 +93,27 @@
             } else {
                 prev = null;
             }
+            Trace.endSection();
             return prev;
         }
     }
 
     @Override
     public void offer(V value) {
+        Trace.beginSection("pool offer");
         if (value.getRefCount() != 0 || !value.isEligibleForPooling()) {
             throw new IllegalArgumentException("unexpected offer of an invalid object: " + value);
         }
         mPool.offer(value);
+        Trace.endSection();
     }
 
     @Override
     public V poll() {
+        Trace.beginSection("pool poll");
         final V pooled = mPool.poll();
         if (pooled != null) {
+            Trace.endSection();
             return pooled;
         }
 
@@ -131,11 +139,13 @@
                 if (DEBUG) System.err.println(
                         "POOL SCAVENGE FAILED, cache not fully warm yet. szDelta="
                         + (mTargetSize-unrefSize));
+                Trace.endSection();
                 return null;
             } else {
                 mCache.remove(eldestUnref.getKey());
                 if (DEBUG) System.err.println("POOL SCAVENGE SUCCESS, oldKey="
                         + eldestUnref.getKey());
+                Trace.endSection();
                 return eldestUnref.getValue();
             }
         }
@@ -209,4 +219,9 @@
 
     }
 
+    @Override
+    public void clear() {
+        mCache.clear();
+        mPool.clear();
+    }
 }
diff --git a/src/com/android/bitmap/BitmapCache.java b/src/com/android/bitmap/BitmapCache.java
index d671c17..fc76c3a 100644
--- a/src/com/android/bitmap/BitmapCache.java
+++ b/src/com/android/bitmap/BitmapCache.java
@@ -19,4 +19,7 @@
 public interface BitmapCache extends PooledCache<DecodeTask.Request, ReusableBitmap> {
 
     void setBlocking(boolean blocking);
+    void setPoolDimensions(int decodeWidth, int decodeHeight);
+    int getDecodeWidth();
+    int getDecodeHeight();
 }
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
index 2210ec6..d7a3cab 100644
--- a/src/com/android/bitmap/DecodeTask.java
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -54,6 +54,7 @@
     public interface Request {
         AssetFileDescriptor createFd() throws IOException;
         InputStream createInputStream() throws IOException;
+        boolean hasOrientationExif() throws IOException;
     }
 
     /**
@@ -65,8 +66,6 @@
          * may have been preempted by the scheduler or queued up by a bottlenecked executor.
          * <p>
          * N.B. this method runs on the UI thread.
-         *
-         * @param key
          */
         void onDecodeBegin(Request key);
         void onDecodeComplete(Request key, ReusableBitmap result);
@@ -86,13 +85,17 @@
 
     @Override
     protected ReusableBitmap doInBackground(Void... params) {
+        // enqueue the 'onDecodeBegin' signal on the main thread
+        publishProgress();
+
+        return decode();
+    }
+
+    public ReusableBitmap decode() {
         if (isCancelled()) {
             return null;
         }
 
-        // enqueue the 'onDecodeBegin' signal on the main thread
-        publishProgress();
-
         ReusableBitmap result = null;
         AssetFileDescriptor fd = null;
         InputStream in = null;
@@ -130,20 +133,26 @@
             Trace.endSection();
 
             Trace.beginSection("get orientation");
-            if (fd != null) {
-                // Creating an input stream from the file descriptor makes it useless afterwards.
-                Trace.beginSection("create fd and stream");
-                final AssetFileDescriptor orientationFd = mKey.createFd();
-                in = orientationFd.createInputStream();
-                Trace.endSection();
-            }
-            final int orientation = Exif.getOrientation(in, byteSize);
-            if (fd != null) {
-                try {
-                    // Close the temporary file descriptor.
-                    in.close();
-                } catch (IOException ex) {
+            final int orientation;
+            if (mKey.hasOrientationExif()) {
+                if (fd != null) {
+                    // Creating an input stream from the file descriptor makes it useless
+                    // afterwards.
+                    Trace.beginSection("create fd and stream");
+                    final AssetFileDescriptor orientationFd = mKey.createFd();
+                    in = orientationFd.createInputStream();
+                    Trace.endSection();
                 }
+                orientation = Exif.getOrientation(in, byteSize);
+                if (fd != null) {
+                    try {
+                        // Close the temporary file descriptor.
+                        in.close();
+                    } catch (IOException ignored) {
+                    }
+                }
+            } else {
+                orientation = 0;
             }
             final boolean isNotRotatedOr180 = orientation == 0 || orientation == 180;
             Trace.endSection();
@@ -246,6 +255,7 @@
                 }
             }
 
+            //noinspection PointlessBooleanExpression
             if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
                 try {
                     Trace.beginSection("decode" + mOpts.inSampleSize);
@@ -307,13 +317,13 @@
             if (fd != null) {
                 try {
                     fd.close();
-                } catch (IOException e) {
+                } catch (IOException ignored) {
                 }
             }
             if (in != null) {
                 try {
                     in.close();
-                } catch (IOException e) {
+                } catch (IOException ignored) {
                 }
             }
             if (result != null) {
@@ -388,7 +398,7 @@
         } else {
             try {
                 in.close();
-            } catch (IOException ex) {
+            } catch (IOException ignored) {
             }
             in = mKey.createInputStream();
         }
@@ -414,6 +424,7 @@
         // round to the nearest power of two, or just truncate
         final boolean stricter = true;
 
+        //noinspection ConstantConditions
         if (stricter) {
             result = (int) Math.pow(2, (int) (0.5 + (Math.log(sz) / Math.log(2))));
         } else {
diff --git a/src/com/android/bitmap/PooledCache.java b/src/com/android/bitmap/PooledCache.java
index 62381b8..6d6684f 100644
--- a/src/com/android/bitmap/PooledCache.java
+++ b/src/com/android/bitmap/PooledCache.java
@@ -24,4 +24,14 @@
     V poll();
     String toDebugString();
 
+    /**
+     * Purge existing Poolables from the pool+cache. Usually, this is done when situations
+     * change and the items in the pool+cache are no longer appropriate. For example,
+     * if the layout changes, the pool+cache may need to hold larger bitmaps.
+     *
+     * <p/>
+     * The existing Poolables will be garbage collected when they are no longer being referenced
+     * by other objects.
+     */
+    void clear();
 }
diff --git a/src/com/android/bitmap/ReusableBitmap.java b/src/com/android/bitmap/ReusableBitmap.java
index 2e0199f..dde9bd1 100644
--- a/src/com/android/bitmap/ReusableBitmap.java
+++ b/src/com/android/bitmap/ReusableBitmap.java
@@ -115,4 +115,37 @@
         return sb.toString();
     }
 
+    /**
+     * Singleton class to represent a null Bitmap. We don't want to just use a regular
+     * ReusableBitmap with a null bmp field because that will render that ReusableBitmap useless
+     * and unable to be used by another decode process.
+     */
+    public final static class NullReusableBitmap extends ReusableBitmap {
+        private static NullReusableBitmap sInstance;
+
+        /**
+         * Get a singleton.
+         */
+        public static NullReusableBitmap getInstance() {
+            if (sInstance == null) {
+                sInstance = new NullReusableBitmap();
+            }
+            return sInstance;
+        }
+
+        private NullReusableBitmap() {
+            super(null /* bmp */, false /* reusable */);
+        }
+
+        @Override
+        public int getByteCount() {
+            return 0;
+        }
+
+        @Override
+        public void releaseReference() { }
+
+        @Override
+        public void acquireReference() { }
+    }
 }
diff --git a/src/com/android/mail/ContactInfo.java b/src/com/android/mail/ContactInfo.java
index b7cecb8..a59dc62 100644
--- a/src/com/android/mail/ContactInfo.java
+++ b/src/com/android/mail/ContactInfo.java
@@ -47,6 +47,6 @@
 
     @Override
     public String toString() {
-        return "{status=" + status + " photo=" + photo + "}";
+        return "{status=" + status + " photo=" + (photo != null ? photo : photoBytes) + "}";
     }
 }
diff --git a/src/com/android/mail/SenderInfoLoader.java b/src/com/android/mail/SenderInfoLoader.java
index 5d01ad4..c69e934 100644
--- a/src/com/android/mail/SenderInfoLoader.java
+++ b/src/com/android/mail/SenderInfoLoader.java
@@ -17,6 +17,7 @@
 
 package com.android.mail;
 
+import com.android.bitmap.Trace;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Maps;
 
@@ -99,40 +100,53 @@
     /**
      * Loads contact photos from the ContentProvider.
      * @param resolver {@link ContentResolver} to use in queries to the ContentProvider.
-     * @param senderSet The email addresses of the sender images to return.
+     * @param emails The email addresses of the sender images to return.
      * @param decodeBitmaps If {@code true}, decode the bitmaps and put them into
      *                      {@link ContactInfo}. Otherwise, just put the raw bytes of the photo
      *                      into the {@link ContactInfo}.
-     * @return A mapping of email addresses to {@link ContactInfo}s. The {@link ContactInfo} will
-     * contain either a byte array or an actual decoded bitmap for the sender image.
+     * @return A mapping of email to {@link ContactInfo}. How to interpret the map:
+     * <ul>
+     *     <li>The email is missing from the key set or maps to null - The email was skipped. Try
+     *     again.</li>
+     *     <li>Either {@link ContactInfo#photoBytes} or {@link ContactInfo#photo} is non-null -
+     *     Photo loaded successfully.</li>
+     *     <li>Both {@link ContactInfo#photoBytes} and {@link ContactInfo#photo} are null -
+     *     Photo load failed.</li>
+     * </ul>
      */
     public static ImmutableMap<String, ContactInfo> loadContactPhotos(
-            final ContentResolver resolver, final Set<String> senderSet,
-            final boolean decodeBitmaps) {
+            final ContentResolver resolver, final Set<String> emails, final boolean decodeBitmaps) {
+        Trace.beginSection("load contact photos util");
         Cursor cursor = null;
 
+        Trace.beginSection("build first query");
         Map<String, ContactInfo> results = Maps.newHashMap();
 
         // temporary structures
         Map<Long, Pair<String, ContactInfo>> photoIdMap = Maps.newHashMap();
         ArrayList<String> photoIdsAsStrings = new ArrayList<String>();
-        ArrayList<String> senders = getTruncatedQueryParams(senderSet);
+        ArrayList<String> emailsList = getTruncatedQueryParams(emails);
 
         // Build first query
         StringBuilder query = new StringBuilder()
                 .append(Data.MIMETYPE).append("='").append(Email.CONTENT_ITEM_TYPE)
                 .append("' AND ").append(Email.DATA).append(" IN (");
-        appendQuestionMarks(query, senders);
+        appendQuestionMarks(query, emailsList);
         query.append(')');
+        Trace.endSection();
 
         try {
+            Trace.beginSection("query 1");
             cursor = resolver.query(Data.CONTENT_URI, DATA_COLS,
-                    query.toString(), toStringArray(senders), null /* sortOrder */);
+                    query.toString(), toStringArray(emailsList), null /* sortOrder */);
+            Trace.endSection();
 
             if (cursor == null) {
+                Trace.endSection();
                 return null;
             }
 
+            Trace.beginSection("get photo id");
             int i = -1;
             while (cursor.moveToPosition(++i)) {
                 String email = cursor.getString(DATA_EMAIL_COLUMN);
@@ -154,11 +168,23 @@
                 results.put(email, result);
             }
             cursor.close();
+            Trace.endSection();
+
+            // Put empty ContactInfo for all the emails that didn't map to a contact.
+            // This allows us to differentiate between lookup failed,
+            // and lookup skipped (truncated above).
+            for (String email : emailsList) {
+                if (!results.containsKey(email)) {
+                    results.put(email, new ContactInfo(null, null));
+                }
+            }
 
             if (photoIdsAsStrings.isEmpty()) {
+                Trace.endSection();
                 return ImmutableMap.copyOf(results);
             }
 
+            Trace.beginSection("build second query");
             // Build second query: photoIDs->blobs
             // based on photo batch-select code in ContactPhotoManager
             photoIdsAsStrings = getTruncatedQueryParams(photoIdsAsStrings);
@@ -166,14 +192,19 @@
             query.append(Photo._ID).append(" IN (");
             appendQuestionMarks(query, photoIdsAsStrings);
             query.append(')');
+            Trace.endSection();
 
+            Trace.beginSection("query 2");
             cursor = resolver.query(Data.CONTENT_URI, PHOTO_COLS,
                     query.toString(), toStringArray(photoIdsAsStrings), null /* sortOrder */);
+            Trace.endSection();
 
             if (cursor == null) {
+                Trace.endSection();
                 return ImmutableMap.copyOf(results);
             }
 
+            Trace.beginSection("get photo blob");
             i = -1;
             while (cursor.moveToPosition(++i)) {
                 byte[] photoBytes = cursor.getBlob(PHOTO_PHOTO_COLUMN);
@@ -187,7 +218,9 @@
                 ContactInfo prevResult = prev.second;
 
                 if (decodeBitmaps) {
+                    Trace.beginSection("decode bitmap");
                     Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
+                    Trace.endSection();
                     // overwrite existing photo-less result
                     results.put(email,
                             new ContactInfo(prevResult.contactUri, prevResult.status, photo));
@@ -197,12 +230,14 @@
                             prevResult.contactUri, prevResult.status, photoBytes));
                 }
             }
+            Trace.endSection();
         } finally {
             if (cursor != null) {
                 cursor.close();
             }
         }
 
+        Trace.endSection();
         return ImmutableMap.copyOf(results);
     }
 
diff --git a/src/com/android/mail/bitmap/AttachmentDrawable.java b/src/com/android/mail/bitmap/AttachmentDrawable.java
index 0252455..49e4008 100644
--- a/src/com/android/mail/bitmap/AttachmentDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentDrawable.java
@@ -147,11 +147,13 @@
         // requests for different renditions of the same attachment
         final boolean onlyRenditionChange = (mCurrKey != null && mCurrKey.matches(key));
 
+        Trace.beginSection("release reference");
         if (mBitmap != null && !onlyRenditionChange) {
             mBitmap.releaseReference();
 //            System.out.println("view.bind() decremented ref to old bitmap: " + mBitmap);
             mBitmap = null;
         }
+        Trace.endSection();
         if (mCurrKey != null && SwipeableListView.ENABLE_ATTACHMENT_DECODE_AGGREGATOR) {
             mDecodeAggregator.forget(mCurrKey);
         }
@@ -169,6 +171,7 @@
         setLoadState(LOAD_STATE_UNINITIALIZED);
 
         if (key == null) {
+            invalidateSelf();
             Trace.endSection();
             return;
         }
@@ -200,7 +203,7 @@
             return;
         }
 
-        if (mBitmap != null) {
+        if (mBitmap != null && mBitmap.bmp != null) {
             BitmapUtils
                     .calculateCroppedSrcRect(mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(),
                             bounds.width(), bounds.height(),
diff --git a/src/com/android/mail/bitmap/AttachmentGridDrawable.java b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
index f5d28ed..c1e5789 100644
--- a/src/com/android/mail/bitmap/AttachmentGridDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentGridDrawable.java
@@ -2,6 +2,7 @@
 
 import android.content.res.Resources;
 import android.graphics.Canvas;
+import android.graphics.ColorFilter;
 import android.graphics.Paint;
 import android.graphics.Paint.Align;
 import android.graphics.Rect;
@@ -47,7 +48,7 @@
     }
 
     @Override
-    protected AttachmentDrawable createDivisionDrawable() {
+    protected AttachmentDrawable createDivisionDrawable(final int i) {
         final AttachmentDrawable result = new AttachmentDrawable(mResources, mCache,
                 mDecodeAggregator, mCoordinates, mPlaceholder, mProgress);
         return result;
@@ -105,8 +106,24 @@
     }
 
     @Override
+    public void setAlpha(final int alpha) {
+        super.setAlpha(alpha);
+        final int old = mPaint.getAlpha();
+        mPaint.setAlpha(alpha);
+        if (alpha != old) {
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setColorFilter(final ColorFilter cf) {
+        super.setColorFilter(cf);
+        mPaint.setColorFilter(cf);
+        invalidateSelf();
+    }
+
+    @Override
     public void setParallaxFraction(float fraction) {
         mParallaxFraction = fraction;
     }
-
 }
diff --git a/src/com/android/mail/bitmap/CompositeDrawable.java b/src/com/android/mail/bitmap/CompositeDrawable.java
index e25bfc7..e372bb5 100644
--- a/src/com/android/mail/bitmap/CompositeDrawable.java
+++ b/src/com/android/mail/bitmap/CompositeDrawable.java
@@ -21,11 +21,13 @@
 public abstract class CompositeDrawable<T extends Drawable> extends Drawable
         implements Drawable.Callback {
 
+    public static final int MAX_COMPOSITE_DRAWABLES = 4;
+
     protected final List<T> mDrawables;
     protected int mCount;
 
     public CompositeDrawable(int maxDivisions) {
-        if (maxDivisions >= 4) {
+        if (maxDivisions > MAX_COMPOSITE_DRAWABLES) {
             throw new IllegalArgumentException("CompositeDrawable only supports 4 divisions");
         }
         mDrawables = new ArrayList<T>(maxDivisions);
@@ -35,7 +37,7 @@
         mCount = 0;
     }
 
-    protected abstract T createDivisionDrawable();
+    protected abstract T createDivisionDrawable(final int i);
 
     public void setCount(int count) {
         // zero out the composite bounds, which will propagate to the division drawables
@@ -56,7 +58,7 @@
         T result = mDrawables.get(i);
         if (result == null) {
             Trace.beginSection("create division drawable");
-            result = createDivisionDrawable();
+            result = createDivisionDrawable(i);
             mDrawables.set(i, result);
             result.setCallback(this);
             // Make sure drawables created after the bounds were already set have their bounds
@@ -109,6 +111,11 @@
 
     @Override
     public void draw(Canvas canvas) {
+        final Rect bounds = getBounds();
+        if (!isVisible() || bounds.isEmpty()) {
+            return;
+        }
+
         for (int i = 0; i < mCount; i++) {
             mDrawables.get(i).draw(canvas);
         }
@@ -132,10 +139,7 @@
     public int getOpacity() {
         int opacity = PixelFormat.OPAQUE;
         for (int i = 0; i < mCount; i++) {
-            if (mDrawables.get(i).getOpacity() != PixelFormat.OPAQUE) {
-                opacity = PixelFormat.TRANSLUCENT;
-                break;
-            }
+            opacity = resolveOpacity(opacity, mDrawables.get(i).getOpacity());
         }
         return opacity;
     }
diff --git a/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java b/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java
new file mode 100644
index 0000000..134439d
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactCheckableGridDrawable.java
@@ -0,0 +1,241 @@
+/*
+ * 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.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R.color;
+import com.android.mail.R.drawable;
+
+/**
+ * Custom FlipDrawable which has a {@link ContactGridDrawable} on the front,
+ * and a {@link CheckmarkDrawable} on the back.
+ */
+public class ContactCheckableGridDrawable extends FlipDrawable implements AnimatorUpdateListener {
+
+    private final ContactGridDrawable mContactGridDrawable;
+    private final CheckmarkDrawable mCheckmarkDrawable;
+
+    private final ValueAnimator mCheckmarkScaleAnimator;
+    private final ValueAnimator mCheckmarkAlphaAnimator;
+
+    private static final int POST_FLIP_DURATION_MS = 150;
+
+    private static final float CHECKMARK_SCALE_BEGIN_VALUE = 0.2f;
+    private static final float CHECKMARK_ALPHA_BEGIN_VALUE = 0f;
+
+    /** Must be <= 1f since the animation value is used as a percentage. */
+    private static final float END_VALUE = 1f;
+
+    public ContactCheckableGridDrawable(final Resources res, final int flipDurationMs) {
+        super(new ContactGridDrawable(res), new CheckmarkDrawable(res), flipDurationMs,
+                0 /* preFlipDurationMs */, POST_FLIP_DURATION_MS);
+
+        mContactGridDrawable = (ContactGridDrawable) mFront;
+        mCheckmarkDrawable = (CheckmarkDrawable) mBack;
+
+        // We will create checkmark animations that are synchronized with the flipping animation.
+        // The entire delay + duration of the checkmark animation needs to equal the entire
+        // duration of the flip animation (where delay is 0).
+
+        // The checkmark animation is in effect only when the back drawable is being shown.
+        // For the flip animation duration    <pre>[_][]|[][_]<post>
+        // The checkmark animation will be    |--delay--|-duration-|
+
+        // Need delay to skip the first half of the flip duration.
+        final long animationDelay = mPreFlipDurationMs + mFlipDurationMs / 2;
+        // Actual duration is the second half of the flip duration.
+        final long animationDuration = mFlipDurationMs / 2 + mPostFlipDurationMs;
+
+        mCheckmarkScaleAnimator = ValueAnimator.ofFloat(CHECKMARK_SCALE_BEGIN_VALUE, END_VALUE)
+                .setDuration(animationDuration);
+        mCheckmarkScaleAnimator.setStartDelay(animationDelay);
+        mCheckmarkScaleAnimator.addUpdateListener(this);
+
+        mCheckmarkAlphaAnimator = ValueAnimator.ofFloat(CHECKMARK_ALPHA_BEGIN_VALUE, END_VALUE)
+                .setDuration(animationDuration);
+        mCheckmarkAlphaAnimator.setStartDelay(animationDelay);
+        mCheckmarkAlphaAnimator.addUpdateListener(this);
+    }
+
+    @Override
+    public void reset(final boolean side) {
+        super.reset(side);
+        if (mCheckmarkScaleAnimator == null) {
+            // Call from super's constructor. Not yet initialized.
+            return;
+        }
+        mCheckmarkScaleAnimator.cancel();
+        mCheckmarkAlphaAnimator.cancel();
+        mCheckmarkDrawable.setScaleAnimatorValue(side ? CHECKMARK_SCALE_BEGIN_VALUE : END_VALUE);
+        mCheckmarkDrawable.setAlphaAnimatorValue(side ? CHECKMARK_ALPHA_BEGIN_VALUE : END_VALUE);
+    }
+
+    @Override
+    public void flip() {
+        super.flip();
+        // Keep the checkmark animators in sync with the flip animator.
+        if (mCheckmarkScaleAnimator.isStarted()) {
+            mCheckmarkScaleAnimator.reverse();
+            mCheckmarkAlphaAnimator.reverse();
+        } else {
+            if (!getSideFlippingTowards() /* front to back */) {
+                mCheckmarkScaleAnimator.start();
+                mCheckmarkAlphaAnimator.start();
+            } else /* back to front */ {
+                mCheckmarkScaleAnimator.reverse();
+                mCheckmarkAlphaAnimator.reverse();
+            }
+        }
+    }
+
+    public ContactDrawable getOrCreateDrawable(final int i) {
+        return mContactGridDrawable.getOrCreateDrawable(i);
+    }
+
+    public void setBitmapCache(final BitmapCache cache) {
+        mContactGridDrawable.setBitmapCache(cache);
+    }
+
+    public void setContactResolver(final ContactResolver contactResolver) {
+        mContactGridDrawable.setContactResolver(contactResolver);
+    }
+
+    public int getCount() {
+        return mContactGridDrawable.getCount();
+    }
+
+    public void setCount(final int count) {
+        mContactGridDrawable.setCount(count);
+        // Side effect needs to happen here too.
+        setBounds(0, 0, 0, 0);
+    }
+
+    @Override
+    public void onAnimationUpdate(final ValueAnimator animation) {
+        //noinspection ConstantConditions
+        final float value = (Float) animation.getAnimatedValue();
+
+        if (animation == mCheckmarkScaleAnimator) {
+            mCheckmarkDrawable.setScaleAnimatorValue(value);
+        } else if (animation == mCheckmarkAlphaAnimator) {
+            mCheckmarkDrawable.setAlphaAnimatorValue(value);
+        }
+    }
+
+    /**
+     * Meant to be used as the with a FlipDrawable. The animator driving this Drawable should be
+     * more or less in sync with the containing FlipDrawable's flip animator.
+     */
+    private static class CheckmarkDrawable extends Drawable {
+
+        private static Bitmap CHECKMARK;
+        private static int sBackgroundColor;
+
+        private final Paint mPaint;
+
+        private float mScaleFraction;
+        private float mAlphaFraction;
+
+        private static final Matrix sMatrix = new Matrix();
+
+        public CheckmarkDrawable(final Resources res) {
+            if (CHECKMARK == null) {
+                CHECKMARK = BitmapFactory.decodeResource(res, drawable.ic_avatar_check);
+                sBackgroundColor = res.getColor(color.checkmark_tile_background_color);
+            }
+            mPaint = new Paint();
+            mPaint.setAntiAlias(true);
+            mPaint.setFilterBitmap(true);
+            mPaint.setColor(sBackgroundColor);
+        }
+
+        @Override
+        public void draw(final Canvas canvas) {
+            final Rect bounds = getBounds();
+            if (!isVisible() || bounds.isEmpty()) {
+                return;
+            }
+
+            canvas.drawRect(getBounds(), mPaint);
+
+            // Scale the checkmark.
+            sMatrix.reset();
+            sMatrix.setScale(mScaleFraction, mScaleFraction, CHECKMARK.getWidth() / 2,
+                    CHECKMARK.getHeight() / 2);
+            sMatrix.postTranslate(bounds.centerX() - CHECKMARK.getWidth() / 2,
+                    bounds.centerY() - CHECKMARK.getHeight() / 2);
+
+            // Fade the checkmark.
+            final int oldAlpha = mPaint.getAlpha();
+            // Interpolate the alpha.
+            mPaint.setAlpha((int) (oldAlpha * mAlphaFraction));
+            canvas.drawBitmap(CHECKMARK, sMatrix, mPaint);
+            // Restore the alpha.
+            mPaint.setAlpha(oldAlpha);
+        }
+
+        @Override
+        public void setAlpha(final int alpha) {
+            mPaint.setAlpha(alpha);
+        }
+
+        @Override
+        public void setColorFilter(final ColorFilter cf) {
+            mPaint.setColorFilter(cf);
+        }
+
+        @Override
+        public int getOpacity() {
+            // Always a gray background.
+            return PixelFormat.OPAQUE;
+        }
+
+        /**
+         * Set value as a fraction from 0f to 1f.
+         */
+        public void setScaleAnimatorValue(final float value) {
+            final float old = mScaleFraction;
+            mScaleFraction = value;
+            if (old != mScaleFraction) {
+                invalidateSelf();
+            }
+        }
+
+        /**
+         * Set value as a fraction from 0f to 1f.
+         */
+        public void setAlphaAnimatorValue(final float value) {
+            final float old = mAlphaFraction;
+            mAlphaFraction = value;
+            if (old != mAlphaFraction) {
+                invalidateSelf();
+            }
+        }
+    }
+}
diff --git a/src/com/android/mail/bitmap/ContactDrawable.java b/src/com/android/mail/bitmap/ContactDrawable.java
new file mode 100644
index 0000000..34aa682
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactDrawable.java
@@ -0,0 +1,252 @@
+/*
+ * 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.bitmap;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.DecodeTask.Request;
+import com.android.bitmap.ReusableBitmap;
+import com.android.mail.R;
+
+/**
+ * A drawable that encapsulates all the functionality needed to display a contact image,
+ * including request creation/cancelling and data unbinding/re-binding. While no contact images
+ * can be shown, a default letter tile will be shown instead.
+ *
+ * <p/>
+ * The actual contact resolving and decoding is handled by {@link ContactResolver}.
+ */
+public class ContactDrawable extends Drawable {
+
+    private final BitmapCache mCache;
+    private final ContactResolver mContactResolver;
+
+    private ContactRequest mContactRequest;
+    private ReusableBitmap mBitmap;
+    private final Paint mPaint;
+    private int mScale;
+
+    /** Letter tile */
+    private static TypedArray sColors;
+    private static int sDefaultColor;
+    private static int sTileLetterFontSize;
+    private static int sTileLetterFontSizeSmall;
+    private static int sTileFontColor;
+    private static Bitmap DEFAULT_AVATAR;
+    /** Reusable components to avoid new allocations */
+    private static final Paint sPaint = new Paint();
+    private static final Rect sRect = new Rect();
+    private static final char[] sFirstChar = new char[1];
+
+    /** This should match the total number of colors defined in colors.xml for letter_tile_color */
+    private static final int NUM_OF_TILE_COLORS = 8;
+
+    public ContactDrawable(final Resources res, final BitmapCache cache,
+            final ContactResolver contactResolver) {
+        mCache = cache;
+        mContactResolver = contactResolver;
+        mPaint = new Paint();
+        mPaint.setFilterBitmap(true);
+        mPaint.setDither(true);
+
+        if (sColors == null) {
+            sColors = res.obtainTypedArray(R.array.letter_tile_colors);
+            sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+            sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
+            sTileLetterFontSizeSmall = res
+                    .getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+            sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+            DEFAULT_AVATAR = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
+
+            sPaint.setTypeface(Typeface.create("sans-serif-light", Typeface.NORMAL));
+            sPaint.setTextAlign(Align.CENTER);
+            sPaint.setAntiAlias(true);
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Rect bounds = getBounds();
+        if (!isVisible() || bounds.isEmpty()) {
+            return;
+        }
+
+        if (mBitmap != null && mBitmap.bmp != null) {
+            // Draw sender image.
+            drawBitmap(mBitmap.bmp, mBitmap.getLogicalWidth(), mBitmap.getLogicalHeight(), canvas);
+        } else {
+            // Draw letter tile.
+            drawLetterTile(canvas);
+        }
+    }
+
+    /**
+     * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
+     */
+    private void drawBitmap(final Bitmap bitmap, final int width, final int height,
+            final Canvas canvas) {
+        final Rect bounds = getBounds();
+        
+        if (mScale != ContactGridDrawable.SCALE_TYPE_HALF) {
+            sRect.set(0, 0, width, height);
+        } else {
+            // For skinny bounds, draw the middle two quarters.
+            sRect.set(width / 4, 0, width / 4 * 3, height);
+        }
+        canvas.drawBitmap(bitmap, sRect, bounds, mPaint);
+    }
+
+    private void drawLetterTile(final Canvas canvas) {
+        if (mContactRequest == null) {
+            return;
+        }
+
+        // Draw background color.
+        final String email = mContactRequest.getEmail();
+        sPaint.setColor(pickColor(email));
+        sPaint.setAlpha(mPaint.getAlpha());
+        canvas.drawRect(getBounds(), sPaint);
+
+        // Draw letter/digit or generic avatar.
+        final String displayName = mContactRequest.getDisplayName();
+        final char firstChar = displayName.charAt(0);
+        final Rect bounds = getBounds();
+        if (isEnglishLetterOrDigit(firstChar)) {
+            // Draw letter or digit.
+            sFirstChar[0] = Character.toUpperCase(firstChar);
+            sPaint.setTextSize(mScale == ContactGridDrawable.SCALE_TYPE_ONE ? sTileLetterFontSize
+                    : sTileLetterFontSizeSmall);
+            sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+            sPaint.setColor(sTileFontColor);
+            canvas.drawText(sFirstChar, 0, 1, bounds.centerX(),
+                    bounds.centerY() + sRect.height() / 2, sPaint);
+        } else {
+            drawBitmap(DEFAULT_AVATAR, DEFAULT_AVATAR.getWidth(), DEFAULT_AVATAR.getHeight(),
+                    canvas);
+        }
+    }
+
+    private static int pickColor(final String email) {
+        // String.hashCode() implementation is not supposed to change across java versions, so
+        // this should guarantee the same email address always maps to the same color.
+        // The email should already have been normalized by the ContactRequest.
+        final int color = Math.abs(email.hashCode()) % NUM_OF_TILE_COLORS;
+        return sColors.getColor(color, sDefaultColor);
+    }
+
+    private static boolean isEnglishLetterOrDigit(final char c) {
+        return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9');
+    }
+
+    @Override
+    public void setAlpha(final int alpha) {
+        mPaint.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(final ColorFilter cf) {
+        mPaint.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return 0;
+    }
+
+    public void setDecodeDimensions(final int decodeWidth, final int decodeHeight) {
+        mCache.setPoolDimensions(decodeWidth, decodeHeight);
+    }
+
+    public void setScale(final int scale) {
+        mScale = scale;
+    }
+
+    public void unbind() {
+        setImage(null);
+    }
+
+    public void bind(final String name, final String email) {
+        setImage(new ContactRequest(name, email));
+    }
+
+    private void setImage(final ContactRequest contactRequest) {
+        if (mContactRequest != null && mContactRequest.equals(contactRequest)) {
+            return;
+        }
+
+        if (mBitmap != null) {
+            mBitmap.releaseReference();
+            mBitmap = null;
+        }
+
+        mContactResolver.remove(mContactRequest, this);
+        mContactRequest = contactRequest;
+
+        if (contactRequest == null) {
+            invalidateSelf();
+            return;
+        }
+
+        final ReusableBitmap cached = mCache.get(contactRequest, true /* incrementRefCount */);
+        if (cached != null) {
+            setBitmap(cached);
+        } else {
+            decode();
+        }
+    }
+
+    private void setBitmap(final ReusableBitmap bmp) {
+        if (mBitmap != null && mBitmap != bmp) {
+            mBitmap.releaseReference();
+        }
+        mBitmap = bmp;
+        invalidateSelf();
+    }
+
+    private void decode() {
+        if (mContactRequest == null) {
+            return;
+        }
+        // Add to batch.
+        mContactResolver.add(mContactRequest, this);
+    }
+
+    public void onDecodeComplete(final Request key, final ReusableBitmap result) {
+        final ContactRequest request = (ContactRequest) key;
+        // Remove from batch.
+        mContactResolver.remove(request, this);
+        if (request.equals(mContactRequest)) {
+            setBitmap(result);
+        } else {
+            // if the requests don't match (i.e. this request is stale), decrement the
+            // ref count to allow the bitmap to be pooled
+            if (result != null) {
+                result.releaseReference();
+            }
+        }
+    }
+}
diff --git a/src/com/android/mail/bitmap/ContactGridDrawable.java b/src/com/android/mail/bitmap/ContactGridDrawable.java
new file mode 100644
index 0000000..590f806
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactGridDrawable.java
@@ -0,0 +1,162 @@
+/*
+ * 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.bitmap;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.Rect;
+
+import com.android.bitmap.BitmapCache;
+import com.android.mail.R;
+
+/**
+ * A 2x2 grid of contact drawables. Adds horizontal and vertical dividers.
+ */
+public class ContactGridDrawable extends CompositeDrawable<ContactDrawable> {
+
+    public static final int SCALE_TYPE_ONE = 0;
+    public static final int SCALE_TYPE_HALF = 1;
+    public static final int SCALE_TYPE_QUARTER = 2;
+
+    private static final int MAX_CONTACTS_COUNT = 4;
+
+    private ContactResolver mContactResolver;
+    private BitmapCache mCache;
+    private final Resources mRes;
+    private Paint mPaint;
+
+    private static int sDividerWidth = -1;
+    private static int sDividerColor;
+
+    public ContactGridDrawable(final Resources res) {
+        super(MAX_CONTACTS_COUNT);
+
+        if (sDividerWidth == -1) {
+            sDividerWidth = res.getDimensionPixelSize(R.dimen.tile_divider_width);
+            sDividerColor = res.getColor(R.color.tile_divider_color);
+        }
+
+        mRes = res;
+        mPaint = new Paint();
+        mPaint.setStrokeWidth(sDividerWidth);
+        mPaint.setColor(sDividerColor);
+    }
+
+    @Override
+    protected ContactDrawable createDivisionDrawable(final int i) {
+        final ContactDrawable drawable = new ContactDrawable(mRes, mCache, mContactResolver);
+        drawable.setScale(calculateScale(i));
+        return drawable;
+    }
+
+    @Override
+    public void setCount(final int count) {
+        super.setCount(count);
+
+        for (int i = 0; i < mCount; i++) {
+            final ContactDrawable drawable = mDrawables.get(i);
+            if (drawable != null) {
+                drawable.setScale(calculateScale(i));
+            }
+        }
+    }
+
+    /**
+     * Given which section a drawable is in, calculate its scale based on the current total count.
+     * @param i The section, indexed by 0.
+     */
+    private int calculateScale(final int i) {
+        switch (mCount) {
+            case 1:
+                // 1 bitmap: passthrough
+                return SCALE_TYPE_ONE;
+            case 2:
+                // 2 bitmaps split vertically
+                return SCALE_TYPE_HALF;
+            case 3:
+                // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+                return i == 0 ? SCALE_TYPE_HALF : SCALE_TYPE_QUARTER;
+            case 4:
+                // 4 bitmaps in a 2x2 grid
+                return SCALE_TYPE_QUARTER;
+            default:
+                return SCALE_TYPE_ONE;
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        super.draw(canvas);
+
+        final Rect bounds = getBounds();
+        if (!isVisible() || bounds.isEmpty()) {
+            return;
+        }
+
+        // Draw horizontal and vertical dividers.
+        switch (mCount) {
+            case 1:
+                // 1 bitmap: passthrough
+                break;
+            case 2:
+                // 2 bitmaps split vertically
+                canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+                        mPaint);
+                break;
+            case 3:
+                // 1st is tall on the left, 2nd/3rd stacked vertically on the right
+                canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+                        mPaint);
+                canvas.drawLine(bounds.centerX(), bounds.centerY(), bounds.right, bounds.centerY(),
+                        mPaint);
+                break;
+            case 4:
+                // 4 bitmaps in a 2x2 grid
+                canvas.drawLine(bounds.centerX(), bounds.top, bounds.centerX(), bounds.bottom,
+                        mPaint);
+                canvas.drawLine(bounds.left, bounds.centerY(), bounds.right, bounds.centerY(),
+                        mPaint);
+                break;
+        }
+    }
+
+    @Override
+    public void setAlpha(final int alpha) {
+        super.setAlpha(alpha);
+        final int old = mPaint.getAlpha();
+        mPaint.setAlpha(alpha);
+        if (alpha != old) {
+            invalidateSelf();
+        }
+    }
+
+    @Override
+    public void setColorFilter(final ColorFilter cf) {
+        super.setColorFilter(cf);
+        mPaint.setColorFilter(cf);
+        invalidateSelf();
+    }
+
+    public void setBitmapCache(final BitmapCache cache) {
+        mCache = cache;
+    }
+
+    public void setContactResolver(final ContactResolver contactResolver) {
+        mContactResolver = contactResolver;
+    }
+}
diff --git a/src/com/android/mail/bitmap/ContactRequest.java b/src/com/android/mail/bitmap/ContactRequest.java
new file mode 100644
index 0000000..b0bd2a0
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactRequest.java
@@ -0,0 +1,167 @@
+/*
+ * 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.bitmap;
+
+import android.content.res.AssetFileDescriptor;
+import android.text.TextUtils;
+
+import com.android.bitmap.DecodeTask;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A request object for contact images. ContactRequests have a destination because multiple
+ * ContactRequests can share the same decoded data.
+ */
+public class ContactRequest implements DecodeTask.Request {
+
+    private final String mName;
+    private final String mEmail;
+
+    public byte[] bytes;
+
+    public ContactRequest(final String name, final String email) {
+        mName = name;
+        mEmail = normalizeEmail(email);
+    }
+
+    private String normalizeEmail(final String email) {
+        if (TextUtils.isEmpty(email)) {
+            throw new IllegalArgumentException("Email must not be empty.");
+        }
+        // todo: b/10258788
+        return email;
+    }
+
+    @Override
+    public boolean equals(final Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        final ContactRequest that = (ContactRequest) o;
+
+        // Only count email, so we can pull results out of the cache that are from other contact
+        // requests.
+        //noinspection RedundantIfStatement
+        if (mEmail != null ? !mEmail.equals(that.mEmail) : that.mEmail != null) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        // Only count email, so we can pull results out of the cache that are from other contact
+        // requests.
+        return mEmail != null ? mEmail.hashCode() : 0;
+    }
+
+    @Override
+    public String toString() {
+        return "[" + super.toString() + " mName=" + mName + " mEmail=" + mEmail + "]";
+    }
+
+    @Override
+    public AssetFileDescriptor createFd() throws IOException {
+        return null;
+    }
+
+    @Override
+    public InputStream createInputStream() throws IOException {
+        return new ByteArrayInputStream(bytes);
+    }
+
+    @Override
+    public boolean hasOrientationExif() throws IOException {
+        return false;
+    }
+
+    public String getEmail() {
+        return mEmail;
+    }
+
+    public String getDisplayName() {
+        return !TextUtils.isEmpty(mName) ? mName : mEmail;
+    }
+
+    /**
+     * This ContactRequest wrapper provides implementations of equals() and hashcode() that
+     * include the destination. We need to put multiple ContactRequests in a set,
+     * but its implementations of equals() and hashcode() don't include the destination.
+     */
+    public static class ContactRequestHolder {
+
+        public final ContactRequest contactRequest;
+        public final ContactDrawable destination;
+
+        public ContactRequestHolder(final ContactRequest contactRequest,
+                final ContactDrawable destination) {
+            this.contactRequest = contactRequest;
+            this.destination = destination;
+        }
+
+        @Override
+        public boolean equals(final Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final ContactRequestHolder that = (ContactRequestHolder) o;
+
+            if (contactRequest != null ? !contactRequest.equals(that.contactRequest)
+                    : that.contactRequest != null) {
+                return false;
+            }
+            //noinspection RedundantIfStatement
+            if (destination != null ? !destination.equals(that.destination)
+                    : that.destination != null) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = contactRequest != null ? contactRequest.hashCode() : 0;
+            result = 31 * result + (destination != null ? destination.hashCode() : 0);
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return contactRequest.toString();
+        }
+
+        public String getEmail() {
+            return contactRequest.getEmail();
+        }
+
+        public String getDisplayName() {
+            return contactRequest.getDisplayName();
+        }
+    }
+}
diff --git a/src/com/android/mail/bitmap/ContactResolver.java b/src/com/android/mail/bitmap/ContactResolver.java
new file mode 100644
index 0000000..b3d1fd5
--- /dev/null
+++ b/src/com/android/mail/bitmap/ContactResolver.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2013 Google Inc.
+ * Licensed to 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.bitmap;
+
+import android.content.ContentResolver;
+import android.os.AsyncTask;
+import android.os.AsyncTask.Status;
+import android.os.Handler;
+
+import com.android.bitmap.BitmapCache;
+import com.android.bitmap.DecodeTask;
+import com.android.bitmap.ReusableBitmap;
+import com.android.ex.photo.util.Trace;
+import com.android.mail.ContactInfo;
+import com.android.mail.SenderInfoLoader;
+import com.android.mail.bitmap.ContactRequest.ContactRequestHolder;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Batches up ContactRequests so we can efficiently query the contacts provider. Kicks off a
+ * ContactResolverTask to query for contact images in the background.
+ */
+public class ContactResolver implements Runnable {
+
+    private static final String TAG = LogTag.getLogTag();
+
+    private final ContentResolver mResolver;
+    private final BitmapCache mCache;
+    /** Insertion ordered set allows us to work from the top down. */
+    private final LinkedHashSet<ContactRequestHolder> mBatch;
+
+    private final Handler mHandler = new Handler();
+    private ContactResolverTask mTask;
+
+
+    /** Size 1 pool mostly to make systrace output traces on one line. */
+    private static final Executor SMALL_POOL_EXECUTOR = new ThreadPoolExecutor(1, 1,
+            1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+    private static final Executor EXECUTOR = SMALL_POOL_EXECUTOR;
+
+    public ContactResolver(final ContentResolver resolver, final BitmapCache cache) {
+        mResolver = resolver;
+        mCache = cache;
+        mBatch = new LinkedHashSet<ContactRequestHolder>();
+    }
+
+    @Override
+    public void run() {
+        // Start to process a new batch.
+        if (mBatch.isEmpty()) {
+            return;
+        }
+
+        if (mTask != null && mTask.getStatus() == Status.RUNNING) {
+            LogUtils.d(TAG, "ContactResolver << batch skip");
+            return;
+        }
+
+        Trace.beginSection("ContactResolver run");
+        LogUtils.d(TAG, "ContactResolver >> batch start");
+
+        // Make a copy of the batch.
+        LinkedHashSet<ContactRequestHolder> batch = new LinkedHashSet<ContactRequestHolder>(mBatch);
+
+        if (mTask != null) {
+            mTask.cancel(true);
+        }
+
+        mTask = new ContactResolverTask(batch, mResolver, mCache, this);
+        mTask.executeOnExecutor(EXECUTOR);
+        Trace.endSection();
+    }
+
+    public void add(final ContactRequest request, final ContactDrawable drawable) {
+        mBatch.add(new ContactRequestHolder(request, drawable));
+        notifyBatchReady();
+    }
+
+    public void remove(final ContactRequest request, final ContactDrawable drawable) {
+        mBatch.remove(new ContactRequestHolder(request, drawable));
+    }
+
+    /**
+     * A layout pass traverses the whole tree during a single iteration of the event loop. That
+     * means that every ContactDrawable on the screen will add its ContactRequest to the batch in
+     * a single iteration of the event loop.
+     *
+     * <p/>
+     * We take advantage of this by posting a Runnable (happens to be this object) at the end of
+     * the event queue. Every time something is added to the batch as part of the same layout pass,
+     * the Runnable is moved to the back of the queue. When the next layout pass occurs,
+     * it is placed in the event loop behind this Runnable. That allows us to process the batch
+     * that was added previously.
+     */
+    private void notifyBatchReady() {
+        LogUtils.d(TAG, "ContactResolver  > batch   %d", mBatch.size());
+        mHandler.removeCallbacks(this);
+        mHandler.post(this);
+    }
+
+    /**
+     * This is not a very traditional AsyncTask, in the sense that we do not care about what gets
+     * returned in doInBackground(). Instead, we signal traditional "return values" through
+     * publishProgress().
+     *
+     * <p/>
+     * The reason we do this is because this task is responsible for decoding an entire batch of
+     * ContactRequests. But, we do not want to have to wait to decode all of them before updating
+     * any views. So we must do all the work in doInBackground(),
+     * but upon finishing each individual task, we need to jump out to the UI thread and update
+     * that view.
+     */
+    private static class ContactResolverTask extends AsyncTask<Void, Result, Void> {
+
+        private final Set<ContactRequestHolder> mContactRequests;
+        private final ContentResolver mResolver;
+        private final BitmapCache mCache;
+        private final ContactResolver mCallback;
+
+        public ContactResolverTask(final Set<ContactRequestHolder> contactRequests,
+                final ContentResolver resolver, final BitmapCache cache,
+                final ContactResolver callback) {
+            mContactRequests = contactRequests;
+            mResolver = resolver;
+            mCache = cache;
+            mCallback = callback;
+        }
+
+        @Override
+        protected Void doInBackground(final Void... params) {
+            Trace.beginSection("set up");
+            final Set<String> emails = new HashSet<String>(mContactRequests.size());
+            for (ContactRequestHolder request : mContactRequests) {
+                final String email = request.getEmail();
+                emails.add(email);
+            }
+            Trace.endSection();
+
+            Trace.beginSection("load contact photo bytes");
+            // Query the contacts provider for the current batch of emails.
+            ImmutableMap<String, ContactInfo> contactInfos = SenderInfoLoader
+                    .loadContactPhotos(mResolver, emails, false /* decodeBitmaps */);
+            Trace.endSection();
+
+            for (ContactRequestHolder request : mContactRequests) {
+                Trace.beginSection("decode");
+                final String email = request.getEmail();
+                if (contactInfos == null) {
+                    // Query failed.
+                    LogUtils.d(TAG, "ContactResolver -- failed  %s", email);
+                    publishProgress(new Result(request, null));
+                    Trace.endSection();
+                    continue;
+                }
+
+                final ContactInfo contactInfo = contactInfos.get(email);
+                if (contactInfo == null) {
+                    // Request skipped. Try again next batch.
+                    LogUtils.d(TAG, "ContactResolver  = skipped %s", email);
+                    Trace.endSection();
+                    continue;
+                }
+
+                // Query attempted.
+                final byte[] photo = contactInfo.photoBytes;
+                if (photo == null) {
+                    // No photo bytes found.
+                    LogUtils.d(TAG, "ContactResolver -- failed  %s", email);
+                    publishProgress(new Result(request, null));
+                    Trace.endSection();
+                    continue;
+                }
+
+                // Query succeeded. Photo bytes found.
+                request.contactRequest.bytes = photo;
+
+                // Start decode.
+                LogUtils.d(TAG, "ContactResolver ++ found   %s", email);
+                final ReusableBitmap result;
+                final int width = mCache.getDecodeWidth();
+                final int height = mCache.getDecodeHeight();
+                // Synchronously decode the photo bytes. We are already in a background
+                // thread, and we want decodes to finish in order. The decodes are blazing
+                // fast so we don't need to kick off multiple threads.
+                result = new DecodeTask(request.contactRequest, width, height, width, height, null,
+                        mCache).decode();
+                request.contactRequest.bytes = null;
+
+                // Decode success.
+                publishProgress(new Result(request, result));
+                Trace.endSection();
+            }
+
+            return null;
+        }
+
+        /**
+         * We use progress updates to jump to the UI thread so we can decode the batch
+         * incrementally.
+         */
+        @Override
+        protected void onProgressUpdate(final Result... values) {
+            final ContactRequestHolder request = values[0].request;
+            final ReusableBitmap bitmap = values[0].bitmap;
+
+            // DecodeTask does not add null results to the cache.
+            if (bitmap == null) {
+                // Cache null result.
+                mCache.put(request.contactRequest, null);
+            }
+
+            request.destination.onDecodeComplete(request.contactRequest, bitmap);
+        }
+
+        @Override
+        protected void onPostExecute(final Void aVoid) {
+            // Batch completed. Start next batch.
+            mCallback.notifyBatchReady();
+        }
+    }
+
+    /**
+     * Wrapper for the ContactRequest and its decoded bitmap. This class is used to pass results
+     * to onProgressUpdate().
+     */
+    private static class Result {
+        public final ContactRequestHolder request;
+        public final ReusableBitmap bitmap;
+
+        private Result(final ContactRequestHolder request, final ReusableBitmap bitmap) {
+            this.request = request;
+            this.bitmap = bitmap;
+        }
+    }
+}
diff --git a/src/com/android/mail/bitmap/FlipDrawable.java b/src/com/android/mail/bitmap/FlipDrawable.java
new file mode 100644
index 0000000..6cc7b26
--- /dev/null
+++ b/src/com/android/mail/bitmap/FlipDrawable.java
@@ -0,0 +1,269 @@
+/*
+ * 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.bitmap;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.mail.utils.LogUtils;
+
+/**
+ * A drawable that wraps two other drawables and allows flipping between them. The flipping
+ * animation is a 2D rotation around the y axis.
+ *
+ * <p/>
+ * The 3 durations are: (best viewed in documentation form)
+ * <pre>
+ * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+ *   |       |       |
+ *   V       V       V
+ * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
+ * </pre>
+ */
+public class FlipDrawable extends Drawable implements Drawable.Callback {
+
+    /**
+     * The inner drawables.
+     */
+    protected final Drawable mFront;
+    protected final Drawable mBack;
+
+    protected final int mFlipDurationMs;
+    protected final int mPreFlipDurationMs;
+    protected final int mPostFlipDurationMs;
+    private final ValueAnimator mFlipAnimator;
+
+    private static final float END_VALUE = 2f;
+
+    /**
+     * From 0f to END_VALUE. Determines the flip progress between mFront and mBack. 0f means
+     * mFront is fully shown, while END_VALUE means mBack is fully shown.
+     */
+    private float mFlipFraction = 0f;
+
+    /**
+     * True if flipping towards front, false if flipping towards back.
+     */
+    private boolean mFlipToSide = true;
+
+    /**
+     * Create a new FlipDrawable. The front is fully shown by default.
+     *
+     * <p/>
+     * The 3 durations are: (best viewed in documentation form)
+     * <pre>
+     * &lt;pre&gt;[_][]|[][_]&lt;post&gt;
+     *   |       |       |
+     *   V       V       V
+     * &lt;pre>&lt;   flip  &gt;&lt;post&gt;
+     * </pre>
+     *
+     * @param front              The front drawable.
+     * @param back               The back drawable.
+     * @param flipDurationMs     The duration of the actual flip. This duration includes both
+     *                           animating away one side and showing the other.
+     * @param preFlipDurationMs  The duration before the actual flip begins. Subclasses can use this
+     *                           to add flourish.
+     * @param postFlipDurationMs The duration after the actual flip begins. Subclasses can use this
+     *                           to add flourish.
+     */
+    public FlipDrawable(final Drawable front, final Drawable back, final int flipDurationMs,
+            final int preFlipDurationMs, final int postFlipDurationMs) {
+        if (front == null || back == null) {
+            throw new IllegalArgumentException("Front and back drawables must not be null.");
+        }
+        mFront = front;
+        mBack = back;
+
+        mFront.setCallback(this);
+        mBack.setCallback(this);
+
+        mFlipDurationMs = flipDurationMs;
+        mPreFlipDurationMs = preFlipDurationMs;
+        mPostFlipDurationMs = postFlipDurationMs;
+
+        mFlipAnimator = ValueAnimator.ofFloat(0f, END_VALUE)
+                .setDuration(mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs);
+        mFlipAnimator.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(final ValueAnimator animation) {
+                final float old = mFlipFraction;
+                //noinspection ConstantConditions
+                mFlipFraction = (Float) animation.getAnimatedValue();
+                if (old != mFlipFraction) {
+                    invalidateSelf();
+                }
+            }
+        });
+
+        reset(true);
+    }
+
+    @Override
+    protected void onBoundsChange(final Rect bounds) {
+        super.onBoundsChange(bounds);
+        if (bounds.isEmpty()) {
+            mFront.setBounds(0, 0, 0, 0);
+            mBack.setBounds(0, 0, 0, 0);
+        } else {
+            mFront.setBounds(bounds);
+            mBack.setBounds(bounds);
+        }
+    }
+
+    @Override
+    public void draw(final Canvas canvas) {
+        final Rect bounds = getBounds();
+        if (!isVisible() || bounds.isEmpty()) {
+            return;
+        }
+
+        final Drawable inner = getSideShown() /* == front */ ? mFront : mBack;
+
+        final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+
+        final float scaleX;
+        if (mFlipFraction / 2 <= mPreFlipDurationMs / totalDurationMs) {
+            // During pre-flip.
+            scaleX = 1;
+        } else if (mFlipFraction / 2 >= (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) {
+            // During post-flip.
+            scaleX = 1;
+        } else {
+            // During flip.
+            final float flipFraction = mFlipFraction / 2;
+            final float flipMiddle = (mPreFlipDurationMs / totalDurationMs
+                    + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+            final float distFraction = Math.abs(flipFraction - flipMiddle);
+            final float multiplier = 1 / (flipMiddle - (mPreFlipDurationMs / totalDurationMs));
+            scaleX = distFraction * multiplier;
+        }
+
+        canvas.save();
+        // The flip is a simple 1 dimensional scale.
+        canvas.scale(scaleX, 1, bounds.exactCenterX(), bounds.exactCenterY());
+        inner.draw(canvas);
+        canvas.restore();
+    }
+
+    @Override
+    public void setAlpha(final int alpha) {
+        mFront.setAlpha(alpha);
+        mBack.setAlpha(alpha);
+    }
+
+    @Override
+    public void setColorFilter(final ColorFilter cf) {
+        mFront.setColorFilter(cf);
+        mBack.setColorFilter(cf);
+    }
+
+    @Override
+    public int getOpacity() {
+        return resolveOpacity(mFront.getOpacity(), mBack.getOpacity());
+    }
+
+    @Override
+    protected boolean onLevelChange(final int level) {
+        return mFront.setLevel(level) || mBack.setLevel(level);
+    }
+
+    @Override
+    public void invalidateDrawable(final Drawable who) {
+        invalidateSelf();
+    }
+
+    @Override
+    public void scheduleDrawable(final Drawable who, final Runnable what, final long when) {
+        scheduleSelf(what, when);
+    }
+
+    @Override
+    public void unscheduleDrawable(final Drawable who, final Runnable what) {
+        unscheduleSelf(what);
+    }
+
+    /**
+     * Stop animating the flip and reset to one side.
+     * @param side Pass true if reset to front, false if reset to back.
+     */
+    public void reset(final boolean side) {
+        final float old = mFlipFraction;
+        mFlipAnimator.cancel();
+        mFlipFraction = side ? 0f : 2f;
+        mFlipToSide = side;
+        if (mFlipFraction != old) {
+            invalidateSelf();
+        }
+    }
+
+    /**
+     * Returns true if the front is shown. Returns false if the back is shown.
+     */
+    public boolean getSideShown() {
+        final float totalDurationMs = mPreFlipDurationMs + mFlipDurationMs + mPostFlipDurationMs;
+        final float middleFraction = (mPreFlipDurationMs / totalDurationMs
+                + (totalDurationMs - mPostFlipDurationMs) / totalDurationMs) / 2;
+        return mFlipFraction / 2 < middleFraction;
+    }
+
+    /**
+     * Returns true if the front is being flipped towards. Returns false if the back is being
+     * flipped towards.
+     */
+    public boolean getSideFlippingTowards() {
+        return mFlipToSide;
+    }
+
+    /**
+     * Starts an animated flip to the other side. If a flip animation is currently started,
+     * it will be reversed.
+     */
+    public void flip() {
+        mFlipToSide = !mFlipToSide;
+        if (mFlipAnimator.isStarted()) {
+            mFlipAnimator.reverse();
+        } else {
+            if (!mFlipToSide /* front to back */) {
+                mFlipAnimator.start();
+            } else /* back to front */ {
+                mFlipAnimator.reverse();
+            }
+        }
+    }
+
+    /**
+     * Start an animated flip to a side. This works regardless of whether a flip animation is
+     * currently started.
+     * @param side Pass true if flip to front, false if flip to back.
+     */
+    public void flipTo(final boolean side) {
+        if (mFlipToSide != side) {
+            flip();
+        }
+    }
+
+    /**
+     * Returns whether flipping is in progress.
+     */
+    public boolean isFlipping() {
+        return mFlipAnimator.isStarted();
+    }
+}
diff --git a/src/com/android/mail/bitmap/ImageAttachmentRequest.java b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
index 6c58772..b680db5 100644
--- a/src/com/android/mail/bitmap/ImageAttachmentRequest.java
+++ b/src/com/android/mail/bitmap/ImageAttachmentRequest.java
@@ -1,6 +1,5 @@
 package com.android.mail.bitmap;
 
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.AssetFileDescriptor;
 import android.database.Cursor;
@@ -25,6 +24,9 @@
     private final int mRendition;
     public final int mDestW;
 
+    private Uri mCachedUri;
+    private String mCachedMimeType;
+
     public ImageAttachmentRequest(final Context context, final String lookupUri,
             final int rendition, final int destW) {
         mContext = context;
@@ -78,26 +80,40 @@
 
     @Override
     public AssetFileDescriptor createFd() throws IOException {
-        AssetFileDescriptor result = null;
+        if (mCachedUri == null) {
+            cacheValues();
+        }
+        return mContext.getContentResolver().openAssetFileDescriptor(mCachedUri, "r");
+    }
+
+    private void cacheValues() throws IOException {
         Cursor cursor = null;
-        final ContentResolver cr = mContext.getContentResolver();
         try {
-            cursor = cr.query(Uri.parse(mLookupUri), UIProvider.ATTACHMENT_PROJECTION, null, null,
-                    null);
+            cursor = mContext.getContentResolver().query(Uri.parse(mLookupUri),
+                    UIProvider.ATTACHMENT_PROJECTION, null, null, null);
             if (cursor != null && cursor.moveToFirst()) {
                 final Attachment a = new Attachment(cursor);
-                result = cr.openAssetFileDescriptor(a.getUriForRendition(mRendition), "r");
+                mCachedUri = a.getUriForRendition(mRendition);
+                final String mimeType = a.getContentType();
+                mCachedMimeType = mimeType != null ? mimeType.toLowerCase() : null;
             }
         } finally {
             if (cursor != null) {
                 cursor.close();
             }
         }
-        return result;
     }
 
     @Override
     public InputStream createInputStream() throws IOException {
         return null;
     }
+
+    @Override
+    public boolean hasOrientationExif() throws IOException {
+        if (mCachedUri == null) {
+            cacheValues();
+        }
+        return mCachedMimeType == null || mCachedMimeType.equals("image/jpeg");
+    }
 }
diff --git a/src/com/android/mail/browse/ConversationCursor.java b/src/com/android/mail/browse/ConversationCursor.java
index 6c15e88..7a3ca0f 100644
--- a/src/com/android/mail/browse/ConversationCursor.java
+++ b/src/com/android/mail/browse/ConversationCursor.java
@@ -165,6 +165,8 @@
 
     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
 
+    private final boolean mCachingEnabled;
+
     private void setCursor(UnderlyingCursorWrapper cursor) {
         // If we have an existing underlying cursor, make sure it's closed
         if (mUnderlyingCursor != null) {
@@ -193,6 +195,9 @@
         mName = name;
         qProjection = UIProvider.CONVERSATION_PROJECTION;
         mCursorObserver = new CursorObserver(new Handler(Looper.getMainLooper()));
+
+        // Disable caching on low memory devices
+        mCachingEnabled = !Utils.isLowRamDevice(activity);
     }
 
     /**
@@ -357,7 +362,7 @@
          * notes on thread safety.
          */
         private int mCachePos;
-        private boolean mCachingEnabled = true;
+        private boolean mCachingEnabled;
         private final NewCursorUpdateObserver mCursorUpdateObserver;
         private boolean mUpdateObserverRegistered = false;
 
@@ -370,9 +375,11 @@
 
         private boolean mCursorUpdated = false;
 
-        public UnderlyingCursorWrapper(Cursor result) {
+        public UnderlyingCursorWrapper(Cursor result, boolean cachingEnabled) {
             super(result);
 
+            mCachingEnabled = cachingEnabled;
+
             // Register the content observer immediately, as we want to make sure that we don't miss
             // any updates
             mCursorUpdateObserver =
@@ -639,7 +646,8 @@
                     uri, time, result.getCount());
         }
         System.gc();
-        return new UnderlyingCursorWrapper(result);
+
+        return new UnderlyingCursorWrapper(result, mCachingEnabled);
     }
 
     static boolean offUiThread() {
@@ -1946,6 +1954,15 @@
     }
 
     @Override
+    public Uri getNotificationUri() {
+        if (mUnderlyingCursor == null) {
+            return null;
+        } else {
+            return mUnderlyingCursor.getNotificationUri();
+        }
+    }
+
+    @Override
     public void setNotificationUri(ContentResolver cr, Uri uri) {
         throw new UnsupportedOperationException();
     }
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index 94d5dfc..5dd2267 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -18,8 +18,6 @@
 package com.android.mail.browse;
 
 import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.AnimatorListenerAdapter;
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.ClipData;
@@ -31,7 +29,6 @@
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.LinearGradient;
-import android.graphics.Matrix;
 import android.graphics.Paint;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -60,7 +57,6 @@
 import android.view.ViewGroup;
 import android.view.ViewParent;
 import android.view.animation.DecelerateInterpolator;
-import android.view.animation.LinearInterpolator;
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
 import android.widget.TextView;
@@ -72,11 +68,10 @@
 import com.android.mail.analytics.Analytics;
 import com.android.mail.bitmap.AttachmentDrawable;
 import com.android.mail.bitmap.AttachmentGridDrawable;
+import com.android.mail.bitmap.ContactCheckableGridDrawable;
+import com.android.mail.bitmap.ContactDrawable;
 import com.android.mail.browse.ConversationItemViewModel.SenderFragment;
 import com.android.mail.perf.Timer;
-import com.android.mail.photomanager.ContactPhotoManager;
-import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
 import com.android.mail.providers.Address;
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Conversation;
@@ -87,9 +82,9 @@
 import com.android.mail.providers.UIProvider.ConversationListIcon;
 import com.android.mail.providers.UIProvider.FolderType;
 import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
 import com.android.mail.ui.ControllableActivity;
 import com.android.mail.ui.ConversationSelectionSet;
+import com.android.mail.ui.ConversationSetObserver;
 import com.android.mail.ui.DividedImageCanvas;
 import com.android.mail.ui.DividedImageCanvas.InvalidateCallback;
 import com.android.mail.ui.FolderDisplayer;
@@ -102,13 +97,12 @@
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.Utils;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.collect.Lists;
 
 import java.util.ArrayList;
-import java.util.List;
 
 public class ConversationItemView extends View
-        implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener {
+        implements SwipeableItemView, ToggleableItem, InvalidateCallback, OnScrollListener,
+        ConversationSetObserver {
 
     // Timer.
     private static int sLayoutCount = 0;
@@ -125,7 +119,6 @@
     // Static bitmaps.
     private static Bitmap STAR_OFF;
     private static Bitmap STAR_ON;
-    private static Bitmap CHECK;
     private static Bitmap ATTACHMENT;
     private static Bitmap ONLY_TO_ME;
     private static Bitmap TO_ME_AND_OTHERS;
@@ -200,15 +193,12 @@
     private float mAnimatedHeightFraction = 1.0f;
     private final String mAccount;
     private ControllableActivity mActivity;
-    private ConversationListListener mConversationListListener;
     private final TextView mSubjectTextView;
     private final TextView mSendersTextView;
     private int mGadgetMode;
     private boolean mAttachmentPreviewsEnabled;
     private boolean mParallaxSpeedAlternative;
     private boolean mParallaxDirectionAlternative;
-    private final DividedImageCanvas mContactImagesHolder;
-    private static ContactPhotoManager sContactPhotoManager;
 
     private static int sFoldersLeftPadding;
     private static TextAppearanceSpan sSubjectTextUnreadSpan;
@@ -218,19 +208,9 @@
     private static int sScrollSlop;
     private static CharacterStyle sActivatedTextSpan;
 
+    private final ContactCheckableGridDrawable mSendersImageView;
     private final AttachmentGridDrawable mAttachmentsView;
 
-    private final Matrix mPhotoFlipMatrix = new Matrix();
-    private final Matrix mCheckMatrix = new Matrix();
-
-    private final CabAnimator mPhotoFlipAnimator;
-
-    /**
-     * The conversation id, if this conversation was selected the last time we were in a selection
-     * mode. This is reset after any animations complete upon exiting the selection mode.
-     */
-    private long mLastSelectedId = -1;
-
     /** The resource id of the color to use to override the background. */
     private int mBackgroundOverrideResId = -1;
     /** The bitmap to use, or <code>null</code> for the default */
@@ -258,19 +238,6 @@
         sCheckBackgroundPaint.setColor(Color.GRAY);
     }
 
-    public static void setScrollStateChanged(final int scrollState) {
-        if (sContactPhotoManager == null) {
-            return;
-        }
-        final boolean flinging = scrollState == OnScrollListener.SCROLL_STATE_FLING;
-
-        if (flinging) {
-            sContactPhotoManager.pause();
-        } else {
-            sContactPhotoManager.resume();
-        }
-    }
-
     /**
      * Handles displaying folders in a conversation header view.
      */
@@ -421,7 +388,6 @@
             // Initialize static bitmaps.
             STAR_OFF = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_off);
             STAR_ON = BitmapFactory.decodeResource(res, R.drawable.ic_btn_star_on);
-            CHECK = BitmapFactory.decodeResource(res, R.drawable.ic_avatar_check);
             ATTACHMENT = BitmapFactory.decodeResource(res, R.drawable.ic_attachment_holo_light);
             ONLY_TO_ME = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_double);
             TO_ME_AND_OTHERS = BitmapFactory.decodeResource(res, R.drawable.ic_email_caret_single);
@@ -467,24 +433,10 @@
             sElidedPaddingToken = res.getString(R.string.elided_padding_token);
             sScrollSlop = res.getInteger(R.integer.swipeScrollSlop);
             sFoldersLeftPadding = res.getDimensionPixelOffset(R.dimen.folders_left_padding);
-            sContactPhotoManager = ContactPhotoManager.createContactPhotoManager(context);
             sOverflowCountMax = res.getInteger(integer.ap_overflow_max_count);
-            sCabAnimationDuration =
-                    res.getInteger(R.integer.conv_item_view_cab_anim_duration);
+            sCabAnimationDuration = res.getInteger(R.integer.conv_item_view_cab_anim_duration);
         }
 
-        mPhotoFlipAnimator = new CabAnimator("photoFlipFraction", 0, 2,
-                sCabAnimationDuration) {
-            @Override
-            public void invalidateArea() {
-                final int left = mCoordinates.contactImagesX;
-                final int right = left + mContactImagesHolder.getWidth();
-                final int top = mCoordinates.contactImagesY;
-                final int bottom = top + mContactImagesHolder.getHeight();
-                invalidate(left, top, right, bottom);
-            }
-        };
-
         mSendersTextView = new TextView(mContext);
         mSendersTextView.setIncludeFontPadding(false);
 
@@ -492,27 +444,16 @@
         mSubjectTextView.setEllipsize(TextUtils.TruncateAt.END);
         mSubjectTextView.setIncludeFontPadding(false);
 
-        mContactImagesHolder = new DividedImageCanvas(context, new InvalidateCallback() {
-            @Override
-            public void invalidate() {
-                if (mCoordinates == null) {
-                    return;
-                }
-                ConversationItemView.this.invalidate(mCoordinates.contactImagesX,
-                        mCoordinates.contactImagesY,
-                        mCoordinates.contactImagesX + mCoordinates.contactImagesWidth,
-                        mCoordinates.contactImagesY + mCoordinates.contactImagesHeight);
-            }
-        });
-
         mAttachmentsView = new AttachmentGridDrawable(res, PLACEHOLDER, PROGRESS_BAR);
         mAttachmentsView.setCallback(this);
 
+        mSendersImageView = new ContactCheckableGridDrawable(res, sCabAnimationDuration);
+        mSendersImageView.setCallback(this);
+
         Utils.traceEndSection();
     }
 
     public void bind(final Conversation conversation, final ControllableActivity activity,
-            final ConversationListListener conversationListListener,
             final ConversationSelectionSet set, final Folder folder,
             final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
             final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
@@ -520,31 +461,28 @@
             final AnimatedAdapter adapter) {
         Utils.traceBeginSection("CIVC.bind");
         bind(ConversationItemViewModel.forConversation(mAccount, conversation), activity,
-                conversationListListener, null /* conversationItemAreaClickListener */, set, folder,
-                checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
-                parallaxDirectionAlternative, swipeEnabled, priorityArrowEnabled, adapter,
-                -1 /* backgroundOverrideResId */,
-                null /* photoBitmap */);
+                null /* conversationItemAreaClickListener */,
+                set, folder, checkboxOrSenderImage, showAttachmentPreviews,
+                parallaxSpeedAlternative, parallaxDirectionAlternative, swipeEnabled,
+                priorityArrowEnabled, adapter, -1 /* backgroundOverrideResId */, null /* photoBitmap */);
         Utils.traceEndSection();
     }
 
     public void bindAd(final ConversationItemViewModel conversationItemViewModel,
             final ControllableActivity activity,
-            final ConversationListListener conversationListListener,
             final ConversationItemAreaClickListener conversationItemAreaClickListener,
             final Folder folder, final int checkboxOrSenderImage, final AnimatedAdapter adapter,
             final int backgroundOverrideResId, final Bitmap photoBitmap) {
         Utils.traceBeginSection("CIVC.bindAd");
-        bind(conversationItemViewModel, activity, conversationListListener,
-                conversationItemAreaClickListener, null /* set */, folder, checkboxOrSenderImage,
-                false /* attachment previews */, false /* parallax */, false /* parallax */,
-                true /* swipeEnabled */, false /* priorityArrowEnabled */, adapter,
-                backgroundOverrideResId, photoBitmap);
+        bind(conversationItemViewModel, activity, conversationItemAreaClickListener, null /* set */,
+                folder, checkboxOrSenderImage, false /* attachment previews */,
+                false /* parallax */, false /* parallax */, true /* swipeEnabled */,
+                false /* priorityArrowEnabled */,
+                adapter, backgroundOverrideResId, photoBitmap);
         Utils.traceEndSection();
     }
 
     private void bind(final ConversationItemViewModel header, final ControllableActivity activity,
-            final ConversationListListener conversationListListener,
             final ConversationItemAreaClickListener conversationItemAreaClickListener,
             final ConversationSelectionSet set, final Folder folder,
             final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
@@ -556,28 +494,25 @@
         mConversationItemAreaClickListener = conversationItemAreaClickListener;
 
         if (mHeader != null) {
+            Utils.traceBeginSection("unbind");
+            final boolean newlyBound = header.conversation.id != mHeader.conversation.id;
             // If this was previously bound to a different conversation, remove any contact photo
             // manager requests.
-            if (header.conversation.id != mHeader.conversation.id ||
-                    (mHeader.displayableSenderNames != null && !mHeader.displayableSenderNames
-                    .equals(header.displayableSenderNames))) {
-                ArrayList<String> divisionIds = mContactImagesHolder.getDivisionIds();
-                if (divisionIds != null) {
-                    mContactImagesHolder.reset();
-                    for (int pos = 0; pos < divisionIds.size(); pos++) {
-                        sContactPhotoManager.removePhoto(ContactPhotoManager.generateHash(
-                                mContactImagesHolder, pos, divisionIds.get(pos)));
-                    }
+            if (newlyBound || (mHeader.displayableSenderNames != null && !mHeader
+                    .displayableSenderNames.equals(
+                            header.displayableSenderNames))) {
+                for (int i = 0; i < mSendersImageView.getCount(); i++) {
+                    mSendersImageView.getOrCreateDrawable(i).unbind();
                 }
+                mSendersImageView.setCount(0);
             }
 
             // If this was previously bound to a different conversation,
             // remove any attachment preview manager requests.
-            if (header.conversation.id != mHeader.conversation.id
-                    || header.conversation.attachmentPreviewsCount
-                            != mHeader.conversation.attachmentPreviewsCount
-                    || !header.conversation.getAttachmentPreviewUris()
-                            .equals(mHeader.conversation.getAttachmentPreviewUris())) {
+            if (newlyBound || header.conversation.attachmentPreviewsCount
+                    != mHeader.conversation.attachmentPreviewsCount || !header.conversation
+                    .getAttachmentPreviewUris().equals(
+                            mHeader.conversation.getAttachmentPreviewUris())) {
 
                 // unbind the attachments view (releasing bitmap references)
                 // (this also cancels all async tasks)
@@ -588,22 +523,29 @@
                 mAttachmentsView.setCount(0);
             }
 
-            if (header.conversation.id != mHeader.conversation.id) {
+            if (newlyBound) {
                 // Stop the photo flip animation
-                mPhotoFlipAnimator.stopAnimation();
+                final boolean showSenders = !isSelected();
+                mSendersImageView.reset(showSenders);
             }
+            Utils.traceEndSection();
         }
         mCoordinates = null;
         mHeader = header;
         mActivity = activity;
-        mConversationListListener = conversationListListener;
         mSelectedConversationSet = set;
+        mSelectedConversationSet.addObserver(this);
         mDisplayedFolder = folder;
         mStarEnabled = folder != null && !folder.isTrash();
         mSwipeEnabled = swipeEnabled;
         mAdapter = adapter;
-        mAttachmentsView.setBitmapCache(mAdapter.getBitmapCache());
-        mAttachmentsView.setDecodeAggregator(mAdapter.getDecodeAggregator());
+
+        Utils.traceBeginSection("drawables");
+        mAttachmentsView.setBitmapCache(mAdapter.getAttachmentPreviewsCache());
+        mAttachmentsView.setDecodeAggregator(mAdapter.getAttachmentPreviewsDecodeAggregator());
+        mSendersImageView.setBitmapCache(mAdapter.getSendersImagesCache());
+        mSendersImageView.setContactResolver(mAdapter.getContactResolver());
+        Utils.traceEndSection();
 
         if (checkboxOrSenderImage == ConversationListIcon.SENDER_IMAGE) {
             mGadgetMode = ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO;
@@ -615,12 +557,14 @@
         mParallaxSpeedAlternative = parallaxSpeedAlternative;
         mParallaxDirectionAlternative = parallaxDirectionAlternative;
 
+        Utils.traceBeginSection("folder displayer");
         // Initialize folder displayer.
         if (mHeader.folderDisplayer == null) {
             mHeader.folderDisplayer = new ConversationItemFolderDisplayer(mContext);
         } else {
             mHeader.folderDisplayer.reset();
         }
+        Utils.traceEndSection();
 
         final int ignoreFolderType;
         if (mDisplayedFolder.isInbox()) {
@@ -629,16 +573,21 @@
             ignoreFolderType = -1;
         }
 
+        Utils.traceBeginSection("load folders");
         mHeader.folderDisplayer.loadConversationFolders(mHeader.conversation,
                 mDisplayedFolder.folderUri, ignoreFolderType);
+        Utils.traceEndSection();
 
         if (mHeader.dateOverrideText == null) {
+            Utils.traceBeginSection("relative time");
             mHeader.dateText = DateUtils.getRelativeTimeSpanString(mContext,
                     mHeader.conversation.dateMs);
+            Utils.traceEndSection();
         } else {
             mHeader.dateText = mHeader.dateOverrideText;
         }
 
+        Utils.traceBeginSection("config setup");
         mConfig = new ConversationItemViewCoordinates.Config()
             .withGadget(mGadgetMode)
             .withAttachmentPreviews(getAttachmentPreviewsMode());
@@ -651,6 +600,7 @@
         if (mHeader.conversation.color != 0) {
             mConfig.showColorBlock();
         }
+
         // Personal level.
         mHeader.personalLevelBitmap = null;
         if (true) { // TODO: hook this up to a setting
@@ -672,15 +622,20 @@
         if (mHeader.personalLevelBitmap != null) {
             mConfig.showPersonalIndicator();
         }
+        Utils.traceEndSection();
 
+        Utils.traceBeginSection("overflow");
         mAttachmentsView.setOverflowText(null);
+        Utils.traceEndSection();
 
+        Utils.traceBeginSection("content description");
         setContentDescription();
+        Utils.traceEndSection();
         requestLayout();
     }
 
     @Override
-    public void invalidateDrawable(Drawable who) {
+    public void invalidateDrawable(final Drawable who) {
         boolean handled = false;
         if (mCoordinates != null) {
             if (mAttachmentsView.equals(who)) {
@@ -688,6 +643,11 @@
                 r.offset(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
                 ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
                 handled = true;
+            } else if (mSendersImageView.equals(who)) {
+                final Rect r = new Rect(who.getBounds());
+                r.offset(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
+                ConversationItemView.this.invalidate(r.left, r.top, r.right, r.bottom);
+                handled = true;
             }
         }
         if (!handled) {
@@ -761,12 +721,14 @@
         Utils.traceEndSection();
 
         // Subject.
+        Utils.traceBeginSection("subject");
         createSubject(mHeader.unread);
 
         if (!mHeader.isLayoutValid()) {
             setContentDescription();
         }
         mHeader.validate();
+        Utils.traceEndSection();
 
         pauseTimer(PERF_TAG_LAYOUT);
         if (sTimer != null && ++sLayoutCount >= PERF_LAYOUT_ITERATIONS) {
@@ -916,34 +878,36 @@
     // FIXME(ath): maybe move this to bind(). the only dependency on layout is on tile W/H, which
     // is immutable.
     private void loadSenderImages() {
-        if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
-                && mHeader.displayableSenderEmails != null
-                && mHeader.displayableSenderEmails.size() > 0) {
-            if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
-                LogUtils.w(LOG_TAG,
-                        "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
-                        mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
-                        mCoordinates.getMode());
-                return;
-            }
-
-            int size = mHeader.displayableSenderEmails.size();
-            final List<Object> keys = Lists.newArrayListWithCapacity(size);
-            for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
-                keys.add(mHeader.displayableSenderEmails.get(i));
-            }
-
-            mContactImagesHolder.setDimensions(mCoordinates.contactImagesWidth,
-                    mCoordinates.contactImagesHeight);
-            mContactImagesHolder.setDivisionIds(keys);
-            String emailAddress;
-            for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < size; i++) {
-                emailAddress = mHeader.displayableSenderEmails.get(i);
-                PhotoIdentifier photoIdentifier = new ContactIdentifier(
-                        mHeader.displayableSenderNames.get(i), emailAddress, i);
-                sContactPhotoManager.loadThumbnail(photoIdentifier, mContactImagesHolder);
-            }
+        if (mGadgetMode != ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO
+                || mHeader.displayableSenderEmails == null
+                || mHeader.displayableSenderEmails.size() <= 0) {
+            return;
         }
+        if (mCoordinates.contactImagesWidth <= 0 || mCoordinates.contactImagesHeight <= 0) {
+            LogUtils.w(LOG_TAG,
+                    "Contact image width(%d) or height(%d) is 0 for mode: (%d).",
+                    mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
+                    mCoordinates.getMode());
+            return;
+        }
+
+        Utils.traceBeginSection("load sender images");
+        final int count = mHeader.displayableSenderEmails.size();
+
+        mSendersImageView.setCount(count);
+        mSendersImageView
+                .setBounds(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight);
+
+        for (int i = 0; i < DividedImageCanvas.MAX_DIVISIONS && i < count; i++) {
+            Utils.traceBeginSection("load single sender image");
+            final ContactDrawable drawable = mSendersImageView.getOrCreateDrawable(i);
+            drawable.setDecodeDimensions(mCoordinates.contactImagesWidth,
+                    mCoordinates.contactImagesHeight);
+            drawable.bind(mHeader.displayableSenderNames.get(i),
+                    mHeader.displayableSenderEmails.get(i));
+            Utils.traceEndSection();
+        }
+        Utils.traceEndSection();
     }
 
     private void loadAttachmentPreviews() {
@@ -1391,7 +1355,9 @@
         // Contact photo
         if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
             canvas.save();
-            drawContactImageArea(canvas);
+            Utils.traceBeginSection("draw senders image");
+            drawSendersImage(canvas);
+            Utils.traceEndSection();
             canvas.restore();
         }
 
@@ -1500,113 +1466,19 @@
         Utils.traceEndSection();
     }
 
-    /**
-     * Draws the contact images or check, in the correct animated state.
-     */
-    private void drawContactImageArea(final Canvas canvas) {
-        if (isSelected()) {
-            mLastSelectedId = mHeader.conversation.id;
-
-            // Since this is selected, we draw the checkbox if the animation is not running, or if
-            // it's running, and is past the half-way point
-            if (mPhotoFlipAnimator.getValue() > 1 || !mPhotoFlipAnimator.isStarted()) {
-                // Flash in the check
-                drawCheckbox(canvas);
-            } else {
-                // Flip out the contact photo
-                drawContactImages(canvas);
-            }
-        } else {
-            if ((mConversationListListener.isExitingSelectionMode()
-                    && mLastSelectedId == mHeader.conversation.id)
-                    || mPhotoFlipAnimator.isStarted()) {
-                // Animate back to the photo
-                if (!mPhotoFlipAnimator.isStarted()) {
-                    mPhotoFlipAnimator.startAnimation(true /* reverse */);
-                }
-
-                if (mPhotoFlipAnimator.getValue() > 1) {
-                    // Flash out the check
-                    drawCheckbox(canvas);
-                } else {
-                    // Flip in the contact photo
-                    drawContactImages(canvas);
-                }
-            } else {
-                mLastSelectedId = -1; // We don't care anymore
-                mPhotoFlipAnimator.stopAnimation(); // It's not running, but we want to reset state
-
-                // Contact photos
-                drawContactImages(canvas);
-            }
+    private void drawSendersImage(final Canvas canvas) {
+        if (!mSendersImageView.isFlipping()) {
+            final boolean showSenders = !isSelected();
+            mSendersImageView.reset(showSenders);
         }
-    }
-
-    private void drawContactImages(final Canvas canvas) {
-        // mPhotoFlipFraction goes from 0 to 1
-        final float value = mPhotoFlipAnimator.getValue();
-
-        final float scale = 1f - value;
-        final float xOffset = mContactImagesHolder.getWidth() * value / 2;
-
-        mPhotoFlipMatrix.reset();
-        mPhotoFlipMatrix.postScale(scale, 1);
-
-        final float x = mCoordinates.contactImagesX + xOffset;
-        final float y = mCoordinates.contactImagesY;
-
-        canvas.translate(x, y);
-
+        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
         if (mPhotoBitmap == null) {
-            mContactImagesHolder.draw(canvas, mPhotoFlipMatrix);
+            mSendersImageView.draw(canvas);
         } else {
             canvas.drawBitmap(mPhotoBitmap, null, mPhotoRect, sPaint);
         }
     }
 
-    private void drawCheckbox(final Canvas canvas) {
-        // mPhotoFlipFraction goes from 1 to 2
-
-        // Draw the background
-        canvas.save();
-        canvas.translate(mCoordinates.contactImagesX, mCoordinates.contactImagesY);
-        canvas.drawRect(0, 0, mCoordinates.contactImagesWidth, mCoordinates.contactImagesHeight,
-                sCheckBackgroundPaint);
-        canvas.restore();
-
-        final int x = mCoordinates.contactImagesX
-                + (mCoordinates.contactImagesWidth - CHECK.getWidth()) / 2;
-        final int y = mCoordinates.contactImagesY
-                + (mCoordinates.contactImagesHeight - CHECK.getHeight()) / 2;
-
-        final float value = mPhotoFlipAnimator.getValue();
-        final float scale;
-
-        if (!mPhotoFlipAnimator.isStarted()) {
-            // We're not animating
-            scale = 1;
-        } else if (value < 1.9) {
-            // 1.0 to 1.9 will scale 0 to 1
-            scale = (value - 1f) / 0.9f;
-        } else if (value < 1.95) {
-            // 1.9 to 1.95 will scale 1 to 19/18
-            scale = (value - 1f) / 0.9f;
-        } else {
-            // 1.95 to 2.0 will scale 19/18 to 1
-            scale = (0.95f - (value - 1.95f)) / 0.9f;
-        }
-
-        final float xOffset = CHECK.getWidth() * (1f - scale) / 2f;
-        final float yOffset = CHECK.getHeight() * (1f - scale) / 2f;
-
-        mCheckMatrix.reset();
-        mCheckMatrix.postScale(scale, scale);
-
-        canvas.translate(x + xOffset, y + yOffset);
-
-        canvas.drawBitmap(CHECK, mCheckMatrix, sPaint);
-    }
-
     private void drawAttachmentPreviews(Canvas canvas) {
         canvas.translate(mCoordinates.attachmentPreviewsX, mCoordinates.attachmentPreviewsY);
         final float fraction;
@@ -1685,13 +1557,13 @@
         return toggleSelectedState(null);
     }
 
-    private boolean toggleSelectedState(String sourceOpt) {
+    private boolean toggleSelectedState(final String sourceOpt) {
         if (mHeader != null && mHeader.conversation != null && mSelectedConversationSet != null) {
             mSelected = !mSelected;
             setSelected(mSelected);
-            Conversation conv = mHeader.conversation;
+            final Conversation conv = mHeader.conversation;
             // Set the list position of this item in the conversation
-            SwipeableListView listView = getListView();
+            final SwipeableListView listView = getListView();
 
             try {
                 conv.position = mSelected && listView != null ? listView.getPositionForView(this)
@@ -1710,9 +1582,8 @@
                 listView.commitDestructiveActions(true);
             }
 
-            final boolean reverse = !mSelected;
-            mPhotoFlipAnimator.startAnimation(reverse);
-            mPhotoFlipAnimator.invalidateArea();
+            final boolean front = !mSelected;
+            mSendersImageView.flipTo(front);
 
             // We update the background after the checked state has changed
             // now that we have a selected background asset. Setting the background
@@ -1726,6 +1597,17 @@
         return false;
     }
 
+    @Override
+    public void onSetEmpty() {
+        mSendersImageView.flipTo(true);
+    }
+
+    @Override
+    public void onSetPopulated(final ConversationSelectionSet set) { }
+
+    @Override
+    public void onSetChanged(final ConversationSelectionSet set) { }
+
     /**
      * Toggle the star on this view and update the conversation.
      */
@@ -2173,124 +2055,6 @@
         return sScrollSlop;
     }
 
-    private abstract class CabAnimator {
-        private ObjectAnimator mAnimator = null;
-
-        private final String mPropertyName;
-
-        private float mValue;
-
-        private final float mStartValue;
-        private final float mEndValue;
-
-        private final long mDuration;
-
-        private boolean mReversing = false;
-
-        public CabAnimator(final String propertyName, final float startValue, final float endValue,
-                final long duration) {
-            mPropertyName = propertyName;
-
-            mStartValue = startValue;
-            mEndValue = endValue;
-
-            mDuration = duration;
-        }
-
-        private ObjectAnimator createAnimator() {
-            final ObjectAnimator animator = ObjectAnimator.ofFloat(ConversationItemView.this,
-                    mPropertyName, mStartValue, mEndValue);
-            animator.setDuration(mDuration);
-            animator.setInterpolator(new LinearInterpolator());
-            animator.addListener(new AnimatorListenerAdapter() {
-                @Override
-                public void onAnimationEnd(final Animator animation) {
-                    invalidateArea();
-                }
-            });
-            animator.addListener(mAnimatorListener);
-            return animator;
-        }
-
-        private final AnimatorListener mAnimatorListener = new AnimatorListener() {
-            @Override
-            public void onAnimationStart(final Animator animation) {
-                // Do nothing
-            }
-
-            @Override
-            public void onAnimationEnd(final Animator animation) {
-                if (mReversing) {
-                    mReversing = false;
-                    // We no longer want to track whether we were last selected,
-                    // since we no longer are selected
-                    mLastSelectedId = -1;
-                }
-            }
-
-            @Override
-            public void onAnimationCancel(final Animator animation) {
-                // Do nothing
-            }
-
-            @Override
-            public void onAnimationRepeat(final Animator animation) {
-                // Do nothing
-            }
-        };
-
-        public abstract void invalidateArea();
-
-        public void setValue(final float fraction) {
-            if (mValue == fraction) {
-                return;
-            }
-            mValue = fraction;
-            invalidateArea();
-        }
-
-        public float getValue() {
-            return mValue;
-        }
-
-        /**
-         * @param reverse <code>true</code> to animate in reverse
-         */
-        public void startAnimation(final boolean reverse) {
-            if (mAnimator != null) {
-                mAnimator.cancel();
-            }
-
-            mAnimator = createAnimator();
-            mReversing = reverse;
-
-            if (reverse) {
-                mAnimator.reverse();
-            } else {
-                mAnimator.start();
-            }
-        }
-
-        public void stopAnimation() {
-            if (mAnimator != null) {
-                mAnimator.cancel();
-                mAnimator = null;
-            }
-
-            mReversing = false;
-
-            setValue(0);
-        }
-
-        public boolean isStarted() {
-            return mAnimator != null && mAnimator.isStarted();
-        }
-    }
-
-    public void setPhotoFlipFraction(final float fraction) {
-        mPhotoFlipAnimator.setValue(fraction);
-    }
-
     public String getAccount() {
         return mAccount;
     }
diff --git a/src/com/android/mail/browse/SwipeableConversationItemView.java b/src/com/android/mail/browse/SwipeableConversationItemView.java
index cd17c2f..919a5aa 100644
--- a/src/com/android/mail/browse/SwipeableConversationItemView.java
+++ b/src/com/android/mail/browse/SwipeableConversationItemView.java
@@ -28,7 +28,6 @@
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Folder;
 import com.android.mail.ui.AnimatedAdapter;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
 import com.android.mail.ui.ControllableActivity;
 import com.android.mail.ui.ConversationSelectionSet;
 
@@ -56,15 +55,14 @@
     }
 
     public void bind(final Conversation conversation, final ControllableActivity activity,
-            final ConversationListListener conversationListListener,
             final ConversationSelectionSet set, final Folder folder,
             final int checkboxOrSenderImage, final boolean showAttachmentPreviews,
             final boolean parallaxSpeedAlternative, final boolean parallaxDirectionAlternative,
             final boolean swipeEnabled, final boolean priorityArrowsEnabled,
             final AnimatedAdapter animatedAdapter) {
-        mConversationItemView.bind(conversation, activity, conversationListListener, set, folder,
-                checkboxOrSenderImage, showAttachmentPreviews, parallaxSpeedAlternative,
-                parallaxDirectionAlternative, swipeEnabled, priorityArrowsEnabled, animatedAdapter);
+        mConversationItemView.bind(conversation, activity, set, folder, checkboxOrSenderImage,
+                showAttachmentPreviews, parallaxSpeedAlternative, parallaxDirectionAlternative,
+                swipeEnabled, priorityArrowsEnabled, animatedAdapter);
     }
 
     public void startUndoAnimation(AnimatorListener listener, boolean swipe) {
diff --git a/src/com/android/mail/photomanager/ContactPhotoManager.java b/src/com/android/mail/photomanager/ContactPhotoManager.java
deleted file mode 100644
index 06184b0..0000000
--- a/src/com/android/mail/photomanager/ContactPhotoManager.java
+++ /dev/null
@@ -1,218 +0,0 @@
-/*
- * Copyright (C) 2012 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.ContentResolver;
-import android.content.Context;
-import android.text.TextUtils;
-import android.util.LruCache;
-
-import com.android.mail.ContactInfo;
-import com.android.mail.SenderInfoLoader;
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.google.common.base.Objects;
-import com.google.common.collect.ImmutableMap;
-
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * Asynchronously loads contact photos and maintains a cache of photos.
- */
-public class ContactPhotoManager extends PhotoManager {
-    public static final String CONTACT_PHOTO_SERVICE = "contactPhotos";
-
-    /**
-     * An LRU cache for photo ids mapped to contact addresses.
-     */
-    private final LruCache<String, Long> mPhotoIdCache;
-    private final LetterTileProvider mLetterTileProvider;
-
-    /** Cache size for {@link #mPhotoIdCache}. Starting with 500 entries. */
-    private static final int PHOTO_ID_CACHE_SIZE = 500;
-
-    /**
-     * Requests the singleton instance with data bound from the available authenticators. This
-     * method can safely be called from the UI thread.
-     */
-    public static ContactPhotoManager getInstance(Context context) {
-        Context applicationContext = context.getApplicationContext();
-        ContactPhotoManager service =
-                (ContactPhotoManager) applicationContext.getSystemService(CONTACT_PHOTO_SERVICE);
-        if (service == null) {
-            service = createContactPhotoManager(applicationContext);
-            LogUtils.e(TAG, "No contact photo service in context: " + applicationContext);
-        }
-        return service;
-    }
-
-    public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
-        return new ContactPhotoManager(context);
-    }
-
-    public static int generateHash(ImageCanvas view, int pos, Object key) {
-        return Objects.hashCode(view, pos, key);
-    }
-
-    private ContactPhotoManager(Context context) {
-        super(context);
-        mPhotoIdCache = new LruCache<String, Long>(PHOTO_ID_CACHE_SIZE);
-        mLetterTileProvider = new LetterTileProvider(context);
-    }
-
-    @Override
-    protected DefaultImageProvider getDefaultImageProvider() {
-        return mLetterTileProvider;
-    }
-
-    @Override
-    protected int getHash(PhotoIdentifier id, ImageCanvas view) {
-        final ContactIdentifier contactId = (ContactIdentifier) id;
-        return generateHash(view, contactId.pos, contactId.getKey());
-    }
-
-    @Override
-    protected PhotoLoaderThread getLoaderThread(ContentResolver contentResolver) {
-        return new ContactPhotoLoaderThread(contentResolver);
-    }
-
-    @Override
-    public void clear() {
-        super.clear();
-        mPhotoIdCache.evictAll();
-    }
-
-    public static class ContactIdentifier extends PhotoIdentifier {
-        public final String name;
-        public final String emailAddress;
-        public final int pos;
-
-        public ContactIdentifier(String name, String emailAddress, int pos) {
-            this.name = name;
-            this.emailAddress = emailAddress;
-            this.pos = pos;
-        }
-
-        @Override
-        public boolean isValid() {
-            return !TextUtils.isEmpty(emailAddress);
-        }
-
-        @Override
-        public Object getKey() {
-            return emailAddress;
-        }
-
-        @Override
-        public int hashCode() {
-            int hash = 17;
-            hash = 31 * hash + (emailAddress != null ? emailAddress.hashCode() : 0);
-            hash = 31 * hash + (name != null ? name.hashCode() : 0);
-            hash = 31 * hash + pos;
-            return hash;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj)
-                return true;
-            if (obj == null)
-                return false;
-            if (getClass() != obj.getClass())
-                return false;
-            ContactIdentifier other = (ContactIdentifier) obj;
-            return Objects.equal(emailAddress, other.emailAddress)
-                    && Objects.equal(name, other.name) && Objects.equal(pos, other.pos);
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder sb = new StringBuilder("{");
-            sb.append(super.toString());
-            sb.append(" name=");
-            sb.append(name);
-            sb.append(" email=");
-            sb.append(emailAddress);
-            sb.append(" pos=");
-            sb.append(pos);
-            sb.append("}");
-            return sb.toString();
-        }
-
-        @Override
-        public int compareTo(PhotoIdentifier another) {
-            return 0;
-        }
-    }
-
-    public class ContactPhotoLoaderThread extends PhotoLoaderThread {
-        public ContactPhotoLoaderThread(ContentResolver resolver) {
-            super(resolver);
-        }
-
-        @Override
-        protected Map<String, BitmapHolder> loadPhotos(Collection<Request> requests) {
-            Map<String, BitmapHolder> photos = new HashMap<String, BitmapHolder>(requests.size());
-
-            Set<String> addresses = new HashSet<String>();
-            Set<Long> photoIds = new HashSet<Long>();
-            HashMap<Long, String> photoIdMap = new HashMap<Long, String>();
-
-            Long match;
-            String emailAddress;
-            for (Request request : requests) {
-                emailAddress = (String) request.getKey();
-                match = mPhotoIdCache.get(emailAddress);
-                if (match != null) {
-                    photoIds.add(match);
-                    photoIdMap.put(match, emailAddress);
-                } else {
-                    addresses.add(emailAddress);
-                }
-            }
-
-            // get the Map of email addresses to ContactInfo
-            ImmutableMap<String, ContactInfo> emailAddressToContactInfoMap =
-                    SenderInfoLoader.loadContactPhotos(
-                    getResolver(), addresses, false /* decodeBitmaps */);
-
-            // Put all entries into photos map: a mapping of email addresses to photoBytes.
-            // If there is no ContactInfo, it means we couldn't get a photo for this
-            // address so just put null in for the bytes so that the crazy caching
-            // works properly and we don't get an infinite loop of GC churn.
-            if (emailAddressToContactInfoMap != null) {
-                for (final String address : addresses) {
-                    final ContactInfo info = emailAddressToContactInfoMap.get(address);
-                    photos.put(address,
-                            new BitmapHolder(info != null ? info.photoBytes : null, -1, -1));
-                }
-            } else {
-                // Still need to set a null result for all addresses, otherwise we end
-                // up in the loop where photo manager attempts to load these again.
-                for (final String address: addresses) {
-                    photos.put(address, new BitmapHolder(null, -1, -1));
-                }
-            }
-
-            return photos;
-        }
-    }
-}
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
index bc28223..0cb7505 100644
--- a/src/com/android/mail/photomanager/LetterTileProvider.java
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -29,12 +29,8 @@
 import android.text.TextUtils;
 
 import com.android.mail.R;
-import com.android.mail.photomanager.ContactPhotoManager.ContactIdentifier;
-import com.android.mail.photomanager.PhotoManager.DefaultImageProvider;
-import com.android.mail.photomanager.PhotoManager.PhotoIdentifier;
-import com.android.mail.ui.DividedImageCanvas;
-import com.android.mail.ui.ImageCanvas;
 import com.android.mail.ui.ImageCanvas.Dimensions;
+import com.android.mail.utils.BitmapUtil;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 
@@ -46,7 +42,8 @@
  * tile. If there is no English alphabet character (or digit), it creates a
  * bitmap with the default contact avatar.
  */
-public class LetterTileProvider implements DefaultImageProvider {
+@Deprecated
+public class LetterTileProvider {
     private static final String TAG = LogTag.getLogTag();
     private final Bitmap mDefaultBitmap;
     private final Bitmap[] mBitmapBackgroundCache;
@@ -89,30 +86,6 @@
         mDefaultColor = res.getColor(R.color.letter_tile_default_color);
     }
 
-    @Override
-    public void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent) {
-        ContactIdentifier contactIdentifier = (ContactIdentifier) id;
-        DividedImageCanvas dividedImageView = (DividedImageCanvas) view;
-
-        final String displayName = contactIdentifier.name;
-        final String address = (String) contactIdentifier.getKey();
-
-        // don't apply again if existing letter is there (and valid)
-        if (dividedImageView.hasImageFor(address)) {
-            return;
-        }
-
-        dividedImageView.getDesiredDimensions(address, mDims);
-
-        final Bitmap bitmap = getLetterTile(mDims, displayName, address);
-
-        if (bitmap == null) {
-            return;
-        }
-
-        dividedImageView.addDivisionImage(bitmap, address);
-    }
-
     public Bitmap getLetterTile(final Dimensions dimensions, final String displayName,
             final String address) {
         final String display = !TextUtils.isEmpty(displayName) ? displayName : address;
diff --git a/src/com/android/mail/photomanager/MemInfoReader.java b/src/com/android/mail/photomanager/MemInfoReader.java
deleted file mode 100644
index f53c60c..0000000
--- a/src/com/android/mail/photomanager/MemInfoReader.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (C) 2012 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.os.StrictMode;
-
-import java.io.FileInputStream;
-
-public class MemInfoReader {
-    byte[] mBuffer = new byte[1024];
-
-    private long mTotalSize;
-    private long mFreeSize;
-    private long mCachedSize;
-
-    private static boolean matchText(byte[] buffer, int index, String text) {
-        int N = text.length();
-        if ((index + N) >= buffer.length) {
-            return false;
-        }
-        for (int i = 0; i < N; i++) {
-            if (buffer[index + i] != text.charAt(i)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static long extractMemValue(byte[] buffer, int index) {
-        while (index < buffer.length && buffer[index] != '\n') {
-            if (buffer[index] >= '0' && buffer[index] <= '9') {
-                int start = index;
-                index++;
-                while (index < buffer.length && buffer[index] >= '0' && buffer[index] <= '9') {
-                    index++;
-                }
-                String str = new String(buffer, 0, start, index - start);
-                return ((long) Integer.parseInt(str)) * 1024;
-            }
-            index++;
-        }
-        return 0;
-    }
-
-    public void readMemInfo() {
-        // Permit disk reads here, as /proc/meminfo isn't really "on
-        // disk" and should be fast. TODO: make BlockGuard ignore
-        // /proc/ and /sys/ files perhaps?
-        StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
-        try {
-            mTotalSize = 0;
-            mFreeSize = 0;
-            mCachedSize = 0;
-            FileInputStream is = new FileInputStream("/proc/meminfo");
-            int len = is.read(mBuffer);
-            is.close();
-            final int BUFLEN = mBuffer.length;
-            int count = 0;
-            for (int i = 0; i < len && count < 3; i++) {
-                if (matchText(mBuffer, i, "MemTotal")) {
-                    i += 8;
-                    mTotalSize = extractMemValue(mBuffer, i);
-                    count++;
-                } else if (matchText(mBuffer, i, "MemFree")) {
-                    i += 7;
-                    mFreeSize = extractMemValue(mBuffer, i);
-                    count++;
-                } else if (matchText(mBuffer, i, "Cached")) {
-                    i += 6;
-                    mCachedSize = extractMemValue(mBuffer, i);
-                    count++;
-                }
-                while (i < BUFLEN && mBuffer[i] != '\n') {
-                    i++;
-                }
-            }
-        } catch (java.io.FileNotFoundException e) {
-        } catch (java.io.IOException e) {
-        } finally {
-            StrictMode.setThreadPolicy(savedPolicy);
-        }
-    }
-
-    public long getTotalSize() {
-        return mTotalSize;
-    }
-
-    public long getFreeSize() {
-        return mFreeSize;
-    }
-
-    public long getCachedSize() {
-        return mCachedSize;
-    }
-}
diff --git a/src/com/android/mail/photomanager/MemoryUtils.java b/src/com/android/mail/photomanager/MemoryUtils.java
deleted file mode 100644
index 4992026..0000000
--- a/src/com/android/mail/photomanager/MemoryUtils.java
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
- * Copyright (C) 2012 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;
-
-public class MemoryUtils {
-    private MemoryUtils() {
-    }
-
-    public static final int LARGE_RAM_THRESHOLD = 640 * 1024 * 1024;
-    private static long sTotalMemorySize = -1;
-
-    public static long getTotalMemorySize() {
-        if (sTotalMemorySize < 0) {
-            MemInfoReader reader = new MemInfoReader();
-            reader.readMemInfo();
-
-            // getTotalSize() returns the "MemTotal" value from /proc/meminfo.
-            // Because the linux kernel doesn't see all the RAM on the system
-            // (e.g. GPU takes some),
-            // this is usually smaller than the actual RAM size.
-            sTotalMemorySize = reader.getTotalSize();
-        }
-        return sTotalMemorySize;
-    }
-}
diff --git a/src/com/android/mail/photomanager/PhotoManager.java b/src/com/android/mail/photomanager/PhotoManager.java
deleted file mode 100644
index 94f809d..0000000
--- a/src/com/android/mail/photomanager/PhotoManager.java
+++ /dev/null
@@ -1,993 +0,0 @@
-/*
- * 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.ComponentCallbacks2;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.graphics.Bitmap;
-import android.os.Handler;
-import android.os.Handler.Callback;
-import android.os.HandlerThread;
-import android.os.Message;
-import android.os.Process;
-import android.util.LruCache;
-
-import com.android.mail.ui.ImageCanvas;
-import com.android.mail.utils.LogUtils;
-import com.android.mail.utils.Utils;
-import com.google.common.base.Objects;
-import com.google.common.collect.Lists;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.PriorityQueue;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Asynchronously loads photos and maintains a cache of photos
- */
-public abstract class PhotoManager implements ComponentCallbacks2, Callback {
-    /**
-     * Get the default image provider that draws while the photo is being
-     * loaded.
-     */
-    protected abstract DefaultImageProvider getDefaultImageProvider();
-
-    /**
-     * Generate a hashcode unique to each request.
-     */
-    protected abstract int getHash(PhotoIdentifier id, ImageCanvas view);
-
-    /**
-     * Return a specific implementation of PhotoLoaderThread.
-     */
-    protected abstract PhotoLoaderThread getLoaderThread(ContentResolver contentResolver);
-
-    /**
-     * Subclasses can implement this method to alert callbacks that images finished loading.
-     * @param request The original request made.
-     * @param success True if we successfully loaded the image from cache. False if we fell back
-     *                to the default image.
-     */
-    protected void onImageDrawn(final Request request, final boolean success) {
-        // Subclasses can choose to do something about this
-    }
-
-    /**
-     * Subclasses can implement this method to alert callbacks that images started loading.
-     * @param request The original request made.
-     */
-    protected void onImageLoadStarted(final Request request) {
-        // Subclasses can choose to do something about this
-    }
-
-    /**
-     * Subclasses can implement this method to determine whether a previously loaded bitmap can
-     * be reused for a new canvas size.
-     * @param prevWidth The width of the previously loaded bitmap.
-     * @param prevHeight The height of the previously loaded bitmap.
-     * @param newWidth The width of the canvas this request is drawing on.
-     * @param newHeight The height of the canvas this request is drawing on.
-     * @return
-     */
-    protected boolean isSizeCompatible(int prevWidth, int prevHeight, int newWidth, int newHeight) {
-        return true;
-    }
-
-    protected final Context getContext() {
-        return mContext;
-    }
-
-    static final String TAG = "PhotoManager";
-    static final boolean DEBUG = false; // Don't submit with true
-    static final boolean DEBUG_SIZES = false; // Don't submit with true
-
-    private static final String LOADER_THREAD_NAME = "PhotoLoader";
-
-    /**
-     * Type of message sent by the UI thread to itself to indicate that some photos
-     * need to be loaded.
-     */
-    private static final int MESSAGE_REQUEST_LOADING = 1;
-
-    /**
-     * Type of message sent by the loader thread to indicate that some photos have
-     * been loaded.
-     */
-    private static final int MESSAGE_PHOTOS_LOADED = 2;
-
-    /**
-     * Type of message sent by the loader thread to indicate that
-     */
-    private static final int MESSAGE_PHOTO_LOADING = 3;
-
-    public interface DefaultImageProvider {
-        /**
-         * 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 void applyDefaultImage(PhotoIdentifier id, ImageCanvas view, int extent);
-    }
-
-    /**
-     * Maintains the state of a particular photo.
-     */
-    protected static class BitmapHolder {
-        byte[] bytes;
-        int width;
-        int height;
-
-        volatile boolean fresh;
-
-        public BitmapHolder(byte[] bytes, int width, int height) {
-            this.bytes = bytes;
-            this.width = width;
-            this.height = height;
-            this.fresh = true;
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder sb = new StringBuilder("{");
-            sb.append(super.toString());
-            sb.append(" bytes=");
-            sb.append(bytes);
-            sb.append(" size=");
-            sb.append(bytes == null ? 0 : bytes.length);
-            sb.append(" width=");
-            sb.append(width);
-            sb.append(" height=");
-            sb.append(height);
-            sb.append(" fresh=");
-            sb.append(fresh);
-            sb.append("}");
-            return sb.toString();
-        }
-    }
-
-    // todo:ath caches should be member vars
-    /**
-     * An LRU cache for bitmap holders. The cache contains bytes for photos just
-     * as they come from the database. Each holder has a soft reference to the
-     * actual bitmap. The keys are decided by the implementation.
-     */
-    private static final LruCache<Object, BitmapHolder> sBitmapHolderCache;
-
-    /**
-     * Level 2 LRU cache for bitmaps. This is a smaller cache that holds
-     * the most recently used bitmaps to save time on decoding
-     * them from bytes (the bytes are stored in {@link #sBitmapHolderCache}.
-     * The keys are decided by the implementation.
-     */
-    private static final LruCache<BitmapIdentifier, Bitmap> sBitmapCache;
-
-    /** Cache size for {@link #sBitmapHolderCache} for devices with "large" RAM. */
-    private static final int HOLDER_CACHE_SIZE = 2000000;
-
-    /** Cache size for {@link #sBitmapCache} for devices with "large" RAM. */
-    private static final int BITMAP_CACHE_SIZE = 1024 * 1024 * 8; // 8MB
-
-    /** For debug: How many times we had to reload cached photo for a stale entry */
-    private static final AtomicInteger sStaleCacheOverwrite = new AtomicInteger();
-
-    /** For debug: How many times we had to reload cached photo for a fresh entry.  Should be 0. */
-    private static final AtomicInteger sFreshCacheOverwrite = new AtomicInteger();
-
-    static {
-        final float cacheSizeAdjustment =
-                (MemoryUtils.getTotalMemorySize() >= MemoryUtils.LARGE_RAM_THRESHOLD) ?
-                        1.0f : 0.5f;
-        final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
-        sBitmapHolderCache = new LruCache<Object, BitmapHolder>(holderCacheSize) {
-            @Override protected int sizeOf(Object key, BitmapHolder value) {
-                return value.bytes != null ? value.bytes.length : 0;
-            }
-
-            @Override protected void entryRemoved(
-                    boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
-                if (DEBUG) dumpStats();
-            }
-        };
-        final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
-        sBitmapCache = new LruCache<BitmapIdentifier, Bitmap>(bitmapCacheSize) {
-            @Override protected int sizeOf(BitmapIdentifier key, Bitmap value) {
-                return value.getByteCount();
-            }
-
-            @Override protected void entryRemoved(
-                    boolean evicted, BitmapIdentifier key, Bitmap oldValue, Bitmap newValue) {
-                if (DEBUG) dumpStats();
-            }
-        };
-        LogUtils.i(TAG, "Cache adj: " + cacheSizeAdjustment);
-        if (DEBUG) {
-            LogUtils.d(TAG, "Cache size: " + btk(sBitmapHolderCache.maxSize())
-                    + " + " + btk(sBitmapCache.maxSize()));
-        }
-    }
-
-    /**
-     * A map from ImageCanvas hashcode 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 Map<Integer, Request> mPendingRequests = Collections.synchronizedMap(
-            new HashMap<Integer, Request>());
-
-    /**
-     * Handler for messages sent to the UI thread.
-     */
-    private final Handler mMainThreadHandler = new Handler(this);
-
-    /**
-     * Thread responsible for loading photos from the database. Created upon
-     * the first request.
-     */
-    private PhotoLoaderThread mLoaderThread;
-
-    /**
-     * A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time.
-     */
-    private boolean mLoadingRequested;
-
-    /**
-     * Flag indicating if the image loading is paused.
-     */
-    private boolean mPaused;
-
-    private final Context mContext;
-
-    public PhotoManager(Context context) {
-        mContext = context;
-    }
-
-    public void loadThumbnail(PhotoIdentifier id, ImageCanvas view) {
-        loadThumbnail(id, view, null);
-    }
-
-    /**
-     * Load an image
-     *
-     * @param dimensions    Preferred dimensions
-     */
-    public void loadThumbnail(final PhotoIdentifier id, final ImageCanvas view,
-            final ImageCanvas.Dimensions dimensions) {
-        Utils.traceBeginSection("Load thumbnail");
-        final DefaultImageProvider defaultProvider = getDefaultImageProvider();
-        final Request request = new Request(id, defaultProvider, view, dimensions);
-        final int hashCode = request.hashCode();
-
-        if (!id.isValid()) {
-            // No photo is needed
-            request.applyDefaultImage();
-            onImageDrawn(request, false);
-            mPendingRequests.remove(hashCode);
-        } else if (mPendingRequests.containsKey(hashCode)) {
-            LogUtils.d(TAG, "load request dropped for %s", id);
-        } else {
-            if (DEBUG) LogUtils.v(TAG, "loadPhoto request: %s", id.getKey());
-            loadPhoto(hashCode, request);
-        }
-        Utils.traceEndSection();
-    }
-
-    private void loadPhoto(int hashCode, Request request) {
-        if (DEBUG) {
-            LogUtils.v(TAG, "NEW IMAGE REQUEST key=%s r=%s thread=%s",
-                    request.getKey(),
-                    request,
-                    Thread.currentThread());
-        }
-
-        boolean loaded = loadCachedPhoto(request, false);
-        if (loaded) {
-            if (DEBUG) {
-                LogUtils.v(TAG, "image request, cache hit. request queue size=%s",
-                        mPendingRequests.size());
-            }
-        } else {
-            if (DEBUG) {
-                LogUtils.d(TAG, "image request, cache miss: key=%s", request.getKey());
-            }
-            mPendingRequests.put(hashCode, request);
-            if (!mPaused) {
-                // Send a request to start loading photos
-                requestLoading();
-            }
-        }
-    }
-
-    /**
-     * Remove photo from the supplied image view. This also cancels current pending load request
-     * inside this photo manager.
-     */
-    public void removePhoto(int hashcode) {
-        Request r = mPendingRequests.remove(hashcode);
-        if (r != null) {
-            LogUtils.d(TAG, "removed request %s", r.getKey());
-        }
-    }
-
-    private void ensureLoaderThread() {
-        if (mLoaderThread == null) {
-            mLoaderThread = getLoaderThread(mContext.getContentResolver());
-            mLoaderThread.start();
-        }
-    }
-
-    /**
-     * Checks if the photo is present in cache.  If so, sets the photo on the view.
-     *
-     * @param request                   Determines which image to load from cache.
-     * @param afterLoaderThreadFinished Pass true if calling after the LoaderThread has run. Pass
-     *                                  false if the Loader Thread hasn't made any attempts to
-     *                                  load images yet.
-     * @return false if the photo needs to be (re)loaded from the provider.
-     */
-    private boolean loadCachedPhoto(final Request request,
-            final boolean afterLoaderThreadFinished) {
-        Utils.traceBeginSection("Load cached photo");
-        final Bitmap cached = getCachedPhoto(request.bitmapKey);
-        if (cached != null) {
-            if (DEBUG) {
-                LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
-                        afterLoaderThreadFinished ? "DECODED IMG READ"
-                                : "DECODED IMG CACHE HIT",
-                        request.getKey(),
-                        cached.getByteCount(),
-                        Thread.currentThread());
-            }
-            if (request.getView().getGeneration() == request.viewGeneration) {
-                request.getView().drawImage(cached, request.getKey());
-                onImageDrawn(request, true);
-            }
-            Utils.traceEndSection();
-            return true;
-        }
-
-        // We couldn't load the requested image, so try to load a replacement.
-        // This removes the flicker from SIMPLE to BEST transition.
-        final Object replacementKey = request.getPhotoIdentifier().getKeyToShowInsteadOfDefault();
-        if (replacementKey != null) {
-            final BitmapIdentifier replacementBitmapKey = new BitmapIdentifier(replacementKey,
-                    request.bitmapKey.w, request.bitmapKey.h);
-            final Bitmap cachedReplacement = getCachedPhoto(replacementBitmapKey);
-            if (cachedReplacement != null) {
-                if (DEBUG) {
-                    LogUtils.v(TAG, "%s, key=%s decodedSize=%s thread=%s",
-                            afterLoaderThreadFinished ? "DECODED IMG READ"
-                                    : "DECODED IMG CACHE HIT",
-                            replacementKey,
-                            cachedReplacement.getByteCount(),
-                            Thread.currentThread());
-                }
-                if (request.getView().getGeneration() == request.viewGeneration) {
-                    request.getView().drawImage(cachedReplacement, request.getKey());
-                    onImageDrawn(request, true);
-                }
-                Utils.traceEndSection();
-                return false;
-            }
-        }
-
-        // We couldn't load any image, so draw a default image
-        request.applyDefaultImage();
-
-        final BitmapHolder holder = sBitmapHolderCache.get(request.getKey());
-        // Check if we loaded null bytes, which means we meant to not draw anything.
-        if (holder != null && holder.bytes == null) {
-            onImageDrawn(request, holder.fresh);
-            Utils.traceEndSection();
-            return holder.fresh;
-        }
-        Utils.traceEndSection();
-        return false;
-    }
-
-    /**
-     * Takes care of retrieving the Bitmap from both the decoded and holder caches.
-     */
-    private static Bitmap getCachedPhoto(BitmapIdentifier bitmapKey) {
-        Utils.traceBeginSection("Get cached photo");
-        final Bitmap cached = sBitmapCache.get(bitmapKey);
-        Utils.traceEndSection();
-        return cached;
-    }
-
-    /**
-     * Temporarily stops loading photos from the database.
-     */
-    public void pause() {
-        LogUtils.d(TAG, "%s paused.", getClass().getName());
-        mPaused = true;
-    }
-
-    /**
-     * Resumes loading photos from the database.
-     */
-    public void resume() {
-        LogUtils.d(TAG, "%s resumed.", getClass().getName());
-        mPaused = false;
-        if (DEBUG) dumpStats();
-        if (!mPendingRequests.isEmpty()) {
-            requestLoading();
-        }
-    }
-
-    /**
-     * Sends a message to this thread itself to start loading images.  If the current
-     * view contains multiple image views, all of those image views will get a chance
-     * to request their respective photos before any of those requests are executed.
-     * This allows us to load images in bulk.
-     */
-    private void requestLoading() {
-        if (!mLoadingRequested) {
-            mLoadingRequested = true;
-            mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
-        }
-    }
-
-    /**
-     * Processes requests on the main thread.
-     */
-    @Override
-    public boolean handleMessage(final Message msg) {
-        switch (msg.what) {
-            case MESSAGE_REQUEST_LOADING: {
-                mLoadingRequested = false;
-                if (!mPaused) {
-                    ensureLoaderThread();
-                    mLoaderThread.requestLoading();
-                }
-                return true;
-            }
-
-            case MESSAGE_PHOTOS_LOADED: {
-                processLoadedImages();
-                if (DEBUG) dumpStats();
-                return true;
-            }
-
-            case MESSAGE_PHOTO_LOADING: {
-                final int hashcode = msg.arg1;
-                final Request request = mPendingRequests.get(hashcode);
-                onImageLoadStarted(request);
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Goes over pending loading requests and displays loaded photos.  If some of the
-     * photos still haven't been loaded, sends another request for image loading.
-     */
-    private void processLoadedImages() {
-        Utils.traceBeginSection("process loaded images");
-        final List<Integer> toRemove = Lists.newArrayList();
-        for (final Integer hash : mPendingRequests.keySet()) {
-            final Request request = mPendingRequests.get(hash);
-            final boolean loaded = loadCachedPhoto(request, true);
-            // Request can go through multiple attempts if the LoaderThread fails to load any
-            // images for it, or if the images it loads are evicted from the cache before we
-            // could access them in the main thread.
-            if (loaded || request.attempts > 2) {
-                toRemove.add(hash);
-            }
-        }
-        for (final Integer key : toRemove) {
-            mPendingRequests.remove(key);
-        }
-
-        if (!mPaused && !mPendingRequests.isEmpty()) {
-            LogUtils.d(TAG, "Finished loading batch. %d still have to be loaded.",
-                    mPendingRequests.size());
-            requestLoading();
-        }
-        Utils.traceEndSection();
-    }
-
-    /**
-     * Stores the supplied bitmap in cache.
-     */
-    private static void cacheBitmapHolder(final String cacheKey, final BitmapHolder holder) {
-        if (DEBUG) {
-            BitmapHolder prev = sBitmapHolderCache.get(cacheKey);
-            if (prev != null && prev.bytes != null) {
-                LogUtils.d(TAG, "Overwriting cache: key=" + cacheKey
-                        + (prev.fresh ? " FRESH" : " stale"));
-                if (prev.fresh) {
-                    sFreshCacheOverwrite.incrementAndGet();
-                } else {
-                    sStaleCacheOverwrite.incrementAndGet();
-                }
-            }
-            LogUtils.d(TAG, "Caching data: key=" + cacheKey + ", "
-                    + (holder.bytes == null ? "<null>" : btk(holder.bytes.length)));
-        }
-
-        sBitmapHolderCache.put(cacheKey, holder);
-    }
-
-    protected static void cacheBitmap(final BitmapIdentifier bitmapKey, final Bitmap bitmap) {
-        sBitmapCache.put(bitmapKey, bitmap);
-    }
-
-    // ComponentCallbacks2
-    @Override
-    public void onConfigurationChanged(Configuration newConfig) {
-    }
-
-    // ComponentCallbacks2
-    @Override
-    public void onLowMemory() {
-    }
-
-    // ComponentCallbacks2
-    @Override
-    public void onTrimMemory(int 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();
-        }
-    }
-
-    public void clear() {
-        if (DEBUG) LogUtils.d(TAG, "clear");
-        mPendingRequests.clear();
-        sBitmapHolderCache.evictAll();
-        sBitmapCache.evictAll();
-    }
-
-    /**
-     * Dump cache stats on logcat.
-     */
-    private static void dumpStats() {
-        if (!DEBUG) {
-            return;
-        }
-        int numHolders = 0;
-        int rawBytes = 0;
-        int bitmapBytes = 0;
-        int numBitmaps = 0;
-        for (BitmapHolder h : sBitmapHolderCache.snapshot().values()) {
-            numHolders++;
-            if (h.bytes != null) {
-                rawBytes += h.bytes.length;
-                numBitmaps++;
-            }
-        }
-        LogUtils.d(TAG,
-                "L1: " + btk(rawBytes) + " + " + btk(bitmapBytes) + " = "
-                        + btk(rawBytes + bitmapBytes) + ", " + numHolders + " holders, "
-                        + numBitmaps + " bitmaps, avg: " + btk(safeDiv(rawBytes, numBitmaps)));
-        LogUtils.d(TAG, "L1 Stats: %s, overwrite: fresh=%s stale=%s", sBitmapHolderCache,
-                sFreshCacheOverwrite.get(), sStaleCacheOverwrite.get());
-
-        numBitmaps = 0;
-        bitmapBytes = 0;
-        for (Bitmap b : sBitmapCache.snapshot().values()) {
-            numBitmaps++;
-            bitmapBytes += b.getByteCount();
-        }
-        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.
-    }
-
-    /** Converts bytes to K bytes, rounding up.  Used only for debug log. */
-    private static String btk(int bytes) {
-        return ((bytes + 1023) / 1024) + "K";
-    }
-
-    private static final int safeDiv(int dividend, int divisor) {
-        return (divisor  == 0) ? 0 : (dividend / divisor);
-    }
-
-    public static abstract class PhotoIdentifier implements Comparable<PhotoIdentifier> {
-        /**
-         * If this returns false, the PhotoManager will not attempt to load the
-         * bitmap. Instead, the default image provider will be used.
-         */
-        public abstract boolean isValid();
-
-        /**
-         * Identifies this request.
-         */
-        public abstract Object getKey();
-
-        /**
-         * Replacement key to try to load from cache instead of drawing the default image. This
-         * is useful when we've already loaded a SIMPLE rendition, and are now loading the BEST
-         * rendition. We want the BEST image to appear seamlessly on top of the existing SIMPLE
-         * image.
-         */
-        public Object getKeyToShowInsteadOfDefault() {
-            return null;
-        }
-    }
-
-    /**
-     * The thread that performs loading of photos from the database.
-     */
-    protected abstract class PhotoLoaderThread extends HandlerThread implements Callback {
-
-        /**
-         * Return photos mapped from {@link Request#getKey()} to the photo for
-         * that request.
-         */
-        protected abstract Map<String, BitmapHolder> loadPhotos(Collection<Request> requests);
-
-        private static final int MESSAGE_LOAD_PHOTOS = 0;
-
-        private final ContentResolver mResolver;
-
-        private Handler mLoaderThreadHandler;
-
-        public PhotoLoaderThread(ContentResolver resolver) {
-            super(LOADER_THREAD_NAME, Process.THREAD_PRIORITY_BACKGROUND);
-            mResolver = resolver;
-        }
-
-        protected ContentResolver getResolver() {
-            return mResolver;
-        }
-
-        public void ensureHandler() {
-            if (mLoaderThreadHandler == null) {
-                mLoaderThreadHandler = new Handler(getLooper(), this);
-            }
-        }
-
-        /**
-         * Sends a message to this thread to load requested photos.  Cancels a preloading
-         * request, if any: we don't want preloading to impede loading of the photos
-         * we need to display now.
-         */
-        public void requestLoading() {
-            ensureHandler();
-            mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
-        }
-
-        /**
-         * Receives the above message, loads photos and then sends a message
-         * to the main thread to process them.
-         */
-        @Override
-        public boolean handleMessage(Message msg) {
-            switch (msg.what) {
-                case MESSAGE_LOAD_PHOTOS:
-                    loadPhotosInBackground();
-                    break;
-            }
-            return true;
-        }
-
-        /**
-         * Subclasses may specify the maximum number of requests to be given at a time to
-         * #loadPhotos(). For batch count N, the UI will be updated with up to N images at a time.
-         *
-         * @return A positive integer if you would like to limit the number of
-         *         items in a single batch.
-         */
-        protected int getMaxBatchCount() {
-            return -1;
-        }
-
-        private void loadPhotosInBackground() {
-            Utils.traceBeginSection("pre processing");
-            final Collection<Request> loadRequests = new HashSet<PhotoManager.Request>();
-            final Collection<Request> decodeRequests = new HashSet<PhotoManager.Request>();
-            final PriorityQueue<Request> requests;
-            synchronized (mPendingRequests) {
-                requests = new PriorityQueue<Request>(mPendingRequests.values());
-            }
-
-            int batchCount = 0;
-            int maxBatchCount = getMaxBatchCount();
-            while (!requests.isEmpty()) {
-                Request request = requests.poll();
-                final BitmapHolder holder = sBitmapHolderCache
-                        .get(request.getKey());
-                if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
-                        holder.width, holder.height, request.bitmapKey.w, request.bitmapKey.h)) {
-                    loadRequests.add(request);
-                    decodeRequests.add(request);
-                    batchCount++;
-
-                    final Message msg = Message.obtain();
-                    msg.what = MESSAGE_PHOTO_LOADING;
-                    msg.arg1 = request.hashCode();
-                    mMainThreadHandler.sendMessage(msg);
-                } else {
-                    // Even if the image load is already done, this particular decode configuration
-                    // may not yet have run. Be sure to add it to the queue.
-                    if (sBitmapCache.get(request.bitmapKey) == null) {
-                        decodeRequests.add(request);
-                    }
-                }
-                request.attempts++;
-                if (maxBatchCount > 0 && batchCount >= maxBatchCount) {
-                    break;
-                }
-            }
-            Utils.traceEndSection();
-
-            Utils.traceBeginSection("load photos");
-            // Ask subclass to do the actual loading
-            final Map<String, BitmapHolder> photosMap = loadPhotos(loadRequests);
-            Utils.traceEndSection();
-
-            if (DEBUG) {
-                LogUtils.d(TAG,
-                        "worker thread completed read request batch. inputN=%s outputN=%s",
-                        loadRequests.size(),
-                        photosMap.size());
-            }
-            Utils.traceBeginSection("post processing");
-            for (String cacheKey : photosMap.keySet()) {
-                if (DEBUG) {
-                    LogUtils.d(TAG,
-                            "worker thread completed read request key=%s byteCount=%s thread=%s",
-                            cacheKey,
-                            photosMap.get(cacheKey) == null ? 0
-                                    : photosMap.get(cacheKey).bytes.length,
-                            Thread.currentThread());
-                }
-                cacheBitmapHolder(cacheKey, photosMap.get(cacheKey));
-            }
-
-            for (Request r : decodeRequests) {
-                if (sBitmapCache.get(r.bitmapKey) != null) {
-                    continue;
-                }
-
-                final Object cacheKey = r.getKey();
-                final BitmapHolder holder = sBitmapHolderCache.get(cacheKey);
-                if (holder == null || holder.bytes == null || !holder.fresh || !isSizeCompatible(
-                        holder.width, holder.height, r.bitmapKey.w, r.bitmapKey.h)) {
-                    continue;
-                }
-
-                final int w = r.bitmapKey.w;
-                final int h = r.bitmapKey.h;
-                final byte[] src = holder.bytes;
-
-                if (w == 0 || h == 0) {
-                    LogUtils.e(TAG, new Error(), "bad dimensions for request=%s w/h=%s/%s",
-                            r, w, h);
-                }
-
-                final Bitmap decoded = BitmapUtil.decodeByteArrayWithCenterCrop(src, w, h);
-                if (DEBUG) {
-                    LogUtils.i(TAG,
-                            "worker thread completed decode bmpKey=%s decoded=%s holder=%s",
-                            r.bitmapKey, decoded, holder);
-                }
-
-                if (decoded != null) {
-                    cacheBitmap(r.bitmapKey, decoded);
-                }
-            }
-            Utils.traceEndSection();
-
-            mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
-        }
-
-        protected String createInQuery(String value, int itemCount) {
-            // Build first query
-            StringBuilder query = new StringBuilder().append(value + " IN (");
-            appendQuestionMarks(query, itemCount);
-            query.append(')');
-            return query.toString();
-        }
-
-        protected void appendQuestionMarks(StringBuilder query, int itemCount) {
-            boolean first = true;
-            for (int i = 0; i < itemCount; i++) {
-                if (first) {
-                    first = false;
-                } else {
-                    query.append(',');
-                }
-                query.append('?');
-            }
-        }
-    }
-
-    /**
-     * An object to uniquely identify a combination of (Request + decoded size). Multiple requests
-     * may require the same src image, but want to decode it into different sizes.
-     */
-    public static final class BitmapIdentifier {
-        public final Object key;
-        public final int w;
-        public final int h;
-
-        // OK to be static as long as all Requests are created on the same
-        // thread
-        private static final ImageCanvas.Dimensions sWorkDims = new ImageCanvas.Dimensions();
-
-        public static BitmapIdentifier getBitmapKey(PhotoIdentifier id, ImageCanvas view,
-                ImageCanvas.Dimensions dimensions) {
-            final int width;
-            final int height;
-            if (dimensions != null) {
-                width = dimensions.width;
-                height = dimensions.height;
-            } else {
-                view.getDesiredDimensions(id.getKey(), sWorkDims);
-                width = sWorkDims.width;
-                height = sWorkDims.height;
-            }
-            return new BitmapIdentifier(id.getKey(), width, height);
-        }
-
-        public BitmapIdentifier(Object key, int w, int h) {
-            this.key = key;
-            this.w = w;
-            this.h = h;
-        }
-
-        @Override
-        public int hashCode() {
-            int hash = 19;
-            hash = 31 * hash + key.hashCode();
-            hash = 31 * hash + w;
-            hash = 31 * hash + h;
-            return hash;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (obj == null || obj.getClass() != getClass()) {
-                return false;
-            } else if (obj == this) {
-                return true;
-            }
-            final BitmapIdentifier o = (BitmapIdentifier) obj;
-            return Objects.equal(key, o.key) && w == o.w && h == o.h;
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder sb = new StringBuilder("{");
-            sb.append(super.toString());
-            sb.append(" key=");
-            sb.append(key);
-            sb.append(" w=");
-            sb.append(w);
-            sb.append(" h=");
-            sb.append(h);
-            sb.append("}");
-            return sb.toString();
-        }
-    }
-
-    /**
-     * A holder for a contact photo request.
-     */
-    public final class Request implements Comparable<Request> {
-        private final int mRequestedExtent;
-        private final DefaultImageProvider mDefaultProvider;
-        private final PhotoIdentifier mPhotoIdentifier;
-        private final ImageCanvas mView;
-        public final BitmapIdentifier bitmapKey;
-        public final int viewGeneration;
-        public int attempts;
-
-        private Request(final PhotoIdentifier photoIdentifier,
-                final DefaultImageProvider defaultProvider, final ImageCanvas view,
-                final ImageCanvas.Dimensions dimensions) {
-            mPhotoIdentifier = photoIdentifier;
-            mRequestedExtent = -1;
-            mDefaultProvider = defaultProvider;
-            mView = view;
-            viewGeneration = view.getGeneration();
-
-            bitmapKey = BitmapIdentifier.getBitmapKey(photoIdentifier, mView, dimensions);
-        }
-
-        public ImageCanvas getView() {
-            return mView;
-        }
-
-        public PhotoIdentifier getPhotoIdentifier() {
-            return mPhotoIdentifier;
-        }
-
-        /**
-         * @see PhotoIdentifier#getKey()
-         */
-        public Object getKey() {
-            return mPhotoIdentifier.getKey();
-        }
-
-        @Override
-        public int hashCode() {
-            return getHash(mPhotoIdentifier, mView);
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) return true;
-            if (obj == null) return false;
-            if (getClass() != obj.getClass()) return false;
-            final Request that = (Request) obj;
-            if (mRequestedExtent != that.mRequestedExtent) return false;
-            if (!Objects.equal(mPhotoIdentifier, that.mPhotoIdentifier)) return false;
-            if (!Objects.equal(mView, that.mView)) 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
-            // twice.
-            return true;
-        }
-
-        @Override
-        public String toString() {
-            final StringBuilder sb = new StringBuilder("{");
-            sb.append(super.toString());
-            sb.append(" key=");
-            sb.append(getKey());
-            sb.append(" id=");
-            sb.append(mPhotoIdentifier);
-            sb.append(" mView=");
-            sb.append(mView);
-            sb.append(" mExtent=");
-            sb.append(mRequestedExtent);
-            sb.append(" bitmapKey=");
-            sb.append(bitmapKey);
-            sb.append(" viewGeneration=");
-            sb.append(viewGeneration);
-            sb.append("}");
-            return sb.toString();
-        }
-
-        public void applyDefaultImage() {
-            if (mView.getGeneration() != viewGeneration) {
-                // This can legitimately happen when an ImageCanvas is reused and re-purposed to
-                // house a new set of images (e.g. by ListView recycling).
-                // Ignore this now-stale request.
-                if (DEBUG) {
-                    LogUtils.d(TAG,
-                            "ImageCanvas skipping applyDefaultImage; no longer contains" +
-                            " item=%s canvas=%s", getKey(), mView);
-                }
-            }
-            mDefaultProvider.applyDefaultImage(mPhotoIdentifier, mView, mRequestedExtent);
-        }
-
-        @Override
-        public int compareTo(Request another) {
-            // Hold off on loading Requests which have failed before so it don't hold up others
-            if (attempts - another.attempts != 0) {
-                return attempts - another.attempts;
-            }
-            return mPhotoIdentifier.compareTo(another.mPhotoIdentifier);
-        }
-    }
-}
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index c83e6e8..1b6000b 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -23,6 +23,7 @@
 import com.android.mail.providers.Account;
 import com.android.mail.providers.UIProvider;
 import com.android.mail.utils.LogUtils;
+import com.android.mail.utils.Utils;
 import com.android.mail.widget.BaseWidgetProvider;
 
 import com.google.common.collect.ImmutableSet;
@@ -40,7 +41,7 @@
  */
 public final class MailPrefs extends VersionedPrefs {
 
-    public static final boolean SHOW_EXPERIMENTAL_PREFS = false;
+    public static final boolean SHOW_EXPERIMENTAL_PREFS = true;
 
     private static final String PREFS_NAME = "UnifiedEmail";
 
@@ -392,6 +393,11 @@
     }
 
     public boolean getShowSenderImages() {
+        if (Utils.isLowRamDevice(getContext())) {
+            // Do not show sender images in conversation list on low memory devices since they are
+            // expensive to render.
+            return false;
+        }
         final SharedPreferences sharedPreferences = getSharedPreferences();
         return sharedPreferences.getBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, true);
     }
diff --git a/src/com/android/mail/ui/AbstractMailActivity.java b/src/com/android/mail/ui/AbstractMailActivity.java
index 33a7da2..43f3b23 100644
--- a/src/com/android/mail/ui/AbstractMailActivity.java
+++ b/src/com/android/mail/ui/AbstractMailActivity.java
@@ -39,7 +39,7 @@
 
     private final UiHandler mUiHandler = new UiHandler();
 
-    private static final boolean STRICT_MODE = false;
+    private static final boolean STRICT_MODE = true;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index fad7b3a..d47ea9c 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -40,6 +40,7 @@
 import com.android.bitmap.DecodeAggregator;
 import com.android.mail.R;
 import com.android.mail.analytics.Analytics;
+import com.android.mail.bitmap.ContactResolver;
 import com.android.mail.browse.ConversationCursor;
 import com.android.mail.browse.ConversationItemView;
 import com.android.mail.browse.ConversationItemViewCoordinates.CoordinatesCache;
@@ -93,17 +94,6 @@
     private final Handler mHandler;
     protected long mLastLeaveBehind = -1;
 
-    private final BitmapCache mBitmapCache;
-    private final DecodeAggregator mDecodeAggregator;
-
-    public interface ConversationListListener {
-        /**
-         * @return <code>true</code> if the list is just exiting selection mode (so animations may
-         * be required), <code>false</code> otherwise
-         */
-        boolean isExitingSelectionMode();
-    }
-
     private final AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() {
 
         @Override
@@ -184,7 +174,6 @@
     /** True if priority inbox markers are enabled, false otherwise. */
     private boolean mPriorityMarkersEnabled;
     private final ControllableActivity mActivity;
-    private final ConversationListListener mConversationListListener;
     private final AccountObserver mAccountListener = new AccountObserver() {
         @Override
         public void onChanged(Account newAccount) {
@@ -250,31 +239,43 @@
     private static final String LOG_TAG = LogTag.getLogTag();
     private static final int INCREASE_WAIT_COUNT = 2;
 
-    private static final int BITMAP_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+    private final BitmapCache mAttachmentPreviewsCache;
+    private final DecodeAggregator mAttachmentPreviewsDecodeAggregator;
+    private final BitmapCache mSendersImagesCache;
+    private final ContactResolver mContactResolver;
+
+    private static final int ATTACHMENT_PREVIEWS_CACHE_TARGET_SIZE_BYTES = 0; // TODO: enable cache
+    /** 339KB cache fits 10 bitmaps at 33856 bytes each. */
+    private static final int SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES = 1024 * 339;
     /**
      * This is the fractional portion of the total cache size above that's dedicated to non-pooled
      * bitmaps. (This is basically the portion of cache dedicated to GIFs.)
      */
-    private static final float BITMAP_CACHE_NON_POOLED_FRACTION = 0.1f;
+    private static final float ATTACHMENT_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0.1f;
+    private static final float SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION = 0f;
+    /** Each string has upper estimate of 50 bytes, so this cache would be 5KB. */
+    private static final int SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY = 100;
 
     public AnimatedAdapter(Context context, ConversationCursor cursor,
             ConversationSelectionSet batch, ControllableActivity activity,
-            final ConversationListListener conversationListListener, SwipeableListView listView,
-            final List<ConversationSpecialItemView> specialViews,
+            SwipeableListView listView, final List<ConversationSpecialItemView> specialViews,
             final ObjectCursor<Folder> childFolders) {
         super(context, -1, cursor, UIProvider.CONVERSATION_PROJECTION, null, 0);
         mContext = context;
         mBatchConversations = batch;
         setAccount(mAccountListener.initialize(activity.getAccountController()));
         mActivity = activity;
-        mConversationListListener = conversationListListener;
         mShowFooter = false;
         mListView = listView;
         mFolderViews = getNestedFolders(childFolders);
 
-        mBitmapCache = new AltBitmapCache(BITMAP_CACHE_TARGET_SIZE_BYTES,
-                BITMAP_CACHE_NON_POOLED_FRACTION);
-        mDecodeAggregator = new DecodeAggregator();
+        mAttachmentPreviewsCache = new AltBitmapCache(ATTACHMENT_PREVIEWS_CACHE_TARGET_SIZE_BYTES,
+                ATTACHMENT_PREVIEWS_CACHE_NON_POOLED_FRACTION, 0);
+        mAttachmentPreviewsDecodeAggregator = new DecodeAggregator();
+        mSendersImagesCache = new AltBitmapCache(SENDERS_IMAGES_CACHE_TARGET_SIZE_BYTES,
+                SENDERS_IMAGES_PREVIEWS_CACHE_NON_POOLED_FRACTION,
+                SENDERS_IMAGES_PREVIEWS_CACHE_NULL_CAPACITY);
+        mContactResolver = new ContactResolver(mContext.getContentResolver(), mSendersImagesCache);
 
         mHandler = new Handler();
         if (sDismissAllShortDelay == -1) {
@@ -415,10 +416,10 @@
         if (view == null) {
             view = new SwipeableConversationItemView(context, mAccount.name);
         }
-        view.bind(conv, mActivity, mConversationListListener, mBatchConversations, mFolder,
-                getCheckboxSetting(), getAttachmentPreviewsSetting(),
-                getParallaxSpeedAlternativeSetting(), getParallaxDirectionAlternativeSetting(),
-                mSwipeEnabled, mPriorityMarkersEnabled, this);
+        view.bind(conv, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
+                getAttachmentPreviewsSetting(), getParallaxSpeedAlternativeSetting(),
+                getParallaxDirectionAlternativeSetting(), mSwipeEnabled, mPriorityMarkersEnabled,
+                this);
         return view;
     }
 
@@ -803,10 +804,10 @@
         SwipeableConversationItemView view = (SwipeableConversationItemView) super.getView(
                 position, null, parent);
         view.reset();
-        view.bind(conversation, mActivity, mConversationListListener, mBatchConversations, mFolder,
-                getCheckboxSetting(), getAttachmentPreviewsSetting(),
-                getParallaxSpeedAlternativeSetting(), getParallaxDirectionAlternativeSetting(),
-                mSwipeEnabled, mPriorityMarkersEnabled, this);
+        view.bind(conversation, mActivity, mBatchConversations, mFolder, getCheckboxSetting(),
+                getAttachmentPreviewsSetting(), getParallaxSpeedAlternativeSetting(),
+                getParallaxDirectionAlternativeSetting(), mSwipeEnabled, mPriorityMarkersEnabled,
+                this);
         mAnimatingViews.put(conversation.id, view);
         return view;
     }
@@ -1117,12 +1118,20 @@
         return oldCursor;
     }
 
-    public BitmapCache getBitmapCache() {
-        return mBitmapCache;
+    public BitmapCache getAttachmentPreviewsCache() {
+        return mAttachmentPreviewsCache;
     }
 
-    public DecodeAggregator getDecodeAggregator() {
-        return mDecodeAggregator;
+    public DecodeAggregator getAttachmentPreviewsDecodeAggregator() {
+        return mAttachmentPreviewsDecodeAggregator;
+    }
+
+    public BitmapCache getSendersImagesCache() {
+        return mSendersImagesCache;
+    }
+
+    public ContactResolver getContactResolver() {
+        return mContactResolver;
     }
 
     /**
@@ -1178,7 +1187,7 @@
 
     public void onScrollStateChanged(final int scrollState) {
         final boolean scrolling = scrollState != OnScrollListener.SCROLL_STATE_IDLE;
-        mBitmapCache.setBlocking(scrolling);
+        mAttachmentPreviewsCache.setBlocking(scrolling);
     }
 
     public int getViewMode() {
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index 32b8b3d..20a7c9f 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -60,7 +60,6 @@
 import com.android.mail.providers.UIProvider.FolderCapabilities;
 import com.android.mail.providers.UIProvider.FolderType;
 import com.android.mail.providers.UIProvider.Swipe;
-import com.android.mail.ui.AnimatedAdapter.ConversationListListener;
 import com.android.mail.ui.SwipeableListView.ListItemSwipedListener;
 import com.android.mail.ui.SwipeableListView.ListItemsRemovedListener;
 import com.android.mail.ui.ViewMode.ModeChangeListener;
@@ -358,8 +357,7 @@
         }
 
         mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
-                mActivity.getSelectedSet(), mActivity, mConversationListListener, mListView,
-                specialItemViews, null);
+                mActivity.getSelectedSet(), mActivity, mListView, specialItemViews, null);
         mListAdapter.addFooter(mFooterView);
         mListView.setAdapter(mListAdapter);
         mSelectedSet = mActivity.getSelectedSet();
@@ -743,15 +741,6 @@
         mCallbacks.onConversationSelected(conv, false /* inLoaderCallbacks */);
     }
 
-    private final ConversationListListener mConversationListListener =
-            new ConversationListListener() {
-        @Override
-        public boolean isExitingSelectionMode() {
-            return System.currentTimeMillis() <
-                    (mSelectionModeExitedTimestamp + sSelectionModeAnimationDuration);
-        }
-    };
-
     /**
      * Sets the selected conversation to the position given here.
      * @param cursorPosition The position of the conversation in the cursor (as opposed to
diff --git a/src/com/android/mail/ui/SwipeableListView.java b/src/com/android/mail/ui/SwipeableListView.java
index 627a375..2c0ca14 100644
--- a/src/com/android/mail/ui/SwipeableListView.java
+++ b/src/com/android/mail/ui/SwipeableListView.java
@@ -419,7 +419,6 @@
             if (adapter != null) {
                 adapter.onScrollStateChanged(scrollState);
             }
-            ConversationItemView.setScrollStateChanged(scrollState);
         }
     }
 
diff --git a/src/com/android/mail/photomanager/BitmapUtil.java b/src/com/android/mail/utils/BitmapUtil.java
similarity index 93%
rename from src/com/android/mail/photomanager/BitmapUtil.java
rename to src/com/android/mail/utils/BitmapUtil.java
index 9c2ab2b..6f61f56 100644
--- a/src/com/android/mail/photomanager/BitmapUtil.java
+++ b/src/com/android/mail/utils/BitmapUtil.java
@@ -14,18 +14,17 @@
  * limitations under the License.
  */
 
-package com.android.mail.photomanager;
+package com.android.mail.utils;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Matrix;
 
-import com.android.mail.utils.LogUtils;
-
 /**
  * Provides static functions to decode bitmaps at the optimal size
  */
 public class BitmapUtil {
 
+    private static final String TAG = LogTag.getLogTag();
     private static final boolean DEBUG = false;
 
     private BitmapUtil() {
@@ -53,7 +52,7 @@
             opts.inJustDecodeBounds = false;
             return BitmapFactory.decodeByteArray(src, 0, src.length, opts);
         } catch (Throwable t) {
-            LogUtils.w(PhotoManager.TAG, t, "unable to decode image");
+            LogUtils.w(TAG, t, "BitmapUtils unable to decode image");
             return null;
         }
     }
@@ -73,7 +72,7 @@
             return centerCrop(decoded, w, h);
 
         } catch (Throwable t) {
-            LogUtils.w(PhotoManager.TAG, t, "unable to crop image");
+            LogUtils.w(TAG, t, "BitmapUtils unable to crop image");
             return null;
         }
     }
@@ -161,13 +160,13 @@
         final Bitmap cropped = Bitmap.createBitmap(src, srcX, srcY, srcCroppedW, srcCroppedH, m,
                 true /* filter */);
 
-        if (DEBUG) LogUtils.i(PhotoManager.TAG,
-                "IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
+        if (DEBUG) LogUtils.i(TAG,
+                "BitmapUtils IN centerCrop, srcW/H=%s/%s desiredW/H=%s/%s srcX/Y=%s/%s" +
                 " innerW/H=%s/%s scale=%s resultW/H=%s/%s",
                 srcWidth, srcHeight, w, h, srcX, srcY, srcCroppedW, srcCroppedH, scale,
                 cropped.getWidth(), cropped.getHeight());
         if (DEBUG && (w != cropped.getWidth() || h != cropped.getHeight())) {
-            LogUtils.e(PhotoManager.TAG, new Error(), "last center crop violated assumptions.");
+            LogUtils.e(TAG, new Error(), "BitmapUtils last center crop violated assumptions.");
         }
 
         return cropped;
diff --git a/src/com/android/mail/utils/LogUtils.java b/src/com/android/mail/utils/LogUtils.java
index 5d4f434..3203422 100644
--- a/src/com/android/mail/utils/LogUtils.java
+++ b/src/com/android/mail/utils/LogUtils.java
@@ -64,7 +64,7 @@
      * production releases.  This should be set to DEBUG for production releases, and VERBOSE for
      * internal builds.
      */
-    private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+    private static final int MAX_ENABLED_LOG_LEVEL = VERBOSE;
 
     private static Boolean sDebugLoggingEnabledForTests = null;
 
diff --git a/src/com/android/mail/utils/Utils.java b/src/com/android/mail/utils/Utils.java
index de21172..c4927cd 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -22,6 +22,7 @@
 import com.google.android.mail.common.html.parser.HtmlTreeBuilder;
 import com.google.common.collect.Maps;
 
+import android.app.ActivityManager;
 import android.app.Fragment;
 import android.app.SearchManager;
 import android.content.Context;
@@ -146,6 +147,21 @@
     }
 
     /**
+     * @return Whether we are running on a low memory device.  This is used to disable certain
+     * memory intensive features in the app.
+     */
+    public static boolean isLowRamDevice(Context context) {
+        // TODO: use SDK_INT to check if device is KitKat or greater.
+        if (Build.VERSION.CODENAME.startsWith("K")) {
+            final ActivityManager am = (ActivityManager) context.getSystemService(
+                    Context.ACTIVITY_SERVICE);
+            return am.isLowRamDevice();
+        } else {
+            return false;
+        }
+    }
+
+    /**
      * Sets WebView in a restricted mode suitable for email use.
      *
      * @param webView The WebView to restrict