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