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