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