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