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