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