Merge "Menu items to spec + cleanup" into jb-ub-mail-ur9
diff --git a/res/drawable-hdpi/ic_star_off.png b/res/drawable-hdpi/ic_star_off.png
index 4476f91..cf1cd90 100644
--- a/res/drawable-hdpi/ic_star_off.png
+++ b/res/drawable-hdpi/ic_star_off.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_star_on.png b/res/drawable-hdpi/ic_star_on.png
index b4e7ceb..b694bbb 100644
--- a/res/drawable-hdpi/ic_star_on.png
+++ b/res/drawable-hdpi/ic_star_on.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_star_off.png b/res/drawable-mdpi/ic_star_off.png
index 3d959ef..522b5ab 100644
--- a/res/drawable-mdpi/ic_star_off.png
+++ b/res/drawable-mdpi/ic_star_off.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_star_on.png b/res/drawable-mdpi/ic_star_on.png
index 9f4318f..50dd679 100644
--- a/res/drawable-mdpi/ic_star_on.png
+++ b/res/drawable-mdpi/ic_star_on.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_star_off.png b/res/drawable-xhdpi/ic_star_off.png
index 72c50b7..a611a1f 100644
--- a/res/drawable-xhdpi/ic_star_off.png
+++ b/res/drawable-xhdpi/ic_star_off.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_star_on.png b/res/drawable-xhdpi/ic_star_on.png
index 31551a2..f77acd7 100644
--- a/res/drawable-xhdpi/ic_star_on.png
+++ b/res/drawable-xhdpi/ic_star_on.png
Binary files differ
diff --git a/res/layout/conversation_item_view_normal.xml b/res/layout/conversation_item_view_normal.xml
index 350afd7..25eabe4 100644
--- a/res/layout/conversation_item_view_normal.xml
+++ b/res/layout/conversation_item_view_normal.xml
@@ -145,7 +145,7 @@
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_alignParentRight="true"
-                    android:layout_marginTop="-6dp"
+                    android:layout_marginTop="-1dp"
                     android:src="@drawable/ic_star_off" />
 
                 <!-- we assume the star asset is less than 48dp wide -->
diff --git a/res/layout/conversation_item_view_normal_spacious.xml b/res/layout/conversation_item_view_normal_spacious.xml
index b8bb2af..7bc05d3 100644
--- a/res/layout/conversation_item_view_normal_spacious.xml
+++ b/res/layout/conversation_item_view_normal_spacious.xml
@@ -145,7 +145,7 @@
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
                     android:layout_alignParentRight="true"
-                    android:layout_marginTop="-6dp"
+                    android:layout_marginTop="-1dp"
                     android:src="@drawable/ic_star_off" />
 
                 <!-- we assume the star asset is less than 48dp wide -->
diff --git a/res/layout/conversation_item_view_wide.xml b/res/layout/conversation_item_view_wide.xml
index 949e964..3acda63 100644
--- a/res/layout/conversation_item_view_wide.xml
+++ b/res/layout/conversation_item_view_wide.xml
@@ -140,7 +140,7 @@
                     android:id="@+id/star"
                     android:layout_width="wrap_content"
                     android:layout_height="wrap_content"
-                    android:layout_marginTop="-12dp"
+                    android:layout_marginTop="-1dp"
                     android:layout_marginLeft="32dp"
                     android:src="@drawable/ic_star_off" />
 
diff --git a/res/layout/swipe_to_refresh.xml b/res/layout/swipe_to_refresh.xml
index 35dd7ee..6ea9f4b 100644
--- a/res/layout/swipe_to_refresh.xml
+++ b/res/layout/swipe_to_refresh.xml
@@ -26,7 +26,7 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:gravity="center"
-        android:textColor="#0099cc"
+        android:textColor="@color/swipe_to_refresh_text_color"
         android:textSize="17sp" />
 
 </FrameLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index ccc40d9..6f077bc 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -112,8 +112,10 @@
 
     <!-- Teaser colors -->
 
-
     <!-- The color of the section name text in the teaser -->
     <color name="teaser_main_text">#58585b</color>
 
+    <!-- swipe to refresh text color, this matches the color in
+        progressbar_solid_holo.png -->
+    <color name="swipe_to_refresh_text_color">#0099cc</color>
 </resources>
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 11d12a4..3c826ea 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -31,7 +31,8 @@
     <dimen name="compose_scrollview_width">700dp</dimen>
     <dimen name="color_block_width">32dip</dimen>
     <dimen name="color_block_height">6dip</dimen>
-    <dimen name="star_touch_slop">4dip</dimen>
+    <!-- The star is pretty small and does not have padding anymore, so the slop needs to be bigger-->
+    <dimen name="star_touch_slop">16dip</dimen>
     <dimen name="sender_image_touch_slop">8dip</dimen>
     <dimen name="move_slop">4dip</dimen>
     <dimen name="standard_scaled_dimen">100sp</dimen>
diff --git a/src/com/android/mail/browse/MessageCursor.java b/src/com/android/mail/browse/MessageCursor.java
index ff6016a..0858448 100644
--- a/src/com/android/mail/browse/MessageCursor.java
+++ b/src/com/android/mail/browse/MessageCursor.java
@@ -18,34 +18,30 @@
 package com.android.mail.browse;
 
 import android.database.Cursor;
-import android.database.CursorWrapper;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Parcelable;
 
+import com.android.mail.content.CursorCreator;
+import com.android.mail.content.ObjectCursor;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Attachment;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Message;
-import com.android.mail.providers.UIProvider;
 import com.android.mail.providers.UIProvider.CursorExtraKeys;
 import com.android.mail.providers.UIProvider.CursorStatus;
 import com.android.mail.ui.ConversationUpdater;
 
 import com.google.common.base.Objects;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 
 import java.util.List;
-import java.util.Map;
 
 /**
  * MessageCursor contains the messages within a conversation; the public methods within should
  * only be called by the UI thread, as cursor position isn't guaranteed to be maintained
  */
-public class MessageCursor extends CursorWrapper {
-
-    private final Map<Long, ConversationMessage> mCache = Maps.newHashMap();
+public class MessageCursor extends ObjectCursor<MessageCursor.ConversationMessage> {
     /**
      * The current controller that this cursor can use to reference the owning {@link Conversation},
      * and a current {@link ConversationUpdater}. Since this cursor will survive a rotation, but
@@ -79,7 +75,7 @@
 
         private transient ConversationController mController;
 
-        private ConversationMessage(MessageCursor cursor) {
+        private ConversationMessage(Cursor cursor) {
             super(cursor);
         }
 
@@ -122,10 +118,26 @@
             }
         }
 
+        /**
+         * Public object that knows how to construct Messages given Cursors.
+         */
+        public static final CursorCreator<ConversationMessage> FACTORY =
+                new CursorCreator<ConversationMessage>() {
+            @Override
+            public ConversationMessage createFromCursor(Cursor c) {
+                return new ConversationMessage(c);
+            }
+
+            @Override
+            public String toString() {
+                return "ConversationMessage CursorCreator";
+            }
+        };
+
     }
 
     public MessageCursor(Cursor inner) {
-        super(inner);
+        super(inner, ConversationMessage.FACTORY);
     }
 
     public void setController(ConversationController controller) {
@@ -133,12 +145,7 @@
     }
 
     public ConversationMessage getMessage() {
-        final long id = getWrappedCursor().getLong(UIProvider.MESSAGE_ID_COLUMN);
-        ConversationMessage m = mCache.get(id);
-        if (m == null) {
-            m = new ConversationMessage(this);
-            mCache.put(id, m);
-        }
+        final ConversationMessage m = getModel();
         // ALWAYS set up each ConversationMessage with the latest controller.
         // Rotation invalidates everything except this Cursor, its Loader and the cached Messages,
         // so if we want to continue using them after rotate, we have to ensure their controller
diff --git a/src/com/android/mail/content/ObjectCursorLoader.java b/src/com/android/mail/content/ObjectCursorLoader.java
index 3184524..dbb93bb 100644
--- a/src/com/android/mail/content/ObjectCursorLoader.java
+++ b/src/com/android/mail/content/ObjectCursorLoader.java
@@ -36,7 +36,7 @@
     final ForceLoadContentObserver mObserver;
     protected static final String LOG_TAG = LogTag.getLogTag();
 
-    final Uri mUri;
+    private Uri mUri;
     final String[] mProjection;
     // Copied over from CursorLoader, but none of our uses specify this. So these are hardcoded to
     // null right here.
@@ -60,15 +60,12 @@
          * If these are null, it's going to crash anyway in loadInBackground(), but this stack trace
          * is much more useful.
          */
-        if (uri == null) {
-            throw new NullPointerException("The uri cannot be null");
-        }
         if (factory == null) {
             throw new NullPointerException("The factory cannot be null");
         }
 
         mObserver = new ForceLoadContentObserver();
-        mUri = uri;
+        setUri(uri);
         mProjection = projection;
         mFactory = factory;
     }
@@ -84,7 +81,7 @@
             inner.registerContentObserver(mObserver);
         }
         // Modifications to the ObjectCursor, create an Object Cursor and fill the cache.
-        final ObjectCursor<T> cursor = new ObjectCursor<T>(inner, mFactory);
+        final ObjectCursor<T> cursor = getObjectCursor(inner);
         cursor.fillCache();
 
         try {
@@ -96,6 +93,10 @@
         return cursor;
     }
 
+    protected ObjectCursor<T> getObjectCursor(Cursor inner) {
+        return new ObjectCursor<T>(inner, mFactory);
+    }
+
     /* Runs on the UI thread */
     @Override
     public void deliverResult(ObjectCursor<T> cursor) {
@@ -188,4 +189,15 @@
         mDebugDelayMs = delayMs;
         return this;
     }
+
+    protected final Uri getUri() {
+        return mUri;
+    }
+
+    protected final void setUri(Uri uri) {
+        if (uri == null) {
+            throw new NullPointerException("The uri cannot be null");
+        }
+        mUri = uri;
+    }
 }
diff --git a/src/com/android/mail/photomanager/ContactPhotoManager.java b/src/com/android/mail/photomanager/ContactPhotoManager.java
index 2cf23bb..580d67a 100644
--- a/src/com/android/mail/photomanager/ContactPhotoManager.java
+++ b/src/com/android/mail/photomanager/ContactPhotoManager.java
@@ -225,9 +225,13 @@
                     SenderInfoLoader.loadContactPhotos(
                     getResolver(), addresses, false /* decodeBitmaps */);
 
-            // put all entries into photos map: mapping of email addresses to photoBytes
-            for(Map.Entry<String, ContactInfo> entry : emailAddressToContactInfoMap.entrySet()) {
-                photos.put(entry.getKey(), entry.getValue().photoBytes);
+            // 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.
+            for (final String address : addresses) {
+                final ContactInfo info = emailAddressToContactInfoMap.get(address);
+                photos.put(address, info != null ? info.photoBytes : null);
             }
 
             return photos;
diff --git a/src/com/android/mail/providers/Account.java b/src/com/android/mail/providers/Account.java
index 0595db7..391ffe0 100644
--- a/src/com/android/mail/providers/Account.java
+++ b/src/com/android/mail/providers/Account.java
@@ -172,6 +172,10 @@
     public final Uri updateSettingsUri;
 
     /**
+     * Whether message transforms (HTML DOM manipulation) feature is enabled.
+     */
+    public final int enableMessageTransforms;
+    /**
      * Transient cache of parsed {@link #accountFromAddresses}, plus an entry for the main account
      * address.
      */
@@ -214,6 +218,7 @@
                     viewIntentProxyUri);
             json.put(UIProvider.AccountColumns.ACCOUNT_COOKIE_QUERY_URI, accoutCookieQueryUri);
             json.put(UIProvider.AccountColumns.UPDATE_SETTINGS_URI, updateSettingsUri);
+            json.put(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS, enableMessageTransforms);
             if (settings != null) {
                 json.put(SETTINGS_KEY, settings.toJSON());
             }
@@ -301,6 +306,7 @@
                 json.optString(UIProvider.AccountColumns.ACCOUNT_COOKIE_QUERY_URI));
         updateSettingsUri = Utils.getValidUri(
                 json.optString(UIProvider.AccountColumns.UPDATE_SETTINGS_URI));
+        enableMessageTransforms = json.optInt(AccountColumns.ENABLE_MESSAGE_TRANSFORMS);
 
         final Settings jsonSettings = Settings.newInstance(json.optJSONObject(SETTINGS_KEY));
         if (jsonSettings != null) {
@@ -337,6 +343,7 @@
         viewIntentProxyUri = in.readParcelable(null);
         accoutCookieQueryUri = in.readParcelable(null);
         updateSettingsUri = in.readParcelable(null);
+        enableMessageTransforms = in.readInt();
         final String serializedSettings = in.readString();
         final Settings parcelSettings = Settings.newInstance(serializedSettings);
         if (parcelSettings != null) {
@@ -399,6 +406,8 @@
                 cursor.getColumnIndex(UIProvider.AccountColumns.ACCOUNT_COOKIE_QUERY_URI)));
         updateSettingsUri = Utils.getValidUri(cursor.getString(
                 cursor.getColumnIndex(UIProvider.AccountColumns.UPDATE_SETTINGS_URI)));
+        enableMessageTransforms = cursor.getInt(
+                cursor.getColumnIndex(AccountColumns.ENABLE_MESSAGE_TRANSFORMS));
         settings = new Settings(cursor);
     }
 
@@ -472,6 +481,7 @@
         dest.writeParcelable(viewIntentProxyUri, 0);
         dest.writeParcelable(accoutCookieQueryUri, 0);
         dest.writeParcelable(updateSettingsUri, 0);
+        dest.writeInt(enableMessageTransforms);
         if (settings == null) {
             LogUtils.e(LOG_TAG, "unexpected null settings object in writeToParcel");
         }
@@ -562,6 +572,7 @@
                 Objects.equal(viewIntentProxyUri, other.viewIntentProxyUri) &&
                 Objects.equal(accoutCookieQueryUri, other.accoutCookieQueryUri) &&
                 Objects.equal(updateSettingsUri, other.updateSettingsUri) &&
+                Objects.equal(enableMessageTransforms, other.enableMessageTransforms) &&
                 Objects.equal(settings, other.settings);
     }
 
@@ -591,7 +602,7 @@
                         undoUri, settingsIntentUri, helpIntentUri, sendFeedbackIntentUri,
                         reauthenticationIntentUri, syncStatus, composeIntentUri, mimeType,
                         recentFolderListUri, color, defaultRecentFolderListUri, viewIntentProxyUri,
-                        accoutCookieQueryUri, updateSettingsUri);
+                        accoutCookieQueryUri, updateSettingsUri, enableMessageTransforms);
     }
 
     /**
@@ -721,6 +732,7 @@
         map.put(UIProvider.AccountColumns.ACCOUNT_COOKIE_QUERY_URI, accoutCookieQueryUri);
         map.put(UIProvider.AccountColumns.COLOR, color);
         map.put(UIProvider.AccountColumns.UPDATE_SETTINGS_URI, updateSettingsUri);
+        map.put(UIProvider.AccountColumns.ENABLE_MESSAGE_TRANSFORMS, enableMessageTransforms);
         map.put(AccountColumns.SettingsColumns.SIGNATURE, settings.signature);
         map.put(AccountColumns.SettingsColumns.AUTO_ADVANCE, settings.getAutoAdvanceSetting());
         map.put(AccountColumns.SettingsColumns.MESSAGE_TEXT_SIZE, settings.messageTextSize);
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index f3ca8be..2014557 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -166,6 +166,7 @@
             .put(AccountColumns.SettingsColumns.CONVERSATION_VIEW_MODE, Integer.class)
             .put(AccountColumns.SettingsColumns.VEILED_ADDRESS_PATTERN, String.class)
             .put(AccountColumns.UPDATE_SETTINGS_URI, String.class)
+            .put(AccountColumns.ENABLE_MESSAGE_TRANSFORMS, Integer.class)
             .build();
 
     public static final Map<String, Class<?>> ACCOUNTS_COLUMNS =
@@ -449,6 +450,10 @@
          * the new values.
          */
         public static final String UPDATE_SETTINGS_URI = "updateSettingsUri";
+        /**
+         * Whether message transforms (HTML DOM manipulation) should be enabled.
+         */
+        public static final String ENABLE_MESSAGE_TRANSFORMS = "enableMessageTransforms";
 
         public static final class SettingsColumns {
             /**
diff --git a/src/com/android/mail/ui/AbstractActivityController.java b/src/com/android/mail/ui/AbstractActivityController.java
index b0bcd5b..4eb9de1 100644
--- a/src/com/android/mail/ui/AbstractActivityController.java
+++ b/src/com/android/mail/ui/AbstractActivityController.java
@@ -352,7 +352,8 @@
     protected ActionBarDrawerToggle mDrawerToggle;
     protected ListView mListViewForAnimating;
     protected boolean mHasNewAccountOrFolder;
-    protected final MailDrawerListener mDrawerListener = new MailDrawerListener();
+    private boolean mConversationListLoadFinishedIgnored;
+    protected MailDrawerListener mDrawerListener;
 
     public static final String SYNC_ERROR_DIALOG_FRAGMENT_TAG = "SyncErrorDialogFragment";
 
@@ -383,6 +384,7 @@
         mShowUndoBarDelay = r.getInteger(R.integer.show_undo_bar_delay_ms);
         mVeiledMatcher = VeiledAddressMatcher.newInstance(activity.getResources());
         mIsTablet = Utils.useTabletUI(r);
+        mConversationListLoadFinishedIgnored = false;
     }
 
     @Override
@@ -1004,7 +1006,8 @@
 
         mDrawerToggle = new ActionBarDrawerToggle((Activity) mActivity, mDrawerContainer,
                 R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close);
-        mDrawerContainer.setDrawerListener(new MailDrawerListener());
+        mDrawerListener = new MailDrawerListener();
+        mDrawerContainer.setDrawerListener(mDrawerListener);
         mDrawerContainer.setDrawerShadow(
                 mContext.getResources().getDrawable(R.drawable.drawer_shadow), Gravity.START);
 
@@ -3059,8 +3062,9 @@
         public void onLoadFinished(Loader<ConversationCursor> loader, ConversationCursor data) {
             LogUtils.d(LOG_TAG, "IN AAC.ConversationCursor.onLoadFinished, data=%s loader=%s",
                     data, loader);
-            if (isDrawerEnabled() && mDrawerContainer.isDrawerOpen(mDrawerPullout)) {
+            if (isDrawerEnabled() && mDrawerListener.getDrawerState() != DrawerLayout.STATE_IDLE) {
                 LogUtils.d(LOG_TAG, "ConversationListLoaderCallbacks.onLoadFinished: ignoring.");
+                mConversationListLoadFinishedIgnored = true;
                 return;
             }
             // Clear our all pending destructive actions before swapping the conversation cursor
@@ -3825,6 +3829,12 @@
     }
 
     private class MailDrawerListener implements DrawerLayout.DrawerListener {
+        private int mDrawerState;
+
+        public MailDrawerListener() {
+            mDrawerState = DrawerLayout.STATE_IDLE;
+        }
+
         @Override
         public void onDrawerOpened(View drawerView) {
             mDrawerToggle.onDrawerOpened(drawerView);
@@ -3858,9 +3868,20 @@
          */
         @Override
         public void onDrawerStateChanged(int newState) {
-            mDrawerToggle.onDrawerStateChanged(newState);
-            if (mHasNewAccountOrFolder && newState == DrawerLayout.STATE_IDLE) {
-                refreshDrawer();
+            mDrawerState = newState;
+            mDrawerToggle.onDrawerStateChanged(mDrawerState);
+            if (mDrawerState == DrawerLayout.STATE_IDLE) {
+                if (mHasNewAccountOrFolder) {
+                    refreshDrawer();
+                }
+                if (mConversationListLoadFinishedIgnored) {
+                    mConversationListLoadFinishedIgnored = false;
+                    final Bundle args = new Bundle();
+                    args.putParcelable(BUNDLE_ACCOUNT_KEY, mAccount);
+                    args.putParcelable(BUNDLE_FOLDER_KEY, mFolder);
+                    mActivity.getLoaderManager().initLoader(
+                            LOADER_CONVERSATION_LIST, args, mListCursorCallbacks);
+                }
             }
         }
 
@@ -3878,6 +3899,17 @@
             }
             mDrawerObservers.notifyChanged();
         }
+
+        /**
+         * Returns the most recent update of the {@link DrawerLayout}'s state provided
+         * by {@link #onDrawerStateChanged(int)}.
+         * @return The {@link DrawerLayout}'s current state. One of
+         * {@link DrawerLayout#STATE_DRAGGING}, {@link DrawerLayout#STATE_IDLE},
+         * or {@link DrawerLayout#STATE_SETTLING}.
+         */
+        public int getDrawerState() {
+            return mDrawerState;
+        }
     }
 
     @Override
diff --git a/src/com/android/mail/ui/AbstractConversationViewFragment.java b/src/com/android/mail/ui/AbstractConversationViewFragment.java
index 82b00b5..642a9f3 100644
--- a/src/com/android/mail/ui/AbstractConversationViewFragment.java
+++ b/src/com/android/mail/ui/AbstractConversationViewFragment.java
@@ -25,7 +25,6 @@
 import android.app.LoaderManager;
 import android.content.ActivityNotFoundException;
 import android.content.Context;
-import android.content.CursorLoader;
 import android.content.Intent;
 import android.content.Loader;
 import android.content.pm.ActivityInfo;
@@ -56,7 +55,10 @@
 import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
 import com.android.mail.browse.MessageCursor;
 import com.android.mail.browse.MessageCursor.ConversationController;
+import com.android.mail.browse.MessageCursor.ConversationMessage;
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
+import com.android.mail.content.ObjectCursor;
+import com.android.mail.content.ObjectCursorLoader;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.AccountObserver;
 import com.android.mail.providers.Address;
@@ -193,7 +195,8 @@
      * Subclasses must override this, since they may want to display a single or
      * many messages related to this conversation.
      */
-    protected abstract void onMessageCursorLoadFinished(Loader<Cursor> loader,
+    protected abstract void onMessageCursorLoadFinished(
+            Loader<ObjectCursor<ConversationMessage>> loader,
             MessageCursor newCursor, MessageCursor oldCursor);
 
     /**
@@ -502,15 +505,17 @@
         }
     }
 
-    private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
+    private class MessageLoaderCallbacks
+            implements LoaderManager.LoaderCallbacks<ObjectCursor<ConversationMessage>> {
 
         @Override
-        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        public Loader<ObjectCursor<ConversationMessage>> onCreateLoader(int id, Bundle args) {
             return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
         }
 
         @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        public void onLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
+                    ObjectCursor<ConversationMessage> data) {
             // ignore truly duplicate results
             // this can happen when restoring after rotation
             if (mCursor == data) {
@@ -538,7 +543,8 @@
                         // message list update fires first. nothing to do
                         // because we expect to be torn down soon.)
                         LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
-                                + " in anticipation of conv cursor update. c=%s", mConversation.uri);
+                                + " in anticipation of conv cursor update. c=%s",
+                                mConversation.uri);
                     }
                     // existing mCursor will imminently be closed, must stop referencing it
                     // since we expect to be kicked out soon, it doesn't matter what mCursor
@@ -563,7 +569,7 @@
         }
 
         @Override
-        public void onLoaderReset(Loader<Cursor> loader) {
+        public void onLoaderReset(Loader<ObjectCursor<ConversationMessage>>  loader) {
             mCursor = null;
         }
 
@@ -641,20 +647,15 @@
         return new ConversationViewState();
     }
 
-    private static class MessageLoader extends CursorLoader {
+    private static class MessageLoader extends ObjectCursorLoader<ConversationMessage> {
         private boolean mDeliveredFirstResults = false;
 
         public MessageLoader(Context c, Uri messageListUri) {
-            super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null);
+            super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, ConversationMessage.FACTORY);
         }
 
         @Override
-        public Cursor loadInBackground() {
-            return new MessageCursor(super.loadInBackground());
-        }
-
-        @Override
-        public void deliverResult(Cursor result) {
+        public void deliverResult(ObjectCursor<ConversationMessage> result) {
             // We want to deliver these results, and then we want to make sure
             // that any subsequent
             // queries do not hit the network
@@ -677,6 +678,11 @@
                 setUri(uri);
             }
         }
+
+        @Override
+        protected ObjectCursor<ConversationMessage> getObjectCursor(Cursor inner) {
+            return new MessageCursor(inner);
+        }
     }
 
     /**
@@ -870,6 +876,7 @@
      * @return {@code true} if the conversation should be transformed. {@code false}, otherwise.
      */
     public boolean shouldApplyTransforms() {
-        return !mHasConversationTransformBeenReverted;
+        return (mAccount.enableMessageTransforms > 0) &&
+                !mHasConversationTransformBeenReverted;
     }
 }
diff --git a/src/com/android/mail/ui/ConversationViewFragment.java b/src/com/android/mail/ui/ConversationViewFragment.java
index b67b538..c119861 100644
--- a/src/com/android/mail/ui/ConversationViewFragment.java
+++ b/src/com/android/mail/ui/ConversationViewFragment.java
@@ -63,6 +63,7 @@
 import com.android.mail.browse.ScrollIndicatorsView;
 import com.android.mail.browse.SuperCollapsedBlock;
 import com.android.mail.browse.WebViewContextMenu;
+import com.android.mail.content.ObjectCursor;
 import com.android.mail.preferences.MailPrefs;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Address;
@@ -1202,8 +1203,8 @@
     }
 
     @Override
-    public void onMessageCursorLoadFinished(Loader<Cursor> loader, MessageCursor newCursor,
-            MessageCursor oldCursor) {
+    public void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
+            MessageCursor newCursor, MessageCursor oldCursor) {
         /*
          * what kind of changes affect the MessageCursor? 1. new message(s) 2.
          * read/unread state change 3. deleted message, either regular or draft
diff --git a/src/com/android/mail/ui/MailActionBarView.java b/src/com/android/mail/ui/MailActionBarView.java
index 637c03b..da73346 100644
--- a/src/com/android/mail/ui/MailActionBarView.java
+++ b/src/com/android/mail/ui/MailActionBarView.java
@@ -731,7 +731,7 @@
         if (ViewMode.isWaitingForSync(mMode)) {
             // Account is not synced: clear title and update the subtitle.
             setTitle("");
-            removeUnreadCount();
+            removeUnreadCount(true);
             return;
         }
         // Check if we should be changing the actionbar at all, and back off if not.
@@ -757,27 +757,31 @@
         final int toDisplay = (folderUnreadCount > UNREAD_LIMIT)
                 ? (UNREAD_LIMIT + 1) : folderUnreadCount;
         if ((mUnreadCount != toDisplay || folderChanged) && toDisplay != 0) {
-            setSubtitle(Utils.getUnreadMessageString(
-                    mActivity.getApplicationContext(), toDisplay));
-            // This is a new update, remove previous messages, if any.
-            mHandler.removeMessages(SubtitleHandler.EMAIL);
-            // In a short while, show the account name in its place.
-            mHandler.sendEmptyMessageDelayed(SubtitleHandler.EMAIL, ACCOUNT_DELAY_MS);
-        } else {
-            removeUnreadCount();
+            setSubtitle(Utils.getUnreadMessageString(mActivity.getApplicationContext(), toDisplay));
         }
+        // Schedule a removal of unread count for the future, if there isn't one already.
+        removeUnreadCount(false);
         // Remember the new value for the next run
         mUnreadCount = toDisplay;
     }
 
     /**
      * Remove the unread count and show the account name, if required.
+     * @param now true if you want the change to happen immediately. False if you want to enforce
+     *            it happens later.
      */
-    private void removeUnreadCount() {
-        // Remove all previous messages which might change the subtitle
-        mHandler.removeMessages(SubtitleHandler.EMAIL);
-        // Update the subtitle: clear it or show account name.
-        mHandler.sendEmptyMessage(SubtitleHandler.EMAIL);
+    private void removeUnreadCount(boolean now) {
+        if (now) {
+            // Remove all previous messages which might change the subtitle
+            mHandler.removeMessages(SubtitleHandler.EMAIL);
+            // Update the subtitle: clear it or show account name.
+            mHandler.sendEmptyMessage(SubtitleHandler.EMAIL);
+        } else {
+            if (!mHandler.hasMessages(SubtitleHandler.EMAIL)) {
+                // In a short while, show the account name in its place.
+                mHandler.sendEmptyMessageDelayed(SubtitleHandler.EMAIL, ACCOUNT_DELAY_MS);
+            }
+        }
     }
 
     /**
diff --git a/src/com/android/mail/ui/SecureConversationViewFragment.java b/src/com/android/mail/ui/SecureConversationViewFragment.java
index d2eac38..3a74d35 100644
--- a/src/com/android/mail/ui/SecureConversationViewFragment.java
+++ b/src/com/android/mail/ui/SecureConversationViewFragment.java
@@ -40,6 +40,7 @@
 import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
 import com.android.mail.browse.MessageScrollView;
 import com.android.mail.browse.MessageWebView;
+import com.android.mail.content.ObjectCursor;
 import com.android.mail.providers.Account;
 import com.android.mail.providers.Conversation;
 import com.android.mail.providers.Message;
@@ -213,8 +214,8 @@
     }
 
     @Override
-    protected void onMessageCursorLoadFinished(Loader<Cursor> loader, MessageCursor newCursor,
-            MessageCursor oldCursor) {
+    protected void onMessageCursorLoadFinished(Loader<ObjectCursor<ConversationMessage>> loader,
+            MessageCursor newCursor, MessageCursor oldCursor) {
         // ignore cursors that are still loading results
         if (newCursor == null || !newCursor.isLoaded()) {
             LogUtils.i(LOG_TAG, "CONV RENDER: existing cursor is null, rendering from scratch");