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