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