ComposeActivity.java revision 151f1ad1613ecac0207144472872440517172b5a
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.content.ContentResolver;
26import android.content.ContentValues;
27import android.content.Context;
28import android.content.DialogInterface;
29import android.content.Intent;
30import android.content.pm.ActivityInfo;
31import android.database.Cursor;
32import android.net.Uri;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.HandlerThread;
36import android.os.Parcelable;
37import android.provider.BaseColumns;
38import android.text.Editable;
39import android.text.Html;
40import android.text.Spanned;
41import android.text.TextUtils;
42import android.text.TextWatcher;
43import android.text.util.Rfc822Token;
44import android.text.util.Rfc822Tokenizer;
45import android.view.LayoutInflater;
46import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
49import android.view.View;
50import android.view.View.OnClickListener;
51import android.view.ViewGroup;
52import android.view.inputmethod.BaseInputConnection;
53import android.widget.ArrayAdapter;
54import android.widget.Button;
55import android.widget.EditText;
56import android.widget.ImageView;
57import android.widget.TextView;
58import android.widget.Toast;
59
60import com.android.common.Rfc822Validator;
61import com.android.ex.chips.RecipientEditTextView;
62import com.android.mail.R;
63import com.android.mail.compose.AttachmentsView.AttachmentDeletedListener;
64import com.android.mail.compose.AttachmentsView.AttachmentFailureException;
65import com.android.mail.compose.FromAddressSpinner.OnAccountChangedListener;
66import com.android.mail.compose.QuotedTextView.RespondInlineListener;
67import com.android.mail.providers.Account;
68import com.android.mail.providers.Address;
69import com.android.mail.providers.Attachment;
70import com.android.mail.providers.Message;
71import com.android.mail.providers.MessageModification;
72import com.android.mail.providers.ReplyFromAccount;
73import com.android.mail.providers.Settings;
74import com.android.mail.providers.UIProvider;
75import com.android.mail.providers.UIProvider.AccountCapabilities;
76import com.android.mail.providers.UIProvider.DraftType;
77import com.android.mail.utils.AccountUtils;
78import com.android.mail.utils.LogUtils;
79import com.android.mail.utils.Utils;
80import com.google.common.annotations.VisibleForTesting;
81import com.google.common.collect.Lists;
82import com.google.common.collect.Sets;
83
84import org.json.JSONException;
85
86import java.io.UnsupportedEncodingException;
87import java.net.URLDecoder;
88import java.util.ArrayList;
89import java.util.Arrays;
90import java.util.Collection;
91import java.util.HashMap;
92import java.util.HashSet;
93import java.util.List;
94import java.util.Map.Entry;
95import java.util.Set;
96import java.util.concurrent.ConcurrentHashMap;
97
98public class ComposeActivity extends Activity implements OnClickListener, OnNavigationListener,
99        RespondInlineListener, DialogInterface.OnClickListener, TextWatcher,
100        AttachmentDeletedListener, OnAccountChangedListener {
101    // Identifiers for which type of composition this is
102    static final int COMPOSE = -1;
103    static final int REPLY = 0;
104    static final int REPLY_ALL = 1;
105    static final int FORWARD = 2;
106    static final int EDIT_DRAFT = 3;
107
108    // Integer extra holding one of the above compose action
109    private static final String EXTRA_ACTION = "action";
110
111    private static final String EXTRA_SHOW_CC_BCC = "showCcBcc";
112
113    private static final String UTF8_ENCODING_NAME = "UTF-8";
114
115    private static final String MAIL_TO = "mailto";
116
117    private static final String EXTRA_SUBJECT = "subject";
118
119    private static final String EXTRA_BODY = "body";
120
121    private static final String EXTRA_FROM_ACCOUNT_STRING = "fromAccountString";
122
123    // Extra that we can get passed from other activities
124    private static final String EXTRA_TO = "to";
125    private static final String EXTRA_CC = "cc";
126    private static final String EXTRA_BCC = "bcc";
127
128    // List of all the fields
129    static final String[] ALL_EXTRAS = { EXTRA_SUBJECT, EXTRA_BODY, EXTRA_TO, EXTRA_CC, EXTRA_BCC };
130
131    private static SendOrSaveCallback sTestSendOrSaveCallback = null;
132    // Map containing information about requests to create new messages, and the id of the
133    // messages that were the result of those requests.
134    //
135    // This map is used when the activity that initiated the save a of a new message, is killed
136    // before the save has completed (and when we know the id of the newly created message).  When
137    // a save is completed, the service that is running in the background, will update the map
138    //
139    // When a new ComposeActivity instance is created, it will attempt to use the information in
140    // the previously instantiated map.  If ComposeActivity.onCreate() is called, with a bundle
141    // (restoring data from a previous instance), and the map hasn't been created, we will attempt
142    // to populate the map with data stored in shared preferences.
143    private static ConcurrentHashMap<Integer, Long> sRequestMessageIdMap = null;
144    // Key used to store the above map
145    private static final String CACHED_MESSAGE_REQUEST_IDS_KEY = "cache-message-request-ids";
146    /**
147     * Notifies the {@code Activity} that the caller is an Email
148     * {@code Activity}, so that the back behavior may be modified accordingly.
149     *
150     * @see #onAppUpPressed
151     */
152    private static final String EXTRA_FROM_EMAIL_TASK = "fromemail";
153
154    static final String EXTRA_ATTACHMENTS = "attachments";
155
156    //  If this is a reply/forward then this extra will hold the original message
157    private static final String EXTRA_IN_REFERENCE_TO_MESSAGE = "in-reference-to-message";
158    // If this is an action to edit an existing draft messagge, this extra will hold the
159    // draft message
160    private static final String ORIGINAL_DRAFT_MESSAGE = "original-draft-message";
161    private static final String END_TOKEN = ", ";
162    private static final String LOG_TAG = new LogUtils().getLogTag();
163    // Request numbers for activities we start
164    private static final int RESULT_PICK_ATTACHMENT = 1;
165    private static final int RESULT_CREATE_ACCOUNT = 2;
166    // TODO(mindyp) set mime-type for auto send?
167    private static final String AUTO_SEND_ACTION = "com.android.mail.action.AUTO_SEND";
168
169    // Max size for attachments (5 megs). Will be overridden by account settings if found.
170    // TODO(mindyp): read this from account settings?
171    private static final int DEFAULT_MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
172    private static final String EXTRA_SELECTED_REPLY_FROM_ACCOUNT = "replyFromAccount";
173    private static final String EXTRA_REQUEST_ID = "requestId";
174    private static final String EXTRA_FOCUS_SELECTION_START = "focusSelectionStart";
175    private static final String EXTRA_FOCUS_SELECTION_END = null;
176    private static final String EXTRA_MESSAGE = "extraMessage";
177
178    /**
179     * A single thread for running tasks in the background.
180     */
181    private Handler mSendSaveTaskHandler = null;
182    private RecipientEditTextView mTo;
183    private RecipientEditTextView mCc;
184    private RecipientEditTextView mBcc;
185    private Button mCcBccButton;
186    private CcBccView mCcBccView;
187    private AttachmentsView mAttachmentsView;
188    private Account mAccount;
189    private ReplyFromAccount mReplyFromAccount;
190    private Settings mCachedSettings;
191    private Rfc822Validator mValidator;
192    private TextView mSubject;
193
194    private ComposeModeAdapter mComposeModeAdapter;
195    private int mComposeMode = -1;
196    private boolean mForward;
197    private String mRecipient;
198    private QuotedTextView mQuotedTextView;
199    private EditText mBodyView;
200    private View mFromStatic;
201    private TextView mFromStaticText;
202    private View mFromSpinnerWrapper;
203    private FromAddressSpinner mFromSpinner;
204    private boolean mAddingAttachment;
205    private boolean mAttachmentsChanged;
206    private boolean mTextChanged;
207    private boolean mReplyFromChanged;
208    private MenuItem mSave;
209    private MenuItem mSend;
210    private AlertDialog mRecipientErrorDialog;
211    private AlertDialog mSendConfirmDialog;
212    private Message mRefMessage;
213    private long mDraftId = UIProvider.INVALID_MESSAGE_ID;
214    private Message mDraft;
215    private Object mDraftLock = new Object();
216    private ImageView mAttachmentsButton;
217
218    /**
219     * Can be called from a non-UI thread.
220     */
221    public static void editDraft(Context launcher, Account account, Message message) {
222        launch(launcher, account, message, EDIT_DRAFT);
223    }
224
225    /**
226     * Can be called from a non-UI thread.
227     */
228    public static void compose(Context launcher, Account account) {
229        launch(launcher, account, null, COMPOSE);
230    }
231
232    /**
233     * Can be called from a non-UI thread.
234     */
235    public static void reply(Context launcher, Account account, Message message) {
236        launch(launcher, account, message, REPLY);
237    }
238
239    /**
240     * Can be called from a non-UI thread.
241     */
242    public static void replyAll(Context launcher, Account account, Message message) {
243        launch(launcher, account, message, REPLY_ALL);
244    }
245
246    /**
247     * Can be called from a non-UI thread.
248     */
249    public static void forward(Context launcher, Account account, Message message) {
250        launch(launcher, account, message, FORWARD);
251    }
252
253    private static void launch(Context launcher, Account account, Message message, int action) {
254        Intent intent = new Intent(launcher, ComposeActivity.class);
255        intent.putExtra(EXTRA_FROM_EMAIL_TASK, true);
256        intent.putExtra(EXTRA_ACTION, action);
257        intent.putExtra(Utils.EXTRA_ACCOUNT, account);
258        if (action == EDIT_DRAFT) {
259            intent.putExtra(ORIGINAL_DRAFT_MESSAGE, message);
260        } else {
261            intent.putExtra(EXTRA_IN_REFERENCE_TO_MESSAGE, message);
262        }
263        launcher.startActivity(intent);
264    }
265
266    @Override
267    public void onCreate(Bundle savedInstanceState) {
268        super.onCreate(savedInstanceState);
269        setContentView(R.layout.compose);
270        findViews();
271        Intent intent = getIntent();
272        Account account;
273        Message message;
274        int action;
275        if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_MESSAGE)) {
276            action = savedInstanceState.getInt(EXTRA_ACTION, COMPOSE);
277            account = savedInstanceState.getParcelable(Utils.EXTRA_ACCOUNT);
278            message = (Message) savedInstanceState.getParcelable(EXTRA_MESSAGE);
279            mRefMessage = (Message) savedInstanceState.getParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE);
280        } else {
281            account = (Account)intent.getParcelableExtra(Utils.EXTRA_ACCOUNT);
282            action = intent.getIntExtra(EXTRA_ACTION, COMPOSE);
283            // Initialize the message from the message in the intent
284            message = (Message) intent.getParcelableExtra(ORIGINAL_DRAFT_MESSAGE);
285            mRefMessage = (Message) intent.getParcelableExtra(EXTRA_IN_REFERENCE_TO_MESSAGE);
286        }
287        if (account == null) {
288            final Account[] syncingAccounts = AccountUtils.getSyncingAccounts(this);
289            if (syncingAccounts.length > 0) {
290                account = syncingAccounts[0];
291            }
292        }
293
294        setAccount(account);
295        if (mAccount == null) {
296            return;
297        }
298
299        if (message != null && action != EDIT_DRAFT) {
300            initFromDraftMessage(message);
301            initQuotedTextFromRefMessage(mRefMessage, action);
302            showCcBcc(savedInstanceState);
303        } else if (action == EDIT_DRAFT) {
304            initFromDraftMessage(message);
305            // Update the action to the draft type of the previous draft
306            switch (message.draftType) {
307                case UIProvider.DraftType.REPLY:
308                    action = REPLY;
309                    break;
310                case UIProvider.DraftType.REPLY_ALL:
311                    action = REPLY_ALL;
312                    break;
313                case UIProvider.DraftType.FORWARD:
314                    action = FORWARD;
315                    break;
316                case UIProvider.DraftType.COMPOSE:
317                default:
318                    action = COMPOSE;
319                    break;
320            }
321            initQuotedTextFromRefMessage(mRefMessage, action);
322        } else if ((action == REPLY || action == REPLY_ALL || action == FORWARD)) {
323            if (mRefMessage != null) {
324                initFromRefMessage(action, mAccount.name);
325            }
326        } else {
327            initFromExtras(intent);
328        }
329
330        if (action == COMPOSE) {
331            mQuotedTextView.setVisibility(View.GONE);
332        }
333        initRecipients();
334        initAttachmentsFromIntent(intent);
335        initActionBar(action);
336        initFromSpinner(savedInstanceState != null ? savedInstanceState : intent.getExtras(),
337                action);
338        initChangeListeners();
339        setFocus(action);
340    }
341
342    private void setFocus(int action) {
343        if (action == EDIT_DRAFT) {
344            int type = mDraft.draftType;
345            switch (type) {
346                case UIProvider.DraftType.COMPOSE:
347                case UIProvider.DraftType.FORWARD:
348                    action = COMPOSE;
349                    break;
350                case UIProvider.DraftType.REPLY:
351                case UIProvider.DraftType.REPLY_ALL:
352                default:
353                    action = REPLY;
354                    break;
355            }
356        }
357        switch (action) {
358            case FORWARD:
359            case COMPOSE:
360                mTo.requestFocus();
361                break;
362            case REPLY:
363            case REPLY_ALL:
364            default:
365                focusBody();
366                break;
367        }
368    }
369
370    /**
371     * Focus the body of the message.
372     */
373    public void focusBody() {
374        mBodyView.requestFocus();
375        int length = mBodyView.getText().length();
376
377        int signatureStartPos = getSignatureStartPosition(
378                mSignature, mBodyView.getText().toString());
379        if (signatureStartPos > -1) {
380            // In case the user deleted the newlines...
381            mBodyView.setSelection(signatureStartPos);
382        } else if (length > 0) {
383            // Move cursor to the end.
384            mBodyView.setSelection(length);
385        }
386    }
387
388    @Override
389    protected void onResume() {
390        super.onResume();
391        // Update the from spinner as other accounts
392        // may now be available.
393        if (mFromSpinner != null && mAccount != null) {
394            mFromSpinner.asyncInitFromSpinner(mComposeMode, mAccount);
395        }
396    }
397
398    @Override
399    protected void onPause() {
400        super.onPause();
401
402        if (mSendConfirmDialog != null) {
403            mSendConfirmDialog.dismiss();
404        }
405        if (mRecipientErrorDialog != null) {
406            mRecipientErrorDialog.dismiss();
407        }
408        saveIfNeeded();
409    }
410
411    @Override
412    protected final void onActivityResult(int request, int result, Intent data) {
413        mAddingAttachment = false;
414
415        if (result == RESULT_OK && request == RESULT_PICK_ATTACHMENT) {
416            addAttachmentAndUpdateView(data);
417        }
418    }
419
420    @Override
421    public final void onRestoreInstanceState(Bundle savedInstanceState) {
422        super.onRestoreInstanceState(savedInstanceState);
423        if (savedInstanceState != null) {
424            if (savedInstanceState.containsKey(EXTRA_FOCUS_SELECTION_START)) {
425                int selectionStart = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_START);
426                int selectionEnd = savedInstanceState.getInt(EXTRA_FOCUS_SELECTION_END);
427                // There should be a focus and it should be an EditText since we
428                // only save these extras if these conditions are true.
429                EditText focusEditText = (EditText) getCurrentFocus();
430                final int length = focusEditText.getText().length();
431                if (selectionStart < length && selectionEnd < length) {
432                    focusEditText.setSelection(selectionStart, selectionEnd);
433                }
434            }
435        }
436    }
437
438    @Override
439    public final void onSaveInstanceState(Bundle state) {
440        super.onSaveInstanceState(state);
441        // The framework is happy to save and restore the selection but only if it also saves and
442        // restores the contents of the edit text. That's a lot of text to put in a bundle so we do
443        // this manually.
444        View focus = getCurrentFocus();
445        if (focus != null && focus instanceof EditText) {
446            EditText focusEditText = (EditText) focus;
447            state.putInt(EXTRA_FOCUS_SELECTION_START, focusEditText.getSelectionStart());
448            state.putInt(EXTRA_FOCUS_SELECTION_END, focusEditText.getSelectionEnd());
449        }
450
451        final List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
452        final int selectedPos = mFromSpinner.getSelectedItemPosition();
453        final ReplyFromAccount selectedReplyFromAccount =
454                (replyFromAccounts.size() > 0 && replyFromAccounts.size() > selectedPos) ?
455                        replyFromAccounts.get(selectedPos) :
456                        null;
457        if (selectedReplyFromAccount != null) {
458            state.putString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT, selectedReplyFromAccount.serialize()
459                    .toString());
460            state.putParcelable(Utils.EXTRA_ACCOUNT, selectedReplyFromAccount.account);
461        } else {
462            state.putParcelable(Utils.EXTRA_ACCOUNT, mAccount);
463        }
464
465        if (mDraftId == UIProvider.INVALID_MESSAGE_ID && mRequestId !=0) {
466            // We don't have a draft id, and we have a request id,
467            // save the request id.
468            state.putInt(EXTRA_REQUEST_ID, mRequestId);
469        }
470
471        // We want to restore the current mode after a pause
472        // or rotation.
473        int mode = getMode();
474        state.putInt(EXTRA_ACTION, mode);
475
476        Message message = createMessage(selectedReplyFromAccount, mode);
477        state.putParcelable(EXTRA_MESSAGE, message);
478
479        if (mRefMessage != null) {
480            state.putParcelable(EXTRA_IN_REFERENCE_TO_MESSAGE, mRefMessage);
481        }
482        state.putBoolean(EXTRA_SHOW_CC_BCC, mCcBccView.isVisible());
483    }
484
485    private int getMode() {
486        int mode = ComposeActivity.COMPOSE;
487        ActionBar actionBar = getActionBar();
488        if (actionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_LIST) {
489            mode = actionBar.getSelectedNavigationIndex();
490        }
491        return mode;
492    }
493
494    private Message createMessage(ReplyFromAccount selectedReplyFromAccount, int mode) {
495        Message message = new Message();
496        message.id = UIProvider.INVALID_MESSAGE_ID;
497        message.serverId =UIProvider.INVALID_MESSAGE_ID;
498        message.uri = null;
499        message.conversationUri = null;
500        message.subject = mSubject.getText().toString();
501        message.snippet = null;
502        message.from = selectedReplyFromAccount.name;
503        message.to = mTo.getText().toString();
504        message.cc = mCc.getText().toString();
505        message.bcc = mBcc.getText().toString();
506        message.replyTo = null;
507        message.dateReceivedMs = 0;
508        String htmlBody = Html.toHtml(mBodyView.getText());
509        StringBuilder fullBody = new StringBuilder(htmlBody);
510        message.bodyHtml = fullBody.toString();
511        message.bodyText = mBodyView.getText().toString();
512        message.embedsExternalResources = false;
513        message.refMessageId = mRefMessage != null ? mRefMessage.uri.toString() : null;
514        message.draftType = mode;
515        message.appendRefMessageContent = mQuotedTextView.getQuotedTextIfIncluded() != null;
516        ArrayList<Attachment> attachments = mAttachmentsView.getAttachments();
517        message.hasAttachments = attachments != null && attachments.size() > 0;
518        message.attachmentListUri = null;
519        message.messageFlags = 0;
520        message.saveUri = null;
521        message.sendUri = null;
522        message.alwaysShowImages = false;
523        message.attachmentsJson = Attachment.toJSONArray(attachments);
524        CharSequence quotedText = mQuotedTextView.getQuotedText();
525        message.quotedTextOffset = !TextUtils.isEmpty(quotedText) ? QuotedTextView
526                .getQuotedTextOffset(quotedText.toString()) : -1;
527        message.accountUri = null;
528        return message;
529    }
530
531    @VisibleForTesting
532    void setAccount(Account account) {
533        if (account == null) {
534            return;
535        }
536        if (!account.equals(mAccount)) {
537            mAccount = account;
538            mCachedSettings = mAccount.settings;
539            appendSignature();
540        }
541    }
542
543    private void initFromSpinner(Bundle bundle, int action) {
544        String accountString = null;
545        if (action == EDIT_DRAFT && mDraft.draftType == UIProvider.DraftType.COMPOSE) {
546            action = COMPOSE;
547        }
548        mFromSpinner.asyncInitFromSpinner(action, mAccount);
549        if (bundle != null) {
550            if (bundle.containsKey(EXTRA_SELECTED_REPLY_FROM_ACCOUNT)) {
551                mReplyFromAccount = ReplyFromAccount.deserialize(mAccount,
552                        bundle.getString(EXTRA_SELECTED_REPLY_FROM_ACCOUNT));
553            } else if (bundle.containsKey(EXTRA_FROM_ACCOUNT_STRING)) {
554                accountString = bundle.getString(EXTRA_FROM_ACCOUNT_STRING);
555                mReplyFromAccount = mFromSpinner.getMatchingReplyFromAccount(accountString);
556            }
557        }
558        if (mReplyFromAccount == null) {
559            if (mDraft != null) {
560                mReplyFromAccount = getReplyFromAccountFromDraft(mAccount, mDraft);
561            } else if (mRefMessage != null) {
562                mReplyFromAccount = getReplyFromAccountForReply(mAccount, mRefMessage);
563            }
564        }
565        if (mReplyFromAccount == null) {
566            mReplyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
567                    mAccount.name, true, false);
568        }
569
570        mFromSpinner.setCurrentAccount(mReplyFromAccount);
571
572        if (mFromSpinner.getCount() > 1) {
573            // If there is only 1 account, just show that account.
574            // Otherwise, give the user the ability to choose which account to
575            // send mail from / save drafts to.
576            mFromStatic.setVisibility(View.GONE);
577            mFromStaticText.setText(mAccount.name);
578            mFromSpinnerWrapper.setVisibility(View.VISIBLE);
579        } else {
580            mFromStatic.setVisibility(View.VISIBLE);
581            mFromStaticText.setText(mAccount.name);
582            mFromSpinnerWrapper.setVisibility(View.GONE);
583        }
584    }
585
586    private ReplyFromAccount getReplyFromAccountForReply(Account account, Message refMessage) {
587        if (refMessage.accountUri != null) {
588            // This must be from combined inbox.
589            List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
590            for (ReplyFromAccount from : replyFromAccounts) {
591                if (from.account.uri.equals(refMessage.accountUri)) {
592                    return from;
593                }
594            }
595            return null;
596        } else {
597            return getReplyFromAccount(account, refMessage);
598        }
599    }
600
601    /**
602     * Given an account and which email address the message was sent to,
603     * return who the message should be sent from.
604     * @param account Account in which the message arrived.
605     * @param sentTo Email address to which the message was sent.
606     * @return the address from which to reply.
607     */
608    public ReplyFromAccount getReplyFromAccount(Account account, Message refMessage) {
609        // First see if we are supposed to use the default address or
610        // the address it was sentTo.
611        if (false) { //mCachedSettings.forceReplyFromDefault) {
612            return getDefaultReplyFromAccount(account);
613        } else {
614            // If we aren't explicityly told which account to look for, look at
615            // all the message recipients and find one that matches
616            // a custom from or account.
617            List<String> allRecipients = new ArrayList<String>();
618            allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.to)));
619            allRecipients.addAll(Arrays.asList(Utils.splitCommaSeparatedString(refMessage.cc)));
620            return getMatchingRecipient(account, allRecipients);
621        }
622    }
623
624    /**
625     * Compare all the recipients of an email to the current account and all
626     * custom addresses associated with that account. Return the match if there
627     * is one, or the default account if there isn't.
628     */
629    protected ReplyFromAccount getMatchingRecipient(Account account, List<String> sentTo) {
630        // Tokenize the list and place in a hashmap.
631        ReplyFromAccount matchingReplyFrom = null;
632        Rfc822Token[] tokens;
633        HashSet<String> recipientsMap = new HashSet<String>();
634        for (String address : sentTo) {
635            tokens = Rfc822Tokenizer.tokenize(address);
636            for (int i = 0; i < tokens.length; i++) {
637                recipientsMap.add(tokens[i].getAddress());
638            }
639        }
640
641        int matchingAddressCount = 0;
642        List<ReplyFromAccount> customFroms;
643        try {
644            customFroms = FromAddressSpinner.getAccountSpecificFroms(account);
645            if (customFroms != null) {
646                for (ReplyFromAccount entry : customFroms) {
647                    if (recipientsMap.contains(entry.address)) {
648                        matchingReplyFrom = entry;
649                        matchingAddressCount++;
650                    }
651                }
652            }
653        } catch (JSONException e) {
654            LogUtils.wtf(LOG_TAG, "Exception parsing from addresses for account %s",
655                    account.name);
656        }
657        if (matchingAddressCount > 1) {
658            matchingReplyFrom = getDefaultReplyFromAccount(account);
659        }
660        return matchingReplyFrom;
661    }
662
663    private ReplyFromAccount getDefaultReplyFromAccount(Account account) {
664        List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
665        for (ReplyFromAccount from : replyFromAccounts) {
666            if (from.isDefault) {
667                return from;
668            }
669        }
670        return new ReplyFromAccount(account, account.uri, account.name, account.name, true, false);
671    }
672
673    private ReplyFromAccount getReplyFromAccountFromDraft(Account account, Message msg) {
674        String sender = msg.from;
675        ReplyFromAccount replyFromAccount = null;
676        List<ReplyFromAccount> replyFromAccounts = mFromSpinner.getReplyFromAccounts();
677        if (TextUtils.equals(account.name, sender)) {
678            replyFromAccount = new ReplyFromAccount(mAccount, mAccount.uri, mAccount.name,
679                    mAccount.name, true, false);
680        } else {
681            for (ReplyFromAccount fromAccount : replyFromAccounts) {
682                if (TextUtils.equals(fromAccount.name, sender)) {
683                    replyFromAccount = fromAccount;
684                    break;
685                }
686            }
687        }
688        return replyFromAccount;
689    }
690
691    private void findViews() {
692        mCcBccButton = (Button) findViewById(R.id.add_cc_bcc);
693        if (mCcBccButton != null) {
694            mCcBccButton.setOnClickListener(this);
695        }
696        mCcBccView = (CcBccView) findViewById(R.id.cc_bcc_wrapper);
697        mAttachmentsView = (AttachmentsView)findViewById(R.id.attachments);
698        mAttachmentsButton = (ImageView) findViewById(R.id.add_attachment);
699        if (mAttachmentsButton != null) {
700            mAttachmentsButton.setOnClickListener(this);
701        }
702        mTo = (RecipientEditTextView) findViewById(R.id.to);
703        mCc = (RecipientEditTextView) findViewById(R.id.cc);
704        mBcc = (RecipientEditTextView) findViewById(R.id.bcc);
705        // TODO: add special chips text change watchers before adding
706        // this as a text changed watcher to the to, cc, bcc fields.
707        mSubject = (TextView) findViewById(R.id.subject);
708        mQuotedTextView = (QuotedTextView) findViewById(R.id.quoted_text_view);
709        mQuotedTextView.setRespondInlineListener(this);
710        mBodyView = (EditText) findViewById(R.id.body);
711        mFromStatic = findViewById(R.id.static_from_content);
712        mFromStaticText = (TextView) findViewById(R.id.from_account_name);
713        mFromSpinnerWrapper = findViewById(R.id.spinner_from_content);
714        mFromSpinner = (FromAddressSpinner) findViewById(R.id.from_picker);
715    }
716
717    // Now that the message has been initialized from any existing draft or
718    // ref message data, set up listeners for any changes that occur to the
719    // message.
720    private void initChangeListeners() {
721        mSubject.addTextChangedListener(this);
722        mBodyView.addTextChangedListener(this);
723        mTo.addTextChangedListener(new RecipientTextWatcher(mTo, this));
724        mCc.addTextChangedListener(new RecipientTextWatcher(mCc, this));
725        mBcc.addTextChangedListener(new RecipientTextWatcher(mBcc, this));
726        mFromSpinner.setOnAccountChangedListener(this);
727        mAttachmentsView.setAttachmentChangesListener(this);
728    }
729
730    private void initActionBar(int action) {
731        mComposeMode = action;
732        ActionBar actionBar = getActionBar();
733        if (action == ComposeActivity.COMPOSE) {
734            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
735            actionBar.setTitle(R.string.compose);
736        } else {
737            actionBar.setTitle(null);
738            if (mComposeModeAdapter == null) {
739                mComposeModeAdapter = new ComposeModeAdapter(this);
740            }
741            actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
742            actionBar.setListNavigationCallbacks(mComposeModeAdapter, this);
743            switch (action) {
744                case ComposeActivity.REPLY:
745                    actionBar.setSelectedNavigationItem(0);
746                    break;
747                case ComposeActivity.REPLY_ALL:
748                    actionBar.setSelectedNavigationItem(1);
749                    break;
750                case ComposeActivity.FORWARD:
751                    actionBar.setSelectedNavigationItem(2);
752                    break;
753            }
754        }
755        actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME,
756                ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_HOME);
757        actionBar.setHomeButtonEnabled(true);
758    }
759
760    private void initFromRefMessage(int action, String recipientAddress) {
761        setSubject(mRefMessage, action);
762        // Setup recipients
763        if (action == FORWARD) {
764            mForward = true;
765        }
766        initRecipientsFromRefMessage(recipientAddress, mRefMessage, action);
767        initQuotedTextFromRefMessage(mRefMessage, action);
768        if (action == ComposeActivity.FORWARD || mAttachmentsChanged) {
769            initAttachments(mRefMessage);
770        }
771        updateHideOrShowCcBcc();
772    }
773
774    private void initFromDraftMessage(Message message) {
775        LogUtils.d(LOG_TAG, "Intializing draft from previous draft message");
776
777        mDraft = message;
778        mDraftId = message.id;
779        mSubject.setText(message.subject);
780        mForward = message.draftType == UIProvider.DraftType.FORWARD;
781        final List<String> toAddresses = Arrays.asList(message.getToAddresses());
782        addToAddresses(toAddresses);
783        addCcAddresses(Arrays.asList(message.getCcAddresses()), toAddresses);
784        addBccAddresses(Arrays.asList(message.getBccAddresses()));
785        if (message.hasAttachments) {
786            List<Attachment> attachments = message.getAttachments();
787            for (Attachment a : attachments) {
788                addAttachmentAndUpdateView(a);
789            }
790        }
791
792        // Set the body
793        if (!TextUtils.isEmpty(message.bodyHtml)) {
794            mBodyView.setText(Html.fromHtml(message.bodyHtml));
795        } else {
796            mBodyView.setText(message.bodyText);
797        }
798    }
799
800    /**
801     * Fill all the widgets with the content found in the Intent Extra, if any.
802     * Also apply the same style to all widgets. Note: if initFromExtras is
803     * called as a result of switching between reply, reply all, and forward per
804     * the latest revision of Gmail, and the user has already made changes to
805     * attachments on a previous incarnation of the message (as a reply, reply
806     * all, or forward), the original attachments from the message will not be
807     * re-instantiated. The user's changes will be respected. This follows the
808     * web gmail interaction.
809     */
810    public void initFromExtras(Intent intent) {
811        // If we were invoked with a SENDTO intent, the value
812        // should take precedence
813        final Uri dataUri = intent.getData();
814        if (dataUri != null) {
815            if (MAIL_TO.equals(dataUri.getScheme())) {
816                initFromMailTo(dataUri.toString());
817            } else {
818                if (!mAccount.composeIntentUri.equals(dataUri)) {
819                    String toText = dataUri.getSchemeSpecificPart();
820                    if (toText != null) {
821                        mTo.setText("");
822                        addToAddresses(Arrays.asList(TextUtils.split(toText, ",")));
823                    }
824                }
825            }
826        }
827
828        String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
829        if (extraStrings != null) {
830            addToAddresses(Arrays.asList(extraStrings));
831        }
832        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
833        if (extraStrings != null) {
834            addCcAddresses(Arrays.asList(extraStrings), null);
835        }
836        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
837        if (extraStrings != null) {
838            addBccAddresses(Arrays.asList(extraStrings));
839        }
840
841        String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
842        if (extraString != null) {
843            mSubject.setText(extraString);
844        }
845
846        for (String extra : ALL_EXTRAS) {
847            if (intent.hasExtra(extra)) {
848                String value = intent.getStringExtra(extra);
849                if (EXTRA_TO.equals(extra)) {
850                    addToAddresses(Arrays.asList(TextUtils.split(value, ",")));
851                } else if (EXTRA_CC.equals(extra)) {
852                    addCcAddresses(Arrays.asList(TextUtils.split(value, ",")), null);
853                } else if (EXTRA_BCC.equals(extra)) {
854                    addBccAddresses(Arrays.asList(TextUtils.split(value, ",")));
855                } else if (EXTRA_SUBJECT.equals(extra)) {
856                    mSubject.setText(value);
857                } else if (EXTRA_BODY.equals(extra)) {
858                    setBody(value, true /* with signature */);
859                }
860            }
861        }
862
863        Bundle extras = intent.getExtras();
864        if (extras != null) {
865            CharSequence text = extras.getCharSequence(Intent.EXTRA_TEXT);
866            if (text != null) {
867                setBody(text, true /* with signature */);
868            }
869        }
870
871        updateHideOrShowCcBcc();
872    }
873
874    private void initFromMessageInIntent(Message message) {
875        mTo.append(message.to);
876        mCc.append(message.cc);
877        mBcc.append(message.bcc);
878        mBodyView.setText(message.bodyText);
879        mSubject.setText(message.subject);
880        List<Attachment> attachments = message.getAttachments();
881        for (Attachment a : attachments) {
882            mAttachmentsView.addAttachment(a);
883        }
884        mQuotedTextView.updateCheckedState(message.appendRefMessageContent);
885    }
886
887    @VisibleForTesting
888    protected String decodeEmailInUri(String s) throws UnsupportedEncodingException {
889        // TODO: handle the case where there are spaces in the display name as well as the email
890        // such as "Guy with spaces <guy+with+spaces@gmail.com>" as they it could be encoded
891        // ambiguously.
892
893        // Since URLDecode.decode changes + into ' ', and + is a valid
894        // email character, we need to find/ replace these ourselves before
895        // decoding.
896        String replacePlus = s.replace("+", "%2B");
897        return URLDecoder.decode(replacePlus, UTF8_ENCODING_NAME);
898    }
899
900    /**
901     * Initialize the compose view from a String representing a mailTo uri.
902     * @param mailToString The uri as a string.
903     */
904    public void initFromMailTo(String mailToString) {
905        // We need to disguise this string as a URI in order to parse it
906        // TODO:  Remove this hack when http://b/issue?id=1445295 gets fixed
907        Uri uri = Uri.parse("foo://" + mailToString);
908        int index = mailToString.indexOf("?");
909        int length = "mailto".length() + 1;
910        String to;
911        try {
912            // Extract the recipient after mailto:
913            if (index == -1) {
914                to = decodeEmailInUri(mailToString.substring(length));
915            } else {
916                to = decodeEmailInUri(mailToString.substring(length, index));
917            }
918            addToAddresses(Arrays.asList(TextUtils.split(to, ",")));
919        } catch (UnsupportedEncodingException e) {
920            if (LogUtils.isLoggable(LOG_TAG, LogUtils.VERBOSE)) {
921                LogUtils.e(LOG_TAG, "%s while decoding '%s'", e.getMessage(), mailToString);
922            } else {
923                LogUtils.e(LOG_TAG, e, "Exception  while decoding mailto address");
924            }
925        }
926
927        List<String> cc = uri.getQueryParameters("cc");
928        addCcAddresses(Arrays.asList(cc.toArray(new String[cc.size()])), null);
929
930        List<String> otherTo = uri.getQueryParameters("to");
931        addToAddresses(Arrays.asList(otherTo.toArray(new String[otherTo.size()])));
932
933        List<String> bcc = uri.getQueryParameters("bcc");
934        addBccAddresses(Arrays.asList(bcc.toArray(new String[bcc.size()])));
935
936        List<String> subject = uri.getQueryParameters("subject");
937        if (subject.size() > 0) {
938            try {
939                mSubject.setText(URLDecoder.decode(subject.get(0), UTF8_ENCODING_NAME));
940            } catch (UnsupportedEncodingException e) {
941                LogUtils.e(LOG_TAG, "%s while decoding subject '%s'",
942                        e.getMessage(), subject);
943            }
944        }
945
946        List<String> body = uri.getQueryParameters("body");
947        if (body.size() > 0) {
948            try {
949                setBody(URLDecoder.decode(body.get(0), UTF8_ENCODING_NAME),
950                        true /* with signature */);
951            } catch (UnsupportedEncodingException e) {
952                LogUtils.e(LOG_TAG, "%s while decoding body '%s'", e.getMessage(), body);
953            }
954        }
955
956        updateHideOrShowCcBcc();
957    }
958
959    private void initAttachments(Message refMessage) {
960        mAttachmentsView.addAttachments(mAccount, refMessage);
961    }
962
963    private void initAttachmentsFromIntent(Intent intent) {
964        Bundle extras = intent.getExtras();
965        if (extras == null) {
966            extras = Bundle.EMPTY;
967        }
968        final String action = intent.getAction();
969        if (!mAttachmentsChanged) {
970            long totalSize = 0;
971            if (extras.containsKey(EXTRA_ATTACHMENTS)) {
972                String[] uris = (String[]) extras.getSerializable(EXTRA_ATTACHMENTS);
973                for (String uriString : uris) {
974                    final Uri uri = Uri.parse(uriString);
975                    long size = 0;
976                    try {
977                        size =  mAttachmentsView.addAttachment(mAccount, uri);
978                    } catch (AttachmentFailureException e) {
979                        // A toast has already been shown to the user,
980                        // just break out of the loop.
981                        LogUtils.e(LOG_TAG, e, "Error adding attachment");
982                    }
983                    totalSize += size;
984                }
985            }
986            if (Intent.ACTION_SEND.equals(action) && extras.containsKey(Intent.EXTRA_STREAM)) {
987                final Uri uri = (Uri) extras.getParcelable(Intent.EXTRA_STREAM);
988                long size = 0;
989                try {
990                    size =  mAttachmentsView.addAttachment(mAccount, uri);
991                } catch (AttachmentFailureException e) {
992                    // A toast has already been shown to the user, so just
993                    // exit.
994                    LogUtils.e(LOG_TAG, e, "Error adding attachment");
995                }
996                totalSize += size;
997            }
998
999            if (Intent.ACTION_SEND_MULTIPLE.equals(action)
1000                    && extras.containsKey(Intent.EXTRA_STREAM)) {
1001                ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
1002                for (Parcelable uri : uris) {
1003                    long size = 0;
1004                    try {
1005                        size = mAttachmentsView.addAttachment(mAccount, (Uri) uri);
1006                    } catch (AttachmentFailureException e) {
1007                        // A toast has already been shown to the user,
1008                        // just break out of the loop.
1009                        LogUtils.e(LOG_TAG, e, "Error adding attachment");
1010                    }
1011                    totalSize += size;
1012                }
1013            }
1014
1015            if (totalSize > 0) {
1016                mAttachmentsChanged = true;
1017                updateSaveUi();
1018            }
1019        }
1020    }
1021
1022
1023    private void initQuotedTextFromRefMessage(Message refMessage, int action) {
1024        if (mRefMessage != null && (action == REPLY || action == REPLY_ALL || action == FORWARD)) {
1025            mQuotedTextView.setQuotedText(action, refMessage, action != FORWARD);
1026        }
1027    }
1028
1029    private void updateHideOrShowCcBcc() {
1030        // Its possible there is a menu item OR a button.
1031        boolean ccVisible = !TextUtils.isEmpty(mCc.getText());
1032        boolean bccVisible = !TextUtils.isEmpty(mBcc.getText());
1033        if (ccVisible || bccVisible) {
1034            mCcBccView.show(false, ccVisible, bccVisible);
1035        }
1036        if (mCcBccButton != null) {
1037            if (!mCc.isShown() || !mBcc.isShown()) {
1038                mCcBccButton.setVisibility(View.VISIBLE);
1039                mCcBccButton.setText(getString(!mCc.isShown() ? R.string.add_cc_label
1040                        : R.string.add_bcc_label));
1041            } else {
1042                mCcBccButton.setVisibility(View.GONE);
1043            }
1044        }
1045    }
1046
1047    private void showCcBcc(Bundle state) {
1048        if (state != null && state.containsKey(EXTRA_SHOW_CC_BCC)) {
1049            boolean show = state.getBoolean(EXTRA_SHOW_CC_BCC);
1050            if (show) {
1051                mCcBccView.show(false, show, show);
1052            }
1053        }
1054    }
1055
1056    /**
1057     * Add attachment and update the compose area appropriately.
1058     * @param data
1059     */
1060    public void addAttachmentAndUpdateView(Intent data) {
1061        addAttachmentAndUpdateView(data != null ? data.getData() : (Uri) null);
1062    }
1063
1064    public void addAttachmentAndUpdateView(Uri contentUri) {
1065        if (contentUri == null) {
1066            return;
1067        }
1068        try {
1069            addAttachmentAndUpdateView(mAttachmentsView.generateLocalAttachment(contentUri));
1070        } catch (AttachmentFailureException e) {
1071            // A toast has already been shown to the user, no need to do
1072            // anything.
1073            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1074        }
1075    }
1076
1077    public void addAttachmentAndUpdateView(Attachment attachment) {
1078        try {
1079            long size =  mAttachmentsView.addAttachment(mAccount, attachment);
1080            if (size > 0) {
1081                mAttachmentsChanged = true;
1082                updateSaveUi();
1083            }
1084        } catch (AttachmentFailureException e) {
1085            // A toast has already been shown to the user, no need to do
1086            // anything.
1087            LogUtils.e(LOG_TAG, e, "Error adding attachment");
1088        }
1089    }
1090
1091    void initRecipientsFromRefMessage(String recipientAddress, Message refMessage,
1092            int action) {
1093        // Don't populate the address if this is a forward.
1094        if (action == ComposeActivity.FORWARD) {
1095            return;
1096        }
1097        initReplyRecipients(mAccount.name, refMessage, action);
1098    }
1099
1100    @VisibleForTesting
1101    void initReplyRecipients(String account, Message refMessage, int action) {
1102        // This is the email address of the current user, i.e. the one composing
1103        // the reply.
1104        final String accountEmail = Address.getEmailAddress(account).getAddress();
1105        String fromAddress = refMessage.from;
1106        String[] sentToAddresses = Utils.splitCommaSeparatedString(refMessage.to);
1107        String replytoAddress = refMessage.replyTo;
1108        final Collection<String> toAddresses;
1109
1110        // If this is a reply, the Cc list is empty. If this is a reply-all, the
1111        // Cc list is the union of the To and Cc recipients of the original
1112        // message, excluding the current user's email address and any addresses
1113        // already on the To list.
1114        if (action == ComposeActivity.REPLY) {
1115            toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1116                    new String[0]);
1117            addToAddresses(toAddresses);
1118        } else if (action == ComposeActivity.REPLY_ALL) {
1119            final Set<String> ccAddresses = Sets.newHashSet();
1120            toAddresses = initToRecipients(account, accountEmail, fromAddress, replytoAddress,
1121                    new String[0]);
1122            addToAddresses(toAddresses);
1123            addRecipients(accountEmail, ccAddresses, sentToAddresses);
1124            addRecipients(accountEmail, ccAddresses,
1125                    Utils.splitCommaSeparatedString(refMessage.cc));
1126            addCcAddresses(ccAddresses, toAddresses);
1127        }
1128    }
1129
1130    private void addToAddresses(Collection<String> addresses) {
1131        addAddressesToList(addresses, mTo);
1132    }
1133
1134    private void addCcAddresses(Collection<String> addresses, Collection<String> toAddresses) {
1135        addCcAddressesToList(tokenizeAddressList(addresses),
1136                toAddresses != null ? tokenizeAddressList(toAddresses) : null, mCc);
1137    }
1138
1139    private void addBccAddresses(Collection<String> addresses) {
1140        addAddressesToList(addresses, mBcc);
1141    }
1142
1143    @VisibleForTesting
1144    protected void addCcAddressesToList(List<Rfc822Token[]> addresses,
1145            List<Rfc822Token[]> compareToList, RecipientEditTextView list) {
1146        String address;
1147
1148        if (compareToList == null) {
1149            for (Rfc822Token[] tokens : addresses) {
1150                for (int i = 0; i < tokens.length; i++) {
1151                    address = tokens[i].toString();
1152                    list.append(address + END_TOKEN);
1153                }
1154            }
1155        } else {
1156            HashSet<String> compareTo = convertToHashSet(compareToList);
1157            for (Rfc822Token[] tokens : addresses) {
1158                for (int i = 0; i < tokens.length; i++) {
1159                    address = tokens[i].toString();
1160                    // Check if this is a duplicate:
1161                    if (!compareTo.contains(tokens[i].getAddress())) {
1162                        // Get the address here
1163                        list.append(address + END_TOKEN);
1164                    }
1165                }
1166            }
1167        }
1168    }
1169
1170    private HashSet<String> convertToHashSet(List<Rfc822Token[]> list) {
1171        HashSet<String> hash = new HashSet<String>();
1172        for (Rfc822Token[] tokens : list) {
1173            for (int i = 0; i < tokens.length; i++) {
1174                hash.add(tokens[i].getAddress());
1175            }
1176        }
1177        return hash;
1178    }
1179
1180    protected List<Rfc822Token[]> tokenizeAddressList(Collection<String> addresses) {
1181        @VisibleForTesting
1182        List<Rfc822Token[]> tokenized = new ArrayList<Rfc822Token[]>();
1183
1184        for (String address: addresses) {
1185            tokenized.add(Rfc822Tokenizer.tokenize(address));
1186        }
1187        return tokenized;
1188    }
1189
1190    @VisibleForTesting
1191    void addAddressesToList(Collection<String> addresses, RecipientEditTextView list) {
1192        for (String address : addresses) {
1193            addAddressToList(address, list);
1194        }
1195    }
1196
1197    private void addAddressToList(String address, RecipientEditTextView list) {
1198        if (address == null || list == null)
1199            return;
1200
1201        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(address);
1202
1203        for (int i = 0; i < tokens.length; i++) {
1204            list.append(tokens[i] + END_TOKEN);
1205        }
1206    }
1207
1208    @VisibleForTesting
1209    protected Collection<String> initToRecipients(String account, String accountEmail,
1210            String senderAddress, String replyToAddress, String[] inToAddresses) {
1211        // The To recipient is the reply-to address specified in the original
1212        // message, unless it is:
1213        // the current user OR a custom from of the current user, in which case
1214        // it's the To recipient list of the original message.
1215        // OR missing, in which case use the sender of the original message
1216        Set<String> toAddresses = Sets.newHashSet();
1217        if (!TextUtils.isEmpty(replyToAddress)) {
1218            toAddresses.add(replyToAddress);
1219        } else {
1220            toAddresses.add(senderAddress);
1221        }
1222        return toAddresses;
1223    }
1224
1225    private static void addRecipients(String account, Set<String> recipients, String[] addresses) {
1226        for (String email : addresses) {
1227            // Do not add this account, or any of the custom froms, to the list
1228            // of recipients.
1229            final String recipientAddress = Address.getEmailAddress(email).getAddress();
1230            if (!account.equalsIgnoreCase(recipientAddress)) {
1231                recipients.add(email.replace("\"\"", ""));
1232            }
1233        }
1234    }
1235
1236    private void setSubject(Message refMessage, int action) {
1237        String subject = refMessage.subject;
1238        String prefix;
1239        String correctedSubject = null;
1240        if (action == ComposeActivity.COMPOSE) {
1241            prefix = "";
1242        } else if (action == ComposeActivity.FORWARD) {
1243            prefix = getString(R.string.forward_subject_label);
1244        } else {
1245            prefix = getString(R.string.reply_subject_label);
1246        }
1247
1248        // Don't duplicate the prefix
1249        if (subject.toLowerCase().startsWith(prefix.toLowerCase())) {
1250            correctedSubject = subject;
1251        } else {
1252            correctedSubject = String
1253                    .format(getString(R.string.formatted_subject), prefix, subject);
1254        }
1255        mSubject.setText(correctedSubject);
1256    }
1257
1258    private void initRecipients() {
1259        setupRecipients(mTo);
1260        setupRecipients(mCc);
1261        setupRecipients(mBcc);
1262    }
1263
1264    private void setupRecipients(RecipientEditTextView view) {
1265        view.setAdapter(new RecipientAdapter(this, mAccount));
1266        view.setTokenizer(new Rfc822Tokenizer());
1267        if (mValidator == null) {
1268            final String accountName = mAccount.name;
1269            int offset = accountName.indexOf("@") + 1;
1270            String account = accountName;
1271            if (offset > -1) {
1272                account = account.substring(accountName.indexOf("@") + 1);
1273            }
1274            mValidator = new Rfc822Validator(account);
1275        }
1276        view.setValidator(mValidator);
1277    }
1278
1279    @Override
1280    public void onClick(View v) {
1281        int id = v.getId();
1282        switch (id) {
1283            case R.id.add_cc_bcc:
1284                // Verify that cc/ bcc aren't showing.
1285                // Animate in cc/bcc.
1286                showCcBccViews();
1287                break;
1288            case R.id.add_attachment:
1289                doAttach();
1290                break;
1291        }
1292    }
1293
1294    @Override
1295    public boolean onCreateOptionsMenu(Menu menu) {
1296        super.onCreateOptionsMenu(menu);
1297        MenuInflater inflater = getMenuInflater();
1298        inflater.inflate(R.menu.compose_menu, menu);
1299        mSave = menu.findItem(R.id.save);
1300        mSend = menu.findItem(R.id.send);
1301        MenuItem helpItem = menu.findItem(R.id.help_info_menu_item);
1302        MenuItem sendFeedbackItem = menu.findItem(R.id.feedback_menu_item);
1303        if (helpItem != null) {
1304            helpItem.setVisible(mAccount != null
1305                    && mAccount.supportsCapability(AccountCapabilities.HELP_CONTENT));
1306        }
1307        if (sendFeedbackItem != null) {
1308            sendFeedbackItem.setVisible(mAccount != null
1309                    && mAccount.supportsCapability(AccountCapabilities.SEND_FEEDBACK));
1310        }
1311        return true;
1312    }
1313
1314    @Override
1315    public boolean onPrepareOptionsMenu(Menu menu) {
1316        MenuItem ccBcc = menu.findItem(R.id.add_cc_bcc);
1317        if (ccBcc != null && mCc != null) {
1318            // Its possible there is a menu item OR a button.
1319            boolean ccFieldVisible = mCc.isShown();
1320            boolean bccFieldVisible = mBcc.isShown();
1321            if (!ccFieldVisible || !bccFieldVisible) {
1322                ccBcc.setVisible(true);
1323                ccBcc.setTitle(getString(!ccFieldVisible ? R.string.add_cc_label
1324                        : R.string.add_bcc_label));
1325            } else {
1326                ccBcc.setVisible(false);
1327            }
1328        }
1329        if (mSave != null) {
1330            mSave.setEnabled(shouldSave());
1331        }
1332        return true;
1333    }
1334
1335    @Override
1336    public boolean onOptionsItemSelected(MenuItem item) {
1337        int id = item.getItemId();
1338        boolean handled = true;
1339        switch (id) {
1340            case R.id.add_attachment:
1341                doAttach();
1342                break;
1343            case R.id.add_cc_bcc:
1344                showCcBccViews();
1345                break;
1346            case R.id.save:
1347                doSave(true, false);
1348                break;
1349            case R.id.send:
1350                doSend();
1351                break;
1352            case R.id.discard:
1353                doDiscard();
1354                break;
1355            case R.id.settings:
1356                Utils.showSettings(this, mAccount);
1357                break;
1358            case android.R.id.home:
1359                finish();
1360                break;
1361            case R.id.help_info_menu_item:
1362                // TODO: enable context sensitive help
1363                Utils.showHelp(this, mAccount, null);
1364                break;
1365            case R.id.feedback_menu_item:
1366                Utils.sendFeedback(this, mAccount);
1367                break;
1368            default:
1369                handled = false;
1370                break;
1371        }
1372        return !handled ? super.onOptionsItemSelected(item) : handled;
1373    }
1374
1375    private void doSend() {
1376        sendOrSaveWithSanityChecks(false, true, false);
1377    }
1378
1379    private void doSave(boolean showToast, boolean resetIME) {
1380        sendOrSaveWithSanityChecks(true, showToast, false);
1381        if (resetIME) {
1382            // Clear the IME composing suggestions from the body.
1383            BaseInputConnection.removeComposingSpans(mBodyView.getEditableText());
1384        }
1385    }
1386
1387    /*package*/ interface SendOrSaveCallback {
1388        public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask);
1389        public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage, Message message);
1390        public Message getMessage();
1391        public void sendOrSaveFinished(SendOrSaveTask sendOrSaveTask, boolean success);
1392    }
1393
1394    /*package*/ static class SendOrSaveTask implements Runnable {
1395        private final Context mContext;
1396        private final SendOrSaveCallback mSendOrSaveCallback;
1397        @VisibleForTesting
1398        final SendOrSaveMessage mSendOrSaveMessage;
1399
1400        public SendOrSaveTask(Context context, SendOrSaveMessage message,
1401                SendOrSaveCallback callback) {
1402            mContext = context;
1403            mSendOrSaveCallback = callback;
1404            mSendOrSaveMessage = message;
1405        }
1406
1407        @Override
1408        public void run() {
1409            final SendOrSaveMessage sendOrSaveMessage = mSendOrSaveMessage;
1410
1411            final ReplyFromAccount selectedAccount = sendOrSaveMessage.mAccount;
1412            Message message = mSendOrSaveCallback.getMessage();
1413            long messageId = message != null ? message.id : UIProvider.INVALID_MESSAGE_ID;
1414            // If a previous draft has been saved, in an account that is different
1415            // than what the user wants to send from, remove the old draft, and treat this
1416            // as a new message
1417            if (!selectedAccount.equals(sendOrSaveMessage.mAccount)) {
1418                if (messageId != UIProvider.INVALID_MESSAGE_ID) {
1419                    ContentResolver resolver = mContext.getContentResolver();
1420                    ContentValues values = new ContentValues();
1421                    values.put(BaseColumns._ID, messageId);
1422                    if (selectedAccount.account.expungeMessageUri != null) {
1423                        resolver.update(selectedAccount.account.expungeMessageUri, values, null,
1424                                null);
1425                    } else {
1426                        // TODO(mindyp) delete the conversation.
1427                    }
1428                    // reset messageId to 0, so a new message will be created
1429                    messageId = UIProvider.INVALID_MESSAGE_ID;
1430                }
1431            }
1432
1433            final long messageIdToSave = messageId;
1434            if (messageIdToSave != UIProvider.INVALID_MESSAGE_ID) {
1435                sendOrSaveMessage.mValues.put(BaseColumns._ID, messageIdToSave);
1436                mContext.getContentResolver().update(
1437                        Uri.parse(sendOrSaveMessage.mSave ? message.saveUri : message.sendUri),
1438                        sendOrSaveMessage.mValues, null, null);
1439            } else {
1440                ContentResolver resolver = mContext.getContentResolver();
1441                Uri messageUri = resolver
1442                        .insert(sendOrSaveMessage.mSave ? selectedAccount.account.saveDraftUri
1443                                : selectedAccount.account.sendMessageUri,
1444                                sendOrSaveMessage.mValues);
1445                if (sendOrSaveMessage.mSave && messageUri != null) {
1446                    Cursor messageCursor = resolver.query(messageUri,
1447                            UIProvider.MESSAGE_PROJECTION, null, null, null);
1448                    if (messageCursor != null) {
1449                        try {
1450                            if (messageCursor.moveToFirst()) {
1451                                // Broadcast notification that a new message has
1452                                // been allocated
1453                                mSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage,
1454                                        new Message(messageCursor));
1455                            }
1456                        } finally {
1457                            messageCursor.close();
1458                        }
1459                    }
1460                }
1461            }
1462
1463            if (!sendOrSaveMessage.mSave) {
1464                UIProvider.incrementRecipientsTimesContacted(mContext,
1465                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.TO));
1466                UIProvider.incrementRecipientsTimesContacted(mContext,
1467                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.CC));
1468                UIProvider.incrementRecipientsTimesContacted(mContext,
1469                        (String) sendOrSaveMessage.mValues.get(UIProvider.MessageColumns.BCC));
1470            }
1471            mSendOrSaveCallback.sendOrSaveFinished(SendOrSaveTask.this, true);
1472        }
1473    }
1474
1475    // Array of the outstanding send or save tasks.  Access is synchronized
1476    // with the object itself
1477    /* package for testing */
1478    ArrayList<SendOrSaveTask> mActiveTasks = Lists.newArrayList();
1479    private int mRequestId;
1480    private String mSignature;
1481
1482    /*package*/ static class SendOrSaveMessage {
1483        final ReplyFromAccount mAccount;
1484        final ContentValues mValues;
1485        final String mRefMessageId;
1486        final boolean mSave;
1487        final int mRequestId;
1488
1489        public SendOrSaveMessage(ReplyFromAccount account, ContentValues values,
1490                String refMessageId, boolean save) {
1491            mAccount = account;
1492            mValues = values;
1493            mRefMessageId = refMessageId;
1494            mSave = save;
1495            mRequestId = mValues.hashCode() ^ hashCode();
1496        }
1497
1498        int requestId() {
1499            return mRequestId;
1500        }
1501    }
1502
1503    /**
1504     * Get the to recipients.
1505     */
1506    public String[] getToAddresses() {
1507        return getAddressesFromList(mTo);
1508    }
1509
1510    /**
1511     * Get the cc recipients.
1512     */
1513    public String[] getCcAddresses() {
1514        return getAddressesFromList(mCc);
1515    }
1516
1517    /**
1518     * Get the bcc recipients.
1519     */
1520    public String[] getBccAddresses() {
1521        return getAddressesFromList(mBcc);
1522    }
1523
1524    public String[] getAddressesFromList(RecipientEditTextView list) {
1525        if (list == null) {
1526            return new String[0];
1527        }
1528        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(list.getText());
1529        int count = tokens.length;
1530        String[] result = new String[count];
1531        for (int i = 0; i < count; i++) {
1532            result[i] = tokens[i].toString();
1533        }
1534        return result;
1535    }
1536
1537    /**
1538     * Check for invalid email addresses.
1539     * @param to String array of email addresses to check.
1540     * @param wrongEmailsOut Emails addresses that were invalid.
1541     */
1542    public void checkInvalidEmails(String[] to, List<String> wrongEmailsOut) {
1543        for (String email : to) {
1544            if (!mValidator.isValid(email)) {
1545                wrongEmailsOut.add(email);
1546            }
1547        }
1548    }
1549
1550    /**
1551     * Show an error because the user has entered an invalid recipient.
1552     * @param message
1553     */
1554    public void showRecipientErrorDialog(String message) {
1555        // Only 1 invalid recipients error dialog should be allowed up at a
1556        // time.
1557        if (mRecipientErrorDialog != null) {
1558            mRecipientErrorDialog.dismiss();
1559        }
1560        mRecipientErrorDialog = new AlertDialog.Builder(this).setMessage(message).setTitle(
1561                R.string.recipient_error_dialog_title)
1562                .setIconAttribute(android.R.attr.alertDialogIcon)
1563                .setCancelable(false)
1564                .setPositiveButton(
1565                        R.string.ok, new Dialog.OnClickListener() {
1566                            @Override
1567                            public void onClick(DialogInterface dialog, int which) {
1568                                // after the user dismisses the recipient error
1569                                // dialog we want to make sure to refocus the
1570                                // recipient to field so they can fix the issue
1571                                // easily
1572                                if (mTo != null) {
1573                                    mTo.requestFocus();
1574                                }
1575                                mRecipientErrorDialog = null;
1576                            }
1577                        }).show();
1578    }
1579
1580    /**
1581     * Update the state of the UI based on whether or not the current draft
1582     * needs to be saved and the message is not empty.
1583     */
1584    public void updateSaveUi() {
1585        if (mSave != null) {
1586            mSave.setEnabled((shouldSave() && !isBlank()));
1587        }
1588    }
1589
1590    /**
1591     * Returns true if we need to save the current draft.
1592     */
1593    private boolean shouldSave() {
1594        synchronized (mDraftLock) {
1595            // The message should only be saved if:
1596            // It hasn't been sent AND
1597            // Some text has been added to the message OR
1598            // an attachment has been added or removed
1599            return (mTextChanged || mAttachmentsChanged ||
1600                    (mReplyFromChanged && !isBlank()));
1601        }
1602    }
1603
1604    /**
1605     * Check if all fields are blank.
1606     * @return boolean
1607     */
1608    public boolean isBlank() {
1609        return mSubject.getText().length() == 0
1610                && (mBodyView.getText().length() == 0 || getSignatureStartPosition(mSignature,
1611                        mBodyView.getText().toString()) == 0)
1612                && mTo.length() == 0
1613                && mCc.length() == 0 && mBcc.length() == 0
1614                && mAttachmentsView.getAttachments().size() == 0;
1615    }
1616
1617    @VisibleForTesting
1618    protected int getSignatureStartPosition(String signature, String bodyText) {
1619        int startPos = -1;
1620
1621        if (TextUtils.isEmpty(signature) || TextUtils.isEmpty(bodyText)) {
1622            return startPos;
1623        }
1624
1625        int bodyLength = bodyText.length();
1626        int signatureLength = signature.length();
1627        String printableVersion = convertToPrintableSignature(signature);
1628        int printableLength = printableVersion.length();
1629
1630        if (bodyLength >= printableLength
1631                && bodyText.substring(bodyLength - printableLength)
1632                .equals(printableVersion)) {
1633            startPos = bodyLength - printableLength;
1634        } else if (bodyLength >= signatureLength
1635                && bodyText.substring(bodyLength - signatureLength)
1636                .equals(signature)) {
1637            startPos = bodyLength - signatureLength;
1638        }
1639        return startPos;
1640    }
1641
1642    /**
1643     * Allows any changes made by the user to be ignored. Called when the user
1644     * decides to discard a draft.
1645     */
1646    private void discardChanges() {
1647        mTextChanged = false;
1648        mAttachmentsChanged = false;
1649        mReplyFromChanged = false;
1650    }
1651
1652    /**
1653     * @param body
1654     * @param save
1655     * @param showToast
1656     * @return Whether the send or save succeeded.
1657     */
1658    protected boolean sendOrSaveWithSanityChecks(final boolean save, final boolean showToast,
1659            final boolean orientationChanged) {
1660        String[] to, cc, bcc;
1661        Editable body = mBodyView.getEditableText();
1662
1663        if (orientationChanged) {
1664            to = cc = bcc = new String[0];
1665        } else {
1666            to = getToAddresses();
1667            cc = getCcAddresses();
1668            bcc = getBccAddresses();
1669        }
1670
1671        // Don't let the user send to nobody (but it's okay to save a message
1672        // with no recipients)
1673        if (!save && (to.length == 0 && cc.length == 0 && bcc.length == 0)) {
1674            showRecipientErrorDialog(getString(R.string.recipient_needed));
1675            return false;
1676        }
1677
1678        List<String> wrongEmails = new ArrayList<String>();
1679        if (!save) {
1680            checkInvalidEmails(to, wrongEmails);
1681            checkInvalidEmails(cc, wrongEmails);
1682            checkInvalidEmails(bcc, wrongEmails);
1683        }
1684
1685        // Don't let the user send an email with invalid recipients
1686        if (wrongEmails.size() > 0) {
1687            String errorText = String.format(getString(R.string.invalid_recipient),
1688                    wrongEmails.get(0));
1689            showRecipientErrorDialog(errorText);
1690            return false;
1691        }
1692
1693        DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
1694            @Override
1695            public void onClick(DialogInterface dialog, int which) {
1696                sendOrSave(mBodyView.getEditableText(), save, showToast, orientationChanged);
1697            }
1698        };
1699
1700        // Show a warning before sending only if there are no attachments.
1701        if (!save) {
1702            if (mAttachmentsView.getAttachments().isEmpty() && showEmptyTextWarnings()) {
1703                boolean warnAboutEmptySubject = isSubjectEmpty();
1704                boolean emptyBody = TextUtils.getTrimmedLength(body) == 0;
1705
1706                // A warning about an empty body may not be warranted when
1707                // forwarding mails, since a common use case is to forward
1708                // quoted text and not append any more text.
1709                boolean warnAboutEmptyBody = emptyBody && (!mForward || isBodyEmpty());
1710
1711                // When we bring up a dialog warning the user about a send,
1712                // assume that they accept sending the message. If they do not,
1713                // the dialog listener is required to enable sending again.
1714                if (warnAboutEmptySubject) {
1715                    showSendConfirmDialog(R.string.confirm_send_message_with_no_subject, listener);
1716                    return true;
1717                }
1718
1719                if (warnAboutEmptyBody) {
1720                    showSendConfirmDialog(R.string.confirm_send_message_with_no_body, listener);
1721                    return true;
1722                }
1723            }
1724            // Ask for confirmation to send (if always required)
1725            if (showSendConfirmation()) {
1726                showSendConfirmDialog(R.string.confirm_send_message, listener);
1727                return true;
1728            }
1729        }
1730
1731        sendOrSave(body, save, showToast, false);
1732        return true;
1733    }
1734
1735    /**
1736     * Returns a boolean indicating whether warnings should be shown for empty
1737     * subject and body fields
1738     *
1739     * @return True if a warning should be shown for empty text fields
1740     */
1741    protected boolean showEmptyTextWarnings() {
1742        return mAttachmentsView.getAttachments().size() == 0;
1743    }
1744
1745    /**
1746     * Returns a boolean indicating whether the user should confirm each send
1747     *
1748     * @return True if a warning should be on each send
1749     */
1750    protected boolean showSendConfirmation() {
1751        return mCachedSettings != null ? mCachedSettings.confirmSend : false;
1752    }
1753
1754    private void showSendConfirmDialog(int messageId, DialogInterface.OnClickListener listener) {
1755        if (mSendConfirmDialog != null) {
1756            mSendConfirmDialog.dismiss();
1757            mSendConfirmDialog = null;
1758        }
1759        mSendConfirmDialog = new AlertDialog.Builder(this).setMessage(messageId)
1760                .setTitle(R.string.confirm_send_title)
1761                .setIconAttribute(android.R.attr.alertDialogIcon)
1762                .setPositiveButton(R.string.send, listener)
1763                .setNegativeButton(R.string.cancel, this).setCancelable(false).show();
1764    }
1765
1766    /**
1767     * Returns whether the ComposeArea believes there is any text in the body of
1768     * the composition. TODO: When ComposeArea controls the Body as well, add
1769     * that here.
1770     */
1771    public boolean isBodyEmpty() {
1772        return !mQuotedTextView.isTextIncluded();
1773    }
1774
1775    /**
1776     * Test to see if the subject is empty.
1777     *
1778     * @return boolean.
1779     */
1780    // TODO: this will likely go away when composeArea.focus() is implemented
1781    // after all the widget control is moved over.
1782    public boolean isSubjectEmpty() {
1783        return TextUtils.getTrimmedLength(mSubject.getText()) == 0;
1784    }
1785
1786    /* package */
1787    static int sendOrSaveInternal(Context context, ReplyFromAccount replyFromAccount,
1788            Message message, final Message refMessage, Spanned body, final CharSequence quotedText,
1789            SendOrSaveCallback callback, Handler handler, boolean save, int composeMode) {
1790        ContentValues values = new ContentValues();
1791
1792        String refMessageId = refMessage != null ? refMessage.uri.toString() : "";
1793
1794        MessageModification.putToAddresses(values, message.getToAddresses());
1795        MessageModification.putCcAddresses(values, message.getCcAddresses());
1796        MessageModification.putBccAddresses(values, message.getBccAddresses());
1797
1798        MessageModification.putCustomFromAddress(values, message.from);
1799
1800        MessageModification.putSubject(values, message.subject);
1801        String htmlBody = Html.toHtml(body);
1802
1803        boolean includeQuotedText = !TextUtils.isEmpty(quotedText);
1804        StringBuilder fullBody = new StringBuilder(htmlBody);
1805        if (includeQuotedText) {
1806            // HTML gets converted to text for now
1807            final String text = quotedText.toString();
1808            if (QuotedTextView.containsQuotedText(text)) {
1809                int pos = QuotedTextView.getQuotedTextOffset(text);
1810                final int quoteStartPos = fullBody.length() + pos;
1811                fullBody.append(text);
1812                MessageModification.putQuoteStartPos(values, quoteStartPos);
1813                MessageModification.putForward(values, composeMode == ComposeActivity.FORWARD);
1814                MessageModification.putAppendRefMessageContent(values, includeQuotedText);
1815            } else {
1816                LogUtils.w(LOG_TAG, "Couldn't find quoted text");
1817                // This shouldn't happen, but just use what we have,
1818                // and don't do server-side expansion
1819                fullBody.append(text);
1820            }
1821        }
1822        int draftType = -1;
1823        switch (composeMode) {
1824            case ComposeActivity.COMPOSE:
1825                draftType = DraftType.COMPOSE;
1826                break;
1827            case ComposeActivity.REPLY:
1828                draftType = DraftType.REPLY;
1829                break;
1830            case ComposeActivity.REPLY_ALL:
1831                draftType = DraftType.REPLY_ALL;
1832                break;
1833            case ComposeActivity.FORWARD:
1834                draftType = DraftType.FORWARD;
1835                break;
1836        }
1837        MessageModification.putDraftType(values, draftType);
1838        if (refMessage != null) {
1839            if (!TextUtils.isEmpty(refMessage.bodyHtml)) {
1840                MessageModification.putBodyHtml(values, fullBody.toString());
1841            }
1842            if (!TextUtils.isEmpty(refMessage.bodyText)) {
1843                MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1844            }
1845        } else {
1846            MessageModification.putBodyHtml(values, fullBody.toString());
1847            MessageModification.putBody(values, Html.fromHtml(fullBody.toString()).toString());
1848        }
1849        MessageModification.putAttachments(values, message.getAttachments());
1850        if (!TextUtils.isEmpty(refMessageId)) {
1851            MessageModification.putRefMessageId(values, refMessageId);
1852        }
1853
1854        SendOrSaveMessage sendOrSaveMessage = new SendOrSaveMessage(replyFromAccount,
1855                values, refMessageId, save);
1856        SendOrSaveTask sendOrSaveTask = new SendOrSaveTask(context, sendOrSaveMessage, callback);
1857
1858        callback.initializeSendOrSave(sendOrSaveTask);
1859
1860        // Do the send/save action on the specified handler to avoid possible
1861        // ANRs
1862        handler.post(sendOrSaveTask);
1863
1864        return sendOrSaveMessage.requestId();
1865    }
1866
1867    private void sendOrSave(Spanned body, boolean save, boolean showToast,
1868            boolean orientationChanged) {
1869        // Check if user is a monkey. Monkeys can compose and hit send
1870        // button but are not allowed to send anything off the device.
1871        if (ActivityManager.isUserAMonkey()) {
1872            return;
1873        }
1874
1875        String[] to, cc, bcc;
1876        if (orientationChanged) {
1877            to = cc = bcc = new String[0];
1878        } else {
1879            to = getToAddresses();
1880            cc = getCcAddresses();
1881            bcc = getBccAddresses();
1882        }
1883
1884        SendOrSaveCallback callback = new SendOrSaveCallback() {
1885            private int mRestoredRequestId;
1886
1887            @Override
1888            public void initializeSendOrSave(SendOrSaveTask sendOrSaveTask) {
1889                synchronized (mActiveTasks) {
1890                    int numTasks = mActiveTasks.size();
1891                    if (numTasks == 0) {
1892                        // Start service so we won't be killed if this app is
1893                        // put in the background.
1894                        startService(new Intent(ComposeActivity.this, EmptyService.class));
1895                    }
1896
1897                    mActiveTasks.add(sendOrSaveTask);
1898                }
1899                if (sTestSendOrSaveCallback != null) {
1900                    sTestSendOrSaveCallback.initializeSendOrSave(sendOrSaveTask);
1901                }
1902            }
1903
1904            @Override
1905            public void notifyMessageIdAllocated(SendOrSaveMessage sendOrSaveMessage,
1906                    Message message) {
1907                synchronized (mDraftLock) {
1908                    mDraftId = message.id;
1909                    mDraft = message;
1910                    if (sRequestMessageIdMap != null) {
1911                        sRequestMessageIdMap.put(sendOrSaveMessage.requestId(), mDraftId);
1912                    }
1913                    // Cache request message map, in case the process is killed
1914                    saveRequestMap();
1915                }
1916                if (sTestSendOrSaveCallback != null) {
1917                    sTestSendOrSaveCallback.notifyMessageIdAllocated(sendOrSaveMessage, message);
1918                }
1919            }
1920
1921            @Override
1922            public Message getMessage() {
1923                synchronized (mDraftLock) {
1924                    return mDraft;
1925                }
1926            }
1927
1928            @Override
1929            public void sendOrSaveFinished(SendOrSaveTask task, boolean success) {
1930                if (success) {
1931                    // Successfully sent or saved so reset change markers
1932                    discardChanges();
1933                } else {
1934                    // A failure happened with saving/sending the draft
1935                    // TODO(pwestbro): add a better string that should be used
1936                    // when failing to send or save
1937                    Toast.makeText(ComposeActivity.this, R.string.send_failed, Toast.LENGTH_SHORT)
1938                            .show();
1939                }
1940
1941                int numTasks;
1942                synchronized (mActiveTasks) {
1943                    // Remove the task from the list of active tasks
1944                    mActiveTasks.remove(task);
1945                    numTasks = mActiveTasks.size();
1946                }
1947
1948                if (numTasks == 0) {
1949                    // Stop service so we can be killed.
1950                    stopService(new Intent(ComposeActivity.this, EmptyService.class));
1951                }
1952                if (sTestSendOrSaveCallback != null) {
1953                    sTestSendOrSaveCallback.sendOrSaveFinished(task, success);
1954                }
1955            }
1956        };
1957
1958        // Get the selected account if the from spinner has been setup.
1959        ReplyFromAccount selectedAccount = mReplyFromAccount;
1960        String fromAddress = selectedAccount.name;
1961        if (selectedAccount == null || fromAddress == null) {
1962            // We don't have either the selected account or from address,
1963            // use mAccount.
1964            selectedAccount = mReplyFromAccount;
1965            fromAddress = mAccount.name;
1966        }
1967
1968        if (mSendSaveTaskHandler == null) {
1969            HandlerThread handlerThread = new HandlerThread("Send Message Task Thread");
1970            handlerThread.start();
1971
1972            mSendSaveTaskHandler = new Handler(handlerThread.getLooper());
1973        }
1974
1975        Message msg = createMessage(mReplyFromAccount, getMode());
1976        mRequestId = sendOrSaveInternal(this, mReplyFromAccount, msg, mRefMessage, body,
1977                mQuotedTextView.getQuotedTextIfIncluded(), callback,
1978                mSendSaveTaskHandler, save, mComposeMode);
1979
1980        if (mRecipient != null && mRecipient.equals(mAccount.name)) {
1981            mRecipient = selectedAccount.name;
1982        }
1983        setAccount(selectedAccount.account);
1984
1985        // Don't display the toast if the user is just changing the orientation,
1986        // but we still need to save the draft to the cursor because this is how we restore
1987        // the attachments when the configuration change completes.
1988        if (showToast && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
1989            Toast.makeText(this, save ? R.string.message_saved : R.string.sending_message,
1990                    Toast.LENGTH_LONG).show();
1991        }
1992
1993        // Need to update variables here because the send or save completes
1994        // asynchronously even though the toast shows right away.
1995        discardChanges();
1996        updateSaveUi();
1997
1998        // If we are sending, finish the activity
1999        if (!save) {
2000            finish();
2001        }
2002    }
2003
2004    /**
2005     * Save the state of the request messageid map. This allows for the Gmail
2006     * process to be killed, but and still allow for ComposeActivity instances
2007     * to be recreated correctly.
2008     */
2009    private void saveRequestMap() {
2010        // TODO: store the request map in user preferences.
2011    }
2012
2013    public void doAttach() {
2014        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
2015        i.addCategory(Intent.CATEGORY_OPENABLE);
2016        if (android.provider.Settings.System.getInt(getContentResolver(),
2017                UIProvider.getAttachmentTypeSetting(), 0) != 0) {
2018            i.setType("*/*");
2019        } else {
2020            i.setType("image/*");
2021        }
2022        mAddingAttachment = true;
2023        startActivityForResult(Intent.createChooser(i, getText(R.string.select_attachment_type)),
2024                RESULT_PICK_ATTACHMENT);
2025    }
2026
2027    private void showCcBccViews() {
2028        mCcBccView.show(true, true, true);
2029        if (mCcBccButton != null) {
2030            mCcBccButton.setVisibility(View.GONE);
2031        }
2032    }
2033
2034    @Override
2035    public boolean onNavigationItemSelected(int position, long itemId) {
2036        int initialComposeMode = mComposeMode;
2037        if (position == ComposeActivity.REPLY) {
2038            mComposeMode = ComposeActivity.REPLY;
2039        } else if (position == ComposeActivity.REPLY_ALL) {
2040            mComposeMode = ComposeActivity.REPLY_ALL;
2041        } else if (position == ComposeActivity.FORWARD) {
2042            mComposeMode = ComposeActivity.FORWARD;
2043        }
2044        if (initialComposeMode != mComposeMode) {
2045            resetMessageForModeChange();
2046            if (mRefMessage != null) {
2047                initFromRefMessage(mComposeMode, mAccount.name);
2048            }
2049        }
2050        return true;
2051    }
2052
2053    private void resetMessageForModeChange() {
2054        // When switching between reply, reply all, forward,
2055        // follow the behavior of webview.
2056        // The contents of the following fields are cleared
2057        // so that they can be populated directly from the
2058        // ref message:
2059        // 1) Any recipient fields
2060        // 2) The subject
2061        mTo.setText("");
2062        mCc.setText("");
2063        mBcc.setText("");
2064        // Any edits to the subject are replaced with the original subject.
2065        mSubject.setText("");
2066
2067        // Any changes to the contents of the following fields are kept:
2068        // 1) Body
2069        // 2) Attachments
2070        // If the user made changes to attachments, keep their changes.
2071        if (!mAttachmentsChanged) {
2072            mAttachmentsView.deleteAllAttachments();
2073        }
2074    }
2075
2076    private class ComposeModeAdapter extends ArrayAdapter<String> {
2077
2078        private LayoutInflater mInflater;
2079
2080        public ComposeModeAdapter(Context context) {
2081            super(context, R.layout.compose_mode_item, R.id.mode, getResources()
2082                    .getStringArray(R.array.compose_modes));
2083        }
2084
2085        private LayoutInflater getInflater() {
2086            if (mInflater == null) {
2087                mInflater = LayoutInflater.from(getContext());
2088            }
2089            return mInflater;
2090        }
2091
2092        @Override
2093        public View getView(int position, View convertView, ViewGroup parent) {
2094            if (convertView == null) {
2095                convertView = getInflater().inflate(R.layout.compose_mode_display_item, null);
2096            }
2097            ((TextView) convertView.findViewById(R.id.mode)).setText(getItem(position));
2098            return super.getView(position, convertView, parent);
2099        }
2100    }
2101
2102    @Override
2103    public void onRespondInline(String text) {
2104        appendToBody(text, false);
2105    }
2106
2107    /**
2108     * Append text to the body of the message. If there is no existing body
2109     * text, just sets the body to text.
2110     *
2111     * @param text
2112     * @param withSignature True to append a signature.
2113     */
2114    public void appendToBody(CharSequence text, boolean withSignature) {
2115        Editable bodyText = mBodyView.getEditableText();
2116        if (bodyText != null && bodyText.length() > 0) {
2117            bodyText.append(text);
2118        } else {
2119            setBody(text, withSignature);
2120        }
2121    }
2122
2123    /**
2124     * Set the body of the message.
2125     *
2126     * @param text
2127     * @param withSignature True to append a signature.
2128     */
2129    public void setBody(CharSequence text, boolean withSignature) {
2130        mBodyView.setText(text);
2131        if (withSignature) {
2132            appendSignature();
2133        }
2134    }
2135
2136    private void appendSignature() {
2137        String newSignature = mCachedSettings != null ? mCachedSettings.signature : null;
2138        boolean hasFocus = mBodyView.hasFocus();
2139        if (!TextUtils.equals(newSignature, mSignature)) {
2140            mSignature = newSignature;
2141            if (!TextUtils.isEmpty(mSignature)
2142                    && getSignatureStartPosition(mSignature,
2143                            mBodyView.getText().toString()) < 0) {
2144                // Appending a signature does not count as changing text.
2145                mBodyView.removeTextChangedListener(this);
2146                mBodyView.append(convertToPrintableSignature(mSignature));
2147                mBodyView.addTextChangedListener(this);
2148            }
2149            if (hasFocus) {
2150                focusBody();
2151            }
2152        }
2153    }
2154
2155    private String convertToPrintableSignature(String signature) {
2156        String signatureResource = getResources().getString(R.string.signature);
2157        if (signature == null) {
2158            signature = "";
2159        }
2160        return String.format(signatureResource, signature);
2161    }
2162
2163    @Override
2164    public void onAccountChanged() {
2165        mReplyFromAccount = mFromSpinner.getCurrentAccount();
2166        if (!mAccount.equals(mReplyFromAccount.account)) {
2167            setAccount(mReplyFromAccount.account);
2168
2169            // TODO: handle discarding attachments when switching accounts.
2170            // Only enable save for this draft if there is any other content
2171            // in the message.
2172            if (!isBlank()) {
2173                enableSave(true);
2174            }
2175            mReplyFromChanged = true;
2176            initRecipients();
2177        }
2178    }
2179
2180    public void enableSave(boolean enabled) {
2181        if (mSave != null) {
2182            mSave.setEnabled(enabled);
2183        }
2184    }
2185
2186    public void enableSend(boolean enabled) {
2187        if (mSend != null) {
2188            mSend.setEnabled(enabled);
2189        }
2190    }
2191
2192    /**
2193     * Handles button clicks from any error dialogs dealing with sending
2194     * a message.
2195     */
2196    @Override
2197    public void onClick(DialogInterface dialog, int which) {
2198        switch (which) {
2199            case DialogInterface.BUTTON_POSITIVE: {
2200                doDiscardWithoutConfirmation(true /* show toast */ );
2201                break;
2202            }
2203            case DialogInterface.BUTTON_NEGATIVE: {
2204                // If the user cancels the send, re-enable the send button.
2205                enableSend(true);
2206                break;
2207            }
2208        }
2209
2210    }
2211
2212    private void doDiscard() {
2213        new AlertDialog.Builder(this).setMessage(R.string.confirm_discard_text)
2214                .setPositiveButton(R.string.ok, this)
2215                .setNegativeButton(R.string.cancel, null)
2216                .create().show();
2217    }
2218    /**
2219     * Effectively discard the current message.
2220     *
2221     * This method is either invoked from the menu or from the dialog
2222     * once the user has confirmed that they want to discard the message.
2223     * @param showToast show "Message discarded" toast if true
2224     */
2225    private void doDiscardWithoutConfirmation(boolean showToast) {
2226        synchronized (mDraftLock) {
2227            if (mDraftId != UIProvider.INVALID_MESSAGE_ID) {
2228                ContentValues values = new ContentValues();
2229                values.put(BaseColumns._ID, mDraftId);
2230                if (mAccount.expungeMessageUri != null) {
2231                    getContentResolver().update(mAccount.expungeMessageUri, values, null, null);
2232                } else {
2233                    getContentResolver().delete(mDraft.uri, null, null);
2234                }
2235                // This is not strictly necessary (since we should not try to
2236                // save the draft after calling this) but it ensures that if we
2237                // do save again for some reason we make a new draft rather than
2238                // trying to resave an expunged draft.
2239                mDraftId = UIProvider.INVALID_MESSAGE_ID;
2240            }
2241        }
2242
2243        if (showToast) {
2244            // Display a toast to let the user know
2245            Toast.makeText(this, R.string.message_discarded, Toast.LENGTH_SHORT).show();
2246        }
2247
2248        // This prevents the draft from being saved in onPause().
2249        discardChanges();
2250        finish();
2251    }
2252
2253    private void saveIfNeeded() {
2254        if (mAccount == null) {
2255            // We have not chosen an account yet so there's no way that we can save. This is ok,
2256            // though, since we are saving our state before AccountsActivity is activated. Thus, the
2257            // user has not interacted with us yet and there is no real state to save.
2258            return;
2259        }
2260
2261        if (shouldSave()) {
2262            doSave(!mAddingAttachment /* show toast */, true /* reset IME */);
2263        }
2264    }
2265
2266    @Override
2267    public void onAttachmentDeleted() {
2268        mAttachmentsChanged = true;
2269        updateSaveUi();
2270    }
2271
2272
2273    /**
2274     * This is called any time one of our text fields changes.
2275     */
2276    @Override
2277    public void afterTextChanged(Editable s) {
2278        mTextChanged = true;
2279        updateSaveUi();
2280    }
2281
2282    @Override
2283    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2284        // Do nothing.
2285    }
2286
2287    @Override
2288    public void onTextChanged(CharSequence s, int start, int before, int count) {
2289        // Do nothing.
2290    }
2291
2292
2293    // There is a big difference between the text associated with an address changing
2294    // to add the display name or to format properly and a recipient being added or deleted.
2295    // Make sure we only notify of changes when a recipient has been added or deleted.
2296    private class RecipientTextWatcher implements TextWatcher {
2297        private HashMap<String, Integer> mContent = new HashMap<String, Integer>();
2298
2299        private RecipientEditTextView mView;
2300
2301        private TextWatcher mListener;
2302
2303        public RecipientTextWatcher(RecipientEditTextView view, TextWatcher listener) {
2304            mView = view;
2305            mListener = listener;
2306        }
2307
2308        @Override
2309        public void afterTextChanged(Editable s) {
2310            if (hasChanged()) {
2311                mListener.afterTextChanged(s);
2312            }
2313        }
2314
2315        private boolean hasChanged() {
2316            String[] currRecips = tokenizeRecips(getAddressesFromList(mView));
2317            int totalCount = currRecips.length;
2318            int totalPrevCount = 0;
2319            for (Entry<String, Integer> entry : mContent.entrySet()) {
2320                totalPrevCount += entry.getValue();
2321            }
2322            if (totalCount != totalPrevCount) {
2323                return true;
2324            }
2325
2326            for (String recip : currRecips) {
2327                if (!mContent.containsKey(recip)) {
2328                    return true;
2329                } else {
2330                    int count = mContent.get(recip) - 1;
2331                    if (count < 0) {
2332                        return true;
2333                    } else {
2334                        mContent.put(recip, count);
2335                    }
2336                }
2337            }
2338            return false;
2339        }
2340
2341        private String[] tokenizeRecips(String[] recips) {
2342            // Tokenize them all and put them in the list.
2343            String[] recipAddresses = new String[recips.length];
2344            for (int i = 0; i < recips.length; i++) {
2345                recipAddresses[i] = Rfc822Tokenizer.tokenize(recips[i])[0].getAddress();
2346            }
2347            return recipAddresses;
2348        }
2349
2350        @Override
2351        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
2352            String[] recips = tokenizeRecips(getAddressesFromList(mView));
2353            for (String recip : recips) {
2354                if (!mContent.containsKey(recip)) {
2355                    mContent.put(recip, 1);
2356                } else {
2357                    mContent.put(recip, (mContent.get(recip)) + 1);
2358                }
2359            }
2360        }
2361
2362        @Override
2363        public void onTextChanged(CharSequence s, int start, int before, int count) {
2364            // Do nothing.
2365        }
2366    }
2367}
2368