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