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