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