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