ComposeActivity.java revision a954f9914a8fc6c65587db4f1d4660d60319d909
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.ClipData;
30import android.content.ContentResolver;
31import android.content.ContentValues;
32import android.content.Context;
33import android.content.CursorLoader;
34import android.content.DialogInterface;
35import android.content.Intent;
36import android.content.Loader;
37import android.content.pm.ActivityInfo;
38import android.content.res.Resources;
39import android.database.Cursor;
40import android.net.Uri;
41import android.os.Build;
42import android.os.Bundle;
43import android.os.Handler;
44import android.os.HandlerThread;
45import android.os.ParcelFileDescriptor;
46import android.os.Parcelable;
47import android.provider.BaseColumns;
48import android.text.Editable;
49import android.text.Html;
50import android.text.SpannableString;
51import android.text.Spanned;
52import android.text.TextUtils;
53import android.text.TextWatcher;
54import android.text.util.Rfc822Token;
55import android.text.util.Rfc822Tokenizer;
56import android.view.Gravity;
57import android.view.KeyEvent;
58import android.view.LayoutInflater;
59import android.view.Menu;
60import android.view.MenuInflater;
61import android.view.MenuItem;
62import android.view.View;
63import android.view.View.OnClickListener;
64import android.view.ViewGroup;
65import android.view.inputmethod.BaseInputConnection;
66import android.view.inputmethod.EditorInfo;
67import android.widget.ArrayAdapter;
68import android.widget.Button;
69import android.widget.EditText;
70import android.widget.TextView;
71import android.widget.Toast;
72
73import com.android.common.Rfc822Validator;
74import com.android.common.contacts.DataUsageStatUpdater;
75import com.android.ex.chips.RecipientEditTextView;
76import com.android.mail.MailIntentService;
77import com.android.mail.R;
78import com.android.mail.analytics.Analytics;
79import com.android.mail.browse.MessageHeaderView;
80import com.android.mail.compose.AttachmentsView.AttachmentAddedOrDeletedListener;
81import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
82import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
83import com.android.mail.compose.QuotedTextView.RespondInlineListener;
84import com.android.mail.providers.Account;
85import com.android.mail.providers.Address;
86import com.android.mail.providers.Attachment;
87import com.android.mail.providers.Folder;
88import com.android.mail.providers.MailAppProvider;
89import com.android.mail.providers.Message;
90import com.android.mail.providers.MessageModification;
91import com.android.mail.providers.ReplyFromAccount;
92import com.android.mail.providers.Settings;
93import com.android.mail.providers.UIProvider;
94import com.android.mail.providers.UIProvider.AccountCapabilities;
95import com.android.mail.providers.UIProvider.DraftType;
96import com.android.mail.ui.AttachmentTile.AttachmentPreview;
97import com.android.mail.ui.FeedbackEnabledActivity;
98import com.android.mail.ui.MailActivity;
99import com.android.mail.ui.WaitFragment;
100import com.android.mail.utils.AccountUtils;
101import com.android.mail.utils.AttachmentUtils;
102import com.android.mail.utils.ContentProviderTask;
103import com.android.mail.utils.LogTag;
104import com.android.mail.utils.LogUtils;
105import com.android.mail.utils.Utils;
106import com.google.common.annotations.VisibleForTesting;
107import com.google.common.collect.Lists;
108import com.google.common.collect.Sets;
109
110import java.io.FileNotFoundException;
111import java.io.IOException;
112import java.io.UnsupportedEncodingException;
113import java.net.URLDecoder;
114import java.util.ArrayList;
115import java.util.Arrays;
116import java.util.Collection;
117import java.util.HashMap;
118import java.util.HashSet;
119import java.util.List;
120import java.util.Map.Entry;
121import java.util.Set;
122import java.util.concurrent.ConcurrentHashMap;
123
124public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
125        RespondInlineListener, TextWatcher,
126        AttachmentAddedOrDeletedListener, OnAccountChangedListener,
127        LoaderManager.LoaderCallbacks<Cursor>, TextView.OnEditorActionListener,
128        FeedbackEnabledActivity {
129    // Identifiers for which type of composition this is
130    public static final int COMPOSE = -1;
131    public static final int REPLY = 0;
132    public static final int REPLY_ALL = 1;
133    public static final int FORWARD = 2;
134    public static final int EDIT_DRAFT = 3;
135
136    // Integer extra holding one of the above compose action
137    protected static final String EXTRA_ACTION = "action";
138
139    private static final String EXTRA_SHOW_CC = "showCc";
140    private static final String EXTRA_SHOW_BCC = "showBcc";
141    private static final String EXTRA_RESPONDED_INLINE = "respondedInline";
142    private static final String EXTRA_SAVE_ENABLED = "saveEnabled";
143
144    private static final String UTF8_ENCODING_NAME = "UTF-8";
145
146    private static final String MAIL_TO = "mailto";
147
148    private static final String EXTRA_SUBJECT = "subject";
149
150    private static final String EXTRA_BODY = "body";
151
152    /**
153     * Expected to be html formatted text.
154     */
155    private static final String EXTRA_QUOTED_TEXT = "quotedText";
156
157    protected static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
158
159    private static final String EXTRA_ATTACHMENT_PREVIEWS = "attachmentPreviews";
160
161    // Extra that we can get passed from other activities
162    @VisibleForTesting
163    protected static final String EXTRA_TO = "to";
164    private static final String EXTRA_CC = "cc";
165    private static final String EXTRA_BCC = "bcc";
166
167    /**
168     * An optional extra containing a {@link ContentValues} of values to be added to
169     * {@link SendOrSaveMessage#mValues}.
170     */
171    public static final String EXTRA_VALUES = "extra-values";
172
173    // List of all the fields
174    static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC,
175            EXTRA_QUOTED_TEXT };
176
177    private static SendOrSaveCallback sTestSendOrSaveCallback = null;
178    // Map containing information about requests to create new messages, and the id of the
179    // messages that were the result of those requests.
180    //
181    // This map is used when the activity that initiated the save a of a new message, is killed
182    // before the save has completed (and when we know the id of the newly created message).  When
183    // a save is completed, the service that is running in the background, will update the map
184    //
185    // When a new ComposeActivity instance is created, it will attempt to use the information in
186    // the previously instantiated map.  If ComposeActivity.onCreate() is called, with a bundle
187    // (restoring data from a previous instance), and the map hasn't been created, we will attempt
188    // to populate the map with data stored in shared preferences.
189    // FIXME: values in this map are never read.
190    private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
191    /**
192     * Notifies the {@code Activity} that the caller is an Email
193     * {@code Activity}, so that the back behavior may be modified accordingly.
194     *
195     * @see #onAppUpPressed
196     */
197    public static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
198
199    public static final String EXTRA_ATTACHMENTS = "attachments";
200
201    /** If set, we will clear notifications for this folder. */
202    public static final String EXTRA_NOTIFICATION_FOLDER = "extra-notification-folder";
203
204    //  If this is a reply/forward then this extra will hold the original message
205    private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
206    // If this is a reply/forward then this extra will hold a uri we must query
207    // to get the original message.
208    protected static final String EXTRA_IN_REFERENCE_TO_MESSAGE_URI = "in-reference-to-message-uri";
209    // If this is an action to edit an existing draft message, this extra will hold the
210    // draft message
211    private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
212    private static final String END_TOKEN = ", ";
213    private static final String LOG_TAG = LogTag.getLogTag();
214    // Request numbers for activities we start
215    private static final int RESULT_PICK_ATTACHMENT = 1;
216    private static final int RESULT_CREATE_ACCOUNT = 2;
217    // TODO(mindyp) set mime-type for auto send?
218    public static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
219
220    private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
221    private static final String EXTRA_REQUEST_ID = "requestId";
222    private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
223    private static final String EXTRA_FOCUS_SELECTION_END = "focusSelectionEnd";
224    private static final String EXTRA_MESSAGE = "extraMessage";
225    private static final int REFERENCE_MESSAGE_LOADER = 0;
226    private static final int LOADER_ACCOUNT_CURSOR = 1;
227    private static final int INIT_DRAFT_USING_REFERENCE_MESSAGE = 2;
228    private static final String EXTRA_SELECTED_ACCOUNT = "selectedAccount";
229    private static final String TAG_WAIT = "wait-fragment";
230    private static final String MIME_TYPE_ALL = "*/*";
231    private static final String MIME_TYPE_PHOTO = "image/*";
232
233    private static final String KEY_INNER_SAVED_STATE = "compose_state";
234
235    /**
236     * A single thread for running tasks in the background.
237     */
238    private Handler mSendSaveTaskHandler = null;
239    private RecipientEditTextView mTo;
240    private RecipientEditTextView mCc;
241    private RecipientEditTextView mBcc;
242    private Button mCcBccButton;
243    private CcBccView mCcBccView;
244    private AttachmentsView mAttachmentsView;
245    protected Account mAccount;
246    protected ReplyFromAccount mReplyFromAccount;
247    private Settings mCachedSettings;
248    private Rfc822Validator mValidator;
249    private TextView mSubject;
250
251    private ComposeModeAdapter mComposeModeAdapter;
252    protected int mComposeMode = -1;
253    private boolean mForward;
254    private QuotedTextView mQuotedTextView;
255    protected EditText mBodyView;
256    private View mFromStatic;
257    private TextView mFromStaticText;
258    private View mFromSpinnerWrapper;
259    @VisibleForTesting
260    protected FromAddressSpinner mFromSpinner;
261    private boolean mAddingAttachment;
262    private boolean mAttachmentsChanged;
263    private boolean mTextChanged;
264    private boolean mReplyFromChanged;
265    private MenuItem mSave;
266    @VisibleForTesting
267    protected Message mRefMessage;
268    private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
269    private Message mDraft;
270    private ReplyFromAccount mDraftAccount;
271    private Object mDraftLock = new Object();
272    private View mAddAttachmentsButton;
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        mAddAttachmentsButton = findViewById(R.id.add_attachment);
1127        if (mAddAttachmentsButton != null) {
1128            mAddAttachmentsButton.setOnClickListener(this);
1129        }
1130        mTo = (RecipientEditTextView) findViewById(R.id.to);
1131        mTo.setTokenizer(new Rfc822Tokenizer());
1132        mCc = (RecipientEditTextView) findViewById(R.id.cc);
1133        mCc.setTokenizer(new Rfc822Tokenizer());
1134        mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
1135        mBcc.setTokenizer(new Rfc822Tokenizer());
1136        // TODO: add special chips text change watchers before adding
1137        // this as a text changed watcher to the to, cc, bcc fields.
1138        mSubject = (TextView) findViewById(R.id.subject);
1139        mSubject.setOnEditorActionListener(this);
1140        mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
1141        mQuotedTextView.setRespondInlineListener(this);
1142        mBodyView = (EditText) findViewById(R.id.body);
1143        mFromStatic = findViewById(R.id.static_from_content);
1144        mFromStaticText = (TextView) findViewById(R.id.from_account_name);
1145        mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
1146        mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
1147    }
1148
1149    @Override
1150    public boolean onEditorAction(TextView view, int action, KeyEvent keyEvent) {
1151        if (action == EditorInfo.IME_ACTION_DONE) {
1152            focusBody();
1153            return true;
1154        }
1155        return false;
1156    }
1157
1158    protected TextView getBody() {
1159        return mBodyView;
1160    }
1161
1162    @VisibleForTesting
1163    public Account getFromAccount() {
1164        return mReplyFromAccount != null && mReplyFromAccount.account != null ?
1165                mReplyFromAccount.account : mAccount;
1166    }
1167
1168    private void clearChangeListeners() {
1169        mSubject.removeTextChangedListener(this);
1170        mBodyView.removeTextChangedListener(this);
1171        mTo.removeTextChangedListener(mToListener);
1172        mCc.removeTextChangedListener(mCcListener);
1173        mBcc.removeTextChangedListener(mBccListener);
1174        mFromSpinner.setOnAccountChangedListener(null);
1175        mAttachmentsView.setAttachmentChangesListener(null);
1176    }
1177
1178    // Now that the message has been initialized from any existing draft or
1179    // ref message data, set up listeners for any changes that occur to the
1180    // message.
1181    private void initChangeListeners() {
1182        // Make sure we only add text changed listeners once!
1183        clearChangeListeners();
1184        mSubject.addTextChangedListener(this);
1185        mBodyView.addTextChangedListener(this);
1186        if (mToListener == null) {
1187            mToListener = new RecipientTextWatcher(mTo, this);
1188        }
1189        mTo.addTextChangedListener(mToListener);
1190        if (mCcListener == null) {
1191            mCcListener = new RecipientTextWatcher(mCc, this);
1192        }
1193        mCc.addTextChangedListener(mCcListener);
1194        if (mBccListener == null) {
1195            mBccListener = new RecipientTextWatcher(mBcc, this);
1196        }
1197        mBcc.addTextChangedListener(mBccListener);
1198        mFromSpinner.setOnAccountChangedListener(this);
1199        mAttachmentsView.setAttachmentChangesListener(this);
1200    }
1201
1202    private void initActionBar() {
1203        LogUtils.d(LOG_TAG, "initializing action bar in ComposeActivity");
1204        ActionBar actionBar = getActionBar();
1205        if (actionBar == null) {
1206            return;
1207        }
1208        if (mComposeMode == ComposeActivity.COMPOSE) {
1209            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
1210            actionBar.setTitle(R.string.compose);
1211        } else {
1212            actionBar.setTitle(null);
1213            if (mComposeModeAdapter == null) {
1214                mComposeModeAdapter = new ComposeModeAdapter(this);
1215            }
1216            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
1217            actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
1218            switch (mComposeMode) {
1219                case ComposeActivity.REPLY:
1220                    actionBar.setSelectedNavigationItem(0);
1221                    break;
1222                case ComposeActivity.REPLY_ALL:
1223                    actionBar.setSelectedNavigationItem(1);
1224                    break;
1225                case ComposeActivity.FORWARD:
1226                    actionBar.setSelectedNavigationItem(2);
1227                    break;
1228            }
1229        }
1230        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
1231                ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
1232        actionBar.setHomeButtonEnabled(true);
1233    }
1234
1235    private void initFromRefMessage(int action) {
1236        setFieldsFromRefMessage(action);
1237
1238        // Check if To: address and email body needs to be prefilled based on extras.
1239        // This is used for reporting rendering feedback.
1240        if (MessageHeaderView.ENABLE_REPORT_RENDERING_PROBLEM) {
1241            Intent intent = getIntent();
1242            if (intent.getExtras() != null) {
1243                String toAddresses = intent.getStringExtra(EXTRA_TO);
1244                if (toAddresses != null) {
1245                    addToAddresses(Arrays.asList(TextUtils.split(toAddresses, ",")));
1246                }
1247                String body = intent.getStringExtra(EXTRA_BODY);
1248                if (body != null) {
1249                    setBody(body, false /* withSignature */);
1250                }
1251            }
1252        }
1253
1254        if (mRefMessage != null) {
1255            // CC field only gets populated when doing REPLY_ALL.
1256            // BCC never gets auto-populated, unless the user is editing
1257            // a draft with one.
1258            if (!TextUtils.isEmpty(mCc.getText()) && action == REPLY_ALL) {
1259                mCcBccView.show(false, true, false);
1260            }
1261        }
1262        updateHideOrShowCcBcc();
1263    }
1264
1265    private void setFieldsFromRefMessage(int action) {
1266        setSubject(mRefMessage, action);
1267        // Setup recipients
1268        if (action == FORWARD) {
1269            mForward = true;
1270        }
1271        initRecipientsFromRefMessage(mRefMessage, action);
1272        initQuotedTextFromRefMessage(mRefMessage, action);
1273        if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
1274            initAttachments(mRefMessage);
1275        }
1276    }
1277
1278    private void initFromDraftMessage(Message message) {
1279        LogUtils.d(LOG_TAG, "Intializing draft from previous draft message: %s", message);
1280
1281        mDraft = message;
1282        mDraftId = message.id;
1283        mSubject.setText(message.subject);
1284        mForward = message.draftType == UIProvider.DraftType.FORWARD;
1285        final List<String> toAddresses = Arrays.asList(message.getToAddressesUnescaped());
1286        addToAddresses(toAddresses);
1287        addCcAddresses(Arrays.asList(message.getCcAddressesUnescaped()), toAddresses);
1288        addBccAddresses(Arrays.asList(message.getBccAddressesUnescaped()));
1289        if (message.hasAttachments) {
1290            List<Attachment> attachments = message.getAttachments();
1291            for (Attachment a : attachments) {
1292                addAttachmentAndUpdateView(a);
1293            }
1294        }
1295        int quotedTextIndex = message.appendRefMessageContent ?
1296                message.quotedTextOffset : -1;
1297        // Set the body
1298        CharSequence quotedText = null;
1299        if (!TextUtils.isEmpty(message.bodyHtml)) {
1300            CharSequence htmlText = "";
1301            if (quotedTextIndex > -1) {
1302                // Find the offset in the htmltext of the actual quoted text and strip it out.
1303                quotedTextIndex = QuotedTextView.findQuotedTextIndex(message.bodyHtml);
1304                if (quotedTextIndex > -1) {
1305                    htmlText = Utils.convertHtmlToPlainText(message.bodyHtml.substring(0,
1306                            quotedTextIndex));
1307                    quotedText = message.bodyHtml.subSequence(quotedTextIndex,
1308                            message.bodyHtml.length());
1309                }
1310            } else {
1311                htmlText = Utils.convertHtmlToPlainText(message.bodyHtml);
1312            }
1313            mBodyView.setText(htmlText);
1314        } else {
1315            final String body = message.bodyText;
1316            final CharSequence bodyText = !TextUtils.isEmpty(body) ?
1317                    (quotedTextIndex > -1 ?
1318                            message.bodyText.substring(0, quotedTextIndex) : message.bodyText)
1319                            : "";
1320            if (quotedTextIndex > -1) {
1321                quotedText = !TextUtils.isEmpty(body) ? message.bodyText.substring(quotedTextIndex)
1322                        : null;
1323            }
1324            mBodyView.setText(bodyText);
1325        }
1326        if (quotedTextIndex > -1 && quotedText != null) {
1327            mQuotedTextView.setQuotedTextFromDraft(quotedText, mForward);
1328        }
1329    }
1330
1331    /**
1332     * Fill all the widgets with the content found in the Intent Extra, if any.
1333     * Also apply the same style to all widgets. Note: if initFromExtras is
1334     * called as a result of switching between reply, reply all, and forward per
1335     * the latest revision of Gmail, and the user has already made changes to
1336     * attachments on a previous incarnation of the message (as a reply, reply
1337     * all, or forward), the original attachments from the message will not be
1338     * re-instantiated. The user's changes will be respected. This follows the
1339     * web gmail interaction.
1340     * @return {@code true} if the activity should not call {@link #finishSetup}.
1341     */
1342    public boolean initFromExtras(Intent intent) {
1343        // If we were invoked with a SENDTO intent, the value
1344        // should take precedence
1345        final Uri dataUri = intent.getData();
1346        if (dataUri != null) {
1347            if (MAIL_TO.equals(dataUri.getScheme())) {
1348                initFromMailTo(dataUri.toString());
1349            } else {
1350                if (!mAccount.composeIntentUri.equals(dataUri)) {
1351                    String toText = dataUri.getSchemeSpecificPart();
1352                    if (toText != null) {
1353                        mTo.setText("");
1354                        addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
1355                    }
1356                }
1357            }
1358        }
1359
1360        String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1361        if (extraStrings != null) {
1362            addToAddresses(Arrays.asList(extraStrings));
1363        }
1364        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1365        if (extraStrings != null) {
1366            addCcAddresses(Arrays.asList(extraStrings), null);
1367        }
1368        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1369        if (extraStrings != null) {
1370            addBccAddresses(Arrays.asList(extraStrings));
1371        }
1372
1373        String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1374        if (extraString != null) {
1375            mSubject.setText(extraString);
1376        }
1377
1378        for (String extra : ALL_EXTRAS) {
1379            if (intent.hasExtra(extra)) {
1380                String value = intent.getStringExtra(extra);
1381                if (EXTRA_TO.equals(extra)) {
1382                    addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
1383                } else if (EXTRA_CC.equals(extra)) {
1384                    addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
1385                } else if (EXTRA_BCC.equals(extra)) {
1386                    addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
1387                } else if (EXTRA_SUBJECT.equals(extra)) {
1388                    mSubject.setText(value);
1389                } else if (EXTRA_BODY.equals(extra)) {
1390                    setBody(value, true /* with signature */);
1391                } else if (EXTRA_QUOTED_TEXT.equals(extra)) {
1392                    initQuotedText(value, true /* shouldQuoteText */);
1393                }
1394            }
1395        }
1396
1397        Bundle extras = intent.getExtras();
1398        if (extras != null) {
1399            CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
1400            if (text != null) {
1401                setBody(text, true /* with signature */);
1402            }
1403
1404            // TODO - support EXTRA_HTML_TEXT
1405        }
1406
1407        mExtraValues = intent.getParcelableExtra(EXTRA_VALUES);
1408        if (mExtraValues != null) {
1409            LogUtils.d(LOG_TAG, "Launched with extra values: %s", mExtraValues.toString());
1410            initExtraValues(mExtraValues);
1411            return true;
1412        }
1413
1414        return false;
1415    }
1416
1417    protected void initExtraValues(ContentValues extraValues) {
1418        // DO NOTHING - Gmail will override
1419    }
1420
1421
1422    @VisibleForTesting
1423    protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
1424        // TODO: handle the case where there are spaces in the display name as
1425        // well as the email such as "Guy with spaces <guy+with+spaces@gmail.com>"
1426        // as they could be encoded ambiguously.
1427        // Since URLDecode.decode changes + into ' ', and + is a valid
1428        // email character, we need to find/ replace these ourselves before
1429        // decoding.
1430        try {
1431            return URLDecoder.decode(replacePlus(s), UTF8_ENCODING_NAME);
1432        } catch (IllegalArgumentException e) {
1433            if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1434                LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), s);
1435            } else {
1436                LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
1437            }
1438            return null;
1439        }
1440    }
1441
1442    /**
1443     * Replaces all occurrences of '+' with "%2B", to prevent URLDecode.decode from
1444     * changing '+' into ' '
1445     *
1446     * @param toReplace Input string
1447     * @return The string with all "+" characters replaced with "%2B"
1448     */
1449    private static String replacePlus(String toReplace) {
1450        return toReplace.replace("+", "%2B");
1451    }
1452
1453    /**
1454     * Initialize the compose view from a String representing a mailTo uri.
1455     * @param mailToString The uri as a string.
1456     */
1457    public void initFromMailTo(String mailToString) {
1458        // We need to disguise this string as a URI in order to parse it
1459        // TODO:  Remove this hack when http://b/issue?id=1445295 gets fixed
1460        Uri uri = Uri.parse("foo://" + mailToString);
1461        int index = mailToString.indexOf("?");
1462        int length = "mailto".length() + 1;
1463        String to;
1464        try {
1465            // Extract the recipient after mailto:
1466            if (index == -1) {
1467                to = decodeEmailInUri(mailToString.substring(length));
1468            } else {
1469                to = decodeEmailInUri(mailToString.substring(length, index));
1470            }
1471            if (!TextUtils.isEmpty(to)) {
1472                addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
1473            }
1474        } catch (UnsupportedEncodingException e) {
1475            if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
1476                LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
1477            } else {
1478                LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
1479            }
1480        }
1481
1482        List<String> cc = uri.getQueryParameters("cc");
1483        addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
1484
1485        List<String> otherTo = uri.getQueryParameters("to");
1486        addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
1487
1488        List<String> bcc = uri.getQueryParameters("bcc");
1489        addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
1490
1491        List<String> subject = uri.getQueryParameters("subject");
1492        if (subject.size() > 0) {
1493            try {
1494                mSubject.setText(URLDecoder.decode(replacePlus(subject.get(0)),
1495                        UTF8_ENCODING_NAME));
1496            } catch (UnsupportedEncodingException e) {
1497                LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
1498                        e.getMessage(), subject);
1499            }
1500        }
1501
1502        List<String> body = uri.getQueryParameters("body");
1503        if (body.size() > 0) {
1504            try {
1505                setBody(URLDecoder.decode(replacePlus(body.get(0)), UTF8_ENCODING_NAME),
1506                        true /* with signature */);
1507            } catch (UnsupportedEncodingException e) {
1508                LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
1509            }
1510        }
1511    }
1512
1513    @VisibleForTesting
1514    protected void initAttachments(Message refMessage) {
1515        addAttachments(refMessage.getAttachments());
1516    }
1517
1518    public long addAttachments(List<Attachment> attachments) {
1519        long size = 0;
1520        AttachmentFailureException error = null;
1521        for (Attachment a : attachments) {
1522            try {
1523                size += mAttachmentsView.addAttachment(mAccount, a);
1524            } catch (AttachmentFailureException e) {
1525                error = e;
1526            }
1527        }
1528        if (error != null) {
1529            LogUtils.e(LOG_TAG, error, "Error adding attachment");
1530            if (attachments.size() > 1) {
1531                showAttachmentTooBigToast(R.string.too_large_to_attach_multiple);
1532            } else {
1533                showAttachmentTooBigToast(error.getErrorRes());
1534            }
1535        }
1536        return size;
1537    }
1538
1539    /**
1540     * When an attachment is too large to be added to a message, show a toast.
1541     * This method also updates the position of the toast so that it is shown
1542     * clearly above they keyboard if it happens to be open.
1543     */
1544    private void showAttachmentTooBigToast(int errorRes) {
1545        String maxSize = AttachmentUtils.convertToHumanReadableSize(
1546                getApplicationContext(), mAccount.settings.getMaxAttachmentSize());
1547        showErrorToast(getString(errorRes, maxSize));
1548    }
1549
1550    private void showErrorToast(String message) {
1551        Toast t = Toast.makeText(this, message, Toast.LENGTH_LONG);
1552        t.setText(message);
1553        t.setGravity(Gravity.CENTER_HORIZONTAL, 0,
1554                getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset));
1555        t.show();
1556    }
1557
1558    private void initAttachmentsFromIntent(Intent intent) {
1559        Bundle extras = intent.getExtras();
1560        if (extras == null) {
1561            extras = Bundle.EMPTY;
1562        }
1563        final String action = intent.getAction();
1564        if (!mAttachmentsChanged) {
1565            long totalSize = 0;
1566            if (extras.containsKey(EXTRA_ATTACHMENTS)) {
1567                String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
1568                for (String uriString : uris) {
1569                    final Uri uri = Uri.parse(uriString);
1570                    long size = 0;
1571                    try {
1572                        final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1573                        size = mAttachmentsView.addAttachment(mAccount, a);
1574
1575                        Analytics.getInstance().sendEvent("send_intent_attachment",
1576                                Utils.normalizeMimeType(a.getContentType()), null, size);
1577
1578                    } catch (AttachmentFailureException e) {
1579                        LogUtils.e(LOG_TAG, e, "Error adding attachment");
1580                        showAttachmentTooBigToast(e.getErrorRes());
1581                    }
1582                    totalSize += size;
1583                }
1584            }
1585            if (extras.containsKey(Intent.EXTRA_STREAM)) {
1586                if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
1587                    ArrayList<Parcelable> uris = extras
1588                            .getParcelableArrayList(Intent.EXTRA_STREAM);
1589                    ArrayList<Attachment> attachments = new ArrayList<Attachment>();
1590                    for (Parcelable uri : uris) {
1591                        try {
1592                            final Attachment a = mAttachmentsView.generateLocalAttachment(
1593                                    (Uri) uri);
1594                            attachments.add(a);
1595
1596                            Analytics.getInstance().sendEvent("send_intent_attachment",
1597                                    Utils.normalizeMimeType(a.getContentType()), null, a.size);
1598
1599                        } catch (AttachmentFailureException e) {
1600                            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1601                            String maxSize = AttachmentUtils.convertToHumanReadableSize(
1602                                    getApplicationContext(),
1603                                    mAccount.settings.getMaxAttachmentSize());
1604                            showErrorToast(getString
1605                                    (R.string.generic_attachment_problem, maxSize));
1606                        }
1607                    }
1608                    totalSize += addAttachments(attachments);
1609                } else {
1610                    final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
1611                    long size = 0;
1612                    try {
1613                        final Attachment a = mAttachmentsView.generateLocalAttachment(uri);
1614                        size = mAttachmentsView.addAttachment(mAccount, a);
1615
1616                        Analytics.getInstance().sendEvent("send_intent_attachment",
1617                                Utils.normalizeMimeType(a.getContentType()), null, size);
1618
1619                    } catch (AttachmentFailureException e) {
1620                        LogUtils.e(LOG_TAG, e, "Error adding attachment");
1621                        showAttachmentTooBigToast(e.getErrorRes());
1622                    }
1623                    totalSize += size;
1624                }
1625            }
1626
1627            if (totalSize > 0) {
1628                mAttachmentsChanged = true;
1629                updateSaveUi();
1630
1631                Analytics.getInstance().sendEvent("send_intent_with_attachments",
1632                        Integer.toString(getAttachments().size()), null, totalSize);
1633            }
1634        }
1635    }
1636
1637    protected void initQuotedText(CharSequence quotedText, boolean shouldQuoteText) {
1638        mQuotedTextView.setQuotedTextFromHtml(quotedText, shouldQuoteText);
1639        mShowQuotedText = true;
1640    }
1641
1642    private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1643        if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
1644            mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1645        }
1646    }
1647
1648    private void updateHideOrShowCcBcc() {
1649        // Its possible there is a menu item OR a button.
1650        boolean ccVisible = mCcBccView.isCcVisible();
1651        boolean bccVisible = mCcBccView.isBccVisible();
1652        if (mCcBccButton != null) {
1653            if (!ccVisible || !bccVisible) {
1654                mCcBccButton.setVisibility(View.VISIBLE);
1655                mCcBccButton.setText(getString(!ccVisible ? R.string.add_cc_label
1656                        : R.string.add_bcc_label));
1657            } else {
1658                mCcBccButton.setVisibility(View.INVISIBLE);
1659            }
1660        }
1661    }
1662
1663    private void showCcBcc(Bundle state) {
1664        if (state != null && state.containsKey(EXTRA_SHOW_CC)) {
1665            boolean showCc = state.getBoolean(EXTRA_SHOW_CC);
1666            boolean showBcc = state.getBoolean(EXTRA_SHOW_BCC);
1667            if (showCc || showBcc) {
1668                mCcBccView.show(false, showCc, showBcc);
1669            }
1670        }
1671    }
1672
1673    /**
1674     * Add attachment and update the compose area appropriately.
1675     * @param data
1676     */
1677    public void addAttachmentAndUpdateView(Intent data) {
1678        if (data == null) {
1679            return;
1680        }
1681
1682        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
1683            final ClipData clipData = data.getClipData();
1684            if (clipData != null) {
1685                for (int i = 0, size = clipData.getItemCount(); i < size; i++) {
1686                    addAttachmentAndUpdateView(clipData.getItemAt(i).getUri());
1687                }
1688                return;
1689            }
1690        }
1691
1692        addAttachmentAndUpdateView(data.getData());
1693    }
1694
1695    public void addAttachmentAndUpdateView(Uri contentUri) {
1696        if (contentUri == null) {
1697            return;
1698        }
1699        try {
1700            addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1701        } catch (AttachmentFailureException e) {
1702            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1703            showErrorToast(getResources().getString(
1704                    e.getErrorRes(),
1705                    AttachmentUtils.convertToHumanReadableSize(
1706                            getApplicationContext(), mAccount.settings.getMaxAttachmentSize())));
1707        }
1708    }
1709
1710    public void addAttachmentAndUpdateView(Attachment attachment) {
1711        try {
1712            long size = mAttachmentsView.addAttachment(mAccount, attachment);
1713            if (size > 0) {
1714                mAttachmentsChanged = true;
1715                updateSaveUi();
1716            }
1717        } catch (AttachmentFailureException e) {
1718            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1719            showAttachmentTooBigToast(e.getErrorRes());
1720        }
1721    }
1722
1723    void initRecipientsFromRefMessage(Message refMessage, int action) {
1724        // Don't populate the address if this is a forward.
1725        if (action == ComposeActivity.FORWARD) {
1726            return;
1727        }
1728        initReplyRecipients(refMessage, action);
1729    }
1730
1731    // TODO: This should be private.  This method shouldn't be used by ComposeActivityTests, as
1732    // it doesn't setup the state of the activity correctly
1733    @VisibleForTesting
1734    void initReplyRecipients(final Message refMessage, final int action) {
1735        String[] sentToAddresses = refMessage.getToAddressesUnescaped();
1736        final Collection<String> toAddresses;
1737        final String[] replyToAddresses = refMessage.getReplyToAddressesUnescaped();
1738        String replyToAddress = replyToAddresses.length > 0 ? replyToAddresses[0] : null;
1739        final String[] fromAddresses = refMessage.getFromAddressesUnescaped();
1740        final String fromAddress = fromAddresses.length > 0 ? fromAddresses[0] : null;
1741
1742        // If there is no reply to address, the reply to address is the sender.
1743        if (TextUtils.isEmpty(replyToAddress)) {
1744            replyToAddress = fromAddress;
1745        }
1746
1747        // If this is a reply, the Cc list is empty. If this is a reply-all, the
1748        // Cc list is the union of the To and Cc recipients of the original
1749        // message, excluding the current user's email address and any addresses
1750        // already on the To list.
1751        if (action == ComposeActivity.REPLY) {
1752            toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
1753            addToAddresses(toAddresses);
1754        } else if (action == ComposeActivity.REPLY_ALL) {
1755            final Set<String> ccAddresses = Sets.newHashSet();
1756            toAddresses = initToRecipients(fromAddress, replyToAddress, sentToAddresses);
1757            addToAddresses(toAddresses);
1758            addRecipients(ccAddresses, sentToAddresses);
1759            addRecipients(ccAddresses, refMessage.getCcAddressesUnescaped());
1760            addCcAddresses(ccAddresses, toAddresses);
1761        }
1762    }
1763
1764    private void addToAddresses(Collection<String> addresses) {
1765        addAddressesToList(addresses, mTo);
1766    }
1767
1768    private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
1769        addCcAddressesToList(tokenizeAddressList(addresses),
1770                toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
1771    }
1772
1773    private void addBccAddresses(Collection<String> addresses) {
1774        addAddressesToList(addresses, mBcc);
1775    }
1776
1777    @VisibleForTesting
1778    protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1779            List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1780        String address;
1781
1782        if (compareToList == null) {
1783            for (Rfc822Token[] tokens : addresses) {
1784                for (int i = 0; i < tokens.length; i++) {
1785                    address = tokens[i].toString();
1786                    list.append(address + END_TOKEN);
1787                }
1788            }
1789        } else {
1790            HashSet<String> compareTo = convertToHashSet(compareToList);
1791            for (Rfc822Token[] tokens : addresses) {
1792                for (int i = 0; i < tokens.length; i++) {
1793                    address = tokens[i].toString();
1794                    // Check if this is a duplicate:
1795                    if (!compareTo.contains(tokens[i].getAddress())) {
1796                        // Get the address here
1797                        list.append(address + END_TOKEN);
1798                    }
1799                }
1800            }
1801        }
1802    }
1803
1804    private static HashSet<String> convertToHashSet(final List<Rfc822Token[]> list) {
1805        final HashSet<String> hash = new HashSet<String>();
1806        for (final Rfc822Token[] tokens : list) {
1807            for (int i = 0; i < tokens.length; i++) {
1808                hash.add(tokens[i].getAddress());
1809            }
1810        }
1811        return hash;
1812    }
1813
1814    protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1815        @VisibleForTesting
1816        List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1817
1818        for (String address: addresses) {
1819            tokenized.add(Rfc822Tokenizer.tokenize(address));
1820        }
1821        return tokenized;
1822    }
1823
1824    @VisibleForTesting
1825    void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1826        for (String address : addresses) {
1827            addAddressToList(address, list);
1828        }
1829    }
1830
1831    private static void addAddressToList(final String address, final RecipientEditTextView list) {
1832        if (address == null || list == null)
1833            return;
1834
1835        final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1836
1837        for (int i = 0; i < tokens.length; i++) {
1838            list.append(tokens[i] + END_TOKEN);
1839        }
1840    }
1841
1842    @VisibleForTesting
1843    protected Collection<String> initToRecipients(final String fullSenderAddress,
1844            final String replyToAddress, final String[] inToAddresses) {
1845        // The To recipient is the reply-to address specified in the original
1846        // message, unless it is:
1847        // the current user OR a custom from of the current user, in which case
1848        // it's the To recipient list of the original message.
1849        // OR missing, in which case use the sender of the original message
1850        Set<String> toAddresses = Sets.newHashSet();
1851        if (!TextUtils.isEmpty(replyToAddress) && !recipientMatchesThisAccount(replyToAddress)) {
1852            toAddresses.add(replyToAddress);
1853        } else {
1854            // In this case, the user is replying to a message in which their
1855            // current account or one of their custom from addresses is the only
1856            // recipient and they sent the original message.
1857            if (inToAddresses.length == 1 && recipientMatchesThisAccount(fullSenderAddress)
1858                    && recipientMatchesThisAccount(inToAddresses[0])) {
1859                toAddresses.add(inToAddresses[0]);
1860                return toAddresses;
1861            }
1862            // This happens if the user replies to a message they originally
1863            // wrote. In this case, "reply" really means "re-send," so we
1864            // target the original recipients. This works as expected even
1865            // if the user sent the original message to themselves.
1866            for (String address : inToAddresses) {
1867                if (!recipientMatchesThisAccount(address)) {
1868                    toAddresses.add(address);
1869                }
1870            }
1871        }
1872        return toAddresses;
1873    }
1874
1875    private void addRecipients(final Set<String> recipients, final String[] addresses) {
1876        for (final String email : addresses) {
1877            // Do not add this account, or any of its custom from addresses, to
1878            // the list of recipients.
1879            final String recipientAddress = Address.getEmailAddress(email).getAddress();
1880            if (!recipientMatchesThisAccount(recipientAddress)) {
1881                recipients.add(email.replace("\"\"", ""));
1882            }
1883        }
1884    }
1885
1886    /**
1887     * A recipient matches this account if it has the same address as the
1888     * currently selected account OR one of the custom from addresses associated
1889     * with the currently selected account.
1890     * @param recipientAddress address we are comparing with the currently selected account
1891     * @return
1892     */
1893    protected boolean recipientMatchesThisAccount(String recipientAddress) {
1894        return ReplyFromAccount.matchesAccountOrCustomFrom(mAccount, recipientAddress,
1895                        mAccount.getReplyFroms());
1896    }
1897
1898    /**
1899     * Returns a formatted subject string with the appropriate prefix for the action type.
1900     * E.g., "FWD: " is prepended if action is {@link ComposeActivity#FORWARD}.
1901     */
1902    public static String buildFormattedSubject(final Resources res, final String subject,
1903            final int action) {
1904        final String prefix;
1905        final String correctedSubject;
1906        if (action == ComposeActivity.COMPOSE) {
1907            prefix = "";
1908        } else if (action == ComposeActivity.FORWARD) {
1909            prefix = res.getString(R.string.forward_subject_label);
1910        } else {
1911            prefix = res.getString(R.string.reply_subject_label);
1912        }
1913
1914        // Don't duplicate the prefix
1915        if (!TextUtils.isEmpty(subject)
1916                && subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1917            correctedSubject = subject;
1918        } else {
1919            final String subjectOrNoSubject = TextUtils.isEmpty(subject) ?
1920                    res.getString(R.string.no_subject) :
1921                    subject;
1922
1923            correctedSubject =
1924                    res.getString(R.string.formatted_subject, prefix, subjectOrNoSubject);
1925        }
1926
1927        return correctedSubject;
1928    }
1929
1930    private void setSubject(Message refMessage, int action) {
1931        mSubject.setText(buildFormattedSubject(getResources(), refMessage.subject, action));
1932    }
1933
1934    private void initRecipients() {
1935        setupRecipients(mTo);
1936        setupRecipients(mCc);
1937        setupRecipients(mBcc);
1938    }
1939
1940    private void setupRecipients(RecipientEditTextView view) {
1941        view.setAdapter(new RecipientAdapter(this, mAccount));
1942        if (mValidator == null) {
1943            final String accountName = mAccount.getEmailAddress();
1944            int offset = accountName.indexOf("@") + 1;
1945            String account = accountName;
1946            if (offset > 0) {
1947                account = account.substring(offset);
1948            }
1949            mValidator = new Rfc822Validator(account);
1950        }
1951        view.setValidator(mValidator);
1952    }
1953
1954    @Override
1955    public void onClick(View v) {
1956        final int id = v.getId();
1957        if (id == R.id.add_cc_bcc) {
1958            // Verify that cc/ bcc aren't showing.
1959            // Animate in cc/bcc.
1960            showCcBccViews();
1961        } else if (id == R.id.add_attachment) {
1962            doAttach(Utils.isRunningKitkatOrLater() ? MIME_TYPE_ALL : MIME_TYPE_PHOTO);
1963        }
1964    }
1965
1966    @Override
1967    public boolean onCreateOptionsMenu(Menu menu) {
1968        final boolean superCreated = super.onCreateOptionsMenu(menu);
1969        // Don't render any menu items when there are no accounts.
1970        if (mAccounts == null || mAccounts.length == 0) {
1971            return superCreated;
1972        }
1973        MenuInflater inflater = getMenuInflater();
1974        inflater.inflate(R.menu.compose_menu, menu);
1975
1976        /*
1977         * Start save in the correct enabled state.
1978         * 1) If a user launches compose from within gmail, save is disabled
1979         * until they add something, at which point, save is enabled, auto save
1980         * on exit; if the user empties everything, save is disabled, exiting does not
1981         * auto-save
1982         * 2) if a user replies/ reply all/ forwards from within gmail, save is
1983         * disabled until they change something, at which point, save is
1984         * enabled, auto save on exit; if the user empties everything, save is
1985         * disabled, exiting does not auto-save.
1986         * 3) If a user launches compose from another application and something
1987         * gets populated (attachments, recipients, body, subject, etc), save is
1988         * enabled, auto save on exit; if the user empties everything, save is
1989         * disabled, exiting does not auto-save
1990         */
1991        mSave = menu.findItem(R.id.save);
1992        String action = getIntent() != null ? getIntent().getAction() : null;
1993        enableSave(mInnerSavedState != null ?
1994                mInnerSavedState.getBoolean(EXTRA_SAVE_ENABLED)
1995                    : (Intent.ACTION_SEND.equals(action)
1996                            || Intent.ACTION_SEND_MULTIPLE.equals(action)
1997                            || Intent.ACTION_SENDTO.equals(action)
1998                            || shouldSave()));
1999
2000        MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
2001        MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
2002        if (helpItem != null) {
2003            helpItem.setVisible(mAccount != null
2004                    && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
2005        }
2006        if (sendFeedbackItem != null) {
2007            sendFeedbackItem.setVisible(mAccount != null
2008                    && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
2009        }
2010
2011        // Show attach picture on pre-K devices.
2012        menu.findItem(R.id.add_photo_attachment).setVisible(!Utils.isRunningKitkatOrLater());
2013
2014        return true;
2015    }
2016
2017    @Override
2018    public boolean onPrepareOptionsMenu(Menu menu) {
2019        MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
2020        if (ccBcc != null && mCc != null) {
2021            // Its possible there is a menu item OR a button.
2022            boolean ccFieldVisible = mCc.isShown();
2023            boolean bccFieldVisible = mBcc.isShown();
2024            if (!ccFieldVisible || !bccFieldVisible) {
2025                ccBcc.setVisible(true);
2026                ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
2027                        : R.string.add_bcc_label));
2028            } else {
2029                ccBcc.setVisible(false);
2030            }
2031        }
2032        return true;
2033    }
2034
2035    @Override
2036    public boolean onOptionsItemSelected(MenuItem item) {
2037        final int id = item.getItemId();
2038
2039        Analytics.getInstance().sendMenuItemEvent(Analytics.EVENT_CATEGORY_MENU_ITEM, id, null, 0);
2040
2041        boolean handled = true;
2042        if (id == R.id.add_file_attachment) {
2043            doAttach(MIME_TYPE_ALL);
2044        } else if (id == R.id.add_photo_attachment) {
2045            doAttach(MIME_TYPE_PHOTO);
2046        } else if (id == R.id.add_cc_bcc) {
2047            showCcBccViews();
2048        } else if (id == R.id.save) {
2049            doSave(true);
2050        } else if (id == R.id.send) {
2051            doSend();
2052        } else if (id == R.id.discard) {
2053            doDiscard();
2054        } else if (id == R.id.settings) {
2055            Utils.showSettings(this, mAccount);
2056        } else if (id == android.R.id.home) {
2057            onAppUpPressed();
2058        } else if (id == R.id.help_info_menu_item) {
2059            Utils.showHelp(this, mAccount, getString(R.string.compose_help_context));
2060        } else if (id == R.id.feedback_menu_item) {
2061            Utils.sendFeedback(this, mAccount, false);
2062        } else {
2063            handled = false;
2064        }
2065        return !handled ? super.onOptionsItemSelected(item) : handled;
2066    }
2067
2068    @Override
2069    public void onBackPressed() {
2070        // If we are showing the wait fragment, just exit.
2071        if (getWaitFragment() != null) {
2072            finish();
2073        } else {
2074            super.onBackPressed();
2075        }
2076    }
2077
2078    /**
2079     * Carries out the "up" action in the action bar.
2080     */
2081    private void onAppUpPressed() {
2082        if (mLaunchedFromEmail) {
2083            // If this was started from Gmail, simply treat app up as the system back button, so
2084            // that the last view is restored.
2085            onBackPressed();
2086            return;
2087        }
2088
2089        // Fire the main activity to ensure it launches the "top" screen of mail.
2090        // Since the main Activity is singleTask, it should revive that task if it was already
2091        // started.
2092        final Intent mailIntent = Utils.createViewInboxIntent(mAccount);
2093        mailIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
2094                Intent.FLAG_ACTIVITY_TASK_ON_HOME);
2095        startActivity(mailIntent);
2096        finish();
2097    }
2098
2099    private void doSend() {
2100        sendOrSaveWithSanityChecks(false, true, false, false);
2101        logSendOrSave(false /* save */);
2102        mPerformedSendOrDiscard = true;
2103    }
2104
2105    private void doSave(boolean showToast) {
2106        sendOrSaveWithSanityChecks(true, showToast, false, false);
2107    }
2108
2109    @VisibleForTesting
2110    public interface SendOrSaveCallback {
2111        public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
2112        public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
2113        public Message getMessage();
2114        public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
2115    }
2116
2117    @VisibleForTesting
2118    public static class SendOrSaveTask implements Runnable {
2119        private final Context mContext;
2120        @VisibleForTesting
2121        public final SendOrSaveCallback mSendOrSaveCallback;
2122        @VisibleForTesting
2123        public final SendOrSaveMessage mSendOrSaveMessage;
2124        private ReplyFromAccount mExistingDraftAccount;
2125
2126        public SendOrSaveTask(Context context, SendOrSaveMessage message,
2127                SendOrSaveCallback callback, ReplyFromAccount draftAccount) {
2128            mContext = context;
2129            mSendOrSaveCallback = callback;
2130            mSendOrSaveMessage = message;
2131            mExistingDraftAccount = draftAccount;
2132        }
2133
2134        @Override
2135        public void run() {
2136            final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
2137
2138            final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
2139            Message message = mSendOrSaveCallback.getMessage();
2140            long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
2141            // If a previous draft has been saved, in an account that is different
2142            // than what the user wants to send from, remove the old draft, and treat this
2143            // as a new message
2144            if (mExistingDraftAccount != null
2145                    && !selectedAccount.account.uri.equals(mExistingDraftAccount.account.uri)) {
2146                if (messageId != UIProvider.INVALID_MESSAGE_ID) {
2147                    ContentResolver resolver = mContext.getContentResolver();
2148                    ContentValues values = new ContentValues();
2149                    values.put(BaseColumns._ID, messageId);
2150                    if (mExistingDraftAccount.account.expungeMessageUri != null) {
2151                        new ContentProviderTask.UpdateTask()
2152                                .run(resolver, mExistingDraftAccount.account.expungeMessageUri,
2153                                        values, null, null);
2154                    } else {
2155                        // TODO(mindyp) delete the conversation.
2156                    }
2157                    // reset messageId to 0, so a new message will be created
2158                    messageId = UIProvider.INVALID_MESSAGE_ID;
2159                }
2160            }
2161
2162            final long messageIdToSave = messageId;
2163            sendOrSaveMessage(messageIdToSave, sendOrSaveMessage, selectedAccount);
2164
2165            if (!sendOrSaveMessage.mSave) {
2166                incrementRecipientsTimesContacted(mContext,
2167                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
2168                incrementRecipientsTimesContacted(mContext,
2169                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
2170                incrementRecipientsTimesContacted(mContext,
2171                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
2172            }
2173            mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
2174        }
2175
2176        private static void incrementRecipientsTimesContacted(final Context context,
2177                final String addressString) {
2178            if (TextUtils.isEmpty(addressString)) {
2179                return;
2180            }
2181            final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressString);
2182            final ArrayList<String> recipients = new ArrayList<String>(tokens.length);
2183            for (int i = 0; i < tokens.length;i++) {
2184                recipients.add(tokens[i].getAddress());
2185            }
2186            final DataUsageStatUpdater statsUpdater = new DataUsageStatUpdater(context);
2187            statsUpdater.updateWithAddress(recipients);
2188        }
2189
2190        /**
2191         * Send or Save a message.
2192         */
2193        private void sendOrSaveMessage(final long messageIdToSave,
2194                final SendOrSaveMessage sendOrSaveMessage, final ReplyFromAccount selectedAccount) {
2195            final ContentResolver resolver = mContext.getContentResolver();
2196            final boolean updateExistingMessage = messageIdToSave != UIProvider.INVALID_MESSAGE_ID;
2197
2198            final String accountMethod = sendOrSaveMessage.mSave ?
2199                    UIProvider.AccountCallMethods.SAVE_MESSAGE :
2200                    UIProvider.AccountCallMethods.SEND_MESSAGE;
2201
2202            try {
2203                if (updateExistingMessage) {
2204                    sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
2205
2206                    callAccountSendSaveMethod(resolver,
2207                            selectedAccount.account, accountMethod, sendOrSaveMessage);
2208                } else {
2209                    Uri messageUri = null;
2210                    final Bundle result = callAccountSendSaveMethod(resolver,
2211                            selectedAccount.account, accountMethod, sendOrSaveMessage);
2212                    if (result != null) {
2213                        // If a non-null value was returned, then the provider handled the call
2214                        // method
2215                        messageUri = result.getParcelable(UIProvider.MessageColumns.URI);
2216                    }
2217                    if (sendOrSaveMessage.mSave && messageUri != null) {
2218                        final Cursor messageCursor = resolver.query(messageUri,
2219                                UIProvider.MESSAGE_PROJECTION, null, null, null);
2220                        if (messageCursor != null) {
2221                            try {
2222                                if (messageCursor.moveToFirst()) {
2223                                    // Broadcast notification that a new message has
2224                                    // been allocated
2225                                    mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
2226                                            new Message(messageCursor));
2227                                }
2228                            } finally {
2229                                messageCursor.close();
2230                            }
2231                        }
2232                    }
2233                }
2234            } finally {
2235                // Close any opened file descriptors
2236                closeOpenedAttachmentFds(sendOrSaveMessage);
2237            }
2238        }
2239
2240        private static void closeOpenedAttachmentFds(final SendOrSaveMessage sendOrSaveMessage) {
2241            final Bundle openedFds = sendOrSaveMessage.attachmentFds();
2242            if (openedFds != null) {
2243                final Set<String> keys = openedFds.keySet();
2244                for (final String key : keys) {
2245                    final ParcelFileDescriptor fd = openedFds.getParcelable(key);
2246                    if (fd != null) {
2247                        try {
2248                            fd.close();
2249                        } catch (IOException e) {
2250                            // Do nothing
2251                        }
2252                    }
2253                }
2254            }
2255        }
2256
2257        /**
2258         * Use the {@link ContentResolver#call} method to send or save the message.
2259         *
2260         * If this was successful, this method will return an non-null Bundle instance
2261         */
2262        private static Bundle callAccountSendSaveMethod(final ContentResolver resolver,
2263                final Account account, final String method,
2264                final SendOrSaveMessage sendOrSaveMessage) {
2265            // Copy all of the values from the content values to the bundle
2266            final Bundle methodExtras = new Bundle(sendOrSaveMessage.mValues.size());
2267            final Set<Entry<String, Object>> valueSet = sendOrSaveMessage.mValues.valueSet();
2268
2269            for (Entry<String, Object> entry : valueSet) {
2270                final Object entryValue = entry.getValue();
2271                final String key = entry.getKey();
2272                if (entryValue instanceof String) {
2273                    methodExtras.putString(key, (String)entryValue);
2274                } else if (entryValue instanceof Boolean) {
2275                    methodExtras.putBoolean(key, (Boolean)entryValue);
2276                } else if (entryValue instanceof Integer) {
2277                    methodExtras.putInt(key, (Integer)entryValue);
2278                } else if (entryValue instanceof Long) {
2279                    methodExtras.putLong(key, (Long)entryValue);
2280                } else {
2281                    LogUtils.wtf(LOG_TAG, "Unexpected object type: %s",
2282                            entryValue.getClass().getName());
2283                }
2284            }
2285
2286            // If the SendOrSaveMessage has some opened fds, add them to the bundle
2287            final Bundle fdMap = sendOrSaveMessage.attachmentFds();
2288            if (fdMap != null) {
2289                methodExtras.putParcelable(
2290                        UIProvider.SendOrSaveMethodParamKeys.OPENED_FD_MAP, fdMap);
2291            }
2292
2293            return resolver.call(account.uri, method, account.uri.toString(), methodExtras);
2294        }
2295    }
2296
2297    @VisibleForTesting
2298    public static class SendOrSaveMessage {
2299        final ReplyFromAccount mAccount;
2300        final ContentValues mValues;
2301        final String mRefMessageId;
2302        @VisibleForTesting
2303        public final boolean mSave;
2304        final int mRequestId;
2305        private final Bundle mAttachmentFds;
2306
2307        public SendOrSaveMessage(Context context, ReplyFromAccount account, ContentValues values,
2308                String refMessageId, List<Attachment> attachments, boolean save) {
2309            mAccount = account;
2310            mValues = values;
2311            mRefMessageId = refMessageId;
2312            mSave = save;
2313            mRequestId = mValues.hashCode() ^ hashCode();
2314
2315            mAttachmentFds = initializeAttachmentFds(context, attachments);
2316        }
2317
2318        int requestId() {
2319            return mRequestId;
2320        }
2321
2322        Bundle attachmentFds() {
2323            return mAttachmentFds;
2324        }
2325
2326        /**
2327         * Opens {@link ParcelFileDescriptor} for each of the attachments.  This method must be
2328         * called before the ComposeActivity finishes.
2329         * Note: The caller is responsible for closing these file descriptors.
2330         */
2331        private static Bundle initializeAttachmentFds(final Context context,
2332                final List<Attachment> attachments) {
2333            if (attachments == null || attachments.size() == 0) {
2334                return null;
2335            }
2336
2337            final Bundle result = new Bundle(attachments.size());
2338            final ContentResolver resolver = context.getContentResolver();
2339
2340            for (Attachment attachment : attachments) {
2341                if (attachment == null || Utils.isEmpty(attachment.contentUri)) {
2342                    continue;
2343                }
2344
2345                ParcelFileDescriptor fileDescriptor;
2346                try {
2347                    fileDescriptor = resolver.openFileDescriptor(attachment.contentUri, "r");
2348                } catch (FileNotFoundException e) {
2349                    LogUtils.e(LOG_TAG, e, "Exception attempting to open attachment");
2350                    fileDescriptor = null;
2351                } catch (SecurityException e) {
2352                    // We have encountered a security exception when attempting to open the file
2353                    // specified by the content uri.  If the attachment has been cached, this
2354                    // isn't a problem, as even through the original permission may have been
2355                    // revoked, we have cached the file.  This will happen when saving/sending
2356                    // a previously saved draft.
2357                    // TODO(markwei): Expose whether the attachment has been cached through the
2358                    // attachment object.  This would allow us to limit when the log is made, as
2359                    // if the attachment has been cached, this really isn't an error
2360                    LogUtils.e(LOG_TAG, e, "Security Exception attempting to open attachment");
2361                    // Just set the file descriptor to null, as the underlying provider needs
2362                    // to handle the file descriptor not being set.
2363                    fileDescriptor = null;
2364                }
2365
2366                if (fileDescriptor != null) {
2367                    result.putParcelable(attachment.contentUri.toString(), fileDescriptor);
2368                }
2369            }
2370
2371            return result;
2372        }
2373    }
2374
2375    /**
2376     * Get the to recipients.
2377     */
2378    public String[] getToAddresses() {
2379        return getAddressesFromList(mTo);
2380    }
2381
2382    /**
2383     * Get the cc recipients.
2384     */
2385    public String[] getCcAddresses() {
2386        return getAddressesFromList(mCc);
2387    }
2388
2389    /**
2390     * Get the bcc recipients.
2391     */
2392    public String[] getBccAddresses() {
2393        return getAddressesFromList(mBcc);
2394    }
2395
2396    public String[] getAddressesFromList(RecipientEditTextView list) {
2397        if (list == null) {
2398            return new String[0];
2399        }
2400        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
2401        int count = tokens.length;
2402        String[] result = new String[count];
2403        for (int i = 0; i < count; i++) {
2404            result[i] = tokens[i].toString();
2405        }
2406        return result;
2407    }
2408
2409    /**
2410     * Check for invalid email addresses.
2411     * @param to String array of email addresses to check.
2412     * @param wrongEmailsOut Emails addresses that were invalid.
2413     */
2414    public void checkInvalidEmails(final String[] to, final List<String> wrongEmailsOut) {
2415        if (mValidator == null) {
2416            return;
2417        }
2418        for (final String email : to) {
2419            if (!mValidator.isValid(email)) {
2420                wrongEmailsOut.add(email);
2421            }
2422        }
2423    }
2424
2425    public static class RecipientErrorDialogFragment extends DialogFragment {
2426        // Public no-args constructor needed for fragment re-instantiation
2427        public RecipientErrorDialogFragment() {}
2428
2429        public static RecipientErrorDialogFragment newInstance(final String message) {
2430            final RecipientErrorDialogFragment frag = new RecipientErrorDialogFragment();
2431            final Bundle args = new Bundle(1);
2432            args.putString("message", message);
2433            frag.setArguments(args);
2434            return frag;
2435        }
2436
2437        @Override
2438        public Dialog onCreateDialog(Bundle savedInstanceState) {
2439            final String message = getArguments().getString("message");
2440            return new AlertDialog.Builder(getActivity())
2441                    .setMessage(message)
2442                    .setPositiveButton(
2443                            R.string.ok, new Dialog.OnClickListener() {
2444                        @Override
2445                        public void onClick(DialogInterface dialog, int which) {
2446                            ((ComposeActivity)getActivity()).finishRecipientErrorDialog();
2447                        }
2448                    }).create();
2449        }
2450    }
2451
2452    private void finishRecipientErrorDialog() {
2453        // after the user dismisses the recipient error
2454        // dialog we want to make sure to refocus the
2455        // recipient to field so they can fix the issue
2456        // easily
2457        if (mTo != null) {
2458            mTo.requestFocus();
2459        }
2460    }
2461
2462    /**
2463     * Show an error because the user has entered an invalid recipient.
2464     * @param message
2465     */
2466    private void showRecipientErrorDialog(final String message) {
2467        final DialogFragment frag = RecipientErrorDialogFragment.newInstance(message);
2468        frag.show(getFragmentManager(), "recipient error");
2469    }
2470
2471    /**
2472     * Update the state of the UI based on whether or not the current draft
2473     * needs to be saved and the message is not empty.
2474     */
2475    public void updateSaveUi() {
2476        if (mSave != null) {
2477            mSave.setEnabled((shouldSave() && !isBlank()));
2478        }
2479    }
2480
2481    /**
2482     * Returns true if we need to save the current draft.
2483     */
2484    private boolean shouldSave() {
2485        synchronized (mDraftLock) {
2486            // The message should only be saved if:
2487            // It hasn't been sent AND
2488            // Some text has been added to the message OR
2489            // an attachment has been added or removed
2490            // AND there is actually something in the draft to save.
2491            return (mTextChanged || mAttachmentsChanged || mReplyFromChanged)
2492                    && !isBlank();
2493        }
2494    }
2495
2496    /**
2497     * Check if all fields are blank.
2498     * @return boolean
2499     */
2500    public boolean isBlank() {
2501        // Need to check for null since isBlank() can be called from onPause()
2502        // before findViews() is called
2503        if (mSubject == null || mBodyView == null || mTo == null || mCc == null ||
2504                mAttachmentsView == null) {
2505            LogUtils.w(LOG_TAG, "null views in isBlank check");
2506            return true;
2507        }
2508        return mSubject.getText().length() == 0
2509                && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
2510                        mBodyView.getText().toString()) == 0)
2511                && mTo.length() == 0
2512                && mCc.length() == 0 && mBcc.length() == 0
2513                && mAttachmentsView.getAttachments().size() == 0;
2514    }
2515
2516    @VisibleForTesting
2517    protected int getSignatureStartPosition(String signature, String bodyText) {
2518        int startPos = -1;
2519
2520        if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
2521            return startPos;
2522        }
2523
2524        int bodyLength = bodyText.length();
2525        int signatureLength = signature.length();
2526        String printableVersion = convertToPrintableSignature(signature);
2527        int printableLength = printableVersion.length();
2528
2529        if (bodyLength >= printableLength
2530                && bodyText.substring(bodyLength - printableLength)
2531                .equals(printableVersion)) {
2532            startPos = bodyLength - printableLength;
2533        } else if (bodyLength >= signatureLength
2534                && bodyText.substring(bodyLength - signatureLength)
2535                .equals(signature)) {
2536            startPos = bodyLength - signatureLength;
2537        }
2538        return startPos;
2539    }
2540
2541    /**
2542     * Allows any changes made by the user to be ignored. Called when the user
2543     * decides to discard a draft.
2544     */
2545    private void discardChanges() {
2546        mTextChanged = false;
2547        mAttachmentsChanged = false;
2548        mReplyFromChanged = false;
2549    }
2550
2551    /**
2552     * @param save
2553     * @param showToast
2554     * @return Whether the send or save succeeded.
2555     */
2556    protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
2557            final boolean orientationChanged, final boolean autoSend) {
2558        if (mAccounts == null || mAccount == null) {
2559            Toast.makeText(this, R.string.send_failed, Toast.LENGTH_SHORT).show();
2560            if (autoSend) {
2561                finish();
2562            }
2563            return false;
2564        }
2565
2566        final String[] to, cc, bcc;
2567        if (orientationChanged) {
2568            to = cc = bcc = new String[0];
2569        } else {
2570            to = getToAddresses();
2571            cc = getCcAddresses();
2572            bcc = getBccAddresses();
2573        }
2574
2575        // Don't let the user send to nobody (but it's okay to save a message
2576        // with no recipients)
2577        if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
2578            showRecipientErrorDialog(getString(R.string.recipient_needed));
2579            return false;
2580        }
2581
2582        List<String> wrongEmails = new ArrayList<String>();
2583        if (!save) {
2584            checkInvalidEmails(to, wrongEmails);
2585            checkInvalidEmails(cc, wrongEmails);
2586            checkInvalidEmails(bcc, wrongEmails);
2587        }
2588
2589        // Don't let the user send an email with invalid recipients
2590        if (wrongEmails.size() > 0) {
2591            String errorText = String.format(getString(R.string.invalid_recipient),
2592                    wrongEmails.get(0));
2593            showRecipientErrorDialog(errorText);
2594            return false;
2595        }
2596
2597        // Show a warning before sending only if there are no attachments.
2598        if (!save) {
2599            if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
2600                boolean warnAboutEmptySubject = isSubjectEmpty();
2601                boolean emptyBody = TextUtils.getTrimmedLength(mBodyView.getEditableText()) == 0;
2602
2603                // A warning about an empty body may not be warranted when
2604                // forwarding mails, since a common use case is to forward
2605                // quoted text and not append any more text.
2606                boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
2607
2608                // When we bring up a dialog warning the user about a send,
2609                // assume that they accept sending the message. If they do not,
2610                // the dialog listener is required to enable sending again.
2611                if (warnAboutEmptySubject) {
2612                    showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, save,
2613                            showToast);
2614                    return true;
2615                }
2616
2617                if (warnAboutEmptyBody) {
2618                    showSendConfirmDialog(R.string.confirm_send_message_with_no_body, save,
2619                            showToast);
2620                    return true;
2621                }
2622            }
2623            // Ask for confirmation to send (if always required)
2624            if (showSendConfirmation()) {
2625                showSendConfirmDialog(R.string.confirm_send_message, save, showToast);
2626                return true;
2627            }
2628        }
2629
2630        sendOrSave(save, showToast);
2631        return true;
2632    }
2633
2634    /**
2635     * Returns a boolean indicating whether warnings should be shown for empty
2636     * subject and body fields
2637     *
2638     * @return True if a warning should be shown for empty text fields
2639     */
2640    protected boolean showEmptyTextWarnings() {
2641        return mAttachmentsView.getAttachments().size() == 0;
2642    }
2643
2644    /**
2645     * Returns a boolean indicating whether the user should confirm each send
2646     *
2647     * @return True if a warning should be on each send
2648     */
2649    protected boolean showSendConfirmation() {
2650        return mCachedSettings != null ? mCachedSettings.confirmSend : false;
2651    }
2652
2653    public static class SendConfirmDialogFragment extends DialogFragment
2654            implements DialogInterface.OnClickListener {
2655
2656        private boolean mSave;
2657        private boolean mShowToast;
2658
2659        // Public no-args constructor needed for fragment re-instantiation
2660        public SendConfirmDialogFragment() {}
2661
2662        public static SendConfirmDialogFragment newInstance(final int messageId,
2663                final boolean save, final boolean showToast) {
2664            final SendConfirmDialogFragment frag = new SendConfirmDialogFragment();
2665            final Bundle args = new Bundle(3);
2666            args.putInt("messageId", messageId);
2667            args.putBoolean("save", save);
2668            args.putBoolean("showToast", showToast);
2669            frag.setArguments(args);
2670            return frag;
2671        }
2672
2673        @Override
2674        public Dialog onCreateDialog(Bundle savedInstanceState) {
2675            final int messageId = getArguments().getInt("messageId");
2676            mSave = getArguments().getBoolean("save");
2677            mShowToast = getArguments().getBoolean("showToast");
2678
2679            final int confirmTextId = (messageId == R.string.confirm_send_message) ?
2680                    R.string.ok : R.string.send;
2681
2682            return new AlertDialog.Builder(getActivity())
2683                    .setMessage(messageId)
2684                    .setPositiveButton(confirmTextId, this)
2685                    .setNegativeButton(R.string.cancel, null)
2686                    .create();
2687        }
2688
2689        @Override
2690        public void onClick(DialogInterface dialog, int which) {
2691            if (which == DialogInterface.BUTTON_POSITIVE) {
2692                ((ComposeActivity) getActivity()).finishSendConfirmDialog(mSave, mShowToast);
2693            }
2694        }
2695    }
2696
2697    private void finishSendConfirmDialog(final boolean save, final boolean showToast) {
2698        sendOrSave(save, showToast);
2699    }
2700
2701    private void showSendConfirmDialog(final int messageId, final boolean save,
2702            final boolean showToast) {
2703        final DialogFragment frag = SendConfirmDialogFragment.newInstance(messageId, save,
2704                showToast);
2705        frag.show(getFragmentManager(), "send confirm");
2706    }
2707
2708    /**
2709     * Returns whether the ComposeArea believes there is any text in the body of
2710     * the composition. TODO: When ComposeArea controls the Body as well, add
2711     * that here.
2712     */
2713    public boolean isBodyEmpty() {
2714        return !mQuotedTextView.isTextIncluded();
2715    }
2716
2717    /**
2718     * Test to see if the subject is empty.
2719     *
2720     * @return boolean.
2721     */
2722    // TODO: this will likely go away when composeArea.focus() is implemented
2723    // after all the widget control is moved over.
2724    public boolean isSubjectEmpty() {
2725        return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
2726    }
2727
2728    /* package */
2729    static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
2730            Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
2731            SendOrSaveCallback callback, Handler handler, boolean save, int composeMode,
2732            ReplyFromAccount draftAccount, final ContentValues extraValues) {
2733        final ContentValues values = new ContentValues();
2734
2735        final String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
2736
2737        MessageModification.putToAddresses(values, message.getToAddresses());
2738        MessageModification.putCcAddresses(values, message.getCcAddresses());
2739        MessageModification.putBccAddresses(values, message.getBccAddresses());
2740
2741        MessageModification.putCustomFromAddress(values, message.getFrom());
2742
2743        MessageModification.putSubject(values, message.subject);
2744        // Make sure to remove only the composing spans from the Spannable before saving.
2745        final String htmlBody = Html.toHtml(removeComposingSpans(body));
2746
2747        boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
2748        StringBuilder fullBody = new StringBuilder(htmlBody);
2749        if (includeQuotedText) {
2750            // HTML gets converted to text for now
2751            final String text = quotedText.toString();
2752            if (QuotedTextView.containsQuotedText(text)) {
2753                int pos = QuotedTextView.getQuotedTextOffset(text);
2754                final int quoteStartPos = fullBody.length() + pos;
2755                fullBody.append(text);
2756                MessageModification.putQuoteStartPos(values, quoteStartPos);
2757                MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
2758                MessageModification.putAppendRefMessageContent(values, includeQuotedText);
2759            } else {
2760                LogUtils.w(LOG_TAG, "Couldn't find quoted text");
2761                // This shouldn't happen, but just use what we have,
2762                // and don't do server-side expansion
2763                fullBody.append(text);
2764            }
2765        }
2766        int draftType = getDraftType(composeMode);
2767        MessageModification.putDraftType(values, draftType);
2768        if (refMessage != null) {
2769            if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
2770                MessageModification.putBodyHtml(values, fullBody.toString());
2771            }
2772            if (!TextUtils.isEmpty(refMessage.bodyText)) {
2773                MessageModification.putBody(values,
2774                        Utils.convertHtmlToPlainText(fullBody.toString()).toString());
2775            }
2776        } else {
2777            MessageModification.putBodyHtml(values, fullBody.toString());
2778            MessageModification.putBody(values, Utils.convertHtmlToPlainText(fullBody.toString())
2779                    .toString());
2780        }
2781        MessageModification.putAttachments(values, message.getAttachments());
2782        if (!TextUtils.isEmpty(refMessageId)) {
2783            MessageModification.putRefMessageId(values, refMessageId);
2784        }
2785        if (extraValues != null) {
2786            values.putAll(extraValues);
2787        }
2788        SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(context, replyFromAccount,
2789                values, refMessageId, message.getAttachments(), save);
2790        SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback,
2791                draftAccount);
2792
2793        callback.initializeSendOrSave(sendOrSaveTask);
2794        // Do the send/save action on the specified handler to avoid possible
2795        // ANRs
2796        handler.post(sendOrSaveTask);
2797
2798        return sendOrSaveMessage.requestId();
2799    }
2800
2801    /**
2802     * Removes any composing spans from the specified string.  This will create a new
2803     * SpannableString instance, as to not modify the behavior of the EditText view.
2804     */
2805    private static SpannableString removeComposingSpans(Spanned body) {
2806        final SpannableString messageBody = new SpannableString(body);
2807        BaseInputConnection.removeComposingSpans(messageBody);
2808        return messageBody;
2809    }
2810
2811    private static int getDraftType(int mode) {
2812        int draftType = -1;
2813        switch (mode) {
2814            case ComposeActivity.COMPOSE:
2815                draftType = DraftType.COMPOSE;
2816                break;
2817            case ComposeActivity.REPLY:
2818                draftType = DraftType.REPLY;
2819                break;
2820            case ComposeActivity.REPLY_ALL:
2821                draftType = DraftType.REPLY_ALL;
2822                break;
2823            case ComposeActivity.FORWARD:
2824                draftType = DraftType.FORWARD;
2825                break;
2826        }
2827        return draftType;
2828    }
2829
2830    private void sendOrSave(final boolean save, final boolean showToast) {
2831        // Check if user is a monkey. Monkeys can compose and hit send
2832        // button but are not allowed to send anything off the device.
2833        if (ActivityManager.isUserAMonkey()) {
2834            return;
2835        }
2836
2837        final Spanned body = mBodyView.getEditableText();
2838
2839        SendOrSaveCallback callback = new SendOrSaveCallback() {
2840            // FIXME: unused
2841            private int mRestoredRequestId;
2842
2843            @Override
2844            public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
2845                synchronized (mActiveTasks) {
2846                    int numTasks = mActiveTasks.size();
2847                    if (numTasks == 0) {
2848                        // Start service so we won't be killed if this app is
2849                        // put in the background.
2850                        startService(new Intent(ComposeActivity.this, EmptyService.class));
2851                    }
2852
2853                    mActiveTasks.add(sendOrSaveTask);
2854                }
2855                if (sTestSendOrSaveCallback != null) {
2856                    sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
2857                }
2858            }
2859
2860            @Override
2861            public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
2862                    Message message) {
2863                synchronized (mDraftLock) {
2864                    mDraftAccount = sendOrSaveMessage.mAccount;
2865                    mDraftId = message.id;
2866                    mDraft = message;
2867                    if (sRequestMessageIdMap != null) {
2868                        sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
2869                    }
2870                    // Cache request message map, in case the process is killed
2871                    saveRequestMap();
2872                }
2873                if (sTestSendOrSaveCallback != null) {
2874                    sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
2875                }
2876            }
2877
2878            @Override
2879            public Message getMessage() {
2880                synchronized (mDraftLock) {
2881                    return mDraft;
2882                }
2883            }
2884
2885            @Override
2886            public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
2887                // Update the last sent from account.
2888                if (mAccount != null) {
2889                    MailAppProvider.getInstance().setLastSentFromAccount(mAccount.uri.toString());
2890                }
2891                if (success) {
2892                    // Successfully sent or saved so reset change markers
2893                    discardChanges();
2894                } else {
2895                    // A failure happened with saving/sending the draft
2896                    // TODO(pwestbro): add a better string that should be used
2897                    // when failing to send or save
2898                    Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
2899                            .show();
2900                }
2901
2902                int numTasks;
2903                synchronized (mActiveTasks) {
2904                    // Remove the task from the list of active tasks
2905                    mActiveTasks.remove(task);
2906                    numTasks = mActiveTasks.size();
2907                }
2908
2909                if (numTasks == 0) {
2910                    // Stop service so we can be killed.
2911                    stopService(new Intent(ComposeActivity.this, EmptyService.class));
2912                }
2913                if (sTestSendOrSaveCallback != null) {
2914                    sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
2915                }
2916            }
2917        };
2918
2919        setAccount(mReplyFromAccount.account);
2920
2921        if (mSendSaveTaskHandler == null) {
2922            HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
2923            handlerThread.start();
2924
2925            mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
2926        }
2927
2928        Message msg = createMessage(mReplyFromAccount, getMode());
2929        mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
2930                mQuotedTextView.getQuotedTextIfIncluded(), callback,
2931                mSendSaveTaskHandler, save, mComposeMode, mDraftAccount, mExtraValues);
2932
2933        // Don't display the toast if the user is just changing the orientation,
2934        // but we still need to save the draft to the cursor because this is how we restore
2935        // the attachments when the configuration change completes.
2936        if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
2937            Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
2938                    Toast.LENGTH_LONG).show();
2939        }
2940
2941        // Need to update variables here because the send or save completes
2942        // asynchronously even though the toast shows right away.
2943        discardChanges();
2944        updateSaveUi();
2945
2946        // If we are sending, finish the activity
2947        if (!save) {
2948            finish();
2949        }
2950    }
2951
2952    /**
2953     * Save the state of the request messageid map. This allows for the Gmail
2954     * process to be killed, but and still allow for ComposeActivity instances
2955     * to be recreated correctly.
2956     */
2957    private void saveRequestMap() {
2958        // TODO: store the request map in user preferences.
2959    }
2960
2961    private void doAttach(String type) {
2962        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2963        i.addCategory(Intent.CATEGORY_OPENABLE);
2964        i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
2965        i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
2966        i.setType(type);
2967        mAddingAttachment = true;
2968        startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2969                RESULT_PICK_ATTACHMENT);
2970    }
2971
2972    private void showCcBccViews() {
2973        mCcBccView.show(true, true, true);
2974        if (mCcBccButton != null) {
2975            mCcBccButton.setVisibility(View.INVISIBLE);
2976        }
2977    }
2978
2979    private static String getActionString(int action) {
2980        final String msgType;
2981        switch (action) {
2982            case COMPOSE:
2983                msgType = "new_message";
2984                break;
2985            case REPLY:
2986                msgType = "reply";
2987                break;
2988            case REPLY_ALL:
2989                msgType = "reply_all";
2990                break;
2991            case FORWARD:
2992                msgType = "forward";
2993                break;
2994            default:
2995                msgType = "unknown";
2996                break;
2997        }
2998        return msgType;
2999    }
3000
3001    private void logSendOrSave(boolean save) {
3002        if (!Analytics.isLoggable() || mAttachmentsView == null) {
3003            return;
3004        }
3005
3006        final String category = (save) ? "message_save" : "message_send";
3007        final int attachmentCount = getAttachments().size();
3008        final String msgType = getActionString(mComposeMode);
3009        final String label;
3010        final long value;
3011        if (mComposeMode == COMPOSE) {
3012            label = Integer.toString(attachmentCount);
3013            value = attachmentCount;
3014        } else {
3015            label = null;
3016            value = 0;
3017        }
3018        Analytics.getInstance().sendEvent(category, msgType, label, value);
3019    }
3020
3021    @Override
3022    public boolean onNavigationItemSelected(int position, long itemId) {
3023        int initialComposeMode = mComposeMode;
3024        if (position == ComposeActivity.REPLY) {
3025            mComposeMode = ComposeActivity.REPLY;
3026        } else if (position == ComposeActivity.REPLY_ALL) {
3027            mComposeMode = ComposeActivity.REPLY_ALL;
3028        } else if (position == ComposeActivity.FORWARD) {
3029            mComposeMode = ComposeActivity.FORWARD;
3030        }
3031        clearChangeListeners();
3032        if (initialComposeMode != mComposeMode) {
3033            resetMessageForModeChange();
3034            if (mRefMessage != null) {
3035                setFieldsFromRefMessage(mComposeMode);
3036            }
3037            boolean showCc = false;
3038            boolean showBcc = false;
3039            if (mDraft != null) {
3040                // Following desktop behavior, if the user has added a BCC
3041                // field to a draft, we show it regardless of compose mode.
3042                showBcc = !TextUtils.isEmpty(mDraft.getBcc());
3043                // Use the draft to determine what to populate.
3044                // If the Bcc field is showing, show the Cc field whether it is populated or not.
3045                showCc = showBcc
3046                        || (!TextUtils.isEmpty(mDraft.getCc()) && mComposeMode == REPLY_ALL);
3047            }
3048            if (mRefMessage != null) {
3049                showCc = !TextUtils.isEmpty(mCc.getText());
3050                showBcc = !TextUtils.isEmpty(mBcc.getText());
3051            }
3052            mCcBccView.show(false, showCc, showBcc);
3053        }
3054        updateHideOrShowCcBcc();
3055        initChangeListeners();
3056        return true;
3057    }
3058
3059    @VisibleForTesting
3060    protected void resetMessageForModeChange() {
3061        // When switching between reply, reply all, forward,
3062        // follow the behavior of webview.
3063        // The contents of the following fields are cleared
3064        // so that they can be populated directly from the
3065        // ref message:
3066        // 1) Any recipient fields
3067        // 2) The subject
3068        mTo.setText("");
3069        mCc.setText("");
3070        mBcc.setText("");
3071        // Any edits to the subject are replaced with the original subject.
3072        mSubject.setText("");
3073
3074        // Any changes to the contents of the following fields are kept:
3075        // 1) Body
3076        // 2) Attachments
3077        // If the user made changes to attachments, keep their changes.
3078        if (!mAttachmentsChanged) {
3079            mAttachmentsView.deleteAllAttachments();
3080        }
3081    }
3082
3083    private class ComposeModeAdapter extends ArrayAdapter<String> {
3084
3085        private LayoutInflater mInflater;
3086
3087        public ComposeModeAdapter(Context context) {
3088            super(context, R.layout.compose_mode_item, R.id.mode, getResources()
3089                    .getStringArray(R.array.compose_modes));
3090        }
3091
3092        private LayoutInflater getInflater() {
3093            if (mInflater == null) {
3094                mInflater = LayoutInflater.from(getContext());
3095            }
3096            return mInflater;
3097        }
3098
3099        @Override
3100        public View getView(int position, View convertView, ViewGroup parent) {
3101            if (convertView == null) {
3102                convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
3103            }
3104            ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
3105            return super.getView(position, convertView, parent);
3106        }
3107    }
3108
3109    @Override
3110    public void onRespondInline(String text) {
3111        appendToBody(text, false);
3112        mQuotedTextView.setUpperDividerVisible(false);
3113        mRespondedInline = true;
3114        if (!mBodyView.hasFocus()) {
3115            mBodyView.requestFocus();
3116        }
3117    }
3118
3119    /**
3120     * Append text to the body of the message. If there is no existing body
3121     * text, just sets the body to text.
3122     *
3123     * @param text
3124     * @param withSignature True to append a signature.
3125     */
3126    public void appendToBody(CharSequence text, boolean withSignature) {
3127        Editable bodyText = mBodyView.getEditableText();
3128        if (bodyText != null && bodyText.length() > 0) {
3129            bodyText.append(text);
3130        } else {
3131            setBody(text, withSignature);
3132        }
3133    }
3134
3135    /**
3136     * Set the body of the message.
3137     *
3138     * @param text
3139     * @param withSignature True to append a signature.
3140     */
3141    public void setBody(CharSequence text, boolean withSignature) {
3142        mBodyView.setText(text);
3143        if (withSignature) {
3144            appendSignature();
3145        }
3146    }
3147
3148    private void appendSignature() {
3149        String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
3150        boolean hasFocus = mBodyView.hasFocus();
3151        int signaturePos = getSignatureStartPosition(mSignature, mBodyView.getText().toString());
3152        if (!TextUtils.equals(newSignature, mSignature) || signaturePos < 0) {
3153            mSignature = newSignature;
3154            if (!TextUtils.isEmpty(mSignature)) {
3155                // Appending a signature does not count as changing text.
3156                mBodyView.removeTextChangedListener(this);
3157                mBodyView.append(convertToPrintableSignature(mSignature));
3158                mBodyView.addTextChangedListener(this);
3159            }
3160            if (hasFocus) {
3161                focusBody();
3162            }
3163        }
3164    }
3165
3166    private String convertToPrintableSignature(String signature) {
3167        String signatureResource = getResources().getString(R.string.signature);
3168        if (signature == null) {
3169            signature = "";
3170        }
3171        return String.format(signatureResource, signature);
3172    }
3173
3174    @Override
3175    public void onAccountChanged() {
3176        mReplyFromAccount = mFromSpinner.getCurrentAccount();
3177        if (!mAccount.equals(mReplyFromAccount.account)) {
3178            // Clear a signature, if there was one.
3179            mBodyView.removeTextChangedListener(this);
3180            String oldSignature = mSignature;
3181            String bodyText = getBody().getText().toString();
3182            if (!TextUtils.isEmpty(oldSignature)) {
3183                int pos = getSignatureStartPosition(oldSignature, bodyText);
3184                if (pos > -1) {
3185                    mBodyView.setText(bodyText.substring(0, pos));
3186                }
3187            }
3188            setAccount(mReplyFromAccount.account);
3189            mBodyView.addTextChangedListener(this);
3190            // TODO: handle discarding attachments when switching accounts.
3191            // Only enable save for this draft if there is any other content
3192            // in the message.
3193            if (!isBlank()) {
3194                enableSave(true);
3195            }
3196            mReplyFromChanged = true;
3197            initRecipients();
3198        }
3199    }
3200
3201    public void enableSave(boolean enabled) {
3202        if (mSave != null) {
3203            mSave.setEnabled(enabled);
3204        }
3205    }
3206
3207    public static class DiscardConfirmDialogFragment extends DialogFragment {
3208        // Public no-args constructor needed for fragment re-instantiation
3209        public DiscardConfirmDialogFragment() {}
3210
3211        @Override
3212        public Dialog onCreateDialog(Bundle savedInstanceState) {
3213            return new AlertDialog.Builder(getActivity())
3214                    .setMessage(R.string.confirm_discard_text)
3215                    .setPositiveButton(R.string.discard,
3216                            new DialogInterface.OnClickListener() {
3217                                @Override
3218                                public void onClick(DialogInterface dialog, int which) {
3219                                    ((ComposeActivity)getActivity()).doDiscardWithoutConfirmation();
3220                                }
3221                            })
3222                    .setNegativeButton(R.string.cancel, null)
3223                    .create();
3224        }
3225    }
3226
3227    private void doDiscard() {
3228        final DialogFragment frag = new DiscardConfirmDialogFragment();
3229        frag.show(getFragmentManager(), "discard confirm");
3230    }
3231    /**
3232     * Effectively discard the current message.
3233     *
3234     * This method is either invoked from the menu or from the dialog
3235     * once the user has confirmed that they want to discard the message.
3236     */
3237    private void doDiscardWithoutConfirmation() {
3238        synchronized (mDraftLock) {
3239            if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
3240                ContentValues values = new ContentValues();
3241                values.put(BaseColumns._ID, mDraftId);
3242                if (!mAccount.expungeMessageUri.equals(Uri.EMPTY)) {
3243                    getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
3244                } else {
3245                    getContentResolver().delete(mDraft.uri, null, null);
3246                }
3247                // This is not strictly necessary (since we should not try to
3248                // save the draft after calling this) but it ensures that if we
3249                // do save again for some reason we make a new draft rather than
3250                // trying to resave an expunged draft.
3251                mDraftId = UIProvider.INVALID_MESSAGE_ID;
3252            }
3253        }
3254
3255        // Display a toast to let the user know
3256        Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
3257
3258        // This prevents the draft from being saved in onPause().
3259        discardChanges();
3260        mPerformedSendOrDiscard = true;
3261        finish();
3262    }
3263
3264    private void saveIfNeeded() {
3265        if (mAccount == null) {
3266            // We have not chosen an account yet so there's no way that we can save. This is ok,
3267            // though, since we are saving our state before AccountsActivity is activated. Thus, the
3268            // user has not interacted with us yet and there is no real state to save.
3269            return;
3270        }
3271
3272        if (shouldSave()) {
3273            doSave(!mAddingAttachment /* show toast */);
3274        }
3275    }
3276
3277    @Override
3278    public void onAttachmentDeleted() {
3279        mAttachmentsChanged = true;
3280        // If we are showing any attachments, make sure we have an upper
3281        // divider.
3282        mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3283        updateSaveUi();
3284    }
3285
3286    @Override
3287    public void onAttachmentAdded() {
3288        mQuotedTextView.setUpperDividerVisible(mAttachmentsView.getAttachments().size() > 0);
3289        mAttachmentsView.focusLastAttachment();
3290    }
3291
3292    /**
3293     * This is called any time one of our text fields changes.
3294     */
3295    @Override
3296    public void afterTextChanged(Editable s) {
3297        mTextChanged = true;
3298        updateSaveUi();
3299    }
3300
3301    @Override
3302    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3303        // Do nothing.
3304    }
3305
3306    @Override
3307    public void onTextChanged(CharSequence s, int start, int before, int count) {
3308        // Do nothing.
3309    }
3310
3311
3312    // There is a big difference between the text associated with an address changing
3313    // to add the display name or to format properly and a recipient being added or deleted.
3314    // Make sure we only notify of changes when a recipient has been added or deleted.
3315    private class RecipientTextWatcher implements TextWatcher {
3316        private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
3317
3318        private RecipientEditTextView mView;
3319
3320        private TextWatcher mListener;
3321
3322        public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
3323            mView = view;
3324            mListener = listener;
3325        }
3326
3327        @Override
3328        public void afterTextChanged(Editable s) {
3329            if (hasChanged()) {
3330                mListener.afterTextChanged(s);
3331            }
3332        }
3333
3334        private boolean hasChanged() {
3335            String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
3336            int totalCount = currRecips.length;
3337            int totalPrevCount = 0;
3338            for (Entry<String, Integer> entry : mContent.entrySet()) {
3339                totalPrevCount += entry.getValue();
3340            }
3341            if (totalCount != totalPrevCount) {
3342                return true;
3343            }
3344
3345            for (String recip : currRecips) {
3346                if (!mContent.containsKey(recip)) {
3347                    return true;
3348                } else {
3349                    int count = mContent.get(recip) - 1;
3350                    if (count < 0) {
3351                        return true;
3352                    } else {
3353                        mContent.put(recip, count);
3354                    }
3355                }
3356            }
3357            return false;
3358        }
3359
3360        private String[] tokenizeRecips(String[] recips) {
3361            // Tokenize them all and put them in the list.
3362            String[] recipAddresses = new String[recips.length];
3363            for (int i = 0; i < recips.length; i++) {
3364                recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
3365            }
3366            return recipAddresses;
3367        }
3368
3369        @Override
3370        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
3371            String[] recips = tokenizeRecips(getAddressesFromList(mView));
3372            for (String recip : recips) {
3373                if (!mContent.containsKey(recip)) {
3374                    mContent.put(recip, 1);
3375                } else {
3376                    mContent.put(recip, (mContent.get(recip)) + 1);
3377                }
3378            }
3379        }
3380
3381        @Override
3382        public void onTextChanged(CharSequence s, int start, int before, int count) {
3383            // Do nothing.
3384        }
3385    }
3386
3387    public static void registerTestSendOrSaveCallback(SendOrSaveCallback testCallback) {
3388        if (sTestSendOrSaveCallback != null && testCallback != null) {
3389            throw new IllegalStateException("Attempting to register more than one test callback");
3390        }
3391        sTestSendOrSaveCallback = testCallback;
3392    }
3393
3394    @VisibleForTesting
3395    protected ArrayList<Attachment> getAttachments() {
3396        return mAttachmentsView.getAttachments();
3397    }
3398
3399    @Override
3400    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
3401        switch (id) {
3402            case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3403                return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3404                        null, null);
3405            case REFERENCE_MESSAGE_LOADER:
3406                return new CursorLoader(this, mRefMessageUri, UIProvider.MESSAGE_PROJECTION, null,
3407                        null, null);
3408            case LOADER_ACCOUNT_CURSOR:
3409                return new CursorLoader(this, MailAppProvider.getAccountsUri(),
3410                        UIProvider.ACCOUNTS_PROJECTION, null, null, null);
3411        }
3412        return null;
3413    }
3414
3415    @Override
3416    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
3417        int id = loader.getId();
3418        switch (id) {
3419            case INIT_DRAFT_USING_REFERENCE_MESSAGE:
3420                if (data != null && data.moveToFirst()) {
3421                    mRefMessage = new Message(data);
3422                    Intent intent = getIntent();
3423                    initFromRefMessage(mComposeMode);
3424                    finishSetup(mComposeMode, intent, null);
3425                    if (mComposeMode != FORWARD) {
3426                        String to = intent.getStringExtra(EXTRA_TO);
3427                        if (!TextUtils.isEmpty(to)) {
3428                            mRefMessage.setTo(null);
3429                            mRefMessage.setFrom(null);
3430                            clearChangeListeners();
3431                            mTo.append(to);
3432                            initChangeListeners();
3433                        }
3434                    }
3435                } else {
3436                    finish();
3437                }
3438                break;
3439            case REFERENCE_MESSAGE_LOADER:
3440                // Only populate mRefMessage and leave other fields untouched.
3441                if (data != null && data.moveToFirst()) {
3442                    mRefMessage = new Message(data);
3443                }
3444                finishSetup(mComposeMode, getIntent(), mInnerSavedState);
3445                break;
3446            case LOADER_ACCOUNT_CURSOR:
3447                if (data != null && data.moveToFirst()) {
3448                    // there are accounts now!
3449                    Account account;
3450                    final ArrayList<Account> accounts = new ArrayList<Account>();
3451                    final ArrayList<Account> initializedAccounts = new ArrayList<Account>();
3452                    do {
3453                        account = new Account(data);
3454                        if (account.isAccountReady()) {
3455                            initializedAccounts.add(account);
3456                        }
3457                        accounts.add(account);
3458                    } while (data.moveToNext());
3459                    if (initializedAccounts.size() > 0) {
3460                        findViewById(R.id.wait).setVisibility(View.GONE);
3461                        getLoaderManager().destroyLoader(LOADER_ACCOUNT_CURSOR);
3462                        findViewById(R.id.compose).setVisibility(View.VISIBLE);
3463                        mAccounts = initializedAccounts.toArray(
3464                                new Account[initializedAccounts.size()]);
3465
3466                        finishCreate();
3467                        invalidateOptionsMenu();
3468                    } else {
3469                        // Show "waiting"
3470                        account = accounts.size() > 0 ? accounts.get(0) : null;
3471                        showWaitFragment(account);
3472                    }
3473                }
3474                break;
3475        }
3476    }
3477
3478    private void showWaitFragment(Account account) {
3479        WaitFragment fragment = getWaitFragment();
3480        if (fragment != null) {
3481            fragment.updateAccount(account);
3482        } else {
3483            findViewById(R.id.wait).setVisibility(View.VISIBLE);
3484            replaceFragment(WaitFragment.newInstance(account, true),
3485                    FragmentTransaction.TRANSIT_FRAGMENT_OPEN, TAG_WAIT);
3486        }
3487    }
3488
3489    private WaitFragment getWaitFragment() {
3490        return (WaitFragment) getFragmentManager().findFragmentByTag(TAG_WAIT);
3491    }
3492
3493    private int replaceFragment(Fragment fragment, int transition, String tag) {
3494        FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction();
3495        fragmentTransaction.setTransition(transition);
3496        fragmentTransaction.replace(R.id.wait, fragment, tag);
3497        final int transactionId = fragmentTransaction.commitAllowingStateLoss();
3498        return transactionId;
3499    }
3500
3501    @Override
3502    public void onLoaderReset(Loader<Cursor> arg0) {
3503        // Do nothing.
3504    }
3505
3506    @Override
3507    public Context getActivityContext() {
3508        return this;
3509    }
3510}
3511