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