Merge "Support compose with quoted html." into jb-ub-mail-ur10
diff --git a/res/layout/swipe_leavebehind.xml b/res/layout/swipe_leavebehind.xml
index a0bb4f6..9d0a50d 100644
--- a/res/layout/swipe_leavebehind.xml
+++ b/res/layout/swipe_leavebehind.xml
@@ -19,55 +19,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/swiped_bg_color">
-    <LinearLayout
+
+    <include
         android:id="@+id/swipeable_content"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:clickable="true"
-        android:orientation="horizontal">
-        <TextView
-            android:id="@+id/undo_descriptionview"
-            android:layout_width="0dip"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:ellipsize="end"
-            android:singleLine="true"
-            android:text="@string/no_conversations"
-            android:textColor="@android:color/white"
-            android:textAppearance="?android:attr/textAppearanceMedium"
-            android:paddingLeft="16dip"
-            android:clickable="true"
-            android:gravity="center_vertical"/>
+        layout="@layout/swipe_leavebehind_body" />
 
-        <View
-            android:id="@+id/undo_separator"
-            android:layout_width="1dip"
-            android:layout_height="match_parent"
-            android:background="@android:color/white"
-            android:layout_marginTop="16dp"
-            android:layout_marginBottom="16dp" />
-
-        <ImageView
-            android:id="@+id/undo_icon"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:paddingLeft="12dip"
-            android:paddingRight="8dip"
-            android:src="@drawable/ic_menu_revert_holo_dark"
-            android:background="?android:attr/selectableItemBackground"
-            android:duplicateParentState="true" />
-
-        <TextView
-            android:id="@+id/undo_text"
-            style="@style/UndoTextStyle"
-            android:layout_width="wrap_content"
-            android:layout_height="match_parent"
-            android:paddingRight="16dip"
-            android:text="@string/undo"
-            android:textAllCaps="true"
-            android:gravity="center_vertical"
-            android:textColor="@android:color/white"
-            android:background="?android:attr/selectableItemBackground"
-            android:duplicateParentState="true"/>
-    </LinearLayout>
 </com.android.mail.ui.LeaveBehindItem>
diff --git a/res/layout/swipe_leavebehind_body.xml b/res/layout/swipe_leavebehind_body.xml
new file mode 100644
index 0000000..027d33c
--- /dev/null
+++ b/res/layout/swipe_leavebehind_body.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clickable="true"
+    android:orientation="horizontal">
+    <TextView
+        android:id="@+id/undo_descriptionview"
+        android:layout_width="0dip"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        android:ellipsize="end"
+        android:singleLine="true"
+        android:text="@string/no_conversations"
+        android:textColor="@android:color/white"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:paddingLeft="16dip"
+        android:clickable="true"
+        android:gravity="center_vertical"/>
+
+    <View
+        android:id="@+id/undo_separator"
+        android:layout_width="1dip"
+        android:layout_height="match_parent"
+        android:background="@android:color/white"
+        android:layout_marginTop="16dp"
+        android:layout_marginBottom="16dp" />
+
+    <ImageView
+        android:id="@+id/undo_icon"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:paddingLeft="12dip"
+        android:paddingRight="8dip"
+        android:src="@drawable/ic_menu_revert_holo_dark"
+        android:background="?android:attr/selectableItemBackground"
+        android:duplicateParentState="true" />
+
+    <TextView
+        android:id="@+id/undo_text"
+        style="@style/UndoTextStyle"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:paddingRight="16dip"
+        android:text="@string/undo"
+        android:textAllCaps="true"
+        android:gravity="center_vertical"
+        android:textColor="@android:color/white"
+        android:background="?android:attr/selectableItemBackground"
+        android:duplicateParentState="true"/>
+</LinearLayout>
diff --git a/res/menu/photo_view_menu.xml b/res/menu/photo_view_menu.xml
index 5caeca4..d654d40 100644
--- a/res/menu/photo_view_menu.xml
+++ b/res/menu/photo_view_menu.xml
@@ -34,6 +34,10 @@
             android:id="@+id/menu_share_all"
             android:showAsAction="never"
             android:title="@string/menu_photo_share_all"/>
+        <item
+            android:id="@+id/menu_download_again"
+            android:showAsAction="never"
+            android:title="@string/download_again"/>
     </group>
 
 </menu>
\ No newline at end of file
diff --git a/src/com/android/bitmap/AltBitmapCache.java b/src/com/android/bitmap/AltBitmapCache.java
index 2ad047f..fb8e915 100644
--- a/src/com/android/bitmap/AltBitmapCache.java
+++ b/src/com/android/bitmap/AltBitmapCache.java
@@ -70,6 +70,7 @@
                 if (DEBUG) {
                     LogUtils.d(TAG, "AltBitmapCache: %s waiting", Thread.currentThread().getName());
                 }
+                Trace.beginSection("sleep");
                 try {
                     // block
                     mLock.wait();
@@ -79,6 +80,7 @@
                     }
                 } catch (InterruptedException e) {
                 }
+                Trace.endSection();
             }
         }
         return bitmap;
diff --git a/src/com/android/bitmap/ContiguousFIFOAggregator.java b/src/com/android/bitmap/ContiguousFIFOAggregator.java
index 018923c..f93503b 100644
--- a/src/com/android/bitmap/ContiguousFIFOAggregator.java
+++ b/src/com/android/bitmap/ContiguousFIFOAggregator.java
@@ -175,7 +175,7 @@
         Entry<T, Value> first;
         int count = 0;
         while (iter.hasNext()) {
-            Utils.traceBeginSection("pool maybeExecuteNow");
+            Utils.traceBeginSection("pool maybeExecuteNow loop");
             first = iter.next();
             if (count > 0) {
                 // When count == 0, the key is already first.
@@ -183,6 +183,7 @@
             }
 
             if (first.getValue().task == null) {
+                Utils.traceEndSection();
                 break;
             }
 
diff --git a/src/com/android/bitmap/DecodeTask.java b/src/com/android/bitmap/DecodeTask.java
index da87b7d..af4674e 100644
--- a/src/com/android/bitmap/DecodeTask.java
+++ b/src/com/android/bitmap/DecodeTask.java
@@ -94,17 +94,27 @@
         AssetFileDescriptor fd = null;
         InputStream in = null;
         try {
+            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+                Trace.beginSection("poll for reusable bitmap");
+                mInBitmap = mCache.poll();
+                Trace.endSection();
+
+                if (isCancelled()) {
+                    return null;
+                }
+            }
+
             Trace.beginSection("create fd or stream");
             fd = mKey.createFd();
             if (fd == null) {
                 in = mKey.createInputStream();
-                if (in != null && !in.markSupported()) {
-                    Trace.endSection();
-                    throw new IllegalArgumentException("input stream must support reset()");
-                }
             }
             Trace.endSection();
 
+            if (isCancelled()) {
+                return null;
+            }
+
             Trace.beginSection("decodeBounds");
             mOpts.inJustDecodeBounds = true;
             if (fd != null) {
@@ -117,6 +127,7 @@
             if (isCancelled()) {
                 return null;
             }
+
             final int srcW = mOpts.outWidth;
             final int srcH = mOpts.outHeight;
 
@@ -124,14 +135,6 @@
             mOpts.inMutable = true;
             mOpts.inSampleSize = calculateSampleSize(srcW, srcH, mDestW, mDestH);
             if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
-                Trace.beginSection("poll for reusable bitmap");
-                mInBitmap = mCache.poll();
-                Trace.endSection();
-
-                if (isCancelled()) {
-                    return null;
-                }
-
                 if (mInBitmap == null) {
                     if (DEBUG) System.err.println(
                             "decode thread wants a bitmap. cache dump:\n" + mCache.toDebugString());
@@ -140,6 +143,11 @@
                             Bitmap.createBitmap(mDestBufferW, mDestBufferH,
                                     Bitmap.Config.ARGB_8888));
                     Trace.endSection();
+
+                    if (isCancelled()) {
+                        return null;
+                    }
+
                     if (DEBUG) System.err.println("*** allocated new bitmap in decode thread: "
                             + mInBitmap + " key=" + mKey);
                 } else {
@@ -150,15 +158,16 @@
                 mOpts.inBitmap = mInBitmap.bmp;
             }
 
+            Bitmap decodeResult = null;
+
+            if (in != null) {
+                in = mKey.createInputStream();
+            }
+
             if (isCancelled()) {
                 return null;
             }
 
-            Bitmap decodeResult = null;
-
-            if (in != null) {
-                in.reset();
-            }
             final Rect srcRect = new Rect();
             if (CROP_DURING_DECODE) {
                 try {
@@ -170,6 +179,10 @@
                 } finally {
                     Trace.endSection();
                 }
+
+                if (isCancelled()) {
+                    return null;
+                }
             }
 
             if (!CROP_DURING_DECODE || (decodeResult == null && !isCancelled())) {
@@ -194,9 +207,13 @@
                 } finally {
                     Trace.endSection();
                 }
+
+                if (isCancelled()) {
+                    return null;
+                }
             }
 
-            if (!isCancelled() && decodeResult != null) {
+            if (decodeResult != null) {
                 if (mInBitmap != null) {
                     result = mInBitmap;
                     // srcRect is non-empty when using the cropping BitmapRegionDecoder codepath
diff --git a/src/com/android/mail/bitmap/AttachmentDrawable.java b/src/com/android/mail/bitmap/AttachmentDrawable.java
index 96f0eb5..927561a 100644
--- a/src/com/android/mail/bitmap/AttachmentDrawable.java
+++ b/src/com/android/mail/bitmap/AttachmentDrawable.java
@@ -22,6 +22,7 @@
 import com.android.bitmap.DecodeTask;
 import com.android.bitmap.DecodeTask.Request;
 import com.android.bitmap.ReusableBitmap;
+import com.android.bitmap.Trace;
 import com.android.mail.R;
 import com.android.mail.browse.ConversationItemViewCoordinates;
 import com.android.mail.ui.SwipeableListView;
@@ -140,6 +141,7 @@
             return;
         }
 
+        Trace.beginSection("set image");
         // avoid visual state transitions when the existing request and the new one are just
         // requests for different renditions of the same attachment
         final boolean onlyRenditionChange = (mCurrKey != null && mCurrKey.matches(key));
@@ -166,6 +168,7 @@
         setLoadState(LOAD_STATE_UNINITIALIZED);
 
         if (key == null) {
+            Trace.endSection();
             return;
         }
 
@@ -182,6 +185,7 @@
                         mCurrKey, mCache.toDebugString());
             }
         }
+        Trace.endSection();
     }
 
     @Override
@@ -328,6 +332,7 @@
             return;
         }
 
+        Trace.beginSection("decode");
         if (LIMIT_BITMAP_DENSITY) {
             final float scale =
                     Math.min(1f, (float) MAX_BITMAP_DENSITY / DisplayMetrics.DENSITY_DEFAULT
@@ -342,6 +347,7 @@
         }
 
         if (w == 0 || bufferH == 0) {
+            Trace.endSection();
             return;
         }
 //        System.out.println("ITEM " + this + " w=" + w + " h=" + bufferH + " key=" + mCurrKey);
@@ -353,6 +359,7 @@
         }
         mTask = new DecodeTask(mCurrKey, w, bufferH, bufferW, bufferH, this, mCache);
         mTask.executeOnExecutor(EXECUTOR);
+        Trace.endSection();
     }
 
     private void setLoadState(int loadState) {
@@ -363,6 +370,7 @@
             return;
         }
 
+        Trace.beginSection("set load state");
         switch (loadState) {
             // This state differs from LOADED in that the subsequent state transition away from
             // UNINITIALIZED will not have a fancy transition. This allows list item binds to
@@ -390,6 +398,7 @@
                 mProgress.setVisible(false);
                 break;
         }
+        Trace.endSection();
 
         mLoadState = loadState;
         LogUtils.v(LOG_TAG, "OUT stateful AD.setState. new=%s placeholder=%s progress=%s",
diff --git a/src/com/android/mail/bitmap/CompositeDrawable.java b/src/com/android/mail/bitmap/CompositeDrawable.java
index 45af4ab..e25bfc7 100644
--- a/src/com/android/mail/bitmap/CompositeDrawable.java
+++ b/src/com/android/mail/bitmap/CompositeDrawable.java
@@ -6,6 +6,8 @@
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 
+import com.android.bitmap.Trace;
+
 import java.util.ArrayList;
 import java.util.List;
 
@@ -53,12 +55,14 @@
 
         T result = mDrawables.get(i);
         if (result == null) {
+            Trace.beginSection("create division drawable");
             result = createDivisionDrawable();
             mDrawables.set(i, result);
             result.setCallback(this);
             // Make sure drawables created after the bounds were already set have their bounds
             // set initially (the other unaffected drawables basically de-bounce this).
             onBoundsChange(getBounds());
+            Trace.endSection();
         }
         return result;
     }
diff --git a/src/com/android/mail/browse/AttachmentActionHandler.java b/src/com/android/mail/browse/AttachmentActionHandler.java
index 40a66db..02cec4a 100644
--- a/src/com/android/mail/browse/AttachmentActionHandler.java
+++ b/src/com/android/mail/browse/AttachmentActionHandler.java
@@ -122,7 +122,6 @@
     }
 
     public void startRedownloadingAttachment(Attachment attachment) {
-        showDownloadingDialog();
         final ContentValues params = new ContentValues(2);
         params.put(AttachmentColumns.STATE, AttachmentState.REDOWNLOADING);
         params.put(AttachmentColumns.DESTINATION, attachment.destination);
@@ -134,7 +133,7 @@
      * Displays a loading dialog to be used for downloading attachments.
      * Must be called on the UI thread.
      */
-    private void showDownloadingDialog() {
+    public void showDownloadingDialog() {
         final FragmentTransaction ft = mFragmentManager.beginTransaction();
         final Fragment prev = mFragmentManager.findFragmentByTag(PROGRESS_FRAGMENT_TAG);
         if (prev != null) {
diff --git a/src/com/android/mail/browse/ConversationItemView.java b/src/com/android/mail/browse/ConversationItemView.java
index f95f0b6..9bf7c3a 100644
--- a/src/com/android/mail/browse/ConversationItemView.java
+++ b/src/com/android/mail/browse/ConversationItemView.java
@@ -1542,7 +1542,10 @@
         mPhotoFlipMatrix.reset();
         mPhotoFlipMatrix.postScale(scale, 1);
 
-        canvas.translate(mCoordinates.contactImagesX + xOffset, mCoordinates.contactImagesY);
+        final float x = mCoordinates.contactImagesX + xOffset;
+        final float y = mCoordinates.contactImagesY;
+
+        canvas.translate(x, y);
 
         if (mPhotoBitmap == null) {
             mContactImagesHolder.draw(canvas, mPhotoFlipMatrix);
@@ -2263,4 +2266,8 @@
     public void setPhotoFlipFraction(final float fraction) {
         mPhotoFlipAnimator.setValue(fraction);
     }
+
+    public String getAccount() {
+        return mAccount;
+    }
 }
diff --git a/src/com/android/mail/browse/ConversationItemViewCoordinates.java b/src/com/android/mail/browse/ConversationItemViewCoordinates.java
index 559d708..7b958d1 100644
--- a/src/com/android/mail/browse/ConversationItemViewCoordinates.java
+++ b/src/com/android/mail/browse/ConversationItemViewCoordinates.java
@@ -161,6 +161,28 @@
 
     }
 
+    public static class CoordinatesCache {
+        private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache
+                = new SparseArray<ConversationItemViewCoordinates>();
+        private final SparseArray<View> mViewsCache = new SparseArray<View>();
+
+        public ConversationItemViewCoordinates getCoordinates(final int key) {
+            return mCoordinatesCache.get(key);
+        }
+
+        public View getView(final int layoutId) {
+            return mViewsCache.get(layoutId);
+        }
+
+        public void put(final int key, final ConversationItemViewCoordinates coords) {
+            mCoordinatesCache.put(key, coords);
+        }
+
+        public void put(final int layoutId, final View view) {
+            mViewsCache.put(layoutId, view);
+        }
+    }
+
     /**
      * One of either NORMAL_MODE or WIDE_MODE.
      */
@@ -268,7 +290,8 @@
     private final int mFolderCellWidth;
     private final int mFolderMinimumWidth;
 
-    private ConversationItemViewCoordinates(Context context, Config config) {
+    private ConversationItemViewCoordinates(final Context context, final Config config,
+            final CoordinatesCache cache) {
         Utils.traceBeginSection("CIV coordinates constructor");
         final Resources res = context.getResources();
         mFolderCellWidth = res.getDimensionPixelSize(R.dimen.folder_cell_width);
@@ -289,17 +312,23 @@
                 layoutId = R.layout.conversation_item_view_normal;
             }
         }
-        final ViewGroup view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null);
+
+        ViewGroup view = (ViewGroup) cache.getView(layoutId);
+        if (view == null) {
+            view = (ViewGroup) LayoutInflater.from(context).inflate(layoutId, null);
+            cache.put(layoutId, view);
+        }
 
         // Show/hide optional views before measure/layout call
 
-        View attachmentPreviews = null;
+        final View attachmentPreviews = view.findViewById(R.id.attachment_previews);;
         if (config.getAttachmentPreviewMode() != ATTACHMENT_PREVIEW_NONE) {
-            attachmentPreviews = view.findViewById(R.id.attachment_previews);
-            LayoutParams params = attachmentPreviews.getLayoutParams();
+            final LayoutParams params = attachmentPreviews.getLayoutParams();
             attachmentPreviews.setVisibility(View.VISIBLE);
             params.height = getAttachmentPreviewsHeight(context, config.getAttachmentPreviewMode());
             attachmentPreviews.setLayoutParams(params);
+        } else {
+            attachmentPreviews.setVisibility(View.GONE);
         }
         attachmentPreviewsDecodeHeight = getAttachmentPreviewsHeight(context,
                 ATTACHMENT_PREVIEW_UNREAD);
@@ -308,7 +337,7 @@
         folders.setVisibility(config.areFoldersVisible() ? View.VISIBLE : View.GONE);
 
         // Add margin between attachment previews and folders
-        View attachmentPreviewsBottomMargin = view
+        final View attachmentPreviewsBottomMargin = view
                 .findViewById(R.id.attachment_previews_bottom_margin);
         attachmentPreviewsBottomMargin.setVisibility(
                 attachmentPreviews != null && config.areFoldersVisible() ? View.VISIBLE
@@ -630,15 +659,15 @@
      * Returns coordinates for elements inside a conversation header view given
      * the view width.
      */
-    public static ConversationItemViewCoordinates forConfig(Context context, Config config,
-            SparseArray<ConversationItemViewCoordinates> cache) {
+    public static ConversationItemViewCoordinates forConfig(final Context context,
+            final Config config, final CoordinatesCache cache) {
         final int cacheKey = config.getCacheKey();
-        ConversationItemViewCoordinates coordinates = cache.get(cacheKey);
+        ConversationItemViewCoordinates coordinates = cache.getCoordinates(cacheKey);
         if (coordinates != null) {
             return coordinates;
         }
 
-        coordinates = new ConversationItemViewCoordinates(context, config);
+        coordinates = new ConversationItemViewCoordinates(context, config, cache);
         cache.put(cacheKey, coordinates);
         return coordinates;
     }
diff --git a/src/com/android/mail/browse/MessageAttachmentBar.java b/src/com/android/mail/browse/MessageAttachmentBar.java
index 1bb6d2e..18efa86 100644
--- a/src/com/android/mail/browse/MessageAttachmentBar.java
+++ b/src/com/android/mail/browse/MessageAttachmentBar.java
@@ -176,6 +176,7 @@
             }
         } else if (res == R.id.download_again) {
             if (mAttachment.isPresentLocally()) {
+                mActionHandler.showDownloadingDialog();
                 mActionHandler.startRedownloadingAttachment(mAttachment);
             }
         } else if (res == R.id.cancel_attachment) {
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index 61360f5..c80799d 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -69,6 +69,7 @@
 import android.widget.Toast;
 
 import com.android.common.Rfc822Validator;
+import com.android.common.contacts.DataUsageStatUpdater;
 import com.android.ex.chips.RecipientEditTextView;
 import com.android.mail.MailIntentService;
 import com.android.mail.R;
@@ -2039,16 +2040,30 @@
             sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
 
             if (!sendOrSaveMessage.mSave) {
-                UIProvider.incrementRecipientsTimesContacted(mContext,
+                incrementRecipientsTimesContacted(mContext,
                         (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
-                UIProvider.incrementRecipientsTimesContacted(mContext,
+                incrementRecipientsTimesContacted(mContext,
                         (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
-                UIProvider.incrementRecipientsTimesContacted(mContext,
+                incrementRecipientsTimesContacted(mContext,
                         (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
             }
             mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
         }
 
+        private static void incrementRecipientsTimesContacted(final Context context,
+                final String addressString) {
+            if (TextUtils.isEmpty(addressString)) {
+                return;
+            }
+            final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
+            final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
+            for (int i = 0; i < tokens.length;i++) {
+                recipients.add(tokens[i].getAddress());
+            }
+            final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
+            statsUpdater.updateWithAddress(recipients);
+        }
+
         /**
          * Send or Save a message.
          */
diff --git a/src/com/android/mail/photo/MailPhotoViewActivity.java b/src/com/android/mail/photo/MailPhotoViewActivity.java
index ecceedf..8f4216e 100644
--- a/src/com/android/mail/photo/MailPhotoViewActivity.java
+++ b/src/com/android/mail/photo/MailPhotoViewActivity.java
@@ -58,6 +58,11 @@
     private MenuItem mSaveAllItem;
     private MenuItem mShareItem;
     private MenuItem mShareAllItem;
+    /**
+     * Only for attachments that are currently downloading. Attachments that failed show the
+     * retry button.
+     */
+    private MenuItem mDownloadAgainItem;
     private AttachmentActionHandler mActionHandler;
     private Menu mMenu;
 
@@ -121,6 +126,7 @@
         mSaveAllItem = mMenu.findItem(R.id.menu_save_all);
         mShareItem = mMenu.findItem(R.id.menu_share);
         mShareAllItem = mMenu.findItem(R.id.menu_share_all);
+        mDownloadAgainItem = mMenu.findItem(R.id.menu_download_again);
 
         return true;
     }
@@ -145,6 +151,7 @@
             mSaveItem.setEnabled(!attachment.isDownloading()
                     && attachment.canSave() && !attachment.isSavedToExternal());
             mShareItem.setEnabled(attachment.canShare());
+            mDownloadAgainItem.setEnabled(attachment.canSave() && attachment.isDownloading());
         } else {
             if (mMenu != null) {
                 mMenu.setGroupEnabled(R.id.photo_view_menu_group, false);
@@ -201,6 +208,9 @@
         } else if (itemId == R.id.menu_share_all) { // share all of the photos
             shareAllAttachments();
             return true;
+        } else if (itemId == R.id.menu_download_again) { // redownload the current photo
+            redownloadAttachment();
+            return true;
         } else {
             return super.onOptionsItemSelected(item);
         }
@@ -277,7 +287,7 @@
             retryButton.setOnClickListener(new View.OnClickListener() {
                 @Override
                 public void onClick(View view) {
-                    downloadAttachment();
+                    redownloadAttachment();
                     emptyText.setVisibility(View.GONE);
                     retryButton.setVisibility(View.GONE);
                 }
@@ -294,13 +304,17 @@
     }
 
     /**
-     * Downloads the attachment.
+     * Redownloads the attachment.
      */
-    private void downloadAttachment() {
+    private void redownloadAttachment() {
         final Attachment attachment = getCurrentAttachment();
         if (attachment != null && attachment.canSave()) {
+            // REDOWNLOADING command is only for attachments that are finished or failed.
+            // For an attachment that is downloading (or paused in the DownloadManager), we need to
+            // cancel it first.
             mActionHandler.setAttachment(attachment);
-            mActionHandler.startDownloadingAttachment(AttachmentDestination.CACHE);
+            mActionHandler.cancelAttachment();
+            mActionHandler.startDownloadingAttachment(attachment.destination);
         }
     }
 
diff --git a/src/com/android/mail/preferences/MailPrefs.java b/src/com/android/mail/preferences/MailPrefs.java
index bb79652..ac06ced 100644
--- a/src/com/android/mail/preferences/MailPrefs.java
+++ b/src/com/android/mail/preferences/MailPrefs.java
@@ -22,6 +22,7 @@
 
 import com.android.mail.providers.Account;
 import com.android.mail.providers.UIProvider;
+import com.android.mail.utils.LogUtils;
 import com.android.mail.widget.BaseWidgetProvider;
 import com.google.common.collect.ImmutableSet;
 
@@ -159,6 +160,10 @@
     }
 
     public void configureWidget(int appWidgetId, Account account, final String folderUri) {
+        if (account == null) {
+            LogUtils.e(LOG_TAG, "Cannot configure widget with null account");
+            return;
+        }
         getEditor().putString(PreferenceKeys.WIDGET_ACCOUNT_PREFIX + appWidgetId,
                 createWidgetPreferenceValue(account, folderUri)).apply();
     }
diff --git a/src/com/android/mail/providers/Folder.java b/src/com/android/mail/providers/Folder.java
index c1ddf00..271aeeb 100644
--- a/src/com/android/mail/providers/Folder.java
+++ b/src/com/android/mail/providers/Folder.java
@@ -193,7 +193,139 @@
     /** An immutable, empty conversation list */
     public static final Collection<Folder> EMPTY = Collections.emptyList();
 
-    // TODO: we desperately need a Builder here
+    public static final class Builder {
+        private int mId;
+        private String mPersistentId;
+        private Uri mUri;
+        private String mName;
+        private int mCapabilities;
+        private boolean mHasChildren;
+        private int mSyncWindow;
+        private Uri mConversationListUri;
+        private Uri mChildFoldersListUri;
+        private int mUnseenCount;
+        private int mUnreadCount;
+        private int mTotalCount;
+        private Uri mRefreshUri;
+        private int mSyncStatus;
+        private int mLastSyncResult;
+        private int mType;
+        private int mIconResId;
+        private int mNotificationIconResId;
+        private String mBgColor;
+        private String mFgColor;
+        private Uri mLoadMoreUri;
+        private String mHierarchicalDesc;
+        private Uri mParent;
+        private long mLastMessageTimestamp;
+
+        public Folder build() {
+            return new Folder(mId, mPersistentId, mUri, mName, mCapabilities,
+                    mHasChildren, mSyncWindow, mConversationListUri, mChildFoldersListUri,
+                    mUnseenCount, mUnreadCount, mTotalCount, mRefreshUri, mSyncStatus,
+                    mLastSyncResult, mType, mIconResId, mNotificationIconResId, mBgColor,
+                    mFgColor, mLoadMoreUri, mHierarchicalDesc, mParent,
+                    mLastMessageTimestamp);
+        }
+
+        public Builder setId(final int id) {
+            mId = id;
+            return this;
+        }
+        public Builder setPersistentId(final String persistentId) {
+            mPersistentId = persistentId;
+            return this;
+        }
+        public Builder setUri(final Uri uri) {
+            mUri = uri;
+            return this;
+        }
+        public Builder setName(final String name) {
+            mName = name;
+            return this;
+        }
+        public Builder setCapabilities(final int capabilities) {
+            mCapabilities = capabilities;
+            return this;
+        }
+        public Builder setHasChildren(final boolean hasChildren) {
+            mHasChildren = hasChildren;
+            return this;
+        }
+        public Builder setSyncWindow(final int syncWindow) {
+            mSyncWindow = syncWindow;
+            return this;
+        }
+        public Builder setConversationListUri(final Uri conversationListUri) {
+            mConversationListUri = conversationListUri;
+            return this;
+        }
+        public Builder setChildFoldersListUri(final Uri childFoldersListUri) {
+            mChildFoldersListUri = childFoldersListUri;
+            return this;
+        }
+        public Builder setUnseenCount(final int unseenCount) {
+            mUnseenCount = unseenCount;
+            return this;
+        }
+        public Builder setUnreadCount(final int unreadCount) {
+            mUnreadCount = unreadCount;
+            return this;
+        }
+        public Builder setTotalCount(final int totalCount) {
+            mTotalCount = totalCount;
+            return this;
+        }
+        public Builder setRefreshUri(final Uri refreshUri) {
+            mRefreshUri = refreshUri;
+            return this;
+        }
+        public Builder setSyncStatus(final int syncStatus) {
+            mSyncStatus = syncStatus;
+            return this;
+        }
+        public Builder setLastSyncResult(final int lastSyncResult) {
+            mLastSyncResult = lastSyncResult;
+            return this;
+        }
+        public Builder setType(final int type) {
+            mType = type;
+            return this;
+        }
+        public Builder setIconResId(final int iconResId) {
+            mIconResId = iconResId;
+            return this;
+        }
+        public Builder setNotificationIconResId(final int notificationIconResId) {
+            mNotificationIconResId = notificationIconResId;
+            return this;
+        }
+        public Builder setBgColor(final String bgColor) {
+            mBgColor = bgColor;
+            return this;
+        }
+        public Builder setFgColor(final String fgColor) {
+            mFgColor = fgColor;
+            return this;
+        }
+        public Builder setLoadMoreUri(final Uri loadMoreUri) {
+            mLoadMoreUri = loadMoreUri;
+            return this;
+        }
+        public Builder setHierarchicalDesc(final String hierarchicalDesc) {
+            mHierarchicalDesc = hierarchicalDesc;
+            return this;
+        }
+        public Builder setParent(final Uri parent) {
+            mParent = parent;
+            return this;
+        }
+        public Builder setLastMessageTimestamp(final long lastMessageTimestamp) {
+            mLastMessageTimestamp = lastMessageTimestamp;
+            return this;
+        }
+    }
+
     public Folder(int id, String persistentId, Uri uri, String name, int capabilities,
             boolean hasChildren, int syncWindow, Uri conversationListUri, Uri childFoldersListUri,
             int unseenCount, int unreadCount, int totalCount, Uri refreshUri, int syncStatus,
@@ -361,7 +493,7 @@
     public static ObjectCursorLoader<Folder> forSearchResults(Account account, String query,
             Context context) {
         if (account.searchUri != null) {
-            final Builder searchBuilder = account.searchUri.buildUpon();
+            final Uri.Builder searchBuilder = account.searchUri.buildUpon();
             searchBuilder.appendQueryParameter(UIProvider.SearchQueryParameters.QUERY, query);
             final Uri searchUri = searchBuilder.build();
             return new ObjectCursorLoader<Folder>(context, searchUri, UIProvider.FOLDERS_PROJECTION,
diff --git a/src/com/android/mail/providers/UIProvider.java b/src/com/android/mail/providers/UIProvider.java
index e1b2b91..98769e4 100644
--- a/src/com/android/mail/providers/UIProvider.java
+++ b/src/com/android/mail/providers/UIProvider.java
@@ -2020,24 +2020,6 @@
         }
     }
 
-    public static String getAttachmentTypeSetting() {
-        // TODO: query the account to see what kinds of attachments it supports?
-        return "com.google.android.gm.allowAddAnyAttachment";
-    }
-
-    public static void incrementRecipientsTimesContacted(Context context, String addressString) {
-        DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
-        ArrayList<String> recipients = new ArrayList<String>();
-        if (TextUtils.isEmpty(addressString)) {
-            return;
-        }
-        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
-        for (int i = 0; i < tokens.length;i++) {
-            recipients.add(tokens[i].getAddress());
-        }
-        statsUpdater.updateWithAddress(recipients);
-    }
-
     public static final String[] UNDO_PROJECTION = {
         ConversationColumns.MESSAGE_LIST_URI
     };
diff --git a/src/com/android/mail/ui/AnimatedAdapter.java b/src/com/android/mail/ui/AnimatedAdapter.java
index 77af447..61a7793 100644
--- a/src/com/android/mail/ui/AnimatedAdapter.java
+++ b/src/com/android/mail/ui/AnimatedAdapter.java
@@ -41,6 +41,7 @@
 import com.android.mail.browse.ConversationCursor;
 import com.android.mail.browse.ConversationItemView;
 import com.android.mail.browse.ConversationItemViewCoordinates;
+import com.android.mail.browse.ConversationItemViewCoordinates.CoordinatesCache;
 import com.android.mail.browse.SwipeableConversationItemView;
 import com.android.mail.content.ObjectCursor;
 import com.android.mail.preferences.MailPrefs;
@@ -198,8 +199,7 @@
      */
     private final SparseArray<ConversationSpecialItemView> mSpecialViews;
 
-    private final SparseArray<ConversationItemViewCoordinates> mCoordinatesCache =
-            new SparseArray<ConversationItemViewCoordinates>();
+    private final CoordinatesCache mCoordinatesCache = new CoordinatesCache();
 
     /**
      * Temporary views insert at specific positions relative to conversations. These can be
@@ -520,6 +520,7 @@
             if(isPositionFadeLeaveBehind(conv)) {
                 LeaveBehindItem fade  = getFadeLeaveBehindItem(position, conv);
                 fade.startShrinkAnimation(mAnimatorListener);
+                Utils.traceEndSection();
                 return fade;
             }
         }
@@ -540,6 +541,7 @@
                         fadeIn.startFadeInTextAnimation(sDismissAllShortDelay /* delay start */);
                     }
                 }
+                Utils.traceEndSection();
                 return fadeIn;
             }
         }
@@ -649,7 +651,7 @@
         }
     }
 
-    public SparseArray<ConversationItemViewCoordinates> getCoordinatesCache() {
+    public CoordinatesCache getCoordinatesCache() {
         return mCoordinatesCache;
     }