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