am 3ef849c6: Merge "Make sure we don\'t create and drop orphans when sorting folders by hierarchy" into jb-ub-mail-ur10

* commit '3ef849c6836a579d42117d333de7c4ecd67d8f16':
  Make sure we don't create and drop orphans when sorting folders by hierarchy
diff --git a/proguard.flags b/proguard.flags
index 79a8c83..dd94b99 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -64,7 +64,6 @@
 
 -keepclasseswithmembers class com.android.mail.browse.ConversationItemView {
   *** setAnimatedHeightFraction(...);
-  *** setPhotoFlipFraction(...);
 }
 
 -keepclasseswithmembers class com.android.mail.ui.MailActivity {
diff --git a/res/drawable-hdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-hdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..bf09b6f
--- /dev/null
+++ b/res/drawable-hdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v19/list_pressed_holo.9.png b/res/drawable-hdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..2054530
--- /dev/null
+++ b/res/drawable-hdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-hdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-hdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..c271b11
--- /dev/null
+++ b/res/drawable-hdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/drawable-mdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-mdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..c73984e
--- /dev/null
+++ b/res/drawable-mdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-mdpi-v19/list_pressed_holo.9.png b/res/drawable-mdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..061904c
--- /dev/null
+++ b/res/drawable-mdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-mdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-mdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..525120d
--- /dev/null
+++ b/res/drawable-mdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/drawable-v19/conversation_read_selector.xml b/res/drawable-v19/conversation_read_selector.xml
new file mode 100644
index 0000000..3fcf5e6
--- /dev/null
+++ b/res/drawable-v19/conversation_read_selector.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true"
+          android:drawable="@drawable/list_pressed_holo" />
+    <item android:state_selected="true"
+          android:drawable="@drawable/list_checked_holo" />
+    <item android:state_activated="true"
+          android:drawable="@drawable/list_activated_holo" />
+    <item android:state_focused="true"
+          android:drawable="@drawable/list_focused_holo" />
+    <item android:drawable="@drawable/list_read_holo" />
+</selector>
diff --git a/res/drawable-v19/conversation_unread_selector.xml b/res/drawable-v19/conversation_unread_selector.xml
new file mode 100644
index 0000000..a454f8a
--- /dev/null
+++ b/res/drawable-v19/conversation_unread_selector.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_pressed="true"
+          android:drawable="@drawable/list_pressed_holo" />
+    <item android:state_selected="true"
+          android:drawable="@drawable/list_checked_holo" />
+    <item android:state_activated="true"
+          android:drawable="@drawable/list_activated_holo" />
+    <item android:state_focused="true"
+          android:drawable="@drawable/list_focused_holo" />
+    <item android:drawable="@drawable/list_unread_holo" />
+</selector>
diff --git a/res/drawable-v19/conversation_wide_unread_selector.xml b/res/drawable-v19/conversation_wide_unread_selector.xml
new file mode 100644
index 0000000..39de4ac
--- /dev/null
+++ b/res/drawable-v19/conversation_wide_unread_selector.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2011 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.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:state_focused="true"
+          android:drawable="@drawable/list_conversation_wide_unread_focused_holo" />
+    <item android:state_pressed="true"
+          android:drawable="@drawable/list_conversation_wide_unread_pressed_holo" />
+    <item android:state_activated="true"
+          android:drawable="@drawable/list_conversation_wide_unread_pressed_holo" />
+    <item android:state_selected="true"
+          android:drawable="@drawable/list_conversation_wide_unread_selected_holo" />
+    <item android:drawable="@drawable/list_conversation_wide_unread_normal_holo" />
+</selector>
diff --git a/res/drawable-xhdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-xhdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..a4ac0c7
--- /dev/null
+++ b/res/drawable-xhdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xhdpi-v19/list_pressed_holo.9.png b/res/drawable-xhdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..f4af926
--- /dev/null
+++ b/res/drawable-xhdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-xhdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-xhdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..3f054fb
--- /dev/null
+++ b/res/drawable-xhdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi-v19/btn_default_pressed_holo_light.9.png b/res/drawable-xxhdpi-v19/btn_default_pressed_holo_light.9.png
new file mode 100644
index 0000000..9521603
--- /dev/null
+++ b/res/drawable-xxhdpi-v19/btn_default_pressed_holo_light.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi-v19/list_pressed_holo.9.png b/res/drawable-xxhdpi-v19/list_pressed_holo.9.png
new file mode 100644
index 0000000..1352a17
--- /dev/null
+++ b/res/drawable-xxhdpi-v19/list_pressed_holo.9.png
Binary files differ
diff --git a/res/drawable-xxhdpi-v19/notification_bg_normal_pressed.9.png b/res/drawable-xxhdpi-v19/notification_bg_normal_pressed.9.png
new file mode 100644
index 0000000..936fbe5
--- /dev/null
+++ b/res/drawable-xxhdpi-v19/notification_bg_normal_pressed.9.png
Binary files differ
diff --git a/res/menu/conversation_actions.xml b/res/menu/conversation_actions.xml
index 6f408f2..9c722d8 100644
--- a/res/menu/conversation_actions.xml
+++ b/res/menu/conversation_actions.xml
@@ -111,6 +111,10 @@
         android:title="@string/report_phishing"
         android:visible="false" />
 
+    <item android:id="@+id/print"
+          android:title="@string/print"
+          android:visible="false" />
+
     <!-- Always available -->
     <item android:id="@+id/settings"
         android:title="@string/menu_settings" />
diff --git a/res/raw/template_border.html b/res/raw/template_border.html
index 8478cbd..3669d50 100644
--- a/res/raw/template_border.html
+++ b/res/raw/template_border.html
@@ -1 +1,17 @@
+<!--
+     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.
+-->
 <div class="mail-border spacer" style="height: %spx;"></div>
diff --git a/res/raw/template_conversation_lower.html b/res/raw/template_conversation_lower.html
index 9c18cf5..9b08dba 100644
--- a/res/raw/template_conversation_lower.html
+++ b/res/raw/template_conversation_lower.html
@@ -1,3 +1,19 @@
+<!--
+     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.
+-->
 <div id="initial-load-signal" class="%s"></div>
 </body>
 <script type="text/javascript">
diff --git a/res/raw/template_conversation_upper.html b/res/raw/template_conversation_upper.html
index fb69324..fde8985 100644
--- a/res/raw/template_conversation_upper.html
+++ b/res/raw/template_conversation_upper.html
@@ -1,3 +1,19 @@
+<!--
+     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.
+-->
 <!DOCTYPE html>
 <html>
 <head>
diff --git a/res/raw/template_message.html b/res/raw/template_message.html
index 8195f7d..8aa8e00 100644
--- a/res/raw/template_message.html
+++ b/res/raw/template_message.html
@@ -1,3 +1,19 @@
+<!--
+     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.
+-->
 <div id="%s" class="mail-message %s">
     <div class="mail-message-header spacer" style="height: %spx;"></div>
     <div class="mail-message-content collapsible zoom-normal %s" style="display: %s; margin: 16px 0;">%s</div>
diff --git a/res/raw/template_print_conversation_lower.html b/res/raw/template_print_conversation_lower.html
new file mode 100644
index 0000000..6a8c660
--- /dev/null
+++ b/res/raw/template_print_conversation_lower.html
@@ -0,0 +1,32 @@
+<!--
+     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.
+-->
+</div>
+</div>
+</body>
+<script type="text/javascript">
+    var elements = document.documentElement.getElementsByClassName("elided-text");
+    var i, elidedElement, toggleElement;
+    for (i = 0; i < elements.length; i++) {
+        elidedElement = elements[i];
+        toggleElement = document.createElement("div");
+        toggleElement.className = "mail-elided-text";
+        toggleElement.innerHTML = "[%s]";
+        elidedElement.style.display = 'none';
+        elidedElement.parentNode.insertBefore(toggleElement, elidedElement);
+    }
+</script>
+</html>
diff --git a/res/raw/template_print_conversation_lower_no_js.html b/res/raw/template_print_conversation_lower_no_js.html
new file mode 100644
index 0000000..e6e704c
--- /dev/null
+++ b/res/raw/template_print_conversation_lower_no_js.html
@@ -0,0 +1,20 @@
+<!--
+     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.
+-->
+</div>
+</div>
+</body>
+</html>
\ No newline at end of file
diff --git a/res/raw/template_print_conversation_upper.html b/res/raw/template_print_conversation_upper.html
new file mode 100644
index 0000000..8660c83
--- /dev/null
+++ b/res/raw/template_print_conversation_upper.html
@@ -0,0 +1,87 @@
+<!--
+     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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
+        "http://www.w3.org/TR/html4/strict.dtd">
+
+<html>
+<head>
+
+    <meta http-equiv=Content-Type content="text/html; charset=UTF-8">
+
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+
+    <style type="text/css">
+        body, td {
+            font-family:arial,sans-serif;
+            font-size:13px
+        }
+        a:link, a:active {
+            color:#1155CC;
+            text-decoration:none
+        }
+        a:hover {
+            text-decoration:underline;
+            cursor: pointer
+        }
+        a:visited {
+            color:##6611CC
+        }
+        img {
+            border:0px
+        }
+        pre {
+            white-space: pre;
+            white-space: -moz-pre-wrap;
+            white-space: -o-pre-wrap;
+            white-space: pre-wrap;
+            word-wrap: break-word;
+            width: 800px;
+            overflow: auto;
+        }
+        .mail-elided-text {
+            padding:5 0;
+            font-size:10px;
+            color:#888888;
+        }
+    </style>
+</head>
+<body>
+    <div class="bodycontainer">
+        <table width=100%% cellpadding=0 cellspacing=0 border=0>
+            <tr height=14px>
+                <td width=143>
+                    <img src="file:///android_res/drawable/logo"
+                         width=143 height=59 alt="%s">
+                </td>
+                <td align=right>
+                    <font size=-1 color=#777><b>%s &lt;%s&gt;</b></font>
+                </td>
+            </tr>
+        </table>
+        <hr>
+        <div class="maincontent">
+            <table width=100%% cellpadding=0 cellspacing=0 border=0>
+                <tr>
+                    <td>
+                        <font size=+1>
+                            <b>%s</b></font><br>
+                        <!-- num messages -->
+                        <font size=-1 color=#777>%s</font>
+                    </td>
+                </tr>
+            </table>
diff --git a/res/raw/template_print_message.html b/res/raw/template_print_message.html
new file mode 100644
index 0000000..ded5075
--- /dev/null
+++ b/res/raw/template_print_message.html
@@ -0,0 +1,50 @@
+<!--
+     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.
+-->
+<hr>
+<table width=100%% cellpadding=0 cellspacing=0 border=0 class="message">
+    <tr>
+        <td>
+            <!-- author -->
+            <font size=-1><b>%s</b> &lt;%s&gt;</font>
+        </td>
+        <td align=right>
+            <!-- date -->
+            <font size=-1>%s</font>
+        </td>
+    </tr>
+    <tr>
+        <td colspan=2>
+            <!-- headers -->
+            <font size=-1 class="recipient">%s</font>
+        </td>
+    </tr>
+    <tr>
+        <td colspan=2>
+            <table width=100%% cellpadding=12 cellspacing=0 border=0>
+                <tr>
+                    <td>
+                        <!-- body -->
+                        <div style="overflow: hidden;">
+                            <font size=-1>%s</font>
+                        </div>
+                        %s
+                    </td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+</table>
diff --git a/res/raw/template_super_collapsed.html b/res/raw/template_super_collapsed.html
index 386bbae..5ac9dac 100644
--- a/res/raw/template_super_collapsed.html
+++ b/res/raw/template_super_collapsed.html
@@ -1 +1,17 @@
+<!--
+     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.
+-->
 <div class="mail-super-collapsed-block spacer" index="%s" style="height: %spx;"></div>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index d7f2109..1cdf825 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 7b98345..b092c83 100644
--- a/res/values/constants.xml
+++ b/res/values/constants.xml
@@ -119,7 +119,7 @@
     <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>
 
     <!-- Number of nested folders to display in the conversation list, before hiding the remainder under "Show more" -->
     <integer name="nested_folders_collapse_threshold">3</integer>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index a711666..66d093a 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -935,4 +935,25 @@
     <string name="show_n_more_folders">Show <xliff:g id="number">%1$s</xliff:g> more folders</string>
     <!-- Button in conversation list to hide folders [CHAR LIMIT=50] -->
     <string name="hide_folders">Hide folders</string>
+
+    <!-- Menu item text to that when clicked will allow a user to print an email conversation. [CHAR LIMIT=25] -->
+    <string name="print">Print</string>
+    <!-- Number of messages in a conversation [CHAR LIMIT=30]-->
+    <plurals name="num_messages">
+        <item quantity="one"><xliff:g id="count">%1$d</xliff:g> message</item>
+        <item quantity="other"><xliff:g id="count">%1$d</xliff:g> messages</item>
+    </plurals>
+
+    <!-- Display format of an email recipient, displayed in printed message details [CHAR LIMIT=10] -->
+    <string name="address_print_display_format"><xliff:g id="name">%1$s</xliff:g> &amp;lt;<xliff:g id="email">%2$s</xliff:g>&amp;gt;</string>
+    <!-- Shown to display the date of the message [CHAR LIMIT=50] -->
+    <string name="date_message_received_print"><xliff:g id="day_and_date">%1$s</xliff:g> at <xliff:g id="time">%2$s</xliff:g></string>
+    <!-- Shown to display the to recipient(s) of the message if the message is a draft [CHAR LIMIT=20] -->
+    <string name="draft_to_heading">Draft To:\u0020</string>
+
+    <!-- Shown to display in the message header if the message is a draft and there are no "to" addresses in the draft [CHAR LIMIT=10] -->
+    <string name="draft_heading">Draft</string>
+    <!-- Shown to inform the user that the quoted text for this message has been hidden. [CHAR LIMIT=50] -->
+    <string name="quoted_text_hidden_print">Quoted text hidden</string>
+
 </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/FormattedDateBuilder.java b/src/com/android/mail/FormattedDateBuilder.java
index e53b10c..d7c17b2 100644
--- a/src/com/android/mail/FormattedDateBuilder.java
+++ b/src/com/android/mail/FormattedDateBuilder.java
@@ -58,7 +58,7 @@
         }
     }
 
-    private CharSequence formatLongDayAndDate(long when) {
+    public CharSequence formatLongDayAndDate(long when) {
         sb.setLength(0);
         DateUtils.formatDateRange(mContext, dateFormatter, when, when,
                 DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY
@@ -66,7 +66,7 @@
         return sb.toString();
     }
 
-    private CharSequence formatLongTime(long when) {
+    public CharSequence formatLongTime(long when) {
         sb.setLength(0);
         DateUtils.formatDateRange(mContext, dateFormatter, when, when,
                 DateUtils.FORMAT_SHOW_TIME);
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 0d2c3fb..16b9933 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;
@@ -71,11 +67,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;
@@ -86,9 +81,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;
@@ -101,13 +96,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;
@@ -124,7 +118,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;
@@ -199,15 +192,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;
@@ -217,19 +207,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 */
@@ -257,19 +237,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.
      */
@@ -420,7 +387,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);
@@ -466,24 +432,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);
 
@@ -491,27 +443,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,
@@ -519,31 +460,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,
@@ -555,28 +493,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)
@@ -587,22 +522,31 @@
                 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;
+        if (mSelectedConversationSet != null) {
+            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;
@@ -614,12 +558,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()) {
@@ -628,16 +574,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());
@@ -650,6 +601,7 @@
         if (mHeader.conversation.color != 0) {
             mConfig.showColorBlock();
         }
+
         // Personal level.
         mHeader.personalLevelBitmap = null;
         if (true) { // TODO: hook this up to a setting
@@ -671,15 +623,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)) {
@@ -687,6 +644,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) {
@@ -760,12 +722,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) {
@@ -915,34 +879,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() {
@@ -1390,7 +1356,9 @@
         // Contact photo
         if (mGadgetMode == ConversationItemViewCoordinates.GADGET_CONTACT_PHOTO) {
             canvas.save();
-            drawContactImageArea(canvas);
+            Utils.traceBeginSection("draw senders image");
+            drawSendersImage(canvas);
+            Utils.traceEndSection();
             canvas.restore();
         }
 
@@ -1499,113 +1467,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;
@@ -1684,13 +1558,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)
@@ -1709,9 +1583,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
@@ -1725,6 +1598,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.
      */
@@ -1774,13 +1658,13 @@
         }
 
         if (mStarEnabled) {
-            if (mIsExpansiveTablet) {
+            if (mCoordinates.getMode() == ConversationItemViewCoordinates.WIDE_MODE) {
                 // Just check that we're left of the star's touch area
                 if (x >= mCoordinates.starX - sStarTouchSlop) {
                     return false;
                 }
             } else {
-                // We're on a phone or non-expansive tablet
+                // We're on a single pane device with the more condensed layout
 
                 // We allow touches all the way to the right edge, so no x check is necessary
 
@@ -1796,7 +1680,8 @@
     }
 
     private boolean isTouchInStar(float x, float y) {
-        if (mHeader.infoIcon != null && !mIsExpansiveTablet) {
+        if (mHeader.infoIcon != null
+                && mCoordinates.getMode() != ConversationItemViewCoordinates.WIDE_MODE) {
             // We have an info icon, and it's above the star
             // We allow touches everywhere below the top of the subject text
             if (y < mCoordinates.subjectY) {
@@ -2172,124 +2057,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/EmlMessageViewFragment.java b/src/com/android/mail/browse/EmlMessageViewFragment.java
index d1dab2f..c0f7f9b 100644
--- a/src/com/android/mail/browse/EmlMessageViewFragment.java
+++ b/src/com/android/mail/browse/EmlMessageViewFragment.java
@@ -19,6 +19,7 @@
 
 import android.app.Fragment;
 import android.app.LoaderManager;
+import android.content.Context;
 import android.content.CursorLoader;
 import android.content.Loader;
 import android.database.Cursor;
@@ -236,6 +237,11 @@
         return mAccountUri;
     }
 
+    @Override
+    public Context getContext() {
+        return getActivity().getApplicationContext();
+    }
+
     // End SecureConversationViewControllerCallbacks
 
     private class MessageLoadCallbacks
diff --git a/src/com/android/mail/browse/MessageCursor.java b/src/com/android/mail/browse/MessageCursor.java
index 5a4146a..8437d87 100644
--- a/src/com/android/mail/browse/MessageCursor.java
+++ b/src/com/android/mail/browse/MessageCursor.java
@@ -73,6 +73,10 @@
         return m;
     }
 
+    public Conversation getConversation() {
+        return mController != null ? mController.getConversation() : null;
+    }
+
     // Is the conversation starred?
     public boolean isConversationStarred() {
         int pos = -1;
diff --git a/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java b/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java
index 92a4f3c..62099fc 100644
--- a/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java
+++ b/src/com/android/mail/browse/MessageHeaderDetailsDialogFragment.java
@@ -19,15 +19,16 @@
 
 import android.app.AlertDialog;
 import android.app.Dialog;
+import android.app.DialogFragment;
 import android.content.Context;
 import android.os.Bundle;
-import android.app.DialogFragment;
 import android.view.LayoutInflater;
 import android.view.View;
 
 import com.android.mail.R;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Address;
+import com.android.mail.utils.Utils;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -93,7 +94,7 @@
     private static void addAddressesToBundle(
             Bundle addresses, Map<String, Address> addressCache, String[] emails) {
         for (final String email : emails) {
-            addresses.putParcelable(email, MessageHeaderView.getAddress(addressCache, email));
+            addresses.putParcelable(email, Utils.getAddress(addressCache, email));
         }
     }
 
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index ed47bf5..1c3aa33 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -495,23 +495,7 @@
     }
 
     public Address getAddress(String emailStr) {
-        return getAddress(mAddressCache, emailStr);
-    }
-
-    public static Address getAddress(Map<String, Address> cache, String emailStr) {
-        Address addr = null;
-        synchronized (cache) {
-            if (cache != null) {
-                addr = cache.get(emailStr);
-            }
-            if (addr == null) {
-                addr = Address.getEmailAddress(emailStr);
-                if (cache != null) {
-                    cache.put(emailStr, addr);
-                }
-            }
-        }
-        return addr;
+        return Utils.getAddress(mAddressCache, emailStr);
     }
 
     private void updateSpacerHeight() {
@@ -758,7 +742,7 @@
             final int len = Math.min(maxToCopy, rawAddrs.length);
             boolean first = true;
             for (int i = 0; i < len; i++) {
-                final Address email = getAddress(mAddressCache, rawAddrs[i]);
+                final Address email = Utils.getAddress(mAddressCache, rawAddrs[i]);
                 final String emailAddress = email.getAddress();
                 final String name;
                 if (mMatcher != null && mMatcher.isVeiledAddress(emailAddress)) {
@@ -1326,7 +1310,7 @@
         }
         final String[] formattedEmails = new String[emails.length];
         for (int i = 0; i < emails.length; i++) {
-            final Address email = getAddress(addressCache, emails[i]);
+            final Address email = Utils.getAddress(addressCache, emails[i]);
             String name = email.getName();
             final String address = email.getAddress();
             // Check if the address here is a veiled address.  If it is, we need to display an
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/photo/MailPhotoViewActivity.java b/src/com/android/mail/photo/MailPhotoViewActivity.java
index c25e45f..0562568 100644
--- a/src/com/android/mail/photo/MailPhotoViewActivity.java
+++ b/src/com/android/mail/photo/MailPhotoViewActivity.java
@@ -76,7 +76,8 @@
     public static void startMailPhotoViewActivity(final Context context, final Uri imageListUri,
             final int photoIndex) {
         final Intents.PhotoViewIntentBuilder builder =
-                Intents.newPhotoViewIntentBuilder(context, MailPhotoViewActivity.class);
+                Intents.newPhotoViewIntentBuilder(context,
+                        "com.android.mail.photo.MailPhotoViewActivity");
         builder
                 .setPhotosUri(imageListUri.toString())
                 .setProjection(UIProvider.ATTACHMENT_PROJECTION)
@@ -96,7 +97,8 @@
     public static void startMailPhotoViewActivity(final Context context, final Uri imageListUri,
             final String initialPhotoUri) {
         final Intents.PhotoViewIntentBuilder builder =
-                Intents.newPhotoViewIntentBuilder(context, MailPhotoViewActivity.class);
+                Intents.newPhotoViewIntentBuilder(context,
+                        "com.android.mail.photo.MailPhotoViewActivity");
         builder
                 .setPhotosUri(imageListUri.toString())
                 .setProjection(UIProvider.ATTACHMENT_PROJECTION)
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..0c1b374 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";
 
@@ -385,7 +386,6 @@
         editor.apply();
     }
 
-
     public void setShowSenderImages(boolean enable) {
         getEditor().putBoolean(PreferenceKeys.SHOW_SENDER_IMAGES, enable).apply();
         notifyBackupPreferenceChanged();
diff --git a/src/com/android/mail/print/HtmlPrintTemplates.java b/src/com/android/mail/print/HtmlPrintTemplates.java
new file mode 100644
index 0000000..27dfba3
--- /dev/null
+++ b/src/com/android/mail/print/HtmlPrintTemplates.java
@@ -0,0 +1,124 @@
+/**
+ * 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.print;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.android.mail.R;
+import com.android.mail.ui.AbstractHtmlTemplates;
+import com.android.mail.utils.LogTag;
+import com.android.mail.utils.LogUtils;
+
+/**
+ * Renders data into very simple string-substitution HTML templates for printing conversations.
+ */
+public class HtmlPrintTemplates extends AbstractHtmlTemplates {
+
+    private static final String TAG = LogTag.getLogTag();
+
+    private final String mConversationUpper;
+    private final String mMessage;
+    private final String mConversationLower;
+    private final String mConversationLowerNoJs;
+
+    public HtmlPrintTemplates(Context context) {
+        super(context);
+
+        mConversationUpper = readTemplate(R.raw.template_print_conversation_upper);
+        mMessage = readTemplate(R.raw.template_print_message);
+        mConversationLower = readTemplate(R.raw.template_print_conversation_lower);
+        mConversationLowerNoJs = readTemplate(R.raw.template_print_conversation_lower_no_js);
+    }
+
+    /**
+     * Start building the html for a printed conversation. Can only be called once
+     * until {@link #endPrintConversation()} or {@link #endPrintConversationNoJavascript()}
+     * is called.
+     */
+    public void startPrintConversation(String accountName, String accountAddress,
+            String subject, int numMessages) {
+        if (mInProgress) {
+            throw new IllegalStateException("Should not call startPrintConversation twice");
+        }
+
+        reset();
+
+        final Resources res = mContext.getResources();
+        final String numMessageString = res.getQuantityString(
+                R.plurals.num_messages, numMessages, numMessages);
+        append(mConversationUpper, mContext.getString(R.string.app_name),
+                accountName == null ? "" : accountName,
+                accountAddress, subject, numMessageString);
+
+        mInProgress = true;
+    }
+
+    /**
+     * Add a message to the html for this printed conversation.
+     */
+    public void appendMessage(String senderName, String senderAddress, String date,
+            String recipients, String bodyHtml, String attachments) {
+        append(mMessage, senderName, senderAddress, date, recipients, bodyHtml, attachments);
+    }
+
+    /**
+     * Adds the end of the printed conversation to the html. NOTE: this method
+     * includes JavaScript. If you need a version without JavaScript,
+     * use {@link #endPrintConversationNoJavascript()}.<br/><br/>
+     *
+     * One example where we use JavaScript is to hide quoted text.
+     *
+     * @return a {@link String} containing the html for the conversation.
+     */
+    public String endPrintConversation() {
+        if (!mInProgress) {
+            throw new IllegalStateException("must call startConversation first");
+        }
+
+        append(mConversationLower, mContext.getString(R.string.quoted_text_hidden_print));
+
+        mInProgress = false;
+
+        LogUtils.d(TAG, "rendered conversation of %d bytes, buffer capacity=%d",
+                mBuilder.length() << 1, mBuilder.capacity() << 1);
+
+        return emit();
+    }
+
+    /**
+     * Adds the end of the printed conversation to the html. NOTE: this method
+     * does not include any JavaScript. If you need a version with JavaScript,
+     * use {@link #endPrintConversation()}.
+     * @return a {@link String} containing the html for the conversation.
+     */
+    public String endPrintConversationNoJavascript() {
+        if (!mInProgress) {
+            throw new IllegalStateException("must call startConversation first");
+        }
+
+        append(mConversationLowerNoJs);
+
+        mInProgress = false;
+
+        LogUtils.d(TAG, "rendered conversation of %d bytes, buffer capacity=%d",
+                mBuilder.length() << 1, mBuilder.capacity() << 1);
+
+        return emit();
+    }
+}
diff --git a/src/com/android/mail/print/Printer.java b/src/com/android/mail/print/Printer.java
new file mode 100644
index 0000000..7744c37
--- /dev/null
+++ b/src/com/android/mail/print/Printer.java
@@ -0,0 +1,158 @@
+/**
+ * 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.print;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+import com.android.mail.FormattedDateBuilder;
+import com.android.mail.R;
+import com.android.mail.browse.ConversationMessage;
+import com.android.mail.browse.MessageCursor;
+import com.android.mail.providers.Account;
+import com.android.mail.providers.Address;
+import com.android.mail.providers.Conversation;
+import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.Utils;
+
+import java.util.Map;
+
+/**
+ * Static class that provides a {@link #print} function to build a print html document.
+ */
+public class Printer {
+    private static final String DIV_START = "<div>";
+    private static final String REPLY_TO_DIV_START = "<div class=\"replyto\">";
+    private static final String DIV_END = "</div>";
+
+    /**
+     * Builds an html document that is suitable for printing and returns it as a {@link String}.
+     */
+    public static String print(Context context, Account account,
+            MessageCursor cursor, Map<String, Address> addressCache, boolean useJavascript) {
+        final HtmlPrintTemplates templates = new HtmlPrintTemplates(context);
+        final FormattedDateBuilder dateBuilder = new FormattedDateBuilder(context);
+
+        if (!cursor.moveToFirst()) {
+            throw new IllegalStateException("trying to print without a conversation");
+        }
+
+        // TODO - remove account name(not account.name which is email address) or get it somehow
+        final Conversation conversation = cursor.getConversation();
+        templates.startPrintConversation("", account.name,
+                conversation.subject, conversation.getNumMessages());
+
+        // for each message in the conversation, add message html
+        final Resources res = context.getResources();
+        do {
+            final ConversationMessage message = cursor.getMessage();
+            final Address fromAddress = Utils.getAddress(addressCache, message.getFrom());
+            final long when = message.dateReceivedMs;
+            final String date = res.getString(R.string.date_message_received_print,
+                    dateBuilder.formatLongDayAndDate(when), dateBuilder.formatLongTime(when));
+
+
+            templates.appendMessage(fromAddress.getName(), fromAddress.getAddress(), date,
+                    renderRecipients(res, addressCache, message), message.getBodyAsHtml(),
+                    ""); // TODO - attachment html
+        } while (cursor.moveToNext());
+
+        // only include JavaScript if specifically requested
+        return useJavascript ?
+                templates.endPrintConversation() : templates.endPrintConversationNoJavascript();
+    }
+
+    /**
+     * Builds html for the message header. Specifically, the (optional) lists of
+     * reply-to, to, cc, and bcc.
+     */
+    private static String renderRecipients(Resources res, Map<String, Address> addressCache,
+            ConversationMessage message) {
+        final StringBuilder recipients = new StringBuilder();
+
+        // reply-to
+        final String replyTo = renderEmailList(res, message.getReplyToAddresses(), addressCache);
+        buildEmailDiv(res, recipients, replyTo, REPLY_TO_DIV_START, DIV_END,
+                R.string.replyto_heading);
+
+        // to
+        // To has special semantics since the message can be a draft.
+        // If it is a draft and there are no to addresses, we just print "Draft".
+        // If it is a draft and there are to addresses, we print "Draft To: "
+        // If not a draft, we just use "To: ".
+        final boolean isDraft = message.draftType != UIProvider.DraftType.NOT_A_DRAFT;
+        final String to = renderEmailList(res, message.getToAddresses(), addressCache);
+        if (isDraft && to == null) {
+            recipients.append(DIV_START).append(res.getString(R.string.draft_heading))
+                    .append(DIV_END);
+        } else {
+            buildEmailDiv(res, recipients, to, DIV_START, DIV_END,
+                    isDraft ? R.string.draft_to_heading : R.string.to_heading);
+        }
+
+        // cc
+        final String cc = renderEmailList(res, message.getCcAddresses(), addressCache);
+        buildEmailDiv(res, recipients, cc, DIV_START, DIV_END,
+                R.string.cc_heading);
+
+        // bcc
+        final String bcc = renderEmailList(res, message.getBccAddresses(), addressCache);
+        buildEmailDiv(res, recipients, bcc, DIV_START, DIV_END,
+                R.string.bcc_heading);
+
+        return recipients.toString();
+    }
+
+    /**
+     * Appends an html div containing a list of emails based on the passed in data.
+     */
+    private static void buildEmailDiv(Resources res, StringBuilder recipients, String emailList,
+            String divStart, String divEnd, int headingId) {
+        if (emailList != null) {
+            recipients.append(divStart).append(res.getString(headingId))
+                    .append(emailList).append(divEnd);
+        }
+    }
+
+    /**
+     * Builds and returns a list of comma-separated emails of the form "Name &lt;email&gt;".
+     * If the email does not contain a name, "email" is used instead.
+     */
+    private static String renderEmailList(Resources resources, String[] emails,
+            Map<String, Address> addressCache) {
+        if (emails == null || emails.length == 0) {
+            return null;
+        }
+        final String[] formattedEmails = new String[emails.length];
+        for (int i = 0; i < emails.length; i++) {
+            final Address email = Utils.getAddress(addressCache, emails[i]);
+            final String name = email.getName();
+            final String address = email.getAddress();
+
+            if (TextUtils.isEmpty(name)) {
+                formattedEmails[i] = address;
+            } else {
+                formattedEmails[i] = resources.getString(R.string.address_print_display_format,
+                        name, address);
+            }
+        }
+
+        return TextUtils.join(", ", formattedEmails);
+    }
+}
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 0023403..473d158 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -24,6 +24,7 @@
 import android.content.Loader;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.view.Menu;
@@ -326,6 +327,9 @@
         } else if (itemId == R.id.show_original) {
             showUntransformedConversation();
             handled = true;
+        } else if (itemId == R.id.print) {
+            printConversation();
+            handled = true;
         }
         return handled;
     }
@@ -335,6 +339,7 @@
         // Only show option if we support message transforms and message has been transformed.
         Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() &&
                 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted);
+        Utils.setMenuItemVisibility(menu, R.id.print, Utils.isRunningKitkatOrLater());
     }
 
     abstract boolean supportsMessageTransforms();
@@ -634,4 +639,6 @@
         return (mAccount.enableMessageTransforms > 0) &&
                 !mHasConversationTransformBeenReverted;
     }
+
+    protected abstract void printConversation();
 }
diff --git a/src/com/android/mail/ui/AbstractHtmlTemplates.java b/src/com/android/mail/ui/AbstractHtmlTemplates.java
new file mode 100644
index 0000000..25c3bc5
--- /dev/null
+++ b/src/com/android/mail/ui/AbstractHtmlTemplates.java
@@ -0,0 +1,95 @@
+/**
+ * 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.ui;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Formatter;
+
+/**
+ * Abstract class to support common functionality for both
+ * {@link com.android.mail.ui.HtmlConversationTemplates} and
+ * {@link com.android.mail.print.HtmlPrintTemplates}.
+ *
+ * Renders data into very simple string-substitution HTML templates.
+ *
+ * Templates should be UTF-8 encoded HTML with '%s' placeholders to be substituted upon render.
+ * Plain-jane string substitution with '%s' is slightly faster than typed substitution.
+ */
+public abstract class AbstractHtmlTemplates {
+    // TODO: refine. too expensive to iterate over cursor and pre-calculate total. so either
+    // estimate it, or defer assembly until the end when size is known (deferring increases
+    // working set size vs. estimation but is exact).
+    private static final int BUFFER_SIZE_CHARS = 64 * 1024;
+
+    protected Context mContext;
+    protected Formatter mFormatter;
+    protected StringBuilder mBuilder;
+    protected boolean mInProgress = false;
+
+    public AbstractHtmlTemplates(Context context) {
+        mContext = context;
+    }
+
+    public String emit() {
+        final String out = mFormatter.toString();
+        // release the builder memory ASAP
+        mFormatter = null;
+        mBuilder = null;
+        return out;
+    }
+
+    public void reset() {
+        mBuilder = new StringBuilder(BUFFER_SIZE_CHARS);
+        mFormatter = new Formatter(mBuilder, null /* no localization */);
+    }
+
+    protected String readTemplate(int id) throws Resources.NotFoundException {
+        final StringBuilder out = new StringBuilder();
+        InputStreamReader in = null;
+        try {
+            try {
+                in = new InputStreamReader(
+                        mContext.getResources().openRawResource(id), "UTF-8");
+                final char[] buf = new char[4096];
+                int chars;
+
+                while ((chars=in.read(buf)) > 0) {
+                    out.append(buf, 0, chars);
+                }
+
+                return out.toString();
+
+            } finally {
+                if (in != null) {
+                    in.close();
+                }
+            }
+        } catch (IOException e) {
+            throw new Resources.NotFoundException("Unable to open template id="
+                    + Integer.toHexString(id) + " exception=" + e.getMessage());
+        }
+    }
+
+    protected void append(String template, Object... args) {
+        mFormatter.format(template, args);
+    }
+}
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 936fc97..e891249 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;
@@ -91,17 +92,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
@@ -182,7 +172,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) {
@@ -245,29 +234,42 @@
     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) {
         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;
 
-        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(Utils.isLowRamDevice(mContext) ?
+                0 : 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) {
@@ -376,10 +378,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;
     }
 
@@ -764,10 +766,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;
     }
@@ -1068,12 +1070,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;
     }
 
     /**
@@ -1129,7 +1139,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 95f0dc0..2abd2bc 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -56,7 +56,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;
@@ -300,8 +299,7 @@
         }
 
         mListAdapter = new AnimatedAdapter(mActivity.getApplicationContext(), conversationCursor,
-                mActivity.getSelectedSet(), mActivity, mConversationListListener, mListView,
-                specialItemViews);
+                mActivity.getSelectedSet(), mActivity, mListView, specialItemViews);
         mListAdapter.addFooter(mFooterView);
         mListView.setAdapter(mListAdapter);
         mSelectedSet = mActivity.getSelectedSet();
@@ -682,15 +680,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/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index 98cbaae..51484b1 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -27,6 +27,9 @@
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.SystemClock;
+import android.print.PrintAttributes;
+import android.print.PrintJob;
+import android.print.PrintManager;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.ScaleGestureDetector;
@@ -63,6 +66,7 @@
 import com.android.mail.browse.SuperCollapsedBlock;
 import com.android.mail.browse.WebViewContextMenu;
 import com.android.mail.content.ObjectCursor;
+import com.android.mail.print.Printer;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Address;
 import com.android.mail.providers.Conversation;
@@ -512,6 +516,7 @@
             timerMark("CVF.showConversation");
         } else {
             final boolean disableOffscreenLoading = DISABLE_OFFSCREEN_LOADING
+                    || Utils.isLowRamDevice(getContext())
                     || (mConversation != null && (mConversation.isRemote
                             || mConversation.getNumMessages() > mMaxAutoLoadMessages));
 
@@ -576,8 +581,7 @@
         if (DEBUG_DUMP_CONVERSATION_HTML) {
             java.io.FileWriter fw = null;
             try {
-                fw = new java.io.FileWriter("/sdcard/conv" + mConversation.id
-                        + ".html");
+                fw = new java.io.FileWriter(getSdCardFilePath());
                 fw.write(convHtml);
             } catch (java.io.IOException e) {
                 e.printStackTrace();
@@ -602,6 +606,10 @@
         mWebViewLoadStartMs = SystemClock.uptimeMillis();
     }
 
+    protected String getSdCardFilePath() {
+        return "/sdcard/conv" + mConversation.id + ".html";
+    }
+
     /**
      * Populate the adapter with overlay views (message headers, super-collapsed blocks, a
      * conversation header), and return an HTML document with spacer divs inserted for all overlays.
@@ -1570,4 +1578,17 @@
         }
 
     }
+
+    protected void printConversation() {
+        // TODO - offscreen webview stuff so that we don't clobber
+        final String convHtml =
+                Printer.print(getContext(), mAccount, getMessageCursor(),
+                        mAddressCache, true /* userJavascript */);
+        mWebView.loadDataWithBaseURL(mBaseUri, convHtml, "text/html", "utf-8", null);
+        final PrintManager printManager =
+                (PrintManager) getContext().getSystemService(Context.PRINT_SERVICE);
+        printManager.print(getConversation().subject,
+                mWebView.createPrintDocumentAdapter(),
+                new PrintAttributes.Builder().build());
+    }
 }
diff --git a/src/com/android/mail/ui/HtmlConversationTemplates.java b/src/com/android/mail/ui/HtmlConversationTemplates.java
index d368454..104d0c0 100644
--- a/src/com/android/mail/ui/HtmlConversationTemplates.java
+++ b/src/com/android/mail/ui/HtmlConversationTemplates.java
@@ -1,4 +1,4 @@
-/*
+/**
  * Copyright (C) 2012 Google Inc.
  * Licensed to The Android Open Source Project.
  *
@@ -18,7 +18,6 @@
 package com.android.mail.ui;
 
 import android.content.Context;
-import android.content.res.Resources.NotFoundException;
 
 import com.android.mail.R;
 import com.android.mail.utils.LogTag;
@@ -26,30 +25,17 @@
 import com.android.mail.utils.Utils;
 import com.google.common.annotations.VisibleForTesting;
 
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.Formatter;
 import java.util.regex.Pattern;
 
 /**
  * Renders data into very simple string-substitution HTML templates for conversation view.
- *
- * Templates should be UTF-8 encoded HTML with '%s' placeholders to be substituted upon render.
- * Plain-jane string substitution with '%s' is slightly faster than typed substitution.
- *
  */
-public class HtmlConversationTemplates {
+public class HtmlConversationTemplates extends AbstractHtmlTemplates {
 
     /**
      * Prefix applied to a message id for use as a div id
      */
     public static final String MESSAGE_PREFIX = "m";
-    public static final int MESSAGE_PREFIX_LENGTH = MESSAGE_PREFIX.length();
-
-    // TODO: refine. too expensive to iterate over cursor and pre-calculate total. so either
-    // estimate it, or defer assembly until the end when size is known (deferring increases
-    // working set size vs. estimation but is exact).
-    private static final int BUFFER_SIZE_CHARS = 64 * 1024;
 
     private static final String TAG = LogTag.getLogTag();
 
@@ -92,13 +78,8 @@
     private static String sConversationUpper;
     private static String sConversationLower;
 
-    private Context mContext;
-    private Formatter mFormatter;
-    private StringBuilder mBuilder;
-    private boolean mInProgress = false;
-
     public HtmlConversationTemplates(Context context) {
-        mContext = context;
+        super(context);
 
         // The templates are small (~2KB total in ICS MR2), so it's okay to load them once and keep
         // them in memory.
@@ -178,7 +159,8 @@
 
     public void startConversation(int sideMargin, int conversationHeaderHeight) {
         if (mInProgress) {
-            throw new IllegalStateException("must call startConversation first");
+            throw new IllegalStateException(
+                    "Should not call start conversation until end conversation has been called");
         }
 
         reset();
@@ -209,49 +191,4 @@
 
         return emit();
     }
-
-    public String emit() {
-        String out = mFormatter.toString();
-        // release the builder memory ASAP
-        mFormatter = null;
-        mBuilder = null;
-        return out;
-    }
-
-    public void reset() {
-        mBuilder = new StringBuilder(BUFFER_SIZE_CHARS);
-        mFormatter = new Formatter(mBuilder, null /* no localization */);
-    }
-
-    private String readTemplate(int id) throws NotFoundException {
-        StringBuilder out = new StringBuilder();
-        InputStreamReader in = null;
-        try {
-            try {
-                in = new InputStreamReader(
-                        mContext.getResources().openRawResource(id), "UTF-8");
-                char[] buf = new char[4096];
-                int chars;
-
-                while ((chars=in.read(buf)) > 0) {
-                    out.append(buf, 0, chars);
-                }
-
-                return out.toString();
-
-            } finally {
-                if (in != null) {
-                    in.close();
-                }
-            }
-        } catch (IOException e) {
-            throw new NotFoundException("Unable to open template id=" + Integer.toHexString(id)
-                    + " exception=" + e.getMessage());
-        }
-    }
-
-    private void append(String template, Object... args) {
-        mFormatter.format(template, args);
-    }
-
 }
diff --git a/src/com/android/mail/ui/SecureConversationViewController.java b/src/com/android/mail/ui/SecureConversationViewController.java
index 1f9b13e..4a89bd2 100644
--- a/src/com/android/mail/ui/SecureConversationViewController.java
+++ b/src/com/android/mail/ui/SecureConversationViewController.java
@@ -183,6 +183,10 @@
         mConversationHeaderView.setSubject(subject);
     }
 
+    public void printConversation() {
+        // TODO - implement this
+    }
+
     // Start MessageHeaderViewCallbacks implementations
 
     @Override
diff --git a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
index 2e4f5d9..0284a49 100644
--- a/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
+++ b/src/com/android/mail/ui/SecureConversationViewControllerCallbacks.java
@@ -18,6 +18,7 @@
 package com.android.mail.ui;
 
 import android.app.Fragment;
+import android.content.Context;
 import android.net.Uri;
 import android.os.Handler;
 
@@ -46,4 +47,5 @@
     public String getBaseUri();
     public boolean isViewOnlyMode();
     public Uri getAccountUri();
+    public Context getContext();
 }
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index 14937b7..729a774 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -261,4 +261,8 @@
     public boolean supportsMessageTransforms() {
         return false;
     }
+
+    protected void printConversation() {
+        mViewController.printConversation();
+    }
 }
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..e6a0312 100644
--- a/src/com/android/mail/utils/Utils.java
+++ b/src/com/android/mail/utils/Utils.java
@@ -16,12 +16,14 @@
 
 package com.android.mail.utils;
 
+import com.android.mail.providers.Address;
 import com.google.android.mail.common.html.parser.HtmlDocument;
 import com.google.android.mail.common.html.parser.HtmlParser;
 import com.google.android.mail.common.html.parser.HtmlTree;
 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 +148,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
@@ -710,7 +727,7 @@
     /**
      * Create an intent to show a conversation.
      * @param conversation Conversation to open.
-     * @param folder
+     * @param folderUri
      * @param account
      * @return
      */
@@ -733,7 +750,7 @@
     /**
      * Create an intent to open a folder.
      *
-     * @param folder Folder to open.
+     * @param folderUri Folder to open.
      * @param account
      * @return
      */
@@ -963,18 +980,22 @@
             final Bitmap drawingCache = rootView.getDrawingCache();
             // Null check to avoid NPE discovered from monkey crash:
             if (drawingCache != null) {
-                final Bitmap originalBitmap = drawingCache.copy(Bitmap.Config.RGB_565, false);
-                double originalHeight = originalBitmap.getHeight();
-                double originalWidth = originalBitmap.getWidth();
-                int newHeight = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
-                int newWidth = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
-                double scaleX, scaleY;
-                scaleX = newWidth  / originalWidth;
-                scaleY = newHeight / originalHeight;
-                final double scale = Math.min(scaleX, scaleY);
-                newWidth = (int)Math.round(originalWidth * scale);
-                newHeight = (int)Math.round(originalHeight * scale);
-                return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
+                try {
+                    final Bitmap originalBitmap = drawingCache.copy(Bitmap.Config.RGB_565, false);
+                    double originalHeight = originalBitmap.getHeight();
+                    double originalWidth = originalBitmap.getWidth();
+                    int newHeight = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
+                    int newWidth = SCALED_SCREENSHOT_MAX_HEIGHT_WIDTH;
+                    double scaleX, scaleY;
+                    scaleX = newWidth  / originalWidth;
+                    scaleY = newHeight / originalHeight;
+                    final double scale = Math.min(scaleX, scaleY);
+                    newWidth = (int)Math.round(originalWidth * scale);
+                    newHeight = (int)Math.round(originalHeight * scale);
+                    return Bitmap.createScaledBitmap(originalBitmap, newWidth, newHeight, true);
+                } catch (OutOfMemoryError e) {
+                    LogUtils.e(LOG_TAG, e, "OOME when attempting to scale screenshot");
+                }
             }
         }
         return null;
@@ -1023,8 +1044,8 @@
     *
     * @param type MIME data type to normalize
     * @return normalized MIME data type, or null if the input was null
-    * @see {@link #setType}
-    * @see {@link #setTypeAndNormalize}
+    * @see {@link android.content.Intent#setType}
+    * @see {@link android.content.Intent#setTypeAndNormalize}
     */
    public static String normalizeMimeType(String type) {
        if (type == null) {
@@ -1041,7 +1062,7 @@
    }
 
    /**
-    * (copied from {@link Uri#normalize()} for pre-J)
+    * (copied from {@link android.net.Uri#normalizeScheme()} for pre-J)
     *
     * Return a normalized representation of this Uri.
     *
@@ -1063,7 +1084,7 @@
     *
     * @return normalized Uri (never null)
     * @see {@link android.content.Intent#setData}
-    * @see {@link #setNormalizedData}
+    * @see {@link android.content.Intent#setNormalizedData}
     */
    public static Uri normalizeUri(Uri uri) {
        String scheme = uri.getScheme();
@@ -1409,4 +1430,20 @@
         }
     }
 
+
+    public static Address getAddress(Map<String, Address> cache, String emailStr) {
+        Address addr = null;
+        synchronized (cache) {
+            if (cache != null) {
+                addr = cache.get(emailStr);
+            }
+            if (addr == null) {
+                addr = Address.getEmailAddress(emailStr);
+                if (cache != null) {
+                    cache.put(emailStr, addr);
+                }
+            }
+        }
+        return addr;
+    }
 }