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