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