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