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