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