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