Merge "Fix incorrect background for change labels" into ub-gmail-ur14-dev
diff --git a/res/values/dimen.xml b/res/values/dimen.xml
index 8f7909a..791a8ab 100644
--- a/res/values/dimen.xml
+++ b/res/values/dimen.xml
@@ -114,8 +114,9 @@
     <dimen name="widget_margin_bottom">0dip</dimen>
     <dimen name="wait_padding">16dp</dimen>
     <integer name="chips_max_lines">2</integer>
-    <dimen name="tile_letter_font_size">24dp</dimen>
-    <dimen name="tile_letter_font_size_small">16dp</dimen>
+    <dimen name="tile_letter_font_size_tiny">16dp</dimen>
+    <dimen name="tile_letter_font_size_small">24dp</dimen>
+    <dimen name="tile_letter_font_size_medium">40dp</dimen>
     <dimen name="tile_divider_width">1dp</dimen>
     <dimen name="checked_text_padding">16dip</dimen>
     <dimen name="send_mail_as_padding">8dip</dimen>
diff --git a/src/com/android/mail/bitmap/ContactDrawable.java b/src/com/android/mail/bitmap/ContactDrawable.java
index 34983bf..7acb2c9 100644
--- a/src/com/android/mail/bitmap/ContactDrawable.java
+++ b/src/com/android/mail/bitmap/ContactDrawable.java
@@ -96,7 +96,7 @@
         mMatrix = new Matrix();
 
         if (sTileLetterFontSize == 0) {
-            sTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
+            sTileLetterFontSize = 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);
 
diff --git a/src/com/android/mail/browse/ConversationViewAdapter.java b/src/com/android/mail/browse/ConversationViewAdapter.java
index c16c64d..60abb7c 100644
--- a/src/com/android/mail/browse/ConversationViewAdapter.java
+++ b/src/com/android/mail/browse/ConversationViewAdapter.java
@@ -351,6 +351,9 @@
         @Override
         public void setMessage(ConversationMessage message) {
             mMessage = message;
+            // setMessage signifies an in-place update to the message, so let's clear out recipient
+            // summary text so the view will refresh it on the next render.
+            recipientSummaryText = null;
         }
 
         public CharSequence getTimestampShort() {
diff --git a/src/com/android/mail/browse/MessageHeaderView.java b/src/com/android/mail/browse/MessageHeaderView.java
index 9c5e486..6570cd2 100644
--- a/src/com/android/mail/browse/MessageHeaderView.java
+++ b/src/com/android/mail/browse/MessageHeaderView.java
@@ -23,11 +23,6 @@
 import android.content.res.Resources;
 import android.database.DataSetObserver;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
 import android.support.v4.text.BidiFormatter;
 import android.text.Html;
 import android.text.Spannable;
@@ -66,6 +61,7 @@
 import com.android.mail.text.EmailAddressSpan;
 import com.android.mail.ui.AbstractConversationViewFragment;
 import com.android.mail.ui.ImageCanvas;
+import com.android.mail.utils.BitmapUtil;
 import com.android.mail.utils.LogTag;
 import com.android.mail.utils.LogUtils;
 import com.android.mail.utils.StyleUtils;
@@ -861,7 +857,7 @@
             }
 
             if (info.photo != null) {
-                mPhotoView.setImageBitmap(frameBitmapInCircle(info.photo));
+                mPhotoView.setImageBitmap(BitmapUtil.frameBitmapInCircle(info.photo));
                 photoSet = true;
             }
         } else {
@@ -870,14 +866,14 @@
 
         if (!photoSet) {
             mPhotoView.setImageBitmap(
-                    frameBitmapInCircle(makeLetterTile(mSender.getPersonal(), email)));
+                    BitmapUtil.frameBitmapInCircle(makeLetterTile(mSender.getPersonal(), email)));
         }
     }
 
     private Bitmap makeLetterTile(
             String displayName, String senderAddress) {
         if (mLetterTileProvider == null) {
-            mLetterTileProvider = new LetterTileProvider(getContext());
+            mLetterTileProvider = new LetterTileProvider(getContext().getResources());
         }
 
         final ImageCanvas.Dimensions dimensions = new ImageCanvas.Dimensions(
@@ -885,46 +881,6 @@
         return mLetterTileProvider.getLetterTile(dimensions, displayName, senderAddress);
     }
 
-    /**
-     * Frames the input bitmap in a circle.
-     */
-    private static Bitmap frameBitmapInCircle(Bitmap input) {
-        if (input == null) {
-            return null;
-        }
-
-        // Crop the image if not squared.
-        int inputWidth = input.getWidth();
-        int inputHeight = input.getHeight();
-        int targetX, targetY, targetSize;
-        if (inputWidth >= inputHeight) {
-            targetX = inputWidth / 2 - inputHeight / 2;
-            targetY = 0;
-            targetSize = inputHeight;
-        } else {
-            targetX = 0;
-            targetY = inputHeight / 2 - inputWidth / 2;
-            targetSize = inputWidth;
-        }
-
-        // Create an output bitmap and a canvas to draw on it.
-        Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
-        Canvas canvas = new Canvas(output);
-
-        // Create a black paint to draw the mask.
-        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        paint.setColor(Color.BLACK);
-
-        // Draw a circle.
-        canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint);
-
-        // Replace the black parts of the mask with the input image.
-        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
-        canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint);
-
-        return output;
-    }
-
     @Override
     public boolean onMenuItemClick(MenuItem item) {
         mPopup.dismiss();
diff --git a/src/com/android/mail/compose/ComposeActivity.java b/src/com/android/mail/compose/ComposeActivity.java
index 82116c7..e4ef81f 100644
--- a/src/com/android/mail/compose/ComposeActivity.java
+++ b/src/com/android/mail/compose/ComposeActivity.java
@@ -27,6 +27,7 @@
 import android.app.FragmentTransaction;
 import android.app.LoaderManager;
 import android.content.ClipData;
+import android.content.ClipDescription;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
@@ -131,6 +132,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 public class ComposeActivity extends ActionBarActivity
         implements OnClickListener, ActionBar.OnNavigationListener,
@@ -265,6 +267,9 @@
 
     // A single thread for running tasks in the background.
     private static final Handler SEND_SAVE_TASK_HANDLER;
+    @VisibleForTesting
+    public static final AtomicInteger PENDING_SEND_OR_SAVE_TASKS_NUM = new AtomicInteger(0);
+
     // String representing the uri of the data directory (used for attachment uri checking).
     private static final String DATA_DIRECTORY_ROOT;
     private static final String ALTERNATE_DATA_DIRECTORY_ROOT;
@@ -326,11 +331,6 @@
     protected Bundle mInnerSavedState;
     private ContentValues mExtraValues = null;
 
-    // Array of the outstanding send or save tasks.  Access is synchronized
-    // with the object itself
-    /* package for testing */
-    @VisibleForTesting
-    public final ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
     // FIXME: this variable is never read. related to sRequestMessageIdMap.
     private int mRequestId;
     private String mSignature;
@@ -2442,201 +2442,180 @@
 
     @VisibleForTesting
     public interface SendOrSaveCallback {
-        void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
+        void initializeSendOrSave();
         void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
         Message getMessage();
-        void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
-        void incrementRecipientsTimesContacted(List<String> recipients);
+        void sendOrSaveFinished(SendOrSaveMessage message, boolean success);
     }
 
-    @VisibleForTesting
-    public static class SendOrSaveTask implements Runnable {
-        private final Context mContext;
-        @VisibleForTesting
-        public final SendOrSaveCallback mSendOrSaveCallback;
-        @VisibleForTesting
-        public final SendOrSaveMessage mSendOrSaveMessage;
-        private ReplyFromAccount mExistingDraftAccount;
-
-        public SendOrSaveTask(Context context, SendOrSaveMessage message,
-                SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
-            mContext = context;
-            mSendOrSaveCallback = callback;
-            mSendOrSaveMessage = message;
-            mExistingDraftAccount = draftAccount;
-        }
-
-        @Override
-        public void run() {
-            final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
-
-            final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
-            Message message = mSendOrSaveCallback.getMessage();
-            long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
-            // If a previous draft has been saved, in an account that is different
-            // than what the user wants to send from, remove the old draft, and treat this
-            // as a new message
-            if (mExistingDraftAccount != null
-                    && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
-                if (messageId != UIProvider.INVALID_MESSAGE_ID) {
-                    ContentResolver resolver = mContext.getContentResolver();
-                    ContentValues values = new ContentValues();
-                    values.put(BaseColumns._ID, messageId);
-                    if (mExistingDraftAccount.account.expungeMessageUri != null) {
-                        new ContentProviderTask.UpdateTask()
-                                .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
-                                        values, null, null);
-                    } else {
-                        // TODO(mindyp) delete the conversation.
-                    }
-                    // reset messageId to 0, so a new message will be created
-                    messageId = UIProvider.INVALID_MESSAGE_ID;
-                }
-            }
-
-            final long messageIdToSave = messageId;
-            sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
-
-            if (!sendOrSaveMessage.mSave) {
-                incrementRecipientsTimesContacted(
-                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
-                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
-                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
-            }
-            mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
-        }
-
-        private void incrementRecipientsTimesContacted(
-                final String toAddresses, final String ccAddresses, final String bccAddresses) {
-            final List<String> recipients = Lists.newArrayList();
-            addAddressesToRecipientList(recipients, toAddresses);
-            addAddressesToRecipientList(recipients, ccAddresses);
-            addAddressesToRecipientList(recipients, bccAddresses);
-            mSendOrSaveCallback.incrementRecipientsTimesContacted(recipients);
-        }
-
-        private void addAddressesToRecipientList(
-                final List<String> recipients, final String addressString) {
-            if (recipients == null) {
-                throw new IllegalArgumentException("recipientList cannot be null");
-            }
-            if (TextUtils.isEmpty(addressString)) {
-                return;
-            }
-            final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
-            for (final Rfc822Token token : tokens) {
-                recipients.add(token.getAddress());
-            }
-        }
-
-        /**
-         * Send or Save a message.
-         */
-        private void sendOrSaveMessage(final long messageIdToSave,
-                final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
-            final ContentResolver resolver = mContext.getContentResolver();
-            final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
-
-            final String accountMethod = sendOrSaveMessage.mSave ?
-                    UIProvider.AccountCallMethods.SAVE_MESSAGE :
-                    UIProvider.AccountCallMethods.SEND_MESSAGE;
-
-            try {
-                if (updateExistingMessage) {
-                    sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
-
-                    callAccountSendSaveMethod(resolver,
-                            selectedAccount.account, accountMethod, sendOrSaveMessage);
+    private void runSendOrSaveProviderCalls(SendOrSaveMessage sendOrSaveMessage,
+            SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
+        final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
+        Message message = callback.getMessage();
+        long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
+        // If a previous draft has been saved, in an account that is different
+        // than what the user wants to send from, remove the old draft, and treat this
+        // as a new message
+        if (draftAccount != null
+                && !selectedAccount.account.uri.equals(draftAccount.account.uri)) {
+            if (messageId != UIProvider.INVALID_MESSAGE_ID) {
+                ContentResolver resolver = getContentResolver();
+                ContentValues values = new ContentValues();
+                values.put(BaseColumns._ID, messageId);
+                if (draftAccount.account.expungeMessageUri != null) {
+                    new ContentProviderTask.UpdateTask()
+                            .run(resolver, draftAccount.account.expungeMessageUri,
+                                    values, null, null);
                 } else {
-                    Uri messageUri = null;
-                    final Bundle result = callAccountSendSaveMethod(resolver,
-                            selectedAccount.account, accountMethod, sendOrSaveMessage);
-                    if (result != null) {
-                        // If a non-null value was returned, then the provider handled the call
-                        // method
-                        messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
-                    }
-                    if (sendOrSaveMessage.mSave && messageUri != null) {
-                        final Cursor messageCursor = resolver.query(messageUri,
-                                UIProvider.MESSAGE_PROJECTION, null, null, null);
-                        if (messageCursor != null) {
-                            try {
-                                if (messageCursor.moveToFirst()) {
-                                    // Broadcast notification that a new message has
-                                    // been allocated
-                                    mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
-                                            new Message(messageCursor));
-                                }
-                            } finally {
-                                messageCursor.close();
-                            }
-                        }
-                    }
+                    // TODO(mindyp) delete the conversation.
                 }
-            } finally {
-                // Close any opened file descriptors
-                closeOpenedAttachmentFds(sendOrSaveMessage);
+                // reset messageId to 0, so a new message will be created
+                messageId = UIProvider.INVALID_MESSAGE_ID;
             }
         }
 
-        private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
-            final Bundle openedFds = sendOrSaveMessage.attachmentFds();
-            if (openedFds != null) {
-                final Set<String> keys = openedFds.keySet();
-                for (final String key : keys) {
-                    final ParcelFileDescriptor fd = openedFds.getParcelable(key);
-                    if (fd != null) {
+        final long messageIdToSave = messageId;
+        sendOrSaveMessage(callback, messageIdToSave, sendOrSaveMessage, selectedAccount);
+
+        if (!sendOrSaveMessage.mSave) {
+            incrementRecipientsTimesContacted(
+                    (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO),
+                    (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC),
+                    (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
+        }
+        callback.sendOrSaveFinished(sendOrSaveMessage, true);
+    }
+
+    private void incrementRecipientsTimesContacted(
+            final String toAddresses, final String ccAddresses, final String bccAddresses) {
+        final List<String> recipients = Lists.newArrayList();
+        addAddressesToRecipientList(recipients, toAddresses);
+        addAddressesToRecipientList(recipients, ccAddresses);
+        addAddressesToRecipientList(recipients, bccAddresses);
+        incrementRecipientsTimesContacted(recipients);
+    }
+
+    private void addAddressesToRecipientList(
+            final List<String> recipients, final String addressString) {
+        if (recipients == null) {
+            throw new IllegalArgumentException("recipientList cannot be null");
+        }
+        if (TextUtils.isEmpty(addressString)) {
+            return;
+        }
+        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
+        for (final Rfc822Token token : tokens) {
+            recipients.add(token.getAddress());
+        }
+    }
+
+    /**
+     * Send or Save a message.
+     */
+    private void sendOrSaveMessage(SendOrSaveCallback callback, final long messageIdToSave,
+            final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
+        final ContentResolver resolver = getContentResolver();
+        final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
+
+        final String accountMethod = sendOrSaveMessage.mSave ?
+                UIProvider.AccountCallMethods.SAVE_MESSAGE :
+                UIProvider.AccountCallMethods.SEND_MESSAGE;
+
+        try {
+            if (updateExistingMessage) {
+                sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
+
+                callAccountSendSaveMethod(resolver,
+                        selectedAccount.account, accountMethod, sendOrSaveMessage);
+            } else {
+                Uri messageUri = null;
+                final Bundle result = callAccountSendSaveMethod(resolver,
+                        selectedAccount.account, accountMethod, sendOrSaveMessage);
+                if (result != null) {
+                    // If a non-null value was returned, then the provider handled the call
+                    // method
+                    messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
+                }
+                if (sendOrSaveMessage.mSave && messageUri != null) {
+                    final Cursor messageCursor = resolver.query(messageUri,
+                            UIProvider.MESSAGE_PROJECTION, null, null, null);
+                    if (messageCursor != null) {
                         try {
-                            fd.close();
-                        } catch (IOException e) {
-                            // Do nothing
+                            if (messageCursor.moveToFirst()) {
+                                // Broadcast notification that a new message has
+                                // been allocated
+                                callback.notifyMessageIdAllocated(sendOrSaveMessage,
+                                        new Message(messageCursor));
+                            }
+                        } finally {
+                            messageCursor.close();
                         }
                     }
                 }
             }
+        } finally {
+            // Close any opened file descriptors
+            closeOpenedAttachmentFds(sendOrSaveMessage);
         }
+    }
 
-        /**
-         * Use the {@link ContentResolver#call} method to send or save the message.
-         *
-         * If this was successful, this method will return an non-null Bundle instance
-         */
-        private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
-                final Account account, final String method,
-                final SendOrSaveMessage sendOrSaveMessage) {
-            // Copy all of the values from the content values to the bundle
-            final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
-            final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
-
-            for (Entry<String, Object> entry : valueSet) {
-                final Object entryValue = entry.getValue();
-                final String key = entry.getKey();
-                if (entryValue instanceof String) {
-                    methodExtras.putString(key, (String)entryValue);
-                } else if (entryValue instanceof Boolean) {
-                    methodExtras.putBoolean(key, (Boolean)entryValue);
-                } else if (entryValue instanceof Integer) {
-                    methodExtras.putInt(key, (Integer)entryValue);
-                } else if (entryValue instanceof Long) {
-                    methodExtras.putLong(key, (Long)entryValue);
-                } else {
-                    LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
-                            entryValue.getClass().getName());
+    private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
+        final Bundle openedFds = sendOrSaveMessage.attachmentFds();
+        if (openedFds != null) {
+            final Set<String> keys = openedFds.keySet();
+            for (final String key : keys) {
+                final ParcelFileDescriptor fd = openedFds.getParcelable(key);
+                if (fd != null) {
+                    try {
+                        fd.close();
+                    } catch (IOException e) {
+                        // Do nothing
+                    }
                 }
             }
-
-            // If the SendOrSaveMessage has some opened fds, add them to the bundle
-            final Bundle fdMap = sendOrSaveMessage.attachmentFds();
-            if (fdMap != null) {
-                methodExtras.putParcelable(
-                        UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
-            }
-
-            return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
         }
     }
 
     /**
+     * Use the {@link ContentResolver#call} method to send or save the message.
+     *
+     * If this was successful, this method will return an non-null Bundle instance
+     */
+    private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
+            final Account account, final String method,
+            final SendOrSaveMessage sendOrSaveMessage) {
+        // Copy all of the values from the content values to the bundle
+        final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
+        final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
+
+        for (Entry<String, Object> entry : valueSet) {
+            final Object entryValue = entry.getValue();
+            final String key = entry.getKey();
+            if (entryValue instanceof String) {
+                methodExtras.putString(key, (String)entryValue);
+            } else if (entryValue instanceof Boolean) {
+                methodExtras.putBoolean(key, (Boolean)entryValue);
+            } else if (entryValue instanceof Integer) {
+                methodExtras.putInt(key, (Integer)entryValue);
+            } else if (entryValue instanceof Long) {
+                methodExtras.putLong(key, (Long)entryValue);
+            } else {
+                LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
+                        entryValue.getClass().getName());
+            }
+        }
+
+        // If the SendOrSaveMessage has some opened fds, add them to the bundle
+        final Bundle fdMap = sendOrSaveMessage.attachmentFds();
+        if (fdMap != null) {
+            methodExtras.putParcelable(
+                    UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
+        }
+
+        return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
+    }
+
+    /**
      * Reports recipients that have been contacted in order to improve auto-complete
      * suggestions. Default behavior updates usage statistics in ContactsProvider.
      * @param recipients addresses
@@ -2657,14 +2636,20 @@
         private final Bundle mAttachmentFds;
 
         public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
-                String refMessageId, List<Attachment> attachments, boolean save) {
+                String refMessageId, List<Attachment> attachments, Bundle optionalAttachmentFds,
+                boolean save) {
             mAccount = account;
             mValues = values;
             mRefMessageId = refMessageId;
             mSave = save;
             mRequestId = mValues.hashCode() ^ hashCode();
 
-            mAttachmentFds = initializeAttachmentFds(context, attachments);
+            // If the attachments are already open for us (pre-JB), then don't open them again
+            if (optionalAttachmentFds != null) {
+                mAttachmentFds = optionalAttachmentFds;
+            } else {
+                mAttachmentFds = initializeAttachmentFds(context, attachments);
+            }
         }
 
         int requestId() {
@@ -2674,54 +2659,54 @@
         Bundle attachmentFds() {
             return mAttachmentFds;
         }
+    }
 
-        /**
-         * Opens {@link ParcelFileDescriptor} for each of the attachments.  This method must be
-         * called before the ComposeActivity finishes.
-         * Note: The caller is responsible for closing these file descriptors.
-         */
-        private static Bundle initializeAttachmentFds(final Context context,
-                final List<Attachment> attachments) {
-            if (attachments == null || attachments.size() == 0) {
-                return null;
-            }
-
-            final Bundle result = new Bundle(attachments.size());
-            final ContentResolver resolver = context.getContentResolver();
-
-            for (Attachment attachment : attachments) {
-                if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
-                    continue;
-                }
-
-                ParcelFileDescriptor fileDescriptor;
-                try {
-                    fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
-                } catch (FileNotFoundException e) {
-                    LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
-                    fileDescriptor = null;
-                } catch (SecurityException e) {
-                    // We have encountered a security exception when attempting to open the file
-                    // specified by the content uri.  If the attachment has been cached, this
-                    // isn't a problem, as even through the original permission may have been
-                    // revoked, we have cached the file.  This will happen when saving/sending
-                    // a previously saved draft.
-                    // TODO(markwei): Expose whether the attachment has been cached through the
-                    // attachment object.  This would allow us to limit when the log is made, as
-                    // if the attachment has been cached, this really isn't an error
-                    LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
-                    // Just set the file descriptor to null, as the underlying provider needs
-                    // to handle the file descriptor not being set.
-                    fileDescriptor = null;
-                }
-
-                if (fileDescriptor != null) {
-                    result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
-                }
-            }
-
-            return result;
+    /**
+     * Opens {@link ParcelFileDescriptor} for each of the attachments.  This method must be
+     * called before the ComposeActivity finishes.
+     * Note: The caller is responsible for closing these file descriptors.
+     */
+    private static Bundle initializeAttachmentFds(final Context context,
+            final List<Attachment> attachments) {
+        if (attachments == null || attachments.size() == 0) {
+            return null;
         }
+
+        final Bundle result = new Bundle(attachments.size());
+        final ContentResolver resolver = context.getContentResolver();
+
+        for (Attachment attachment : attachments) {
+            if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
+                continue;
+            }
+
+            ParcelFileDescriptor fileDescriptor;
+            try {
+                fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
+            } catch (FileNotFoundException e) {
+                LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
+                fileDescriptor = null;
+            } catch (SecurityException e) {
+                // We have encountered a security exception when attempting to open the file
+                // specified by the content uri.  If the attachment has been cached, this
+                // isn't a problem, as even through the original permission may have been
+                // revoked, we have cached the file.  This will happen when saving/sending
+                // a previously saved draft.
+                // TODO(markwei): Expose whether the attachment has been cached through the
+                // attachment object.  This would allow us to limit when the log is made, as
+                // if the attachment has been cached, this really isn't an error
+                LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
+                // Just set the file descriptor to null, as the underlying provider needs
+                // to handle the file descriptor not being set.
+                fileDescriptor = null;
+            }
+
+            if (fileDescriptor != null) {
+                result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
+            }
+        }
+
+        return result;
     }
 
     /**
@@ -3110,8 +3095,9 @@
 
     private int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
             Message message, final Message refMessage, final CharSequence quotedText,
-            SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
-            ReplyFromAccount draftAccount, final ContentValues extraValues) {
+            SendOrSaveCallback callback, boolean save, int composeMode,
+            ReplyFromAccount draftAccount, final ContentValues extraValues,
+            Bundle optionalAttachmentFds) {
         final ContentValues values = new ContentValues();
 
         final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
@@ -3185,15 +3171,11 @@
         if (extraValues != null) {
             values.putAll(extraValues);
         }
-        SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
-                values, refMessageId, message.getAttachments(), save);
-        SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
-                draftAccount);
 
-        callback.initializeSendOrSave(sendOrSaveTask);
-        // Do the send/save action on the specified handler to avoid possible
-        // ANRs
-        handler.post(sendOrSaveTask);
+        SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
+                values, refMessageId, message.getAttachments(), optionalAttachmentFds, save);
+        runSendOrSaveProviderCalls(sendOrSaveMessage, callback, draftAccount);
+
         LogUtils.i(LOG_TAG, "[compose] SendOrSaveMessage [%s] posted (isSave: %s) - " +
                         "body length: %d, attachment count: %d",
                 sendOrSaveMessage.requestId(), save, message.bodyText.length(),
@@ -3267,19 +3249,40 @@
             private int mRestoredRequestId;
 
             @Override
-            public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
-                synchronized (mActiveTasks) {
-                    int numTasks = mActiveTasks.size();
-                    if (numTasks == 0) {
+            public void initializeSendOrSave() {
+                final Intent i = new Intent(ComposeActivity.this, EmptyService.class);
+
+                // API 16+ allows for setClipData. For pre-16 we are going to open the fds
+                // on the main thread.
+                if (Utils.isRunningJellybeanOrLater()) {
+                    // Grant the READ permission for the attachments to the service so that
+                    // as long as the service stays alive we won't hit PermissionExceptions.
+                    final ClipDescription desc = new ClipDescription("attachment_uris",
+                            new String[]{ClipDescription.MIMETYPE_TEXT_URILIST});
+                    ClipData clipData = null;
+                    for (Attachment a : mAttachmentsView.getAttachments()) {
+                        if (a != null && !Utils.isEmpty(a.contentUri)) {
+                            final ClipData.Item uriItem = new ClipData.Item(a.contentUri);
+                            if (clipData == null) {
+                                clipData = new ClipData(desc, uriItem);
+                            } else {
+                                clipData.addItem(uriItem);
+                            }
+                        }
+                    }
+                    i.setClipData(clipData);
+                    i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                }
+
+                synchronized (PENDING_SEND_OR_SAVE_TASKS_NUM) {
+                    if (PENDING_SEND_OR_SAVE_TASKS_NUM.getAndAdd(1) == 0) {
                         // Start service so we won't be killed if this app is
                         // put in the background.
-                        startService(new Intent(ComposeActivity.this, EmptyService.class));
+                        startService(i);
                     }
-
-                    mActiveTasks.add(sendOrSaveTask);
                 }
                 if (sTestSendOrSaveCallback != null) {
-                    sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
+                    sTestSendOrSaveCallback.initializeSendOrSave();
                 }
             }
 
@@ -3309,7 +3312,7 @@
             }
 
             @Override
-            public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
+            public void sendOrSaveFinished(SendOrSaveMessage message, boolean success) {
                 // Update the last sent from account.
                 if (mAccount != null) {
                     MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
@@ -3325,37 +3328,37 @@
                             .show();
                 }
 
-                int numTasks;
-                synchronized (mActiveTasks) {
-                    // Remove the task from the list of active tasks
-                    mActiveTasks.remove(task);
-                    numTasks = mActiveTasks.size();
-                }
-
-                if (numTasks == 0) {
-                    // Stop service so we can be killed.
-                    stopService(new Intent(ComposeActivity.this, EmptyService.class));
+                synchronized (PENDING_SEND_OR_SAVE_TASKS_NUM) {
+                    if (PENDING_SEND_OR_SAVE_TASKS_NUM.addAndGet(-1) == 0) {
+                        // Stop service so we can be killed.
+                        stopService(new Intent(ComposeActivity.this, EmptyService.class));
+                    }
                 }
                 if (sTestSendOrSaveCallback != null) {
-                    sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
+                    sTestSendOrSaveCallback.sendOrSaveFinished(message, success);
                 }
             }
-
-            @Override
-            public void incrementRecipientsTimesContacted(final List<String> recipients) {
-                ComposeActivity.this.incrementRecipientsTimesContacted(recipients);
-            }
         };
         setAccount(mReplyFromAccount.account);
 
         final Spanned body = removeComposingSpans(mBodyView.getText());
+        callback.initializeSendOrSave();
+
+        // For pre-JB we need to open the fds on the main thread
+        final Bundle attachmentFds;
+        if (!Utils.isRunningJellybeanOrLater()) {
+            attachmentFds = initializeAttachmentFds(this, mAttachmentsView.getAttachments());
+        } else {
+            attachmentFds = null;
+        }
+
         SEND_SAVE_TASK_HANDLER.post(new Runnable() {
             @Override
             public void run() {
                 final Message msg = createMessage(mReplyFromAccount, mRefMessage, getMode(), body);
                 mRequestId = sendOrSaveInternal(ComposeActivity.this, mReplyFromAccount, msg,
                         mRefMessage, mQuotedTextView.getQuotedTextIfIncluded(), callback,
-                        SEND_SAVE_TASK_HANDLER, save, mComposeMode, mDraftAccount, mExtraValues);
+                        save, mComposeMode, mDraftAccount, mExtraValues, attachmentFds);
             }
         });
 
diff --git a/src/com/android/mail/photomanager/LetterTileProvider.java b/src/com/android/mail/photomanager/LetterTileProvider.java
index ca70e72..3aaf7b1 100644
--- a/src/com/android/mail/photomanager/LetterTileProvider.java
+++ b/src/com/android/mail/photomanager/LetterTileProvider.java
@@ -16,9 +16,7 @@
 
 package com.android.mail.photomanager;
 
-import android.content.Context;
 import android.content.res.Resources;
-import android.content.res.TypedArray;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Canvas;
@@ -59,13 +57,15 @@
     private final char[] mFirstChar = new char[1];
 
     private static final int POSSIBLE_BITMAP_SIZES = 3;
-    private ColorPicker sTileColorPicker;
+    private final ColorPicker mTileColorPicker;
 
-    public LetterTileProvider(Context context) {
-        final Resources res = context.getResources();
-        mTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size);
-        mTileLetterFontSizeSmall = res
-                .getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+    public LetterTileProvider(Resources res) {
+        this(res, new ColorPicker.PaletteColorPicker(res));
+    }
+
+    public LetterTileProvider(Resources res, ColorPicker colorPicker) {
+        mTileLetterFontSize = res.getDimensionPixelSize(R.dimen.tile_letter_font_size_small);
+        mTileLetterFontSizeSmall = res.getDimensionPixelSize(R.dimen.tile_letter_font_size_tiny);
         mTileFontColor = res.getColor(R.color.letter_tile_font_color);
         mSansSerifLight = Typeface.create("sans-serif-light", Typeface.NORMAL);
         mBounds = new Rect();
@@ -78,9 +78,7 @@
         mDefaultBitmap = BitmapFactory.decodeResource(res, R.drawable.ic_generic_man);
         mDefaultBitmapCache = new Bitmap[POSSIBLE_BITMAP_SIZES];
 
-        if (sTileColorPicker == null) {
-            sTileColorPicker = new ColorPicker.PaletteColorPicker(res);
-        }
+        mTileColorPicker = colorPicker;
     }
 
     public Bitmap getLetterTile(final Dimensions dimensions, final String displayName,
@@ -98,13 +96,14 @@
 
         final Canvas c = mCanvas;
         c.setBitmap(bitmap);
-        c.drawColor(sTileColorPicker.pickColor(address));
+        c.drawColor(mTileColorPicker.pickColor(address));
 
         // If its a valid English alphabet letter,
         // draw the letter on top of the color
         if (isEnglishLetterOrDigit(firstChar)) {
             mFirstChar[0] = Character.toUpperCase(firstChar);
-            mPaint.setTextSize(getFontSize(dimensions.scale));
+            mPaint.setTextSize(
+                    dimensions.fontSize > 0 ? dimensions.fontSize : getFontSize(dimensions.scale));
             mPaint.getTextBounds(mFirstChar, 0, 1, mBounds);
             c.drawText(mFirstChar, 0, 1, 0 + dimensions.width / 2,
                     0 + dimensions.height / 2 + (mBounds.bottom - mBounds.top) / 2, mPaint);
diff --git a/src/com/android/mail/ui/ActionBarController.java b/src/com/android/mail/ui/ActionBarController.java
index 34a1083..a9dc371 100644
--- a/src/com/android/mail/ui/ActionBarController.java
+++ b/src/com/android/mail/ui/ActionBarController.java
@@ -247,6 +247,7 @@
     }
 
     public boolean onPrepareOptionsMenu(Menu menu) {
+        menu.setQwertyMode(true);
         // We start out with every option enabled. Based on the current view, we disable actions
         // that are possible.
         LogUtils.d(LOG_TAG, "ActionBarView.onPrepareOptionsMenu().");
diff --git a/src/com/android/mail/ui/ConversationListFragment.java b/src/com/android/mail/ui/ConversationListFragment.java
index edbc6b7..a2ae0c9 100644
--- a/src/com/android/mail/ui/ConversationListFragment.java
+++ b/src/com/android/mail/ui/ConversationListFragment.java
@@ -654,10 +654,12 @@
                     keyCode == KeyEvent.KEYCODE_DPAD_DOWN) &&
                     keyEvent.getAction() == KeyEvent.ACTION_UP) {
                 final int position = list.getSelectedItemPosition();
-                final Object item = getAnimatedAdapter().getItem(position);
-                if (item != null && item instanceof ConversationCursor) {
-                    final Conversation conv = ((ConversationCursor) item).getConversation();
-                    mCallbacks.onConversationFocused(conv);
+                if (position >= 0) {
+                    final Object item = getAnimatedAdapter().getItem(position);
+                    if (item != null && item instanceof ConversationCursor) {
+                        final Conversation conv = ((ConversationCursor) item).getConversation();
+                        mCallbacks.onConversationFocused(conv);
+                    }
                 }
             }
         }
diff --git a/src/com/android/mail/ui/ImageCanvas.java b/src/com/android/mail/ui/ImageCanvas.java
index 9fdf0ae..b19b3e8 100644
--- a/src/com/android/mail/ui/ImageCanvas.java
+++ b/src/com/android/mail/ui/ImageCanvas.java
@@ -31,6 +31,7 @@
         public int width;
         public int height;
         public float scale;
+        public float fontSize;
 
         public static final float SCALE_ONE = 1.0f;
         public static final float SCALE_HALF = 0.5f;
@@ -40,9 +41,14 @@
         }
 
         public Dimensions(int w, int h, float s) {
-            width = w;
-            height = h;
-            scale = s;
+            this(w, h, s, -1f);
+        }
+
+        public Dimensions(int width, int height, float scale, float fontSize) {
+            this.width = width;
+            this.height = height;
+            this.scale = scale;
+            this.fontSize = fontSize;
         }
 
         @Override
diff --git a/src/com/android/mail/utils/BitmapUtil.java b/src/com/android/mail/utils/BitmapUtil.java
index 6f61f56..7cde938 100644
--- a/src/com/android/mail/utils/BitmapUtil.java
+++ b/src/com/android/mail/utils/BitmapUtil.java
@@ -17,7 +17,12 @@
 package com.android.mail.utils;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
 
 /**
  * Provides static functions to decode bitmaps at the optimal size
@@ -172,4 +177,43 @@
         return cropped;
     }
 
+    /**
+     * Frames the input bitmap in a circle.
+     */
+    public static Bitmap frameBitmapInCircle(Bitmap input) {
+        if (input == null) {
+            return null;
+        }
+
+        // Crop the image if not squared.
+        int inputWidth = input.getWidth();
+        int inputHeight = input.getHeight();
+        int targetX, targetY, targetSize;
+        if (inputWidth >= inputHeight) {
+            targetX = inputWidth / 2 - inputHeight / 2;
+            targetY = 0;
+            targetSize = inputHeight;
+        } else {
+            targetX = 0;
+            targetY = inputHeight / 2 - inputWidth / 2;
+            targetSize = inputWidth;
+        }
+
+        // Create an output bitmap and a canvas to draw on it.
+        Bitmap output = Bitmap.createBitmap(targetSize, targetSize, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(output);
+
+        // Create a black paint to draw the mask.
+        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        paint.setColor(Color.BLACK);
+
+        // Draw a circle.
+        canvas.drawCircle(targetSize / 2, targetSize / 2, targetSize / 2, paint);
+
+        // Replace the black parts of the mask with the input image.
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+        canvas.drawBitmap(input, targetX /* left */, targetY /* top */, paint);
+
+        return output;
+    }
 }
diff --git a/src/com/android/mail/utils/NotificationUtils.java b/src/com/android/mail/utils/NotificationUtils.java
index 58bf0ac..0b19005 100644
--- a/src/com/android/mail/utils/NotificationUtils.java
+++ b/src/com/android/mail/utils/NotificationUtils.java
@@ -26,11 +26,6 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffXfermode;
-import android.graphics.Rect;
 import android.net.Uri;
 import android.os.Looper;
 import android.provider.ContactsContract;
@@ -1693,13 +1688,13 @@
                 final Dimensions dimensions = new Dimensions(idealIconWidth, idealIconHeight,
                         Dimensions.SCALE_ONE);
 
-                contactIconInfo.icon = new LetterTileProvider(context).getLetterTile(dimensions,
-                        displayName, senderAddress);
+                contactIconInfo.icon = new LetterTileProvider(context.getResources())
+                        .getLetterTile(dimensions, displayName, senderAddress);
             }
 
             // Only turn the square photo/letter tile into a circle for L and later
             if (Utils.isRunningLOrLater()) {
-                contactIconInfo.icon = cropSquareIconToCircle(contactIconInfo.icon);
+                contactIconInfo.icon = BitmapUtil.frameBitmapInCircle(contactIconInfo.icon);
             }
         }
 
@@ -1787,28 +1782,6 @@
         return contactIconInfo;
     }
 
-    /**
-     * Crop a square bitmap into a circular one. Used for both contact photos and letter tiles.
-     * @param icon Square bitmap to crop
-     * @return Circular bitmap
-     */
-    private static Bitmap cropSquareIconToCircle(Bitmap icon) {
-        final int iconWidth = icon.getWidth();
-        final Bitmap newIcon = Bitmap.createBitmap(iconWidth, iconWidth, Bitmap.Config.ARGB_8888);
-        final Canvas canvas = new Canvas(newIcon);
-        final Paint paint = new Paint();
-        final Rect rect = new Rect(0, 0, icon.getWidth(),
-                icon.getHeight());
-
-        paint.setAntiAlias(true);
-        canvas.drawARGB(0, 0, 0, 0);
-        canvas.drawCircle(iconWidth/2, iconWidth/2, iconWidth/2, paint);
-        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
-        canvas.drawBitmap(icon, rect, rect, paint);
-
-        return newIcon;
-    }
-
     private static String getMessageBodyWithoutElidedText(final Message message) {
         return getMessageBodyWithoutElidedText(message.getBodyAsHtml());
     }