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