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