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