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