Home | History | Annotate | Download | only in compose
      1 /**
      2  * Copyright (c) 2011, Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *     http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.mail.compose;
     18 
     19 import android.app.ActionBar;
     20 import android.app.ActionBar.OnNavigationListener;
     21 import android.app.Activity;
     22 import android.app.ActivityManager;
     23 import android.app.AlertDialog;
     24 import android.app.Dialog;
     25 import android.app.DialogFragment;
     26 import android.app.Fragment;
     27 import android.app.FragmentTransaction;
     28 import android.app.LoaderManager;
     29 import android.content.ContentResolver;
     30 import android.content.ContentValues;
     31 import android.content.Context;
     32 import android.content.CursorLoader;
     33 import android.content.DialogInterface;
     34 import android.content.Intent;
     35 import android.content.Loader;
     36 import android.content.pm.ActivityInfo;
     37 import android.content.res.Resources;
     38 import android.database.Cursor;
     39 import android.net.Uri;
     40 import android.os.Bundle;
     41 import android.os.Handler;
     42 import android.os.HandlerThread;
     43 import android.os.ParcelFileDescriptor;
     44 import android.os.Parcelable;
     45 import android.provider.BaseColumns;
     46 import android.text.Editable;
     47 import android.text.Html;
     48 import android.text.SpannableString;
     49 import android.text.Spanned;
     50 import android.text.TextUtils;
     51 import android.text.TextWatcher;
     52 import android.text.util.Rfc822Token;
     53 import android.text.util.Rfc822Tokenizer;
     54 import android.view.Gravity;
     55 import android.view.KeyEvent;
     56 import android.view.LayoutInflater;
     57 import android.view.Menu;
     58 import android.view.MenuInflater;
     59 import android.view.MenuItem;
     60 import android.view.View;
     61 import android.view.View.OnClickListener;
     62 import android.view.ViewGroup;
     63 import android.view.inputmethod.BaseInputConnection;
     64 import android.view.inputmethod.EditorInfo;
     65 import android.widget.ArrayAdapter;
     66 import android.widget.Button;
     67 import android.widget.EditText;
     68 import android.widget.TextView;
     69 import android.widget.Toast;
     70 
     71 import com.android.common.Rfc822Validator;
     72 import com.android.common.contacts.DataUsageStatUpdater;
     73 import com.android.ex.chips.RecipientEditTextView;
     74 import com.android.mail.MailIntentService;
     75 import com.android.mail.R;
     76 import com.android.mail.analytics.Analytics;
     77 import com.android.mail.browse.MessageHeaderView;
     78 import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
     79 import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
     80 import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
     81 import com.android.mail.compose.QuotedTextView.RespondInlineListener;
     82 import com.android.mail.providers.Account;
     83 import com.android.mail.providers.Address;
     84 import com.android.mail.providers.Attachment;
     85 import com.android.mail.providers.Folder;
     86 import com.android.mail.providers.MailAppProvider;
     87 import com.android.mail.providers.Message;
     88 import com.android.mail.providers.MessageModification;
     89 import com.android.mail.providers.ReplyFromAccount;
     90 import com.android.mail.providers.Settings;
     91 import com.android.mail.providers.UIProvider;
     92 import com.android.mail.providers.UIProvider.AccountCapabilities;
     93 import com.android.mail.providers.UIProvider.DraftType;
     94 import com.android.mail.ui.AttachmentTile.AttachmentPreview;
     95 import com.android.mail.ui.FeedbackEnabledActivity;
     96 import com.android.mail.ui.MailActivity;
     97 import com.android.mail.ui.WaitFragment;
     98 import com.android.mail.utils.AccountUtils;
     99 import com.android.mail.utils.AttachmentUtils;
    100 import com.android.mail.utils.ContentProviderTask;
    101 import com.android.mail.utils.LogTag;
    102 import com.android.mail.utils.LogUtils;
    103 import com.android.mail.utils.Utils;
    104 import com.google.common.annotations.VisibleForTesting;
    105 import com.google.common.collect.Lists;
    106 import com.google.common.collect.Sets;
    107 
    108 import java.io.FileNotFoundException;
    109 import java.io.IOException;
    110 import java.io.UnsupportedEncodingException;
    111 import java.net.URLDecoder;
    112 import java.util.ArrayList;
    113 import java.util.Arrays;
    114 import java.util.Collection;
    115 import java.util.HashMap;
    116 import java.util.HashSet;
    117 import java.util.List;
    118 import java.util.Map.Entry;
    119 import java.util.Set;
    120 import java.util.concurrent.ConcurrentHashMap;
    121 
    122 public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
    123         RespondInlineListener, TextWatcher,
    124         AttachmentAddedOrDeletedListener, OnAccountChangedListener,
    125         LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
    126         FeedbackEnabledActivity {
    127     // Identifiers for which type of composition this is
    128     public static final int COMPOSE = -1;
    129     public static final int REPLY = 0;
    130     public static final int REPLY_ALL = 1;
    131     public static final int FORWARD = 2;
    132     public static final int EDIT_DRAFT = 3;
    133 
    134     // Integer extra holding one of the above compose action
    135     protected static final String EXTRA_ACTION = "action";
    136 
    137     private static final String EXTRA_SHOW_CC = "showCc";
    138     private static final String EXTRA_SHOW_BCC = "showBcc";
    139     private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
    140     private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
    141 
    142     private static final String UTF8_ENCODING_NAME = "UTF-8";
    143 
    144     private static final String MAIL_TO = "mailto";
    145 
    146     private static final String EXTRA_SUBJECT = "subject";
    147 
    148     private static final String EXTRA_BODY = "body";
    149 
    150     /**
    151      * Expected to be html formatted text.
    152      */
    153     private static final String EXTRA_QUOTED_TEXT = "quotedText";
    154 
    155     protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
    156 
    157     private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
    158 
    159     // Extra that we can get passed from other activities
    160     private static final String EXTRA_TO = "to";
    161     private static final String EXTRA_CC = "cc";
    162     private static final String EXTRA_BCC = "bcc";
    163 
    164     /**
    165      * An optional extra containing a {@link ContentValues} of values to be added to
    166      * {@link SendOrSaveMessage#mValues}.
    167      */
    168     public static final String EXTRA_VALUES = "extra-values";
    169 
    170     // List of all the fields
    171     static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
    172             EXTRA_QUOTED_TEXT };
    173 
    174     private static SendOrSaveCallback sTestSendOrSaveCallback = null;
    175     // Map containing information about requests to create new messages, and the id of the
    176     // messages that were the result of those requests.
    177     //
    178     // This map is used when the activity that initiated the save a of a new message, is killed
    179     // before the save has completed (and when we know the id of the newly created message).  When
    180     // a save is completed, the service that is running in the background, will update the map
    181     //
    182     // When a new ComposeActivity instance is created, it will attempt to use the information in
    183     // the previously instantiated map.  If ComposeActivity.onCreate() is called, with a bundle
    184     // (restoring data from a previous instance), and the map hasn't been created, we will attempt
    185     // to populate the map with data stored in shared preferences.
    186     // FIXME: values in this map are never read.
    187     private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
    188     /**
    189      * Notifies the {@code Activity} that the caller is an Email
    190      * {@code Activity}, so that the back behavior may be modified accordingly.
    191      *
    192      * @see #onAppUpPressed
    193      */
    194     public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
    195 
    196     public static final String EXTRA_ATTACHMENTS = "attachments";
    197 
    198     /** If set, we will clear notifications for this folder. */
    199     public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
    200 
    201     //  If this is a reply/forward then this extra will hold the original message
    202     private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
    203     // If this is a reply/forward then this extra will hold a uri we must query
    204     // to get the original message.
    205     protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
    206     // If this is an action to edit an existing draft message, this extra will hold the
    207     // draft message
    208     private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
    209     private static final String END_TOKEN = ", ";
    210     private static final String LOG_TAG = LogTag.getLogTag();
    211     // Request numbers for activities we start
    212     private static final int RESULT_PICK_ATTACHMENT = 1;
    213     private static final int RESULT_CREATE_ACCOUNT = 2;
    214     // TODO(mindyp) set mime-type for auto send?
    215     public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
    216 
    217     private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
    218     private static final String EXTRA_REQUEST_ID = "requestId";
    219     private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
    220     private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
    221     private static final String EXTRA_MESSAGE = "extraMessage";
    222     private static final int REFERENCE_MESSAGE_LOADER = 0;
    223     private static final int LOADER_ACCOUNT_CURSOR = 1;
    224     private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
    225     private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
    226     private static final String TAG_WAIT = "wait-fragment";
    227     private static final String MIME_TYPE_PHOTO = "image/*";
    228     private static final String MIME_TYPE_VIDEO = "video/*";
    229 
    230     private static final String KEY_INNER_SAVED_STATE = "compose_state";
    231 
    232     /**
    233      * A single thread for running tasks in the background.
    234      */
    235     private Handler mSendSaveTaskHandler = null;
    236     private RecipientEditTextView mTo;
    237     private RecipientEditTextView mCc;
    238     private RecipientEditTextView mBcc;
    239     private Button mCcBccButton;
    240     private CcBccView mCcBccView;
    241     private AttachmentsView mAttachmentsView;
    242     protected Account mAccount;
    243     protected ReplyFromAccount mReplyFromAccount;
    244     private Settings mCachedSettings;
    245     private Rfc822Validator mValidator;
    246     private TextView mSubject;
    247 
    248     private ComposeModeAdapter mComposeModeAdapter;
    249     protected int mComposeMode = -1;
    250     private boolean mForward;
    251     private QuotedTextView mQuotedTextView;
    252     protected EditText mBodyView;
    253     private View mFromStatic;
    254     private TextView mFromStaticText;
    255     private View mFromSpinnerWrapper;
    256     @VisibleForTesting
    257     protected FromAddressSpinner mFromSpinner;
    258     private boolean mAddingAttachment;
    259     private boolean mAttachmentsChanged;
    260     private boolean mTextChanged;
    261     private boolean mReplyFromChanged;
    262     private MenuItem mSave;
    263     private MenuItem mSend;
    264     @VisibleForTesting
    265     protected Message mRefMessage;
    266     private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
    267     private Message mDraft;
    268     private ReplyFromAccount mDraftAccount;
    269     private Object mDraftLock = new Object();
    270     private View mPhotoAttachmentsButton;
    271     private View mVideoAttachmentsButton;
    272 
    273     /**
    274      * Boolean indicating whether ComposeActivity was launched from a Gmail controlled view.
    275      */
    276     private boolean mLaunchedFromEmail = false;
    277     private RecipientTextWatcher mToListener;
    278     private RecipientTextWatcher mCcListener;
    279     private RecipientTextWatcher mBccListener;
    280     private Uri mRefMessageUri;
    281     private boolean mShowQuotedText = false;
    282     protected Bundle mInnerSavedState;
    283     private ContentValues mExtraValues = null;
    284 
    285     // Array of the outstanding send or save tasks.  Access is synchronized
    286     // with the object itself
    287     /* package for testing */
    288     @VisibleForTesting
    289     public ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
    290     // FIXME: this variable is never read. related to sRequestMessageIdMap.
    291     private int mRequestId;
    292     private String mSignature;
    293     private Account[] mAccounts;
    294     private boolean mRespondedInline;
    295     private boolean mPerformedSendOrDiscard = false;
    296 
    297     /**
    298      * Can be called from a non-UI thread.
    299      */
    300     public static void editDraft(Context launcher, Account account, Message message) {
    301         launch(launcher, account, message, EDIT_DRAFT, null, null, null, null,
    302                 null /* extraValues */);
    303     }
    304 
    305     /**
    306      * Can be called from a non-UI thread.
    307      */
    308     public static void compose(Context launcher, Account account) {
    309         launch(launcher, account, null, COMPOSE, null, null, null, null, null /* extraValues */);
    310     }
    311 
    312     /**
    313      * Can be called from a non-UI thread.
    314      */
    315     public static void composeToAddress(Context launcher, Account account, String toAddress) {
    316         launch(launcher, account, null, COMPOSE, toAddress, null, null, null,
    317                 null /* extraValues */);
    318     }
    319 
    320     /**
    321      * Can be called from a non-UI thread.
    322      */
    323     public static void composeWithQuotedText(Context launcher, Account account,
    324             String quotedText, String subject, final ContentValues extraValues) {
    325         launch(launcher, account, null, COMPOSE, null, null, quotedText, subject, extraValues);
    326     }
    327 
    328     /**
    329      * Can be called from a non-UI thread.
    330      */
    331     public static void composeWithExtraValues(Context launcher, Account account,
    332             String subject, final ContentValues extraValues) {
    333         launch(launcher, account, null, COMPOSE, null, null, null, subject, extraValues);
    334     }
    335 
    336     /**
    337      * Can be called from a non-UI thread.
    338      */
    339     public static Intent createReplyIntent(final Context launcher, final Account account,
    340             final Uri messageUri, final boolean isReplyAll) {
    341         return createActionIntent(launcher, account, messageUri, isReplyAll ? REPLY_ALL : REPLY);
    342     }
    343 
    344     /**
    345      * Can be called from a non-UI thread.
    346      */
    347     public static Intent createForwardIntent(final Context launcher, final Account account,
    348             final Uri messageUri) {
    349         return createActionIntent(launcher, account, messageUri, FORWARD);
    350     }
    351 
    352     private static Intent createActionIntent(final Context launcher, final Account account,
    353             final Uri messageUri, final int action) {
    354         final Intent intent = new Intent(launcher, ComposeActivity.class);
    355 
    356         updateActionIntent(account, messageUri, action, intent);
    357 
    358         return intent;
    359     }
    360 
    361     @VisibleForTesting
    362     static Intent updateActionIntent(Account account, Uri messageUri, int action, Intent intent) {
    363         intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
    364         intent.putExtra(EXTRA_ACTION, action);
    365         intent.putExtra(Utils.EXTRA_ACCOUNT, account);
    366         intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI, messageUri);
    367 
    368         return intent;
    369     }
    370 
    371     /**
    372      * Can be called from a non-UI thread.
    373      */
    374     public static void reply(Context launcher, Account account, Message message) {
    375         launch(launcher, account, message, REPLY, null, null, null, null, null /* extraValues */);
    376     }
    377 
    378     /**
    379      * Can be called from a non-UI thread.
    380      */
    381     public static void replyAll(Context launcher, Account account, Message message) {
    382         launch(launcher, account, message, REPLY_ALL, null, null, null, null,
    383                 null /* extraValues */);
    384     }
    385 
    386     /**
    387      * Can be called from a non-UI thread.
    388      */
    389     public static void forward(Context launcher, Account account, Message message) {
    390         launch(launcher, account, message, FORWARD, null, null, null, null, null /* extraValues */);
    391     }
    392 
    393     public static void reportRenderingFeedback(Context launcher, Account account, Message message,
    394             String body) {
    395         launch(launcher, account, message, FORWARD,
    396                 "android-gmail-readability (at) google.com", body, null, null, null /* extraValues */);
    397     }
    398 
    399     private static void launch(Context launcher, Account account, Message message, int action,
    400             String toAddress, String body, String quotedText, String subject,
    401             final ContentValues extraValues) {
    402         Intent intent = new Intent(launcher, ComposeActivity.class);
    403         intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
    404         intent.putExtra(EXTRA_ACTION, action);
    405         intent.putExtra(Utils.EXTRA_ACCOUNT, account);
    406         if (action == EDIT_DRAFT) {
    407             intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
    408         } else {
    409             intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
    410         }
    411         if (toAddress != null) {
    412             intent.putExtra(EXTRA_TO, toAddress);
    413         }
    414         if (body != null) {
    415             intent.putExtra(EXTRA_BODY, body);
    416         }
    417         if (quotedText != null) {
    418             intent.putExtra(EXTRA_QUOTED_TEXT, quotedText);
    419         }
    420         if (subject != null) {
    421             intent.putExtra(EXTRA_SUBJECT, subject);
    422         }
    423         if (extraValues != null) {
    424             LogUtils.d(LOG_TAG, "Launching with extraValues: %s", extraValues.toString());
    425             intent.putExtra(EXTRA_VALUES, extraValues);
    426         }
    427         launcher.startActivity(intent);
    428     }
    429 
    430     @Override
    431     protected void onCreate(Bundle savedInstanceState) {
    432         super.onCreate(savedInstanceState);
    433         setContentView(R.layout.compose);
    434         mInnerSavedState = (savedInstanceState != null) ?
    435                 savedInstanceState.getBundle(KEY_INNER_SAVED_STATE) : null;
    436         checkValidAccounts();
    437     }
    438 
    439     private void finishCreate() {
    440         final Bundle savedState = mInnerSavedState;
    441         findViews();
    442         Intent intent = getIntent();
    443         Message message;
    444         ArrayList<AttachmentPreview> previews;
    445         mShowQuotedText = false;
    446         CharSequence quotedText = null;
    447         int action;
    448         // Check for any of the possibly supplied accounts.;
    449         Account account = null;
    450         if (hadSavedInstanceStateMessage(savedState)) {
    451             action = savedState.getInt(EXTRA_ACTION, COMPOSE);
    452             account = savedState.getParcelable(Utils.EXTRA_ACCOUNT);
    453             message = (Message) savedState.getParcelable(EXTRA_MESSAGE);
    454 
    455             previews = savedState.getParcelableArrayList(EXTRA_ATTACHMENT_PREVIEWS);
    456             mRefMessage = (Message) savedState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
    457             quotedText = savedState.getCharSequence(EXTRA_QUOTED_TEXT);
    458 
    459             mExtraValues = savedState.getParcelable(EXTRA_VALUES);
    460         } else {
    461             account = obtainAccount(intent);
    462             action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
    463             // Initialize the message from the message in the intent
    464             message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
    465             previews = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_PREVIEWS);
    466             mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
    467             mRefMessageUri = (Uri) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE_URI);
    468 
    469             if (Analytics.isLoggable()) {
    470                 if (intent.getBooleanExtra(Utils.EXTRA_FROM_NOTIFICATION, false)) {
    471                     Analytics.getInstance().sendEvent(
    472                             "notification_action", "compose", getActionString(action), 0);
    473                 }
    474             }
    475         }
    476         mAttachmentsView.setAttachmentPreviews(previews);
    477 
    478         setAccount(account);
    479         if (mAccount == null) {
    480             return;
    481         }
    482 
    483         initRecipients();
    484 
    485         // Clear the notification and mark the conversation as seen, if necessary
    486         final Folder notificationFolder =
    487                 intent.getParcelableExtra(EXTRA_NOTIFICATION_FOLDER);
    488         if (notificationFolder != null) {
    489             final Intent clearNotifIntent =
    490                     new Intent(MailIntentService.ACTION_CLEAR_NEW_MAIL_NOTIFICATIONS);
    491             clearNotifIntent.setPackage(getPackageName());
    492             clearNotifIntent.putExtra(Utils.EXTRA_ACCOUNT, account);
    493             clearNotifIntent.putExtra(Utils.EXTRA_FOLDER, notificationFolder);
    494 
    495             startService(clearNotifIntent);
    496         }
    497 
    498         if (intent.getBooleanExtra(EXTRA_FROM_EMAIL_TASK, false)) {
    499             mLaunchedFromEmail = true;
    500         } else if (Intent.ACTION_SEND.equals(intent.getAction())) {
    501             final Uri dataUri = intent.getData();
    502             if (dataUri != null) {
    503                 final String dataScheme = intent.getData().getScheme();
    504                 final String accountScheme = mAccount.composeIntentUri.getScheme();
    505                 mLaunchedFromEmail = TextUtils.equals(dataScheme, accountScheme);
    506             }
    507         }
    508 
    509         if (mRefMessageUri != null) {
    510             mShowQuotedText = true;
    511             mComposeMode = action;
    512             getLoaderManager().initLoader(INIT_DRAFT_USING_REFERENCE_MESSAGE, null, this);
    513             return;
    514         } else if (message != null && action != EDIT_DRAFT) {
    515             initFromDraftMessage(message);
    516             initQuotedTextFromRefMessage(mRefMessage, action);
    517             showCcBcc(savedState);
    518             mShowQuotedText = message.appendRefMessageContent;
    519             // if we should be showing quoted text but mRefMessage is null
    520             // and we have some quotedText, display that
    521             if (mShowQuotedText && mRefMessage == null) {
    522                 if (quotedText != null) {
    523                     initQuotedText(quotedText, false /* shouldQuoteText */);
    524                 } else if (mExtraValues != null) {
    525                     initExtraValues(mExtraValues);
    526                     return;
    527                 }
    528             }
    529         } else if (action == EDIT_DRAFT) {
    530             initFromDraftMessage(message);
    531             boolean showBcc = !TextUtils.isEmpty(message.getBcc());
    532             boolean showCc = showBcc || !TextUtils.isEmpty(message.getCc());
    533             mCcBccView.show(false, showCc, showBcc);
    534             // Update the action to the draft type of the previous draft
    535             switch (message.draftType) {
    536                 case UIProvider.DraftType.REPLY:
    537                     action = REPLY;
    538                     break;
    539                 case UIProvider.DraftType.REPLY_ALL:
    540                     action = REPLY_ALL;
    541                     break;
    542                 case UIProvider.DraftType.FORWARD:
    543                     action = FORWARD;
    544                     break;
    545                 case UIProvider.DraftType.COMPOSE:
    546                 default:
    547                     action = COMPOSE;
    548                     break;
    549             }
    550             LogUtils.d(LOG_TAG, "Previous draft had action type: %d", action);
    551 
    552             mShowQuotedText = message.appendRefMessageContent;
    553             if (message.refMessageUri != null) {
    554                 // If we're editing an existing draft that was in reference to an existing message,
    555                 // still need to load that original message since we might need to refer to the
    556                 // original sender and recipients if user switches "reply <-> reply-all".
    557                 mRefMessageUri = message.refMessageUri;
    558                 mComposeMode = action;
    559                 getLoaderManager().initLoader(REFERENCE_MESSAGE_LOADER, null, this);
    560                 return;
    561             }
    562         } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
    563             if (mRefMessage != null) {
    564                 initFromRefMessage(action);
    565                 mShowQuotedText = true;
    566             }
    567         } else {
    568             if (initFromExtras(intent)) {
    569                 return;
    570             }
    571         }
    572 
    573         mComposeMode = action;
    574         finishSetup(action, intent, savedState);
    575     }
    576 
    577     private void checkValidAccounts() {
    578         final Account[] allAccounts = AccountUtils.getAccounts(this);
    579         if (allAccounts == null || allAccounts.length == 0) {
    580             final Intent noAccountIntent = MailAppProvider.getNoAccountIntent(this);
    581             if (noAccountIntent != null) {
    582                 mAccounts = null;
    583                 startActivityForResult(noAccountIntent, RESULT_CREATE_ACCOUNT);
    584             }
    585         } else {
    586             // If none of the accounts are syncing, setup a watcher.
    587             boolean anySyncing = false;
    588             for (Account a : allAccounts) {
    589                 if (a.isAccountReady()) {
    590                     anySyncing = true;
    591                     break;
    592                 }
    593             }
    594             if (!anySyncing) {
    595                 // There are accounts, but none are sync'd, which is just like having no accounts.
    596                 mAccounts = null;
    597                 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
    598                 return;
    599             }
    600             mAccounts = AccountUtils.getSyncingAccounts(this);
    601             finishCreate();
    602         }
    603     }
    604 
    605     private Account obtainAccount(Intent intent) {
    606         Account account = null;
    607         Object accountExtra = null;
    608         if (intent != null && intent.getExtras() != null) {
    609             accountExtra = intent.getExtras().get(Utils.EXTRA_ACCOUNT);
    610             if (accountExtra instanceof Account) {
    611                 return (Account) accountExtra;
    612             } else if (accountExtra instanceof String) {
    613                 // This is the Account attached to the widget compose intent.
    614                 account = Account.newinstance((String)accountExtra);
    615                 if (account != null) {
    616                     return account;
    617                 }
    618             }
    619             accountExtra = intent.hasExtra(Utils.EXTRA_ACCOUNT) ?
    620                     intent.getStringExtra(Utils.EXTRA_ACCOUNT) :
    621                         intent.getStringExtra(EXTRA_SELECTED_ACCOUNT);
    622         }
    623         if (account == null) {
    624             MailAppProvider provider = MailAppProvider.getInstance();
    625             String lastAccountUri = provider.getLastSentFromAccount();
    626             if (TextUtils.isEmpty(lastAccountUri)) {
    627                 lastAccountUri = provider.getLastViewedAccount();
    628             }
    629             if (!TextUtils.isEmpty(lastAccountUri)) {
    630                 accountExtra = Uri.parse(lastAccountUri);
    631             }
    632         }
    633         if (mAccounts != null && mAccounts.length > 0) {
    634             if (accountExtra instanceof String && !TextUtils.isEmpty((String) accountExtra)) {
    635                 // For backwards compatibility, we need to check account
    636                 // names.
    637                 for (Account a : mAccounts) {
    638                     if (a.getEmailAddress().equals(accountExtra)) {
    639                         account = a;
    640                     }
    641                 }
    642             } else if (accountExtra instanceof Uri) {
    643                 // The uri of the last viewed account is what is stored in
    644                 // the current code base.
    645                 for (Account a : mAccounts) {
    646                     if (a.uri.equals(accountExtra)) {
    647                         account = a;
    648                     }
    649                 }
    650             }
    651             if (account == null) {
    652                 account = mAccounts[0];
    653             }
    654         }
    655         return account;
    656     }
    657 
    658     protected void finishSetup(int action, Intent intent, Bundle savedInstanceState) {
    659         setFocus(action);
    660         // Don't bother with the intent if we have procured a message from the
    661         // intent already.
    662         if (!hadSavedInstanceStateMessage(savedInstanceState)) {
    663             initAttachmentsFromIntent(intent);
    664         }
    665         initActionBar();
    666         initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
    667                 action);
    668 
    669         // If this is a draft message, the draft account is whatever account was
    670         // used to open the draft message in Compose.
    671         if (mDraft != null) {
    672             mDraftAccount = mReplyFromAccount;
    673         }
    674 
    675         initChangeListeners();
    676         updateHideOrShowCcBcc();
    677         updateHideOrShowQuotedText(mShowQuotedText);
    678 
    679         mRespondedInline = mInnerSavedState != null ?
    680                 mInnerSavedState.getBoolean(EXTRA_RESPONDED_INLINE) : false;
    681         if (mRespondedInline) {
    682             mQuotedTextView.setVisibility(View.GONE);
    683         }
    684     }
    685 
    686     private static boolean hadSavedInstanceStateMessage(final Bundle savedInstanceState) {
    687         return savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE);
    688     }
    689 
    690     private void updateHideOrShowQuotedText(boolean showQuotedText) {
    691         mQuotedTextView.updateCheckedState(showQuotedText);
    692         mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
    693     }
    694 
    695     private void setFocus(int action) {
    696         if (action == EDIT_DRAFT) {
    697             int type = mDraft.draftType;
    698             switch (type) {
    699                 case UIProvider.DraftType.COMPOSE:
    700                 case UIProvider.DraftType.FORWARD:
    701                     action = COMPOSE;
    702                     break;
    703                 case UIProvider.DraftType.REPLY:
    704                 case UIProvider.DraftType.REPLY_ALL:
    705                 default:
    706                     action = REPLY;
    707                     break;
    708             }
    709         }
    710         switch (action) {
    711             case FORWARD:
    712             case COMPOSE:
    713                 if (TextUtils.isEmpty(mTo.getText())) {
    714                     mTo.requestFocus();
    715                     break;
    716                 }
    717                 //$FALL-THROUGH$
    718             case REPLY:
    719             case REPLY_ALL:
    720             default:
    721                 focusBody();
    722                 break;
    723         }
    724     }
    725 
    726     /**
    727      * Focus the body of the message.
    728      */
    729     public void focusBody() {
    730         mBodyView.requestFocus();
    731         int length = mBodyView.getText().length();
    732 
    733         int signatureStartPos = getSignatureStartPosition(
    734                 mSignature, mBodyView.getText().toString());
    735         if (signatureStartPos > -1) {
    736             // In case the user deleted the newlines...
    737             mBodyView.setSelection(signatureStartPos);
    738         } else if (length >= 0) {
    739             // Move cursor to the end.
    740             mBodyView.setSelection(length);
    741         }
    742     }
    743 
    744     @Override
    745     protected void onStart() {
    746         super.onStart();
    747 
    748         Analytics.getInstance().activityStart(this);
    749     }
    750 
    751     @Override
    752     protected void onStop() {
    753         super.onStop();
    754 
    755         Analytics.getInstance().activityStop(this);
    756     }
    757 
    758     @Override
    759     protected void onResume() {
    760         super.onResume();
    761         // Update the from spinner as other accounts
    762         // may now be available.
    763         if (mFromSpinner != null && mAccount != null) {
    764             mFromSpinner.initialize(mComposeMode, mAccount, mAccounts, mRefMessage);
    765         }
    766     }
    767 
    768     @Override
    769     protected void onPause() {
    770         super.onPause();
    771 
    772         // When the user exits the compose view, see if this draft needs saving.
    773         // Don't save unnecessary drafts if we are only changing the orientation.
    774         if (!isChangingConfigurations()) {
    775             saveIfNeeded();
    776 
    777             if (isFinishing() && !mPerformedSendOrDiscard && !isBlank()) {
    778                 // log saving upon backing out of activity. (we avoid logging every sendOrSave()
    779                 // because that method can be invoked many times in a single compose session.)
    780                 logSendOrSave(true /* save */);
    781             }
    782         }
    783     }
    784 
    785     @Override
    786     protected final void onActivityResult(int request, int result, Intent data) {
    787         if (request == RESULT_PICK_ATTACHMENT && result == RESULT_OK) {
    788             addAttachmentAndUpdateView(data);
    789             mAddingAttachment = false;
    790         } else if (request == RESULT_CREATE_ACCOUNT) {
    791             // We were waiting for the user to create an account
    792             if (result != RESULT_OK) {
    793                 finish();
    794             } else {
    795                 // Watch for accounts to show up!
    796                 // restart the loader to get the updated list of accounts
    797                 getLoaderManager().initLoader(LOADER_ACCOUNT_CURSOR, null, this);
    798                 showWaitFragment(null);
    799             }
    800         }
    801     }
    802 
    803     @Override
    804     protected final void onRestoreInstanceState(Bundle savedInstanceState) {
    805         final boolean hasAccounts = mAccounts != null && mAccounts.length > 0;
    806         if (hasAccounts) {
    807             clearChangeListeners();
    808         }
    809         super.onRestoreInstanceState(savedInstanceState);
    810         if (mInnerSavedState != null) {
    811             if (mInnerSavedState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
    812                 int selectionStart = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_START);
    813                 int selectionEnd = mInnerSavedState.getInt(EXTRA_FOCUS_SELECTION_END);
    814                 // There should be a focus and it should be an EditText since we
    815                 // only save these extras if these conditions are true.
    816                 EditText focusEditText = (EditText) getCurrentFocus();
    817                 final int length = focusEditText.getText().length();
    818                 if (selectionStart < length && selectionEnd < length) {
    819                     focusEditText.setSelection(selectionStart, selectionEnd);
    820                 }
    821             }
    822         }
    823         if (hasAccounts) {
    824             initChangeListeners();
    825         }
    826     }
    827 
    828     @Override
    829     protected final void onSaveInstanceState(Bundle state) {
    830         super.onSaveInstanceState(state);
    831         final Bundle inner = new Bundle();
    832         saveState(inner);
    833         state.putBundle(KEY_INNER_SAVED_STATE, inner);
    834     }
    835 
    836     private void saveState(Bundle state) {
    837         // We have no accounts so there is nothing to compose, and therefore, nothing to save.
    838         if (mAccounts == null || mAccounts.length == 0) {
    839             return;
    840         }
    841         // The framework is happy to save and restore the selection but only if it also saves and
    842         // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
    843         // this manually.
    844         View focus = getCurrentFocus();
    845         if (focus != null && focus instanceof EditText) {
    846             EditText focusEditText = (EditText) focus;
    847             state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
    848             state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
    849         }
    850 
    851         final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
    852         final int selectedPos = mFromSpinner.getSelectedItemPosition();
    853         final ReplyFromAccount selectedReplyFromAccount = (replyFromAccounts != null
    854                 && replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
    855                         replyFromAccounts.get(selectedPos) : null;
    856         if (selectedReplyFromAccount != null) {
    857             state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
    858                     .toString());
    859             state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
    860         } else {
    861             state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
    862         }
    863 
    864         if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
    865             // We don't have a draft id, and we have a request id,
    866             // save the request id.
    867             state.putInt(EXTRA_REQUEST_ID, mRequestId);
    868         }
    869 
    870         // We want to restore the current mode after a pause
    871         // or rotation.
    872         int mode = getMode();
    873         state.putInt(EXTRA_ACTION, mode);
    874 
    875         final Message message = createMessage(selectedReplyFromAccount, mode);
    876         if (mDraft != null) {
    877             message.id = mDraft.id;
    878             message.serverId = mDraft.serverId;
    879             message.uri = mDraft.uri;
    880         }
    881         state.putParcelable(EXTRA_MESSAGE, message);
    882 
    883         if (mRefMessage != null) {
    884             state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
    885         } else if (message.appendRefMessageContent) {
    886             // If we have no ref message but should be appending
    887             // ref message content, we have orphaned quoted text. Save it.
    888             state.putCharSequence(EXTRA_QUOTED_TEXT, mQuotedTextView.getQuotedTextIfIncluded());
    889         }
    890         state.putBoolean(EXTRA_SHOW_CC, mCcBccView.isCcVisible());
    891         state.putBoolean(EXTRA_SHOW_BCC, mCcBccView.isBccVisible());
    892         state.putBoolean(EXTRA_RESPONDED_INLINE, mRespondedInline);
    893         state.putBoolean(EXTRA_SAVE_ENABLED, mSave != null && mSave.isEnabled());
    894         state.putParcelableArrayList(
    895                 EXTRA_ATTACHMENT_PREVIEWS, mAttachmentsView.getAttachmentPreviews());
    896 
    897         state.putParcelable(EXTRA_VALUES, mExtraValues);
    898     }
    899 
    900     private int getMode() {
    901         int mode = ComposeActivity.COMPOSE;
    902         ActionBar actionBar = getActionBar();
    903         if (actionBar != null
    904                 && actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
    905             mode = actionBar.getSelectedNavigationIndex();
    906         }
    907         return mode;
    908     }
    909 
    910     private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
    911         Message message = new Message();
    912         message.id = UIProvider.INVALID_MESSAGE_ID;
    913         message.serverId = null;
    914         message.uri = null;
    915         message.conversationUri = null;
    916         message.subject = mSubject.getText().toString();
    917         message.snippet = null;
    918         message.setTo(formatSenders(mTo.getText().toString()));
    919         message.setCc(formatSenders(mCc.getText().toString()));
    920         message.setBcc(formatSenders(mBcc.getText().toString()));
    921         message.setReplyTo(null);
    922         message.dateReceivedMs = 0;
    923         final String htmlBody = Html.toHtml(removeComposingSpans(mBodyView.getText()));
    924         final StringBuilder fullBody = new StringBuilder(htmlBody);
    925         message.bodyHtml = fullBody.toString();
    926         message.bodyText = mBodyView.getText().toString();
    927         message.embedsExternalResources = false;
    928         message.refMessageUri = mRefMessage != null ? mRefMessage.uri : null;
    929         message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
    930         ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
    931         message.hasAttachments = attachments != null && attachments.size() > 0;
    932         message.attachmentListUri = null;
    933         message.messageFlags = 0;
    934         message.alwaysShowImages = false;
    935         message.attachmentsJson = Attachment.toJSONArray(attachments);
    936         CharSequence quotedText = mQuotedTextView.getQuotedText();
    937         message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
    938                 .getQuotedTextOffset(quotedText.toString()) : -1;
    939         message.accountUri = null;
    940         message.setFrom(selectedReplyFromAccount != null ? selectedReplyFromAccount.address
    941                 : mAccount != null ? mAccount.getEmailAddress() : null);
    942         message.draftType = getDraftType(mode);
    943         return message;
    944     }
    945 
    946     private static String formatSenders(final String string) {
    947         if (!TextUtils.isEmpty(string) && string.charAt(string.length() - 1) == ',') {
    948             return string.substring(0, string.length() - 1);
    949         }
    950         return string;
    951     }
    952 
    953     @VisibleForTesting
    954     void setAccount(Account account) {
    955         if (account == null) {
    956             return;
    957         }
    958         if (!account.equals(mAccount)) {
    959             mAccount = account;
    960             mCachedSettings = mAccount.settings;
    961             appendSignature();
    962         }
    963         if (mAccount != null) {
    964             MailActivity.setNfcMessage(mAccount.getEmailAddress());
    965         }
    966     }
    967 
    968     private void initFromSpinner(Bundle bundle, int action) {
    969         if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
    970             action = COMPOSE;
    971         }
    972         mFromSpinner.initialize(action, mAccount, mAccounts, mRefMessage);
    973 
    974         if (bundle != null) {
    975             if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
    976                 mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
    977                         bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
    978             } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
    979                 final String accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
    980                 mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
    981             }
    982         }
    983         if (mReplyFromAccount == null) {
    984             if (mDraft != null) {
    985                 mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
    986             } else if (mRefMessage != null) {
    987                 mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
    988             }
    989         }
    990         if (mReplyFromAccount == null) {
    991             mReplyFromAccount = getDefaultReplyFromAccount(mAccount);
    992         }
    993 
    994         mFromSpinner.setCurrentAccount(mReplyFromAccount);
    995 
    996         if (mFromSpinner.getCount() > 1) {
    997             // If there is only 1 account, just show that account.
    998             // Otherwise, give the user the ability to choose which account to
    999             // send mail from / save drafts to.
   1000             mFromStatic.setVisibility(View.GONE);
   1001             mFromStaticText.setText(mReplyFromAccount.name);
   1002             mFromSpinnerWrapper.setVisibility(View.VISIBLE);
   1003         } else {
   1004             mFromStatic.setVisibility(View.VISIBLE);
   1005             mFromStaticText.setText(mReplyFromAccount.name);
   1006             mFromSpinnerWrapper.setVisibility(View.GONE);
   1007         }
   1008     }
   1009 
   1010     private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
   1011         if (refMessage.accountUri != null) {
   1012             // This must be from combined inbox.
   1013             List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
   1014             for (ReplyFromAccount from : replyFromAccounts) {
   1015                 if (from.account.uri.equals(refMessage.accountUri)) {
   1016                     return from;
   1017                 }
   1018             }
   1019             return null;
   1020         } else {
   1021             return getReplyFromAccount(account, refMessage);
   1022         }
   1023     }
   1024 
   1025     /**
   1026      * Given an account and the message we're replying to,
   1027      * return who the message should be sent from.
   1028      * @param account Account in which the message arrived.
   1029      * @param refMessage Message to analyze for account selection
   1030      * @return the address from which to reply.
   1031      */
   1032     public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
   1033         // First see if we are supposed to use the default address or
   1034         // the address it was sentTo.
   1035         if (mCachedSettings.forceReplyFromDefault) {
   1036             return getDefaultReplyFromAccount(account);
   1037         } else {
   1038             // If we aren't explicitly told which account to look for, look at
   1039             // all the message recipients and find one that matches
   1040             // a custom from or account.
   1041             List<String> allRecipients = new ArrayList<String>();
   1042             allRecipients.addAll(Arrays.asList(refMessage.getToAddressesUnescaped()));
   1043             allRecipients.addAll(Arrays.asList(refMessage.getCcAddressesUnescaped()));
   1044             return getMatchingRecipient(account, allRecipients);
   1045         }
   1046     }
   1047 
   1048     /**
   1049      * Compare all the recipients of an email to the current account and all
   1050      * custom addresses associated with that account. Return the match if there
   1051      * is one, or the default account if there isn't.
   1052      */
   1053     protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
   1054         // Tokenize the list and place in a hashmap.
   1055         ReplyFromAccount matchingReplyFrom = null;
   1056         Rfc822Token[] tokens;
   1057         HashSet<String> recipientsMap = new HashSet<String>();
   1058         for (String address : sentTo) {
   1059             tokens = Rfc822Tokenizer.tokenize(address);
   1060             for (int i = 0; i < tokens.length; i++) {
   1061                 recipientsMap.add(tokens[i].getAddress());
   1062             }
   1063         }
   1064 
   1065         int matchingAddressCount = 0;
   1066         List<ReplyFromAccount> customFroms;
   1067         customFroms = account.getReplyFroms();
   1068         if (customFroms != null) {
   1069             for (ReplyFromAccount entry : customFroms) {
   1070                 if (recipientsMap.contains(entry.address)) {
   1071                     matchingReplyFrom = entry;
   1072                     matchingAddressCount++;
   1073                 }
   1074             }
   1075         }
   1076         if (matchingAddressCount > 1) {
   1077             matchingReplyFrom = getDefaultReplyFromAccount(account);
   1078         }
   1079         return matchingReplyFrom;
   1080     }
   1081 
   1082     private static ReplyFromAccount getDefaultReplyFromAccount(final Account account) {
   1083         for (final ReplyFromAccount from : account.getReplyFroms()) {
   1084             if (from.isDefault) {
   1085                 return from;
   1086             }
   1087         }
   1088         return new ReplyFromAccount(account, account.uri, account.getEmailAddress(), account.name,
   1089                 account.getEmailAddress(), true, false);
   1090     }
   1091 
   1092     private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
   1093         String sender = msg.getFrom();
   1094         ReplyFromAccount replyFromAccount = null;
   1095         List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
   1096         if (TextUtils.equals(account.getEmailAddress(), sender)) {
   1097             replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri,
   1098                     mAccount.getEmailAddress(), mAccount.name, mAccount.getEmailAddress(),
   1099                     true, false);
   1100         } else {
   1101             for (ReplyFromAccount fromAccount : replyFromAccounts) {
   1102                 if (TextUtils.equals(fromAccount.address, sender)) {
   1103                     replyFromAccount = fromAccount;
   1104                     break;
   1105                 }
   1106             }
   1107         }
   1108         return replyFromAccount;
   1109     }
   1110 
   1111     private void findViews() {
   1112         findViewById(R.id.compose).setVisibility(View.VISIBLE);
   1113         mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
   1114         if (mCcBccButton != null) {
   1115             mCcBccButton.setOnClickListener(this);
   1116         }
   1117         mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
   1118         mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
   1119         mPhotoAttachmentsButton = findViewById(R.id.add_photo_attachment);
   1120         if (mPhotoAttachmentsButton != null) {
   1121             mPhotoAttachmentsButton.setOnClickListener(this);
   1122         }
   1123         mVideoAttachmentsButton = findViewById(R.id.add_video_attachment);
   1124         if (mVideoAttachmentsButton != null) {
   1125             mVideoAttachmentsButton.setOnClickListener(this);
   1126         }
   1127         mTo = (RecipientEditTextView) findViewById(R.id.to);
   1128         mTo.setTokenizer(new Rfc822Tokenizer());
   1129         mCc = (RecipientEditTextView) findViewById(R.id.cc);
   1130         mCc.setTokenizer(new Rfc822Tokenizer());
   1131         mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
   1132         mBcc.setTokenizer(new Rfc822Tokenizer());
   1133         // TODO: add special chips text change watchers before adding
   1134         // this as a text changed watcher to the to, cc, bcc fields.
   1135         mSubject = (TextView) findViewById(R.id.subject);
   1136         mSubject.setOnEditorActionListener(this);
   1137         mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
   1138         mQuotedTextView.setRespondInlineListener(this);
   1139         mBodyView = (EditText) findViewById(R.id.body);
   1140         mFromStatic = findViewById(R.id.static_from_content);
   1141         mFromStaticText = (TextView) findViewById(R.id.from_account_name);
   1142         mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
   1143         mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
   1144     }
   1145 
   1146     @Override
   1147     public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
   1148         if (action == EditorInfo.IME_ACTION_DONE) {
   1149             focusBody();
   1150             return true;
   1151         }
   1152         return false;
   1153     }
   1154 
   1155     protected TextView getBody() {
   1156         return mBodyView;
   1157     }
   1158 
   1159     @VisibleForTesting
   1160     public Account getFromAccount() {
   1161         return mReplyFromAccount != null && mReplyFromAccount.account != null ?
   1162                 mReplyFromAccount.account : mAccount;
   1163     }
   1164 
   1165     private void clearChangeListeners() {
   1166         mSubject.removeTextChangedListener(this);
   1167         mBodyView.removeTextChangedListener(this);
   1168         mTo.removeTextChangedListener(mToListener);
   1169         mCc.removeTextChangedListener(mCcListener);
   1170         mBcc.removeTextChangedListener(mBccListener);
   1171         mFromSpinner.setOnAccountChangedListener(null);
   1172         mAttachmentsView.setAttachmentChangesListener(null);
   1173     }
   1174 
   1175     // Now that the message has been initialized from any existing draft or
   1176     // ref message data, set up listeners for any changes that occur to the
   1177     // message.
   1178     private void initChangeListeners() {
   1179         // Make sure we only add text changed listeners once!
   1180         clearChangeListeners();
   1181         mSubject.addTextChangedListener(this);
   1182         mBodyView.addTextChangedListener(this);
   1183         if (mToListener == null) {
   1184             mToListener = new RecipientTextWatcher(mTo, this);
   1185         }
   1186         mTo.addTextChangedListener(mToListener);
   1187         if (mCcListener == null) {
   1188             mCcListener = new RecipientTextWatcher(mCc, this);
   1189         }
   1190         mCc.addTextChangedListener(mCcListener);
   1191         if (mBccListener == null) {
   1192             mBccListener = new RecipientTextWatcher(mBcc, this);
   1193         }
   1194         mBcc.addTextChangedListener(mBccListener);
   1195         mFromSpinner.setOnAccountChangedListener(this);
   1196         mAttachmentsView.setAttachmentChangesListener(this);
   1197     }
   1198 
   1199     private void initActionBar() {
   1200         LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
   1201         ActionBar actionBar = getActionBar();
   1202         if (actionBar == null) {
   1203             return;
   1204         }
   1205         if (mComposeMode == ComposeActivity.COMPOSE) {
   1206             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
   1207             actionBar.setTitle(R.string.compose);
   1208         } else {
   1209             actionBar.setTitle(null);
   1210             if (mComposeModeAdapter == null) {
   1211                 mComposeModeAdapter = new ComposeModeAdapter(this);
   1212             }
   1213             actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
   1214             actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
   1215             switch (mComposeMode) {
   1216                 case ComposeActivity.REPLY:
   1217                     actionBar.setSelectedNavigationItem(0);
   1218                     break;
   1219                 case ComposeActivity.REPLY_ALL:
   1220                     actionBar.setSelectedNavigationItem(1);
   1221                     break;
   1222                 case ComposeActivity.FORWARD:
   1223                     actionBar.setSelectedNavigationItem(2);
   1224                     break;
   1225             }
   1226         }
   1227         actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
   1228                 ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
   1229         actionBar.setHomeButtonEnabled(true);
   1230     }
   1231 
   1232     private void initFromRefMessage(int action) {
   1233         setFieldsFromRefMessage(action);
   1234 
   1235         // Check if To: address and email body needs to be prefilled based on extras.
   1236         // This is used for reporting rendering feedback.
   1237         if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
   1238             Intent intent = getIntent();
   1239             if (intent.getExtras() != null) {
   1240                 String toAddresses = intent.getStringExtra(EXTRA_TO);
   1241                 if (toAddresses != null) {
   1242                     addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
   1243                 }
   1244                 String body = intent.getStringExtra(EXTRA_BODY);
   1245                 if (body != null) {
   1246                     setBody(body, false /* withSignature */);
   1247                 }
   1248             }
   1249         }
   1250 
   1251         if (mRefMessage != null) {
   1252             // CC field only gets populated when doing REPLY_ALL.
   1253             // BCC never gets auto-populated, unless the user is editing
   1254             // a draft with one.
   1255             if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
   1256                 mCcBccView.show(false, true, false);
   1257             }
   1258         }
   1259         updateHideOrShowCcBcc();
   1260     }
   1261 
   1262     private void setFieldsFromRefMessage(int action) {
   1263         setSubject(mRefMessage, action);
   1264         // Setup recipients
   1265         if (action == FORWARD) {
   1266             mForward = true;
   1267         }
   1268         initRecipientsFromRefMessage(mRefMessage, action);
   1269         initQuotedTextFromRefMessage(mRefMessage, action);
   1270         if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
   1271             initAttachments(mRefMessage);
   1272         }
   1273     }
   1274 
   1275     private void initFromDraftMessage(Message message) {
   1276         LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
   1277 
   1278         mDraft = message;
   1279         mDraftId = message.id;
   1280         mSubject.setText(message.subject);
   1281         mForward = message.draftType == UIProvider.DraftType.FORWARD;
   1282         final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
   1283         addToAddresses(toAddresses);
   1284         addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
   1285         addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
   1286         if (message.hasAttachments) {
   1287             List<Attachment> attachments = message.getAttachments();
   1288             for (Attachment a : attachments) {
   1289                 addAttachmentAndUpdateView(a);
   1290             }
   1291         }
   1292         int quotedTextIndex = message.appendRefMessageContent ?
   1293                 message.quotedTextOffset : -1;
   1294         // Set the body
   1295         CharSequence quotedText = null;
   1296         if (!TextUtils.isEmpty(message.bodyHtml)) {
   1297             CharSequence htmlText = "";
   1298             if (quotedTextIndex > -1) {
   1299                 // Find the offset in the htmltext of the actual quoted text and strip it out.
   1300                 quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
   1301                 if (quotedTextIndex > -1) {
   1302                     htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
   1303                             quotedTextIndex));
   1304                     quotedText = message.bodyHtml.subSequence(quotedTextIndex,
   1305                             message.bodyHtml.length());
   1306                 }
   1307             } else {
   1308                 htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
   1309             }
   1310             mBodyView.setText(htmlText);
   1311         } else {
   1312             final String body = message.bodyText;
   1313             final CharSequence bodyText = !TextUtils.isEmpty(body) ?
   1314                     (quotedTextIndex > -1 ?
   1315                             message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
   1316                             : "";
   1317             if (quotedTextIndex > -1) {
   1318                 quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
   1319                         : null;
   1320             }
   1321             mBodyView.setText(bodyText);
   1322         }
   1323         if (quotedTextIndex > -1 && quotedText != null) {
   1324             mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
   1325         }
   1326     }
   1327 
   1328     /**
   1329      * Fill all the widgets with the content found in the Intent Extra, if any.
   1330      * Also apply the same style to all widgets. Note: if initFromExtras is
   1331      * called as a result of switching between reply, reply all, and forward per
   1332      * the latest revision of Gmail, and the user has already made changes to
   1333      * attachments on a previous incarnation of the message (as a reply, reply
   1334      * all, or forward), the original attachments from the message will not be
   1335      * re-instantiated. The user's changes will be respected. This follows the
   1336      * web gmail interaction.
   1337      * @return {@code true} if the activity should not call {@link #finishSetup}.
   1338      */
   1339     public boolean initFromExtras(Intent intent) {
   1340         // If we were invoked with a SENDTO intent, the value
   1341         // should take precedence
   1342         final Uri dataUri = intent.getData();
   1343         if (dataUri != null) {
   1344             if (MAIL_TO.equals(dataUri.getScheme())) {
   1345                 initFromMailTo(dataUri.toString());
   1346             } else {
   1347                 if (!mAccount.composeIntentUri.equals(dataUri)) {
   1348                     String toText = dataUri.getSchemeSpecificPart();
   1349                     if (toText != null) {
   1350                         mTo.setText("");
   1351                         addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
   1352                     }
   1353                 }
   1354             }
   1355         }
   1356 
   1357         String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
   1358         if (extraStrings != null) {
   1359             addToAddresses(Arrays.asList(extraStrings));
   1360         }
   1361         extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
   1362         if (extraStrings != null) {
   1363             addCcAddresses(Arrays.asList(extraStrings), null);
   1364         }
   1365         extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
   1366         if (extraStrings != null) {
   1367             addBccAddresses(Arrays.asList(extraStrings));
   1368         }
   1369 
   1370         String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
   1371         if (extraString != null) {
   1372             mSubject.setText(extraString);
   1373         }
   1374 
   1375         for (String extra : ALL_EXTRAS) {
   1376             if (intent.hasExtra(extra)) {
   1377                 String value = intent.getStringExtra(extra);
   1378                 if (EXTRA_TO.equals(extra)) {
   1379                     addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
   1380                 } else if (EXTRA_CC.equals(extra)) {
   1381                     addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
   1382                 } else if (EXTRA_BCC.equals(extra)) {
   1383                     addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
   1384                 } else if (EXTRA_SUBJECT.equals(extra)) {
   1385                     mSubject.setText(value);
   1386                 } else if (EXTRA_BODY.equals(extra)) {
   1387                     setBody(value, true /* with signature */);
   1388                 } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
   1389                     initQuotedText(value, true /* shouldQuoteText */);
   1390                 }
   1391             }
   1392         }
   1393 
   1394         Bundle extras = intent.getExtras();
   1395         if (extras != null) {
   1396             CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
   1397             if (text != null) {
   1398                 setBody(text, true /* with signature */);
   1399             }
   1400 
   1401             // TODO - support EXTRA_HTML_TEXT
   1402         }
   1403 
   1404         mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
   1405         if (mExtraValues != null) {
   1406             LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
   1407             initExtraValues(mExtraValues);
   1408             return true;
   1409         }
   1410 
   1411         return false;
   1412     }
   1413 
   1414     protected void initExtraValues(ContentValues extraValues) {
   1415         // DO NOTHING - Gmail will override
   1416     }
   1417 
   1418 
   1419     @VisibleForTesting
   1420     protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
   1421         // TODO: handle the case where there are spaces in the display name as
   1422         // well as the email such as "Guy with spaces <guy+with+spaces (at) gmail.com>"
   1423         // as they could be encoded ambiguously.
   1424         // Since URLDecode.decode changes + into ' ', and + is a valid
   1425         // email character, we need to find/ replace these ourselves before
   1426         // decoding.
   1427         try {
   1428             return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
   1429         } catch (IllegalArgumentException e) {
   1430             if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
   1431                 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
   1432             } else {
   1433                 LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
   1434             }
   1435             return null;
   1436         }
   1437     }
   1438 
   1439     /**
   1440      * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
   1441      * changing '+' into ' '
   1442      *
   1443      * @param toReplace Input string
   1444      * @return The string with all "+" characters replaced with "%2B"
   1445      */
   1446     private static String replacePlus(String toReplace) {
   1447         return toReplace.replace("+", "%2B");
   1448     }
   1449 
   1450     /**
   1451      * Initialize the compose view from a String representing a mailTo uri.
   1452      * @param mailToString The uri as a string.
   1453      */
   1454     public void initFromMailTo(String mailToString) {
   1455         // We need to disguise this string as a URI in order to parse it
   1456         // TODO:  Remove this hack when http://b/issue?id=1445295 gets fixed
   1457         Uri uri = Uri.parse("foo://" + mailToString);
   1458         int index = mailToString.indexOf("?");
   1459         int length = "mailto".length() + 1;
   1460         String to;
   1461         try {
   1462             // Extract the recipient after mailto:
   1463             if (index == -1) {
   1464                 to = decodeEmailInUri(mailToString.substring(length));
   1465             } else {
   1466                 to = decodeEmailInUri(mailToString.substring(length, index));
   1467             }
   1468             if (!TextUtils.isEmpty(to)) {
   1469                 addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
   1470             }
   1471         } catch (UnsupportedEncodingException e) {
   1472             if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
   1473                 LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
   1474             } else {
   1475                 LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
   1476             }
   1477         }
   1478 
   1479         List<String> cc = uri.getQueryParameters("cc");
   1480         addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
   1481 
   1482         List<String> otherTo = uri.getQueryParameters("to");
   1483         addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
   1484 
   1485         List<String> bcc = uri.getQueryParameters("bcc");
   1486         addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
   1487 
   1488         List<String> subject = uri.getQueryParameters("subject");
   1489         if (subject.size() > 0) {
   1490             try {
   1491                 mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
   1492                         UTF8_ENCODING_NAME));
   1493             } catch (UnsupportedEncodingException e) {
   1494                 LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
   1495                         e.getMessage(), subject);
   1496             }
   1497         }
   1498 
   1499         List<String> body = uri.getQueryParameters("body");
   1500         if (body.size() > 0) {
   1501             try {
   1502                 setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
   1503                         true /* with signature */);
   1504             } catch (UnsupportedEncodingException e) {
   1505                 LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
   1506             }
   1507         }
   1508     }
   1509 
   1510     @VisibleForTesting
   1511     protected void initAttachments(Message refMessage) {
   1512         addAttachments(refMessage.getAttachments());
   1513     }
   1514 
   1515     public long addAttachments(List<Attachment> attachments) {
   1516         long size = 0;
   1517         AttachmentFailureException error = null;
   1518         for (Attachment a : attachments) {
   1519             try {
   1520                 size += mAttachmentsView.addAttachment(mAccount, a);
   1521             } catch (AttachmentFailureException e) {
   1522                 error = e;
   1523             }
   1524         }
   1525         if (error != null) {
   1526             LogUtils.e(LOG_TAG, error, "Error adding attachment");
   1527             if (attachments.size() > 1) {
   1528                 showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
   1529             } else {
   1530                 showAttachmentTooBigToast(error.getErrorRes());
   1531             }
   1532         }
   1533         return size;
   1534     }
   1535 
   1536     /**
   1537      * When an attachment is too large to be added to a message, show a toast.
   1538      * This method also updates the position of the toast so that it is shown
   1539      * clearly above they keyboard if it happens to be open.
   1540      */
   1541     private void showAttachmentTooBigToast(int errorRes) {
   1542         String maxSize = AttachmentUtils.convertToHumanReadableSize(
   1543                 getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
   1544         showErrorToast(getString(errorRes, maxSize));
   1545     }
   1546 
   1547     private void showErrorToast(String message) {
   1548         Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
   1549         t.setText(message);
   1550         t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
   1551                 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
   1552         t.show();
   1553     }
   1554 
   1555     private void initAttachmentsFromIntent(Intent intent) {
   1556         Bundle extras = intent.getExtras();
   1557         if (extras == null) {
   1558             extras = Bundle.EMPTY;
   1559         }
   1560         final String action = intent.getAction();
   1561         if (!mAttachmentsChanged) {
   1562             long totalSize = 0;
   1563             if (extras.containsKey(EXTRA_ATTACHMENTS)) {
   1564                 String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
   1565                 for (String uriString : uris) {
   1566                     final Uri uri = Uri.parse(uriString);
   1567                     long size = 0;
   1568                     try {
   1569                         final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
   1570                         size = mAttachmentsView.addAttachment(mAccount, a);
   1571 
   1572                         Analytics.getInstance().sendEvent("send_intent_attachment",
   1573                                 Utils.normalizeMimeType(a.getContentType()), null, size);
   1574 
   1575                     } catch (AttachmentFailureException e) {
   1576                         LogUtils.e(LOG_TAG, e, "Error adding attachment");
   1577                         showAttachmentTooBigToast(e.getErrorRes());
   1578                     }
   1579                     totalSize += size;
   1580                 }
   1581             }
   1582             if (extras.containsKey(Intent.EXTRA_STREAM)) {
   1583                 if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
   1584                     ArrayList<Parcelable> uris = extras
   1585                             .getParcelableArrayList(Intent.EXTRA_STREAM);
   1586                     ArrayList<Attachment> attachments = new ArrayList<Attachment>();
   1587                     for (Parcelable uri : uris) {
   1588                         try {
   1589                             final Attachment a = mAttachmentsView.generateLocalAttachment(
   1590                                     (Uri) uri);
   1591                             attachments.add(a);
   1592 
   1593                             Analytics.getInstance().sendEvent("send_intent_attachment",
   1594                                     Utils.normalizeMimeType(a.getContentType()), null, a.size);
   1595 
   1596                         } catch (AttachmentFailureException e) {
   1597                             LogUtils.e(LOG_TAG, e, "Error adding attachment");
   1598                             String maxSize = AttachmentUtils.convertToHumanReadableSize(
   1599                                     getApplicationContext(),
   1600                                     mAccount.settings.getMaxAttachmentSize());
   1601                             showErrorToast(getString
   1602                                     (R.string.generic_attachment_problem, maxSize));
   1603                         }
   1604                     }
   1605                     totalSize += addAttachments(attachments);
   1606                 } else {
   1607                     final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
   1608                     long size = 0;
   1609                     try {
   1610                         final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
   1611                         size = mAttachmentsView.addAttachment(mAccount, a);
   1612 
   1613                         Analytics.getInstance().sendEvent("send_intent_attachment",
   1614                                 Utils.normalizeMimeType(a.getContentType()), null, size);
   1615 
   1616                     } catch (AttachmentFailureException e) {
   1617                         LogUtils.e(LOG_TAG, e, "Error adding attachment");
   1618                         showAttachmentTooBigToast(e.getErrorRes());
   1619                     }
   1620                     totalSize += size;
   1621                 }
   1622             }
   1623 
   1624             if (totalSize > 0) {
   1625                 mAttachmentsChanged = true;
   1626                 updateSaveUi();
   1627 
   1628                 Analytics.getInstance().sendEvent("send_intent_with_attachments",
   1629                         Integer.toString(getAttachments().size()), null, totalSize);
   1630             }
   1631         }
   1632     }
   1633 
   1634     protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
   1635         mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
   1636         mShowQuotedText = true;
   1637     }
   1638 
   1639     private void initQuotedTextFromRefMessage(Message refMessage, int action) {
   1640         if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
   1641             mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
   1642         }
   1643     }
   1644 
   1645     private void updateHideOrShowCcBcc() {
   1646         // Its possible there is a menu item OR a button.
   1647         boolean ccVisible = mCcBccView.isCcVisible();
   1648         boolean bccVisible = mCcBccView.isBccVisible();
   1649         if (mCcBccButton != null) {
   1650             if (!ccVisible || !bccVisible) {
   1651                 mCcBccButton.setVisibility(View.VISIBLE);
   1652                 mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
   1653                         : R.string.add_bcc_label));
   1654             } else {
   1655                 mCcBccButton.setVisibility(View.INVISIBLE);
   1656             }
   1657         }
   1658     }
   1659 
   1660     private void showCcBcc(Bundle state) {
   1661         if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
   1662             boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
   1663             boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
   1664             if (showCc || showBcc) {
   1665                 mCcBccView.show(false, showCc, showBcc);
   1666             }
   1667         }
   1668     }
   1669 
   1670     /**
   1671      * Add attachment and update the compose area appropriately.
   1672      * @param data
   1673      */
   1674     public void addAttachmentAndUpdateView(Intent data) {
   1675         addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
   1676     }
   1677 
   1678     public void addAttachmentAndUpdateView(Uri contentUri) {
   1679         if (contentUri == null) {
   1680             return;
   1681         }
   1682         try {
   1683             addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
   1684         } catch (AttachmentFailureException e) {
   1685             LogUtils.e(LOG_TAG, e, "Error adding attachment");
   1686             showErrorToast(getResources().getString(
   1687                     e.getErrorRes(),
   1688                     AttachmentUtils.convertToHumanReadableSize(
   1689                             getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
   1690         }
   1691     }
   1692 
   1693     public void addAttachmentAndUpdateView(Attachment attachment) {
   1694         try {
   1695             long size = mAttachmentsView.addAttachment(mAccount, attachment);
   1696             if (size > 0) {
   1697                 mAttachmentsChanged = true;
   1698                 updateSaveUi();
   1699             }
   1700         } catch (AttachmentFailureException e) {
   1701             LogUtils.e(LOG_TAG, e, "Error adding attachment");
   1702             showAttachmentTooBigToast(e.getErrorRes());
   1703         }
   1704     }
   1705 
   1706     void initRecipientsFromRefMessage(Message refMessage, int action) {
   1707         // Don't populate the address if this is a forward.
   1708         if (action == ComposeActivity.FORWARD) {
   1709             return;
   1710         }
   1711         initReplyRecipients(refMessage, action);
   1712     }
   1713 
   1714     // TODO: This should be private.  This method shouldn't be used by ComposeActivityTests, as
   1715     // it doesn't setup the state of the activity correctly
   1716     @VisibleForTesting
   1717     void initReplyRecipients(final Message refMessage, final int action) {
   1718         String[] sentToAddresses = refMessage.getToAddressesUnescaped();
   1719         final Collection<String> toAddresses;
   1720         final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
   1721         String replyToAddress = replyToAddresses.length > 0 ? replyToAddresses[0] : null;
   1722         final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
   1723         final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
   1724 
   1725         // If there is no reply to address, the reply to address is the sender.
   1726         if (TextUtils.isEmpty(replyToAddress)) {
   1727             replyToAddress = fromAddress;
   1728         }
   1729 
   1730         // If this is a reply, the Cc list is empty. If this is a reply-all, the
   1731         // Cc list is the union of the To and Cc recipients of the original
   1732         // message, excluding the current user's email address and any addresses
   1733         // already on the To list.
   1734         if (action == ComposeActivity.REPLY) {
   1735             toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
   1736             addToAddresses(toAddresses);
   1737         } else if (action == ComposeActivity.REPLY_ALL) {
   1738             final Set<String> ccAddresses = Sets.newHashSet();
   1739             toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
   1740             addToAddresses(toAddresses);
   1741             addRecipients(ccAddresses, sentToAddresses);
   1742             addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
   1743             addCcAddresses(ccAddresses, toAddresses);
   1744         }
   1745     }
   1746 
   1747     private void addToAddresses(Collection<String> addresses) {
   1748         addAddressesToList(addresses, mTo);
   1749     }
   1750 
   1751     private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
   1752         addCcAddressesToList(tokenizeAddressList(addresses),
   1753                 toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
   1754     }
   1755 
   1756     private void addBccAddresses(Collection<String> addresses) {
   1757         addAddressesToList(addresses, mBcc);
   1758     }
   1759 
   1760     @VisibleForTesting
   1761     protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
   1762             List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
   1763         String address;
   1764 
   1765         if (compareToList == null) {
   1766             for (Rfc822Token[] tokens : addresses) {
   1767                 for (int i = 0; i < tokens.length; i++) {
   1768                     address = tokens[i].toString();
   1769                     list.append(address + END_TOKEN);
   1770                 }
   1771             }
   1772         } else {
   1773             HashSet<String> compareTo = convertToHashSet(compareToList);
   1774             for (Rfc822Token[] tokens : addresses) {
   1775                 for (int i = 0; i < tokens.length; i++) {
   1776                     address = tokens[i].toString();
   1777                     // Check if this is a duplicate:
   1778                     if (!compareTo.contains(tokens[i].getAddress())) {
   1779                         // Get the address here
   1780                         list.append(address + END_TOKEN);
   1781                     }
   1782                 }
   1783             }
   1784         }
   1785     }
   1786 
   1787     private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
   1788         final HashSet<String> hash = new HashSet<String>();
   1789         for (final Rfc822Token[] tokens : list) {
   1790             for (int i = 0; i < tokens.length; i++) {
   1791                 hash.add(tokens[i].getAddress());
   1792             }
   1793         }
   1794         return hash;
   1795     }
   1796 
   1797     protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
   1798         @VisibleForTesting
   1799         List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
   1800 
   1801         for (String address: addresses) {
   1802             tokenized.add(Rfc822Tokenizer.tokenize(address));
   1803         }
   1804         return tokenized;
   1805     }
   1806 
   1807     @VisibleForTesting
   1808     void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
   1809         for (String address : addresses) {
   1810             addAddressToList(address, list);
   1811         }
   1812     }
   1813 
   1814     private static void addAddressToList(final String address, final RecipientEditTextView list) {
   1815         if (address == null || list == null)
   1816             return;
   1817 
   1818         final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
   1819 
   1820         for (int i = 0; i < tokens.length; i++) {
   1821             list.append(tokens[i] + END_TOKEN);
   1822         }
   1823     }
   1824 
   1825     @VisibleForTesting
   1826     protected Collection<String> initToRecipients(final String fullSenderAddress,
   1827             final String replyToAddress, final String[] inToAddresses) {
   1828         // The To recipient is the reply-to address specified in the original
   1829         // message, unless it is:
   1830         // the current user OR a custom from of the current user, in which case
   1831         // it's the To recipient list of the original message.
   1832         // OR missing, in which case use the sender of the original message
   1833         Set<String> toAddresses = Sets.newHashSet();
   1834         if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
   1835             toAddresses.add(replyToAddress);
   1836         } else {
   1837             // In this case, the user is replying to a message in which their
   1838             // current account or one of their custom from addresses is the only
   1839             // recipient and they sent the original message.
   1840             if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
   1841                     && recipientMatchesThisAccount(inToAddresses[0])) {
   1842                 toAddresses.add(inToAddresses[0]);
   1843                 return toAddresses;
   1844             }
   1845             // This happens if the user replies to a message they originally
   1846             // wrote. In this case, "reply" really means "re-send," so we
   1847             // target the original recipients. This works as expected even
   1848             // if the user sent the original message to themselves.
   1849             for (String address : inToAddresses) {
   1850                 if (!recipientMatchesThisAccount(address)) {
   1851                     toAddresses.add(address);
   1852                 }
   1853             }
   1854         }
   1855         return toAddresses;
   1856     }
   1857 
   1858     private void addRecipients(final Set<String> recipients, final String[] addresses) {
   1859         for (final String email : addresses) {
   1860             // Do not add this account, or any of its custom from addresses, to
   1861             // the list of recipients.
   1862             final String recipientAddress = Address.getEmailAddress(email).getAddress();
   1863             if (!recipientMatchesThisAccount(recipientAddress)) {
   1864                 recipients.add(email.replace("\"\"", ""));
   1865             }
   1866         }
   1867     }
   1868 
   1869     /**
   1870      * A recipient matches this account if it has the same address as the
   1871      * currently selected account OR one of the custom from addresses associated
   1872      * with the currently selected account.
   1873      * @param recipientAddress address we are comparing with the currently selected account
   1874      * @return
   1875      */
   1876     protected boolean recipientMatchesThisAccount(String recipientAddress) {
   1877         return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
   1878                         mAccount.getReplyFroms());
   1879     }
   1880 
   1881     /**
   1882      * Returns a formatted subject string with the appropriate prefix for the action type.
   1883      * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
   1884      */
   1885     public static String buildFormattedSubject(Resources res, String subject, int action) {
   1886         String prefix;
   1887         String correctedSubject = null;
   1888         if (action == ComposeActivity.COMPOSE) {
   1889             prefix = "";
   1890         } else if (action == ComposeActivity.FORWARD) {
   1891             prefix = res.getString(R.string.forward_subject_label);
   1892         } else {
   1893             prefix = res.getString(R.string.reply_subject_label);
   1894         }
   1895 
   1896         // Don't duplicate the prefix
   1897         if (!TextUtils.isEmpty(subject)
   1898                 && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
   1899             correctedSubject = subject;
   1900         } else {
   1901             correctedSubject = String.format(
   1902                     res.getString(R.string.formatted_subject), prefix, subject);
   1903         }
   1904 
   1905         return correctedSubject;
   1906     }
   1907 
   1908     private void setSubject(Message refMessage, int action) {
   1909         mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
   1910     }
   1911 
   1912     private void initRecipients() {
   1913         setupRecipients(mTo);
   1914         setupRecipients(mCc);
   1915         setupRecipients(mBcc);
   1916     }
   1917 
   1918     private void setupRecipients(RecipientEditTextView view) {
   1919         view.setAdapter(new RecipientAdapter(this, mAccount));
   1920         if (mValidator == null) {
   1921             final String accountName = mAccount.getEmailAddress();
   1922             int offset = accountName.indexOf("@") + 1;
   1923             String account = accountName;
   1924             if (offset > 0) {
   1925                 account = account.substring(offset);
   1926             }
   1927             mValidator = new Rfc822Validator(account);
   1928         }
   1929         view.setValidator(mValidator);
   1930     }
   1931 
   1932     @Override
   1933     public void onClick(View v) {
   1934         final int id = v.getId();
   1935         if (id == R.id.add_cc_bcc) {
   1936             // Verify that cc/ bcc aren't showing.
   1937             // Animate in cc/bcc.
   1938             showCcBccViews();
   1939         } else if (id == R.id.add_photo_attachment) {
   1940             doAttach(MIME_TYPE_PHOTO);
   1941         } else if (id == R.id.add_video_attachment) {
   1942             doAttach(MIME_TYPE_VIDEO);
   1943         }
   1944     }
   1945 
   1946     @Override
   1947     public boolean onCreateOptionsMenu(Menu menu) {
   1948         super.onCreateOptionsMenu(menu);
   1949         // Don't render any menu items when there are no accounts.
   1950         if (mAccounts == null || mAccounts.length == 0) {
   1951             return true;
   1952         }
   1953         MenuInflater inflater = getMenuInflater();
   1954         inflater.inflate(R.menu.compose_menu, menu);
   1955 
   1956         /*
   1957          * Start save in the correct enabled state.
   1958          * 1) If a user launches compose from within gmail, save is disabled
   1959          * until they add something, at which point, save is enabled, auto save
   1960          * on exit; if the user empties everything, save is disabled, exiting does not
   1961          * auto-save
   1962          * 2) if a user replies/ reply all/ forwards from within gmail, save is
   1963          * disabled until they change something, at which point, save is
   1964          * enabled, auto save on exit; if the user empties everything, save is
   1965          * disabled, exiting does not auto-save.
   1966          * 3) If a user launches compose from another application and something
   1967          * gets populated (attachments, recipients, body, subject, etc), save is
   1968          * enabled, auto save on exit; if the user empties everything, save is
   1969          * disabled, exiting does not auto-save
   1970          */
   1971         mSave = menu.findItem(R.id.save);
   1972         String action = getIntent() != null ? getIntent().getAction() : null;
   1973         enableSave(mInnerSavedState != null ?
   1974                 mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
   1975                     : (Intent.ACTION_SEND.equals(action)
   1976                             || Intent.ACTION_SEND_MULTIPLE.equals(action)
   1977                             || Intent.ACTION_SENDTO.equals(action)
   1978                             || shouldSave()));
   1979 
   1980         mSend = menu.findItem(R.id.send);
   1981         MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
   1982         MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
   1983         if (helpItem != null) {
   1984             helpItem.setVisible(mAccount != null
   1985                     && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
   1986         }
   1987         if (sendFeedbackItem != null) {
   1988             sendFeedbackItem.setVisible(mAccount != null
   1989                     && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
   1990         }
   1991         return true;
   1992     }
   1993 
   1994     @Override
   1995     public boolean onPrepareOptionsMenu(Menu menu) {
   1996         MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
   1997         if (ccBcc != null && mCc != null) {
   1998             // Its possible there is a menu item OR a button.
   1999             boolean ccFieldVisible = mCc.isShown();
   2000             boolean bccFieldVisible = mBcc.isShown();
   2001             if (!ccFieldVisible || !bccFieldVisible) {
   2002                 ccBcc.setVisible(true);
   2003                 ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
   2004                         : R.string.add_bcc_label));
   2005             } else {
   2006                 ccBcc.setVisible(false);
   2007             }
   2008         }
   2009         return true;
   2010     }
   2011 
   2012     @Override
   2013     public boolean onOptionsItemSelected(MenuItem item) {
   2014         final int id = item.getItemId();
   2015 
   2016         Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, null, 0);
   2017 
   2018         boolean handled = true;
   2019         if (id == R.id.add_photo_attachment) {
   2020             doAttach(MIME_TYPE_PHOTO);
   2021         } else if (id == R.id.add_video_attachment) {
   2022             doAttach(MIME_TYPE_VIDEO);
   2023         } else if (id == R.id.add_cc_bcc) {
   2024             showCcBccViews();
   2025         } else if (id == R.id.save) {
   2026             doSave(true);
   2027         } else if (id == R.id.send) {
   2028             doSend();
   2029         } else if (id == R.id.discard) {
   2030             doDiscard();
   2031         } else if (id == R.id.settings) {
   2032             Utils.showSettings(this, mAccount);
   2033         } else if (id == android.R.id.home) {
   2034             onAppUpPressed();
   2035         } else if (id == R.id.help_info_menu_item) {
   2036             Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
   2037         } else if (id == R.id.feedback_menu_item) {
   2038             Utils.sendFeedback(this, mAccount, false);
   2039         } else {
   2040             handled = false;
   2041         }
   2042         return !handled ? super.onOptionsItemSelected(item) : handled;
   2043     }
   2044 
   2045     @Override
   2046     public void onBackPressed() {
   2047         // If we are showing the wait fragment, just exit.
   2048         if (getWaitFragment() != null) {
   2049             finish();
   2050         } else {
   2051             super.onBackPressed();
   2052         }
   2053     }
   2054 
   2055     /**
   2056      * Carries out the "up" action in the action bar.
   2057      */
   2058     private void onAppUpPressed() {
   2059         if (mLaunchedFromEmail) {
   2060             // If this was started from Gmail, simply treat app up as the system back button, so
   2061             // that the last view is restored.
   2062             onBackPressed();
   2063             return;
   2064         }
   2065 
   2066         // Fire the main activity to ensure it launches the "top" screen of mail.
   2067         // Since the main Activity is singleTask, it should revive that task if it was already
   2068         // started.
   2069         final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
   2070         mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
   2071                 Intent.FLAG_ACTIVITY_TASK_ON_HOME);
   2072         startActivity(mailIntent);
   2073         finish();
   2074     }
   2075 
   2076     private void doSend() {
   2077         sendOrSaveWithSanityChecks(false, true, false, false);
   2078         logSendOrSave(false /* save */);
   2079         mPerformedSendOrDiscard = true;
   2080     }
   2081 
   2082     private void doSave(boolean showToast) {
   2083         sendOrSaveWithSanityChecks(true, showToast, false, false);
   2084     }
   2085 
   2086     @VisibleForTesting
   2087     public interface SendOrSaveCallback {
   2088         public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
   2089         public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
   2090         public Message getMessage();
   2091         public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
   2092     }
   2093 
   2094     @VisibleForTesting
   2095     public static class SendOrSaveTask implements Runnable {
   2096         private final Context mContext;
   2097         @VisibleForTesting
   2098         public final SendOrSaveCallback mSendOrSaveCallback;
   2099         @VisibleForTesting
   2100         public final SendOrSaveMessage mSendOrSaveMessage;
   2101         private ReplyFromAccount mExistingDraftAccount;
   2102 
   2103         public SendOrSaveTask(Context context, SendOrSaveMessage message,
   2104                 SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
   2105             mContext = context;
   2106             mSendOrSaveCallback = callback;
   2107             mSendOrSaveMessage = message;
   2108             mExistingDraftAccount = draftAccount;
   2109         }
   2110 
   2111         @Override
   2112         public void run() {
   2113             final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
   2114 
   2115             final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
   2116             Message message = mSendOrSaveCallback.getMessage();
   2117             long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
   2118             // If a previous draft has been saved, in an account that is different
   2119             // than what the user wants to send from, remove the old draft, and treat this
   2120             // as a new message
   2121             if (mExistingDraftAccount != null
   2122                     && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
   2123                 if (messageId != UIProvider.INVALID_MESSAGE_ID) {
   2124                     ContentResolver resolver = mContext.getContentResolver();
   2125                     ContentValues values = new ContentValues();
   2126                     values.put(BaseColumns._ID, messageId);
   2127                     if (mExistingDraftAccount.account.expungeMessageUri != null) {
   2128                         new ContentProviderTask.UpdateTask()
   2129                                 .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
   2130                                         values, null, null);
   2131                     } else {
   2132                         // TODO(mindyp) delete the conversation.
   2133                     }
   2134                     // reset messageId to 0, so a new message will be created
   2135                     messageId = UIProvider.INVALID_MESSAGE_ID;
   2136                 }
   2137             }
   2138 
   2139             final long messageIdToSave = messageId;
   2140             sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
   2141 
   2142             if (!sendOrSaveMessage.mSave) {
   2143                 incrementRecipientsTimesContacted(mContext,
   2144                         (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
   2145                 incrementRecipientsTimesContacted(mContext,
   2146                         (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
   2147                 incrementRecipientsTimesContacted(mContext,
   2148                         (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
   2149             }
   2150             mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
   2151         }
   2152 
   2153         private static void incrementRecipientsTimesContacted(final Context context,
   2154                 final String addressString) {
   2155             if (TextUtils.isEmpty(addressString)) {
   2156                 return;
   2157             }
   2158             final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
   2159             final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
   2160             for (int i = 0; i < tokens.length;i++) {
   2161                 recipients.add(tokens[i].getAddress());
   2162             }
   2163             final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
   2164             statsUpdater.updateWithAddress(recipients);
   2165         }
   2166 
   2167         /**
   2168          * Send or Save a message.
   2169          */
   2170         private void sendOrSaveMessage(final long messageIdToSave,
   2171                 final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
   2172             final ContentResolver resolver = mContext.getContentResolver();
   2173             final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
   2174 
   2175             final String accountMethod = sendOrSaveMessage.mSave ?
   2176                     UIProvider.AccountCallMethods.SAVE_MESSAGE :
   2177                     UIProvider.AccountCallMethods.SEND_MESSAGE;
   2178 
   2179             try {
   2180                 if (updateExistingMessage) {
   2181                     sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
   2182 
   2183                     callAccountSendSaveMethod(resolver,
   2184                             selectedAccount.account, accountMethod, sendOrSaveMessage);
   2185                 } else {
   2186                     Uri messageUri = null;
   2187                     final Bundle result = callAccountSendSaveMethod(resolver,
   2188                             selectedAccount.account, accountMethod, sendOrSaveMessage);
   2189                     if (result != null) {
   2190                         // If a non-null value was returned, then the provider handled the call
   2191                         // method
   2192                         messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
   2193                     }
   2194                     if (sendOrSaveMessage.mSave && messageUri != null) {
   2195                         final Cursor messageCursor = resolver.query(messageUri,
   2196                                 UIProvider.MESSAGE_PROJECTION, null, null, null);
   2197                         if (messageCursor != null) {
   2198                             try {
   2199                                 if (messageCursor.moveToFirst()) {
   2200                                     // Broadcast notification that a new message has
   2201                                     // been allocated
   2202                                     mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
   2203                                             new Message(messageCursor));
   2204                                 }
   2205                             } finally {
   2206                                 messageCursor.close();
   2207                             }
   2208                         }
   2209                     }
   2210                 }
   2211             } finally {
   2212                 // Close any opened file descriptors
   2213                 closeOpenedAttachmentFds(sendOrSaveMessage);
   2214             }
   2215         }
   2216 
   2217         private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
   2218             final Bundle openedFds = sendOrSaveMessage.attachmentFds();
   2219             if (openedFds != null) {
   2220                 final Set<String> keys = openedFds.keySet();
   2221                 for (final String key : keys) {
   2222                     final ParcelFileDescriptor fd = openedFds.getParcelable(key);
   2223                     if (fd != null) {
   2224                         try {
   2225                             fd.close();
   2226                         } catch (IOException e) {
   2227                             // Do nothing
   2228                         }
   2229                     }
   2230                 }
   2231             }
   2232         }
   2233 
   2234         /**
   2235          * Use the {@link ContentResolver#call} method to send or save the message.
   2236          *
   2237          * If this was successful, this method will return an non-null Bundle instance
   2238          */
   2239         private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
   2240                 final Account account, final String method,
   2241                 final SendOrSaveMessage sendOrSaveMessage) {
   2242             // Copy all of the values from the content values to the bundle
   2243             final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
   2244             final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
   2245 
   2246             for (Entry<String, Object> entry : valueSet) {
   2247                 final Object entryValue = entry.getValue();
   2248                 final String key = entry.getKey();
   2249                 if (entryValue instanceof String) {
   2250                     methodExtras.putString(key, (String)entryValue);
   2251                 } else if (entryValue instanceof Boolean) {
   2252                     methodExtras.putBoolean(key, (Boolean)entryValue);
   2253                 } else if (entryValue instanceof Integer) {
   2254                     methodExtras.putInt(key, (Integer)entryValue);
   2255                 } else if (entryValue instanceof Long) {
   2256                     methodExtras.putLong(key, (Long)entryValue);
   2257                 } else {
   2258                     LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
   2259                             entryValue.getClass().getName());
   2260                 }
   2261             }
   2262 
   2263             // If the SendOrSaveMessage has some opened fds, add them to the bundle
   2264             final Bundle fdMap = sendOrSaveMessage.attachmentFds();
   2265             if (fdMap != null) {
   2266                 methodExtras.putParcelable(
   2267                         UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
   2268             }
   2269 
   2270             return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
   2271         }
   2272     }
   2273 
   2274     @VisibleForTesting
   2275     public static class SendOrSaveMessage {
   2276         final ReplyFromAccount mAccount;
   2277         final ContentValues mValues;
   2278         final String mRefMessageId;
   2279         @VisibleForTesting
   2280         public final boolean mSave;
   2281         final int mRequestId;
   2282         private final Bundle mAttachmentFds;
   2283 
   2284         public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
   2285                 String refMessageId, List<Attachment> attachments, boolean save) {
   2286             mAccount = account;
   2287             mValues = values;
   2288             mRefMessageId = refMessageId;
   2289             mSave = save;
   2290             mRequestId = mValues.hashCode() ^ hashCode();
   2291 
   2292             mAttachmentFds = initializeAttachmentFds(context, attachments);
   2293         }
   2294 
   2295         int requestId() {
   2296             return mRequestId;
   2297         }
   2298 
   2299         Bundle attachmentFds() {
   2300             return mAttachmentFds;
   2301         }
   2302 
   2303         /**
   2304          * Opens {@link ParcelFileDescriptor} for each of the attachments.  This method must be
   2305          * called before the ComposeActivity finishes.
   2306          * Note: The caller is responsible for closing these file descriptors.
   2307          */
   2308         private static Bundle initializeAttachmentFds(final Context context,
   2309                 final List<Attachment> attachments) {
   2310             if (attachments == null || attachments.size() == 0) {
   2311                 return null;
   2312             }
   2313 
   2314             final Bundle result = new Bundle(attachments.size());
   2315             final ContentResolver resolver = context.getContentResolver();
   2316 
   2317             for (Attachment attachment : attachments) {
   2318                 if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
   2319                     continue;
   2320                 }
   2321 
   2322                 ParcelFileDescriptor fileDescriptor;
   2323                 try {
   2324                     fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
   2325                 } catch (FileNotFoundException e) {
   2326                     LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
   2327                     fileDescriptor = null;
   2328                 } catch (SecurityException e) {
   2329                     // We have encountered a security exception when attempting to open the file
   2330                     // specified by the content uri.  If the attachment has been cached, this
   2331                     // isn't a problem, as even through the original permission may have been
   2332                     // revoked, we have cached the file.  This will happen when saving/sending
   2333                     // a previously saved draft.
   2334                     // TODO(markwei): Expose whether the attachment has been cached through the
   2335                     // attachment object.  This would allow us to limit when the log is made, as
   2336                     // if the attachment has been cached, this really isn't an error
   2337                     LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
   2338                     // Just set the file descriptor to null, as the underlying provider needs
   2339                     // to handle the file descriptor not being set.
   2340                     fileDescriptor = null;
   2341                 }
   2342 
   2343                 if (fileDescriptor != null) {
   2344                     result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
   2345                 }
   2346             }
   2347 
   2348             return result;
   2349         }
   2350     }
   2351 
   2352     /**
   2353      * Get the to recipients.
   2354      */
   2355     public String[] getToAddresses() {
   2356         return getAddressesFromList(mTo);
   2357     }
   2358 
   2359     /**
   2360      * Get the cc recipients.
   2361      */
   2362     public String[] getCcAddresses() {
   2363         return getAddressesFromList(mCc);
   2364     }
   2365 
   2366     /**
   2367      * Get the bcc recipients.
   2368      */
   2369     public String[] getBccAddresses() {
   2370         return getAddressesFromList(mBcc);
   2371     }
   2372 
   2373     public String[] getAddressesFromList(RecipientEditTextView list) {
   2374         if (list == null) {
   2375             return new String[0];
   2376         }
   2377         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
   2378         int count = tokens.length;
   2379         String[] result = new String[count];
   2380         for (int i = 0; i < count; i++) {
   2381             result[i] = tokens[i].toString();
   2382         }
   2383         return result;
   2384     }
   2385 
   2386     /**
   2387      * Check for invalid email addresses.
   2388      * @param to String array of email addresses to check.
   2389      * @param wrongEmailsOut Emails addresses that were invalid.
   2390      */
   2391     public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
   2392         if (mValidator == null) {
   2393             return;
   2394         }
   2395         for (final String email : to) {
   2396             if (!mValidator.isValid(email)) {
   2397                 wrongEmailsOut.add(email);
   2398             }
   2399         }
   2400     }
   2401 
   2402     public static class RecipientErrorDialogFragment extends DialogFragment {
   2403         // Public no-args constructor needed for fragment re-instantiation
   2404         public RecipientErrorDialogFragment() {}
   2405 
   2406         public static RecipientErrorDialogFragment newInstance(final String message) {
   2407             final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
   2408             final Bundle args = new Bundle(1);
   2409             args.putString("message", message);
   2410             frag.setArguments(args);
   2411             return frag;
   2412         }
   2413 
   2414         @Override
   2415         public Dialog onCreateDialog(Bundle savedInstanceState) {
   2416             final String message = getArguments().getString("message");
   2417             return new AlertDialog.Builder(getActivity()).setMessage(message).setTitle(
   2418                     R.string.recipient_error_dialog_title)
   2419                     .setIconAttribute(android.R.attr.alertDialogIcon)
   2420                     .setPositiveButton(
   2421                             R.string.ok, new Dialog.OnClickListener() {
   2422                         @Override
   2423                         public void onClick(DialogInterface dialog, int which) {
   2424                             ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
   2425                         }
   2426                     }).create();
   2427         }
   2428     }
   2429 
   2430     private void finishRecipientErrorDialog() {
   2431         // after the user dismisses the recipient error
   2432         // dialog we want to make sure to refocus the
   2433         // recipient to field so they can fix the issue
   2434         // easily
   2435         if (mTo != null) {
   2436             mTo.requestFocus();
   2437         }
   2438     }
   2439 
   2440     /**
   2441      * Show an error because the user has entered an invalid recipient.
   2442      * @param message
   2443      */
   2444     private void showRecipientErrorDialog(final String message) {
   2445         final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
   2446         frag.show(getFragmentManager(), "recipient error");
   2447     }
   2448 
   2449     /**
   2450      * Update the state of the UI based on whether or not the current draft
   2451      * needs to be saved and the message is not empty.
   2452      */
   2453     public void updateSaveUi() {
   2454         if (mSave != null) {
   2455             mSave.setEnabled((shouldSave() && !isBlank()));
   2456         }
   2457     }
   2458 
   2459     /**
   2460      * Returns true if we need to save the current draft.
   2461      */
   2462     private boolean shouldSave() {
   2463         synchronized (mDraftLock) {
   2464             // The message should only be saved if:
   2465             // It hasn't been sent AND
   2466             // Some text has been added to the message OR
   2467             // an attachment has been added or removed
   2468             // AND there is actually something in the draft to save.
   2469             return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
   2470                     && !isBlank();
   2471         }
   2472     }
   2473 
   2474     /**
   2475      * Check if all fields are blank.
   2476      * @return boolean
   2477      */
   2478     public boolean isBlank() {
   2479         // Need to check for null since isBlank() can be called from onPause()
   2480         // before findViews() is called
   2481         if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
   2482                 mAttachmentsView == null) {
   2483             LogUtils.w(LOG_TAG, "null views in isBlank check");
   2484             return true;
   2485         }
   2486         return mSubject.getText().length() == 0
   2487                 && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
   2488                         mBodyView.getText().toString()) == 0)
   2489                 && mTo.length() == 0
   2490                 && mCc.length() == 0 && mBcc.length() == 0
   2491                 && mAttachmentsView.getAttachments().size() == 0;
   2492     }
   2493 
   2494     @VisibleForTesting
   2495     protected int getSignatureStartPosition(String signature, String bodyText) {
   2496         int startPos = -1;
   2497 
   2498         if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
   2499             return startPos;
   2500         }
   2501 
   2502         int bodyLength = bodyText.length();
   2503         int signatureLength = signature.length();
   2504         String printableVersion = convertToPrintableSignature(signature);
   2505         int printableLength = printableVersion.length();
   2506 
   2507         if (bodyLength >= printableLength
   2508                 && bodyText.substring(bodyLength - printableLength)
   2509                 .equals(printableVersion)) {
   2510             startPos = bodyLength - printableLength;
   2511         } else if (bodyLength >= signatureLength
   2512                 && bodyText.substring(bodyLength - signatureLength)
   2513                 .equals(signature)) {
   2514             startPos = bodyLength - signatureLength;
   2515         }
   2516         return startPos;
   2517     }
   2518 
   2519     /**
   2520      * Allows any changes made by the user to be ignored. Called when the user
   2521      * decides to discard a draft.
   2522      */
   2523     private void discardChanges() {
   2524         mTextChanged = false;
   2525         mAttachmentsChanged = false;
   2526         mReplyFromChanged = false;
   2527     }
   2528 
   2529     /**
   2530      * @param save
   2531      * @param showToast
   2532      * @return Whether the send or save succeeded.
   2533      */
   2534     protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
   2535             final boolean orientationChanged, final boolean autoSend) {
   2536         if (mAccounts == null || mAccount == null) {
   2537             Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
   2538             if (autoSend) {
   2539                 finish();
   2540             }
   2541             return false;
   2542         }
   2543 
   2544         final String[] to, cc, bcc;
   2545         if (orientationChanged) {
   2546             to = cc = bcc = new String[0];
   2547         } else {
   2548             to = getToAddresses();
   2549             cc = getCcAddresses();
   2550             bcc = getBccAddresses();
   2551         }
   2552 
   2553         // Don't let the user send to nobody (but it's okay to save a message
   2554         // with no recipients)
   2555         if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
   2556             showRecipientErrorDialog(getString(R.string.recipient_needed));
   2557             return false;
   2558         }
   2559 
   2560         List<String> wrongEmails = new ArrayList<String>();
   2561         if (!save) {
   2562             checkInvalidEmails(to, wrongEmails);
   2563             checkInvalidEmails(cc, wrongEmails);
   2564             checkInvalidEmails(bcc, wrongEmails);
   2565         }
   2566 
   2567         // Don't let the user send an email with invalid recipients
   2568         if (wrongEmails.size() > 0) {
   2569             String errorText = String.format(getString(R.string.invalid_recipient),
   2570                     wrongEmails.get(0));
   2571             showRecipientErrorDialog(errorText);
   2572             return false;
   2573         }
   2574 
   2575         // Show a warning before sending only if there are no attachments.
   2576         if (!save) {
   2577             if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
   2578                 boolean warnAboutEmptySubject = isSubjectEmpty();
   2579                 boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
   2580 
   2581                 // A warning about an empty body may not be warranted when
   2582                 // forwarding mails, since a common use case is to forward
   2583                 // quoted text and not append any more text.
   2584                 boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
   2585 
   2586                 // When we bring up a dialog warning the user about a send,
   2587                 // assume that they accept sending the message. If they do not,
   2588                 // the dialog listener is required to enable sending again.
   2589                 if (warnAboutEmptySubject) {
   2590                     showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
   2591                             showToast);
   2592                     return true;
   2593                 }
   2594 
   2595                 if (warnAboutEmptyBody) {
   2596                     showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
   2597                             showToast);
   2598                     return true;
   2599                 }
   2600             }
   2601             // Ask for confirmation to send (if always required)
   2602             if (showSendConfirmation()) {
   2603                 showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
   2604                 return true;
   2605             }
   2606         }
   2607 
   2608         sendOrSave(save, showToast);
   2609         return true;
   2610     }
   2611 
   2612     /**
   2613      * Returns a boolean indicating whether warnings should be shown for empty
   2614      * subject and body fields
   2615      *
   2616      * @return True if a warning should be shown for empty text fields
   2617      */
   2618     protected boolean showEmptyTextWarnings() {
   2619         return mAttachmentsView.getAttachments().size() == 0;
   2620     }
   2621 
   2622     /**
   2623      * Returns a boolean indicating whether the user should confirm each send
   2624      *
   2625      * @return True if a warning should be on each send
   2626      */
   2627     protected boolean showSendConfirmation() {
   2628         return mCachedSettings != null ? mCachedSettings.confirmSend : false;
   2629     }
   2630 
   2631     public static class SendConfirmDialogFragment extends DialogFragment {
   2632         // Public no-args constructor needed for fragment re-instantiation
   2633         public SendConfirmDialogFragment() {}
   2634 
   2635         public static SendConfirmDialogFragment newInstance(final int messageId,
   2636                 final boolean save, final boolean showToast) {
   2637             final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
   2638             final Bundle args = new Bundle(3);
   2639             args.putInt("messageId", messageId);
   2640             args.putBoolean("save", save);
   2641             args.putBoolean("showToast", showToast);
   2642             frag.setArguments(args);
   2643             return frag;
   2644         }
   2645 
   2646         @Override
   2647         public Dialog onCreateDialog(Bundle savedInstanceState) {
   2648             final int messageId = getArguments().getInt("messageId");
   2649             final boolean save = getArguments().getBoolean("save");
   2650             final boolean showToast = getArguments().getBoolean("showToast");
   2651 
   2652             return new AlertDialog.Builder(getActivity())
   2653                     .setMessage(messageId)
   2654                     .setTitle(R.string.confirm_send_title)
   2655                     .setIconAttribute(android.R.attr.alertDialogIcon)
   2656                     .setPositiveButton(R.string.send,
   2657                             new DialogInterface.OnClickListener() {
   2658                                 @Override
   2659                                 public void onClick(DialogInterface dialog, int whichButton) {
   2660                                     ((ComposeActivity)getActivity()).finishSendConfirmDialog(save,
   2661                                             showToast);
   2662                                 }
   2663                             })
   2664                     .create();
   2665         }
   2666     }
   2667 
   2668     private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
   2669         sendOrSave(save, showToast);
   2670     }
   2671 
   2672     private void showSendConfirmDialog(final int messageId, final boolean save,
   2673             final boolean showToast) {
   2674         final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
   2675                 showToast);
   2676         frag.show(getFragmentManager(), "send confirm");
   2677     }
   2678 
   2679     /**
   2680      * Returns whether the ComposeArea believes there is any text in the body of
   2681      * the composition. TODO: When ComposeArea controls the Body as well, add
   2682      * that here.
   2683      */
   2684     public boolean isBodyEmpty() {
   2685         return !mQuotedTextView.isTextIncluded();
   2686     }
   2687 
   2688     /**
   2689      * Test to see if the subject is empty.
   2690      *
   2691      * @return boolean.
   2692      */
   2693     // TODO: this will likely go away when composeArea.focus() is implemented
   2694     // after all the widget control is moved over.
   2695     public boolean isSubjectEmpty() {
   2696         return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
   2697     }
   2698 
   2699     /* package */
   2700     static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
   2701             Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
   2702             SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
   2703             ReplyFromAccount draftAccount, final ContentValues extraValues) {
   2704         final ContentValues values = new ContentValues();
   2705 
   2706         final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
   2707 
   2708         MessageModification.putToAddresses(values, message.getToAddresses());
   2709         MessageModification.putCcAddresses(values, message.getCcAddresses());
   2710         MessageModification.putBccAddresses(values, message.getBccAddresses());
   2711 
   2712         MessageModification.putCustomFromAddress(values, message.getFrom());
   2713 
   2714         MessageModification.putSubject(values, message.subject);
   2715         // Make sure to remove only the composing spans from the Spannable before saving.
   2716         final String htmlBody = Html.toHtml(removeComposingSpans(body));
   2717 
   2718         boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
   2719         StringBuilder fullBody = new StringBuilder(htmlBody);
   2720         if (includeQuotedText) {
   2721             // HTML gets converted to text for now
   2722             final String text = quotedText.toString();
   2723             if (QuotedTextView.containsQuotedText(text)) {
   2724                 int pos = QuotedTextView.getQuotedTextOffset(text);
   2725                 final int quoteStartPos = fullBody.length() + pos;
   2726                 fullBody.append(text);
   2727                 MessageModification.putQuoteStartPos(values, quoteStartPos);
   2728                 MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
   2729                 MessageModification.putAppendRefMessageContent(values, includeQuotedText);
   2730             } else {
   2731                 LogUtils.w(LOG_TAG, "Couldn't find quoted text");
   2732                 // This shouldn't happen, but just use what we have,
   2733                 // and don't do server-side expansion
   2734                 fullBody.append(text);
   2735             }
   2736         }
   2737         int draftType = getDraftType(composeMode);
   2738         MessageModification.putDraftType(values, draftType);
   2739         if (refMessage != null) {
   2740             if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
   2741                 MessageModification.putBodyHtml(values, fullBody.toString());
   2742             }
   2743             if (!TextUtils.isEmpty(refMessage.bodyText)) {
   2744                 MessageModification.putBody(values,
   2745                         Utils.convertHtmlToPlainText(fullBody.toString()).toString());
   2746             }
   2747         } else {
   2748             MessageModification.putBodyHtml(values, fullBody.toString());
   2749             MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
   2750                     .toString());
   2751         }
   2752         MessageModification.putAttachments(values, message.getAttachments());
   2753         if (!TextUtils.isEmpty(refMessageId)) {
   2754             MessageModification.putRefMessageId(values, refMessageId);
   2755         }
   2756         if (extraValues != null) {
   2757             values.putAll(extraValues);
   2758         }
   2759         SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
   2760                 values, refMessageId, message.getAttachments(), save);
   2761         SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
   2762                 draftAccount);
   2763 
   2764         callback.initializeSendOrSave(sendOrSaveTask);
   2765         // Do the send/save action on the specified handler to avoid possible
   2766         // ANRs
   2767         handler.post(sendOrSaveTask);
   2768 
   2769         return sendOrSaveMessage.requestId();
   2770     }
   2771 
   2772     /**
   2773      * Removes any composing spans from the specified string.  This will create a new
   2774      * SpannableString instance, as to not modify the behavior of the EditText view.
   2775      */
   2776     private static SpannableString removeComposingSpans(Spanned body) {
   2777         final SpannableString messageBody = new SpannableString(body);
   2778         BaseInputConnection.removeComposingSpans(messageBody);
   2779         return messageBody;
   2780     }
   2781 
   2782     private static int getDraftType(int mode) {
   2783         int draftType = -1;
   2784         switch (mode) {
   2785             case ComposeActivity.COMPOSE:
   2786                 draftType = DraftType.COMPOSE;
   2787                 break;
   2788             case ComposeActivity.REPLY:
   2789                 draftType = DraftType.REPLY;
   2790                 break;
   2791             case ComposeActivity.REPLY_ALL:
   2792                 draftType = DraftType.REPLY_ALL;
   2793                 break;
   2794             case ComposeActivity.FORWARD:
   2795                 draftType = DraftType.FORWARD;
   2796                 break;
   2797         }
   2798         return draftType;
   2799     }
   2800 
   2801     private void sendOrSave(final boolean save, final boolean showToast) {
   2802         // Check if user is a monkey. Monkeys can compose and hit send
   2803         // button but are not allowed to send anything off the device.
   2804         if (ActivityManager.isUserAMonkey()) {
   2805             return;
   2806         }
   2807 
   2808         final Spanned body = mBodyView.getEditableText();
   2809 
   2810         SendOrSaveCallback callback = new SendOrSaveCallback() {
   2811             // FIXME: unused
   2812             private int mRestoredRequestId;
   2813 
   2814             @Override
   2815             public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
   2816                 synchronized (mActiveTasks) {
   2817                     int numTasks = mActiveTasks.size();
   2818                     if (numTasks == 0) {
   2819                         // Start service so we won't be killed if this app is
   2820                         // put in the background.
   2821                         startService(new Intent(ComposeActivity.this, EmptyService.class));
   2822                     }
   2823 
   2824                     mActiveTasks.add(sendOrSaveTask);
   2825                 }
   2826                 if (sTestSendOrSaveCallback != null) {
   2827                     sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
   2828                 }
   2829             }
   2830 
   2831             @Override
   2832             public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
   2833                     Message message) {
   2834                 synchronized (mDraftLock) {
   2835                     mDraftAccount = sendOrSaveMessage.mAccount;
   2836                     mDraftId = message.id;
   2837                     mDraft = message;
   2838                     if (sRequestMessageIdMap != null) {
   2839                         sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
   2840                     }
   2841                     // Cache request message map, in case the process is killed
   2842                     saveRequestMap();
   2843                 }
   2844                 if (sTestSendOrSaveCallback != null) {
   2845                     sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
   2846                 }
   2847             }
   2848 
   2849             @Override
   2850             public Message getMessage() {
   2851                 synchronized (mDraftLock) {
   2852                     return mDraft;
   2853                 }
   2854             }
   2855 
   2856             @Override
   2857             public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
   2858                 // Update the last sent from account.
   2859                 if (mAccount != null) {
   2860                     MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
   2861                 }
   2862                 if (success) {
   2863                     // Successfully sent or saved so reset change markers
   2864                     discardChanges();
   2865                 } else {
   2866                     // A failure happened with saving/sending the draft
   2867                     // TODO(pwestbro): add a better string that should be used
   2868                     // when failing to send or save
   2869                     Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
   2870                             .show();
   2871                 }
   2872 
   2873                 int numTasks;
   2874                 synchronized (mActiveTasks) {
   2875                     // Remove the task from the list of active tasks
   2876                     mActiveTasks.remove(task);
   2877                     numTasks = mActiveTasks.size();
   2878                 }
   2879 
   2880                 if (numTasks == 0) {
   2881                     // Stop service so we can be killed.
   2882                     stopService(new Intent(ComposeActivity.this, EmptyService.class));
   2883                 }
   2884                 if (sTestSendOrSaveCallback != null) {
   2885                     sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
   2886                 }
   2887             }
   2888         };
   2889 
   2890         setAccount(mReplyFromAccount.account);
   2891 
   2892         if (mSendSaveTaskHandler == null) {
   2893             HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
   2894             handlerThread.start();
   2895 
   2896             mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
   2897         }
   2898 
   2899         Message msg = createMessage(mReplyFromAccount, getMode());
   2900         mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
   2901                 mQuotedTextView.getQuotedTextIfIncluded(), callback,
   2902                 mSendSaveTaskHandler, save, mComposeMode, mDraftAccount, mExtraValues);
   2903 
   2904         // Don't display the toast if the user is just changing the orientation,
   2905         // but we still need to save the draft to the cursor because this is how we restore
   2906         // the attachments when the configuration change completes.
   2907         if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
   2908             Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
   2909                     Toast.LENGTH_LONG).show();
   2910         }
   2911 
   2912         // Need to update variables here because the send or save completes
   2913         // asynchronously even though the toast shows right away.
   2914         discardChanges();
   2915         updateSaveUi();
   2916 
   2917         // If we are sending, finish the activity
   2918         if (!save) {
   2919             finish();
   2920         }
   2921     }
   2922 
   2923     /**
   2924      * Save the state of the request messageid map. This allows for the Gmail
   2925      * process to be killed, but and still allow for ComposeActivity instances
   2926      * to be recreated correctly.
   2927      */
   2928     private void saveRequestMap() {
   2929         // TODO: store the request map in user preferences.
   2930     }
   2931 
   2932     private void doAttach(String type) {
   2933         Intent i = new Intent(Intent.ACTION_GET_CONTENT);
   2934         i.addCategory(Intent.CATEGORY_OPENABLE);
   2935         i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
   2936         i.setType(type);
   2937         mAddingAttachment = true;
   2938         startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
   2939                 RESULT_PICK_ATTACHMENT);
   2940     }
   2941 
   2942     private void showCcBccViews() {
   2943         mCcBccView.show(true, true, true);
   2944         if (mCcBccButton != null) {
   2945             mCcBccButton.setVisibility(View.INVISIBLE);
   2946         }
   2947     }
   2948 
   2949     private static String getActionString(int action) {
   2950         final String msgType;
   2951         switch (action) {
   2952             case COMPOSE:
   2953                 msgType = "new_message";
   2954                 break;
   2955             case REPLY:
   2956                 msgType = "reply";
   2957                 break;
   2958             case REPLY_ALL:
   2959                 msgType = "reply_all";
   2960                 break;
   2961             case FORWARD:
   2962                 msgType = "forward";
   2963                 break;
   2964             default:
   2965                 msgType = "unknown";
   2966                 break;
   2967         }
   2968         return msgType;
   2969     }
   2970 
   2971     private void logSendOrSave(boolean save) {
   2972         if (!Analytics.isLoggable() || mAttachmentsView == null) {
   2973             return;
   2974         }
   2975 
   2976         final String category = (save) ? "message_save" : "message_send";
   2977         final int attachmentCount = getAttachments().size();
   2978         final String msgType = getActionString(mComposeMode);
   2979         final String label;
   2980         final long value;
   2981         if (mComposeMode == COMPOSE) {
   2982             label = Integer.toString(attachmentCount);
   2983             value = attachmentCount;
   2984         } else {
   2985             label = null;
   2986             value = 0;
   2987         }
   2988         Analytics.getInstance().sendEvent(category, msgType, label, value);
   2989     }
   2990 
   2991     @Override
   2992     public boolean onNavigationItemSelected(int position, long itemId) {
   2993         int initialComposeMode = mComposeMode;
   2994         if (position == ComposeActivity.REPLY) {
   2995             mComposeMode = ComposeActivity.REPLY;
   2996         } else if (position == ComposeActivity.REPLY_ALL) {
   2997             mComposeMode = ComposeActivity.REPLY_ALL;
   2998         } else if (position == ComposeActivity.FORWARD) {
   2999             mComposeMode = ComposeActivity.FORWARD;
   3000         }
   3001         clearChangeListeners();
   3002         if (initialComposeMode != mComposeMode) {
   3003             resetMessageForModeChange();
   3004             if (mRefMessage != null) {
   3005                 setFieldsFromRefMessage(mComposeMode);
   3006             }
   3007             boolean showCc = false;
   3008             boolean showBcc = false;
   3009             if (mDraft != null) {
   3010                 // Following desktop behavior, if the user has added a BCC
   3011                 // field to a draft, we show it regardless of compose mode.
   3012                 showBcc = !TextUtils.isEmpty(mDraft.getBcc());
   3013                 // Use the draft to determine what to populate.
   3014                 // If the Bcc field is showing, show the Cc field whether it is populated or not.
   3015                 showCc = showBcc
   3016                         || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
   3017             }
   3018             if (mRefMessage != null) {
   3019                 showCc = !TextUtils.isEmpty(mCc.getText());
   3020                 showBcc = !TextUtils.isEmpty(mBcc.getText());
   3021             }
   3022             mCcBccView.show(false, showCc, showBcc);
   3023         }
   3024         updateHideOrShowCcBcc();
   3025         initChangeListeners();
   3026         return true;
   3027     }
   3028 
   3029     @VisibleForTesting
   3030     protected void resetMessageForModeChange() {
   3031         // When switching between reply, reply all, forward,
   3032         // follow the behavior of webview.
   3033         // The contents of the following fields are cleared
   3034         // so that they can be populated directly from the
   3035         // ref message:
   3036         // 1) Any recipient fields
   3037         // 2) The subject
   3038         mTo.setText("");
   3039         mCc.setText("");
   3040         mBcc.setText("");
   3041         // Any edits to the subject are replaced with the original subject.
   3042         mSubject.setText("");
   3043 
   3044         // Any changes to the contents of the following fields are kept:
   3045         // 1) Body
   3046         // 2) Attachments
   3047         // If the user made changes to attachments, keep their changes.
   3048         if (!mAttachmentsChanged) {
   3049             mAttachmentsView.deleteAllAttachments();
   3050         }
   3051     }
   3052 
   3053     private class ComposeModeAdapter extends ArrayAdapter<String> {
   3054 
   3055         private LayoutInflater mInflater;
   3056 
   3057         public ComposeModeAdapter(Context context) {
   3058             super(context, R.layout.compose_mode_item, R.id.mode, getResources()
   3059                     .getStringArray(R.array.compose_modes));
   3060         }
   3061 
   3062         private LayoutInflater getInflater() {
   3063             if (mInflater == null) {
   3064                 mInflater = LayoutInflater.from(getContext());
   3065             }
   3066             return mInflater;
   3067         }
   3068 
   3069         @Override
   3070         public View getView(int position, View convertView, ViewGroup parent) {
   3071             if (convertView == null) {
   3072                 convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
   3073             }
   3074             ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
   3075             return super.getView(position, convertView, parent);
   3076         }
   3077     }
   3078 
   3079     @Override
   3080     public void onRespondInline(String text) {
   3081         appendToBody(text, false);
   3082         mQuotedTextView.setUpperDividerVisible(false);
   3083         mRespondedInline = true;
   3084         if (!mBodyView.hasFocus()) {
   3085             mBodyView.requestFocus();
   3086         }
   3087     }
   3088 
   3089     /**
   3090      * Append text to the body of the message. If there is no existing body
   3091      * text, just sets the body to text.
   3092      *
   3093      * @param text
   3094      * @param withSignature True to append a signature.
   3095      */
   3096     public void appendToBody(CharSequence text, boolean withSignature) {
   3097         Editable bodyText = mBodyView.getEditableText();
   3098         if (bodyText != null && bodyText.length() > 0) {
   3099             bodyText.append(text);
   3100         } else {
   3101             setBody(text, withSignature);
   3102         }
   3103     }
   3104 
   3105     /**
   3106      * Set the body of the message.
   3107      *
   3108      * @param text
   3109      * @param withSignature True to append a signature.
   3110      */
   3111     public void setBody(CharSequence text, boolean withSignature) {
   3112         mBodyView.setText(text);
   3113         if (withSignature) {
   3114             appendSignature();
   3115         }
   3116     }
   3117 
   3118     private void appendSignature() {
   3119         String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
   3120         boolean hasFocus = mBodyView.hasFocus();
   3121         int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
   3122         if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
   3123             mSignature = newSignature;
   3124             if (!TextUtils.isEmpty(mSignature)) {
   3125                 // Appending a signature does not count as changing text.
   3126                 mBodyView.removeTextChangedListener(this);
   3127                 mBodyView.append(convertToPrintableSignature(mSignature));
   3128                 mBodyView.addTextChangedListener(this);
   3129             }
   3130             if (hasFocus) {
   3131                 focusBody();
   3132             }
   3133         }
   3134     }
   3135 
   3136     private String convertToPrintableSignature(String signature) {
   3137         String signatureResource = getResources().getString(R.string.signature);
   3138         if (signature == null) {
   3139             signature = "";
   3140         }
   3141         return String.format(signatureResource, signature);
   3142     }
   3143 
   3144     @Override
   3145     public void onAccountChanged() {
   3146         mReplyFromAccount = mFromSpinner.getCurrentAccount();
   3147         if (!mAccount.equals(mReplyFromAccount.account)) {
   3148             // Clear a signature, if there was one.
   3149             mBodyView.removeTextChangedListener(this);
   3150             String oldSignature = mSignature;
   3151             String bodyText = getBody().getText().toString();
   3152             if (!TextUtils.isEmpty(oldSignature)) {
   3153                 int pos = getSignatureStartPosition(oldSignature, bodyText);
   3154                 if (pos > -1) {
   3155                     mBodyView.setText(bodyText.substring(0, pos));
   3156                 }
   3157             }
   3158             setAccount(mReplyFromAccount.account);
   3159             mBodyView.addTextChangedListener(this);
   3160             // TODO: handle discarding attachments when switching accounts.
   3161             // Only enable save for this draft if there is any other content
   3162             // in the message.
   3163             if (!isBlank()) {
   3164                 enableSave(true);
   3165             }
   3166             mReplyFromChanged = true;
   3167             initRecipients();
   3168         }
   3169     }
   3170 
   3171     public void enableSave(boolean enabled) {
   3172         if (mSave != null) {
   3173             mSave.setEnabled(enabled);
   3174         }
   3175     }
   3176 
   3177     public static class DiscardConfirmDialogFragment extends DialogFragment {
   3178         // Public no-args constructor needed for fragment re-instantiation
   3179         public DiscardConfirmDialogFragment() {}
   3180 
   3181         @Override
   3182         public Dialog onCreateDialog(Bundle savedInstanceState) {
   3183             return new AlertDialog.Builder(getActivity())
   3184                     .setMessage(R.string.confirm_discard_text)
   3185                     .setPositiveButton(R.string.discard,
   3186                             new DialogInterface.OnClickListener() {
   3187                                 @Override
   3188                                 public void onClick(DialogInterface dialog, int which) {
   3189                                     ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
   3190                                 }
   3191                             })
   3192                     .setNegativeButton(R.string.cancel, null)
   3193                     .create();
   3194         }
   3195     }
   3196 
   3197     private void doDiscard() {
   3198         final DialogFragment frag = new DiscardConfirmDialogFragment();
   3199         frag.show(getFragmentManager(), "discard confirm");
   3200     }
   3201     /**
   3202      * Effectively discard the current message.
   3203      *
   3204      * This method is either invoked from the menu or from the dialog
   3205      * once the user has confirmed that they want to discard the message.
   3206      */
   3207     private void doDiscardWithoutConfirmation() {
   3208         synchronized (mDraftLock) {
   3209             if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
   3210                 ContentValues values = new ContentValues();
   3211                 values.put(BaseColumns._ID, mDraftId);
   3212                 if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
   3213                     getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
   3214                 } else {
   3215                     getContentResolver().delete(mDraft.uri, null, null);
   3216                 }
   3217                 // This is not strictly necessary (since we should not try to
   3218                 // save the draft after calling this) but it ensures that if we
   3219                 // do save again for some reason we make a new draft rather than
   3220                 // trying to resave an expunged draft.
   3221                 mDraftId = UIProvider.INVALID_MESSAGE_ID;
   3222             }
   3223         }
   3224 
   3225         // Display a toast to let the user know
   3226         Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
   3227 
   3228         // This prevents the draft from being saved in onPause().
   3229         discardChanges();
   3230         mPerformedSendOrDiscard = true;
   3231         finish();
   3232     }
   3233 
   3234     private void saveIfNeeded() {
   3235         if (mAccount == null) {
   3236             // We have not chosen an account yet so there's no way that we can save. This is ok,
   3237             // though, since we are saving our state before AccountsActivity is activated. Thus, the
   3238             // user has not interacted with us yet and there is no real state to save.
   3239             return;
   3240         }
   3241 
   3242         if (shouldSave()) {
   3243             doSave(!mAddingAttachment /* show toast */);
   3244         }
   3245     }
   3246 
   3247     @Override
   3248     public void onAttachmentDeleted() {
   3249         mAttachmentsChanged = true;
   3250         // If we are showing any attachments, make sure we have an upper
   3251         // divider.
   3252         mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
   3253         updateSaveUi();
   3254     }
   3255 
   3256     @Override
   3257     public void onAttachmentAdded() {
   3258         mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
   3259         mAttachmentsView.focusLastAttachment();
   3260     }
   3261 
   3262     /**
   3263      * This is called any time one of our text fields changes.
   3264      */
   3265     @Override
   3266     public void afterTextChanged(Editable s) {
   3267         mTextChanged = true;
   3268         updateSaveUi();
   3269     }
   3270 
   3271     @Override
   3272     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
   3273         // Do nothing.
   3274     }
   3275 
   3276     @Override
   3277     public void onTextChanged(CharSequence s, int start, int before, int count) {
   3278         // Do nothing.
   3279     }
   3280 
   3281 
   3282     // There is a big difference between the text associated with an address changing
   3283     // to add the display name or to format properly and a recipient being added or deleted.
   3284     // Make sure we only notify of changes when a recipient has been added or deleted.
   3285     private class RecipientTextWatcher implements TextWatcher {
   3286         private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
   3287 
   3288         private RecipientEditTextView mView;
   3289 
   3290         private TextWatcher mListener;
   3291 
   3292         public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
   3293             mView = view;
   3294             mListener = listener;
   3295         }
   3296 
   3297         @Override
   3298         public void afterTextChanged(Editable s) {
   3299             if (hasChanged()) {
   3300                 mListener.afterTextChanged(s);
   3301             }
   3302         }
   3303 
   3304         private boolean hasChanged() {
   3305             String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
   3306             int totalCount = currRecips.length;
   3307             int totalPrevCount = 0;
   3308             for (Entry<String, Integer> entry : mContent.entrySet()) {
   3309                 totalPrevCount += entry.getValue();
   3310             }
   3311             if (totalCount != totalPrevCount) {
   3312                 return true;
   3313             }
   3314 
   3315             for (String recip : currRecips) {
   3316                 if (!mContent.containsKey(recip)) {
   3317                     return true;
   3318                 } else {
   3319                     int count = mContent.get(recip) - 1;
   3320                     if (count < 0) {
   3321                         return true;
   3322                     } else {
   3323                         mContent.put(recip, count);
   3324                     }
   3325                 }
   3326             }
   3327             return false;
   3328         }
   3329 
   3330         private String[] tokenizeRecips(String[] recips) {
   3331             // Tokenize them all and put them in the list.
   3332             String[] recipAddresses = new String[recips.length];
   3333             for (int i = 0; i < recips.length; i++) {
   3334                 recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
   3335             }
   3336             return recipAddresses;
   3337         }
   3338 
   3339         @Override
   3340         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
   3341             String[] recips = tokenizeRecips(getAddressesFromList(mView));
   3342             for (String recip : recips) {
   3343                 if (!mContent.containsKey(recip)) {
   3344                     mContent.put(recip, 1);
   3345                 } else {
   3346                     mContent.put(recip, (mContent.get(recip)) + 1);
   3347                 }
   3348             }
   3349         }
   3350 
   3351         @Override
   3352         public void onTextChanged(CharSequence s, int start, int before, int count) {
   3353             // Do nothing.
   3354         }
   3355     }
   3356 
   3357     public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
   3358         if (sTestSendOrSaveCallback != null && testCallback != null) {
   3359             throw new IllegalStateException("Attempting to register more than one test callback");
   3360         }
   3361         sTestSendOrSaveCallback = testCallback;
   3362     }
   3363 
   3364     @VisibleForTesting
   3365     protected ArrayList<Attachment> getAttachments() {
   3366         return mAttachmentsView.getAttachments();
   3367     }
   3368 
   3369     @Override
   3370     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
   3371         switch (id) {
   3372             case INIT_DRAFT_USING_REFERENCE_MESSAGE:
   3373                 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
   3374                         null, null);
   3375             case REFERENCE_MESSAGE_LOADER:
   3376                 return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
   3377                         null, null);
   3378             case LOADER_ACCOUNT_CURSOR:
   3379                 return new CursorLoader(this, MailAppProvider.getAccountsUri(),
   3380                         UIProvider.ACCOUNTS_PROJECTION, null, null, null);
   3381         }
   3382         return null;
   3383     }
   3384 
   3385     @Override
   3386     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
   3387         int id = loader.getId();
   3388         switch (id) {
   3389             case INIT_DRAFT_USING_REFERENCE_MESSAGE:
   3390                 if (data != null && data.moveToFirst()) {
   3391                     mRefMessage = new Message(data);
   3392                     Intent intent = getIntent();
   3393                     initFromRefMessage(mComposeMode);
   3394                     finishSetup(mComposeMode, intent, null);
   3395                     if (mComposeMode != FORWARD) {
   3396                         String to = intent.getStringExtra(EXTRA_TO);
   3397                         if (!TextUtils.isEmpty(to)) {
   3398                             mRefMessage.setTo(null);
   3399                             mRefMessage.setFrom(null);
   3400                             clearChangeListeners();
   3401                             mTo.append(to);
   3402                             initChangeListeners();
   3403                         }
   3404                     }
   3405                 } else {
   3406                     finish();
   3407                 }
   3408                 break;
   3409             case REFERENCE_MESSAGE_LOADER:
   3410                 // Only populate mRefMessage and leave other fields untouched.
   3411                 if (data != null && data.moveToFirst()) {
   3412                     mRefMessage = new Message(data);
   3413                 }
   3414                 finishSetup(mComposeMode, getIntent(), mInnerSavedState);
   3415                 break;
   3416             case LOADER_ACCOUNT_CURSOR:
   3417                 if (data != null && data.moveToFirst()) {
   3418                     // there are accounts now!
   3419                     Account account;
   3420                     final ArrayList<Account> accounts = new ArrayList<Account>();
   3421                     final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
   3422                     do {
   3423                         account = new Account(data);
   3424                         if (account.isAccountReady()) {
   3425                             initializedAccounts.add(account);
   3426                         }
   3427                         accounts.add(account);
   3428                     } while (data.moveToNext());
   3429                     if (initializedAccounts.size() > 0) {
   3430                         findViewById(R.id.wait).setVisibility(View.GONE);
   3431                         getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
   3432                         findViewById(R.id.compose).setVisibility(View.VISIBLE);
   3433                         mAccounts = initializedAccounts.toArray(
   3434                                 new Account[initializedAccounts.size()]);
   3435 
   3436                         finishCreate();
   3437                         invalidateOptionsMenu();
   3438                     } else {
   3439                         // Show "waiting"
   3440                         account = accounts.size() > 0 ? accounts.get(0) : null;
   3441                         showWaitFragment(account);
   3442                     }
   3443                 }
   3444                 break;
   3445         }
   3446     }
   3447 
   3448     private void showWaitFragment(Account account) {
   3449         WaitFragment fragment = getWaitFragment();
   3450         if (fragment != null) {
   3451             fragment.updateAccount(account);
   3452         } else {
   3453             findViewById(R.id.wait).setVisibility(View.VISIBLE);
   3454             replaceFragment(WaitFragment.newInstance(account, true),
   3455                     FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
   3456         }
   3457     }
   3458 
   3459     private WaitFragment getWaitFragment() {
   3460         return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
   3461     }
   3462 
   3463     private int replaceFragment(Fragment fragment, int transition, String tag) {
   3464         FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
   3465         fragmentTransaction.setTransition(transition);
   3466         fragmentTransaction.replace(R.id.wait, fragment, tag);
   3467         final int transactionId = fragmentTransaction.commitAllowingStateLoss();
   3468         return transactionId;
   3469     }
   3470 
   3471     @Override
   3472     public void onLoaderReset(Loader<Cursor> arg0) {
   3473         // Do nothing.
   3474     }
   3475 
   3476     @Override
   3477     public Context getActivityContext() {
   3478         return this;
   3479     }
   3480 }
   3481