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