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