MessageCompose.java revision 2f4e87c2234387b6c3bc4fff8f7f1da1e83258e8
1/*
2 * Copyright (C) 2008 The Android Open Source Project
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.email.activity;
18
19import com.android.email.Controller;
20import com.android.email.Email;
21import com.android.email.EmailAddressAdapter;
22import com.android.email.EmailAddressValidator;
23import com.android.email.R;
24import com.android.email.Utility;
25import com.android.email.mail.Address;
26import com.android.email.mail.internet.EmailHtmlUtil;
27import com.android.email.mail.internet.MimeUtility;
28import com.android.email.provider.EmailContent;
29import com.android.email.provider.EmailContent.Account;
30import com.android.email.provider.EmailContent.Attachment;
31import com.android.email.provider.EmailContent.Body;
32import com.android.email.provider.EmailContent.BodyColumns;
33import com.android.email.provider.EmailContent.Message;
34import com.android.email.provider.EmailContent.MessageColumns;
35import com.android.exchange.provider.GalEmailAddressAdapter;
36
37import android.app.ActionBar;
38import android.app.Activity;
39import android.content.ActivityNotFoundException;
40import android.content.ContentResolver;
41import android.content.ContentUris;
42import android.content.ContentValues;
43import android.content.Context;
44import android.content.Intent;
45import android.content.pm.ActivityInfo;
46import android.database.Cursor;
47import android.net.Uri;
48import android.os.AsyncTask;
49import android.os.Bundle;
50import android.os.Parcelable;
51import android.provider.OpenableColumns;
52import android.text.InputFilter;
53import android.text.SpannableStringBuilder;
54import android.text.Spanned;
55import android.text.TextUtils;
56import android.text.TextWatcher;
57import android.text.util.Rfc822Tokenizer;
58import android.util.Log;
59import android.view.Menu;
60import android.view.MenuItem;
61import android.view.View;
62import android.view.View.OnClickListener;
63import android.view.View.OnFocusChangeListener;
64import android.webkit.WebView;
65import android.widget.Button;
66import android.widget.EditText;
67import android.widget.ImageButton;
68import android.widget.LinearLayout;
69import android.widget.MultiAutoCompleteTextView;
70import android.widget.TextView;
71import android.widget.Toast;
72
73import java.io.UnsupportedEncodingException;
74import java.net.URLDecoder;
75import java.util.ArrayList;
76import java.util.List;
77
78
79/**
80 * Activity to compose a message.
81 *
82 * TODO Revive shortcuts command for removed menu options.
83 * S: send
84 * D: save draft
85 * Q: discard
86 */
87public class MessageCompose extends Activity implements OnClickListener, OnFocusChangeListener {
88    private static final String ACTION_REPLY = "com.android.email.intent.action.REPLY";
89    private static final String ACTION_REPLY_ALL = "com.android.email.intent.action.REPLY_ALL";
90    private static final String ACTION_FORWARD = "com.android.email.intent.action.FORWARD";
91    private static final String ACTION_EDIT_DRAFT = "com.android.email.intent.action.EDIT_DRAFT";
92
93    private static final String EXTRA_ACCOUNT_ID = "account_id";
94    private static final String EXTRA_MESSAGE_ID = "message_id";
95    private static final String STATE_KEY_CC_SHOWN =
96        "com.android.email.activity.MessageCompose.ccShown";
97    private static final String STATE_KEY_BCC_SHOWN =
98        "com.android.email.activity.MessageCompose.bccShown";
99    private static final String STATE_KEY_QUOTED_TEXT_SHOWN =
100        "com.android.email.activity.MessageCompose.quotedTextShown";
101    private static final String STATE_KEY_SOURCE_MESSAGE_PROCED =
102        "com.android.email.activity.MessageCompose.stateKeySourceMessageProced";
103    private static final String STATE_KEY_DRAFT_ID =
104        "com.android.email.activity.MessageCompose.draftId";
105
106    private static final int ACTIVITY_REQUEST_PICK_ATTACHMENT = 1;
107
108    private static final String[] ATTACHMENT_META_COLUMNS = {
109        OpenableColumns.DISPLAY_NAME,
110        OpenableColumns.SIZE
111    };
112
113    // Is set while the draft is saved by a background thread.
114    // Is static in order to be shared between the two activity instances
115    // on orientation change.
116    private static boolean sSaveInProgress = false;
117    // lock and condition for sSaveInProgress
118    private static final Object sSaveInProgressCondition = new Object();
119
120    private Account mAccount;
121
122    // mDraft has mId > 0 after the first draft save.
123    private Message mDraft = new Message();
124
125    // mSource is only set for REPLY, REPLY_ALL and FORWARD, and contains the source message.
126    private Message mSource;
127
128    // we use mAction instead of Intent.getAction() because sometimes we need to
129    // re-write the action to EDIT_DRAFT.
130    private String mAction;
131
132    /**
133     * Indicates that the source message has been processed at least once and should not
134     * be processed on any subsequent loads. This protects us from adding attachments that
135     * have already been added from the restore of the view state.
136     */
137    private boolean mSourceMessageProcessed = false;
138
139    private ActionBar mActionBar;
140    private MultiAutoCompleteTextView mToView;
141    private MultiAutoCompleteTextView mCcView;
142    private MultiAutoCompleteTextView mBccView;
143    private EditText mSubjectView;
144    private EditText mMessageContentView;
145    private Button mSendButton;
146    private Button mDiscardButton;
147    private Button mSaveButton;
148    private LinearLayout mAttachments;
149    private View mQuotedTextBar;
150    private ImageButton mQuotedTextDelete;
151    private WebView mQuotedText;
152
153    private Controller mController;
154    private boolean mDraftNeedsSaving;
155    private boolean mMessageLoaded;
156    private AsyncTask<Long, Void, Attachment[]> mLoadAttachmentsTask;
157    private AsyncTask<Long, Void, Object[]> mLoadMessageTask;
158
159    private EmailAddressAdapter mAddressAdapterTo;
160    private EmailAddressAdapter mAddressAdapterCc;
161    private EmailAddressAdapter mAddressAdapterBcc;
162
163    /**
164     * Compose a new message using the given account. If account is -1 the default account
165     * will be used.
166     * @param context
167     * @param accountId
168     */
169    public static void actionCompose(Context context, long accountId) {
170       try {
171           Intent i = new Intent(context, MessageCompose.class);
172           i.putExtra(EXTRA_ACCOUNT_ID, accountId);
173           context.startActivity(i);
174       } catch (ActivityNotFoundException anfe) {
175           // Swallow it - this is usually a race condition, especially under automated test.
176           // (The message composer might have been disabled)
177           Email.log(anfe.toString());
178       }
179    }
180
181    /**
182     * Compose a new message using a uri (mailto:) and a given account.  If account is -1 the
183     * default account will be used.
184     * @param context
185     * @param uriString
186     * @param accountId
187     * @return true if startActivity() succeeded
188     */
189    public static boolean actionCompose(Context context, String uriString, long accountId) {
190        try {
191            Intent i = new Intent(context, MessageCompose.class);
192            i.setAction(Intent.ACTION_SEND);
193            i.setData(Uri.parse(uriString));
194            i.putExtra(EXTRA_ACCOUNT_ID, accountId);
195            context.startActivity(i);
196            return true;
197        } catch (ActivityNotFoundException anfe) {
198            // Swallow it - this is usually a race condition, especially under automated test.
199            // (The message composer might have been disabled)
200            Email.log(anfe.toString());
201            return false;
202        }
203    }
204
205    /**
206     * Compose a new message as a reply to the given message. If replyAll is true the function
207     * is reply all instead of simply reply.
208     * @param context
209     * @param messageId
210     * @param replyAll
211     */
212    public static void actionReply(Context context, long messageId, boolean replyAll) {
213        startActivityWithMessage(context, replyAll ? ACTION_REPLY_ALL : ACTION_REPLY, messageId);
214    }
215
216    /**
217     * Compose a new message as a forward of the given message.
218     * @param context
219     * @param messageId
220     */
221    public static void actionForward(Context context, long messageId) {
222        startActivityWithMessage(context, ACTION_FORWARD, messageId);
223    }
224
225    /**
226     * Continue composition of the given message. This action modifies the way this Activity
227     * handles certain actions.
228     * Save will attempt to replace the message in the given folder with the updated version.
229     * Discard will delete the message from the given folder.
230     * @param context
231     * @param messageId the message id.
232     */
233    public static void actionEditDraft(Context context, long messageId) {
234        startActivityWithMessage(context, ACTION_EDIT_DRAFT, messageId);
235    }
236
237    private static void startActivityWithMessage(Context context, String action, long messageId) {
238        Intent i = new Intent(context, MessageCompose.class);
239        i.putExtra(EXTRA_MESSAGE_ID, messageId);
240        i.setAction(action);
241        context.startActivity(i);
242    }
243
244    private void setAccount(Intent intent) {
245        long accountId = intent.getLongExtra(EXTRA_ACCOUNT_ID, -1);
246        if (accountId == -1) {
247            accountId = Account.getDefaultAccountId(this);
248        }
249        if (accountId == -1) {
250            // There are no accounts set up. This should not have happened. Prompt the
251            // user to set up an account as an acceptable bailout.
252            AccountFolderList.actionShowAccounts(this);
253            finish();
254        } else {
255            setAccount(Account.restoreAccountWithId(this, accountId));
256        }
257    }
258
259    private void setAccount(Account account) {
260        mAccount = account;
261        if (account != null) {
262            mActionBar.setSubtitle(account.mDisplayName);
263            mAddressAdapterTo.setAccount(account);
264            mAddressAdapterCc.setAccount(account);
265            mAddressAdapterBcc.setAccount(account);
266        }
267    }
268
269    @Override
270    public void onCreate(Bundle savedInstanceState) {
271        super.onCreate(savedInstanceState);
272        setContentView(R.layout.message_compose);
273
274        mController = Controller.getInstance(getApplication());
275        initViews();
276        setDraftNeedsSaving(false);
277
278        long draftId = -1;
279        if (savedInstanceState != null) {
280            // This data gets used in onCreate, so grab it here instead of onRestoreInstanceState
281            mSourceMessageProcessed =
282                savedInstanceState.getBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, false);
283            draftId = savedInstanceState.getLong(STATE_KEY_DRAFT_ID, -1);
284        }
285
286        Intent intent = getIntent();
287        mAction = intent.getAction();
288
289        if (draftId != -1) {
290            // this means that we saved the draft earlier,
291            // so now we need to disregard the intent action and do
292            // EDIT_DRAFT instead.
293            mAction = ACTION_EDIT_DRAFT;
294            mDraft.mId = draftId;
295        }
296
297        // Handle the various intents that launch the message composer
298        if (Intent.ACTION_VIEW.equals(mAction)
299                || Intent.ACTION_SENDTO.equals(mAction)
300                || Intent.ACTION_SEND.equals(mAction)
301                || Intent.ACTION_SEND_MULTIPLE.equals(mAction)) {
302            setAccount(intent);
303            // Use the fields found in the Intent to prefill as much of the message as possible
304            initFromIntent(intent);
305            setDraftNeedsSaving(true);
306            mMessageLoaded = true;
307            mSourceMessageProcessed = true;
308        } else {
309            // Otherwise, handle the internal cases (Message Composer invoked from within app)
310            long messageId = draftId != -1 ? draftId : intent.getLongExtra(EXTRA_MESSAGE_ID, -1);
311            if (messageId != -1) {
312                mLoadMessageTask = new LoadMessageTask().execute(messageId);
313            } else {
314                setAccount(intent);
315                // Since this is a new message, we don't need to call LoadMessageTask.
316                // But we DO need to set mMessageLoaded to indicate the message can be sent
317                mMessageLoaded = true;
318                mSourceMessageProcessed = true;
319            }
320            setInitialComposeText(null, (mAccount != null) ? mAccount.mSignature : null);
321        }
322
323        if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction) ||
324                ACTION_FORWARD.equals(mAction) || ACTION_EDIT_DRAFT.equals(mAction)) {
325            /*
326             * If we need to load the message we add ourself as a message listener here
327             * so we can kick it off. Normally we add in onResume but we don't
328             * want to reload the message every time the activity is resumed.
329             * There is no harm in adding twice.
330             */
331            // TODO: signal the controller to load the message
332        }
333    }
334
335    // needed for unit tests
336    @Override
337    public void setIntent(Intent intent) {
338        super.setIntent(intent);
339        mAction = intent.getAction();
340    }
341
342    @Override
343    public void onResume() {
344        super.onResume();
345
346        // Exit immediately if the accounts list has changed (e.g. externally deleted)
347        if (Email.getNotifyUiAccountsChanged()) {
348            Welcome.actionStart(this);
349            finish();
350            return;
351        }
352    }
353
354    @Override
355    public void onPause() {
356        super.onPause();
357        saveIfNeeded();
358    }
359
360    /**
361     * We override onDestroy to make sure that the WebView gets explicitly destroyed.
362     * Otherwise it can leak native references.
363     */
364    @Override
365    public void onDestroy() {
366        super.onDestroy();
367        mQuotedText.destroy();
368        mQuotedText = null;
369
370        Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
371        mLoadAttachmentsTask = null;
372        Utility.cancelTaskInterrupt(mLoadMessageTask);
373        mLoadMessageTask = null;
374
375        if (mAddressAdapterTo != null) {
376            mAddressAdapterTo.changeCursor(null);
377        }
378        if (mAddressAdapterCc != null) {
379            mAddressAdapterCc.changeCursor(null);
380        }
381        if (mAddressAdapterBcc != null) {
382            mAddressAdapterBcc.changeCursor(null);
383        }
384    }
385
386    /**
387     * The framework handles most of the fields, but we need to handle stuff that we
388     * dynamically show and hide:
389     * Cc field,
390     * Bcc field,
391     * Quoted text,
392     */
393    @Override
394    protected void onSaveInstanceState(Bundle outState) {
395        super.onSaveInstanceState(outState);
396        long draftId = getOrCreateDraftId();
397        if (draftId != -1) {
398            outState.putLong(STATE_KEY_DRAFT_ID, draftId);
399        }
400        outState.putBoolean(STATE_KEY_CC_SHOWN, mCcView.getVisibility() == View.VISIBLE);
401        outState.putBoolean(STATE_KEY_BCC_SHOWN, mBccView.getVisibility() == View.VISIBLE);
402        outState.putBoolean(STATE_KEY_QUOTED_TEXT_SHOWN,
403                mQuotedTextBar.getVisibility() == View.VISIBLE);
404        outState.putBoolean(STATE_KEY_SOURCE_MESSAGE_PROCED, mSourceMessageProcessed);
405    }
406
407    @Override
408    protected void onRestoreInstanceState(Bundle savedInstanceState) {
409        super.onRestoreInstanceState(savedInstanceState);
410        mCcView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_CC_SHOWN) ?
411                View.VISIBLE : View.GONE);
412        mBccView.setVisibility(savedInstanceState.getBoolean(STATE_KEY_BCC_SHOWN) ?
413                View.VISIBLE : View.GONE);
414        mQuotedTextBar.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ?
415                View.VISIBLE : View.GONE);
416        mQuotedText.setVisibility(savedInstanceState.getBoolean(STATE_KEY_QUOTED_TEXT_SHOWN) ?
417                View.VISIBLE : View.GONE);
418        setDraftNeedsSaving(false);
419    }
420
421    private void setDraftNeedsSaving(boolean needsSaving) {
422        mDraftNeedsSaving = needsSaving;
423        mSaveButton.setEnabled(needsSaving);
424    }
425
426    private void initViews() {
427        mActionBar = getActionBar();
428        mToView = (MultiAutoCompleteTextView)findViewById(R.id.to);
429        mCcView = (MultiAutoCompleteTextView)findViewById(R.id.cc);
430        mBccView = (MultiAutoCompleteTextView)findViewById(R.id.bcc);
431        mSubjectView = (EditText)findViewById(R.id.subject);
432        mMessageContentView = (EditText)findViewById(R.id.message_content);
433        mSendButton = (Button)findViewById(R.id.send);
434        mDiscardButton = (Button)findViewById(R.id.discard);
435        mSaveButton = (Button)findViewById(R.id.save);
436        mAttachments = (LinearLayout)findViewById(R.id.attachments);
437        mQuotedTextBar = findViewById(R.id.quoted_text_bar);
438        mQuotedTextDelete = (ImageButton)findViewById(R.id.quoted_text_delete);
439        mQuotedText = (WebView)findViewById(R.id.quoted_text);
440
441        TextWatcher watcher = new TextWatcher() {
442            public void beforeTextChanged(CharSequence s, int start,
443                                          int before, int after) { }
444
445            public void onTextChanged(CharSequence s, int start,
446                                          int before, int count) {
447                setDraftNeedsSaving(true);
448            }
449
450            public void afterTextChanged(android.text.Editable s) { }
451        };
452
453        /**
454         * Implements special address cleanup rules:
455         * The first space key entry following an "@" symbol that is followed by any combination
456         * of letters and symbols, including one+ dots and zero commas, should insert an extra
457         * comma (followed by the space).
458         */
459        InputFilter recipientFilter = new InputFilter() {
460
461            public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
462                    int dstart, int dend) {
463
464                // quick check - did they enter a single space?
465                if (end-start != 1 || source.charAt(start) != ' ') {
466                    return null;
467                }
468
469                // determine if the characters before the new space fit the pattern
470                // follow backwards and see if we find a comma, dot, or @
471                int scanBack = dstart;
472                boolean dotFound = false;
473                while (scanBack > 0) {
474                    char c = dest.charAt(--scanBack);
475                    switch (c) {
476                        case '.':
477                            dotFound = true;    // one or more dots are req'd
478                            break;
479                        case ',':
480                            return null;
481                        case '@':
482                            if (!dotFound) {
483                                return null;
484                            }
485
486                            // we have found a comma-insert case.  now just do it
487                            // in the least expensive way we can.
488                            if (source instanceof Spanned) {
489                                SpannableStringBuilder sb = new SpannableStringBuilder(",");
490                                sb.append(source);
491                                return sb;
492                            } else {
493                                return ", ";
494                            }
495                        default:
496                            // just keep going
497                    }
498                }
499
500                // no termination cases were found, so don't edit the input
501                return null;
502            }
503        };
504        InputFilter[] recipientFilters = new InputFilter[] { recipientFilter };
505
506        mToView.addTextChangedListener(watcher);
507        mCcView.addTextChangedListener(watcher);
508        mBccView.addTextChangedListener(watcher);
509        mSubjectView.addTextChangedListener(watcher);
510        mMessageContentView.addTextChangedListener(watcher);
511
512        // NOTE: assumes no other filters are set
513        mToView.setFilters(recipientFilters);
514        mCcView.setFilters(recipientFilters);
515        mBccView.setFilters(recipientFilters);
516
517        /*
518         * We set this to invisible by default. Other methods will turn it back on if it's
519         * needed.
520         */
521        mQuotedTextBar.setVisibility(View.GONE);
522        mQuotedText.setVisibility(View.GONE);
523
524        mQuotedTextDelete.setOnClickListener(this);
525
526        EmailAddressValidator addressValidator = new EmailAddressValidator();
527
528        setupAddressAdapters();
529        mToView.setAdapter(mAddressAdapterTo);
530        mToView.setTokenizer(new Rfc822Tokenizer());
531        mToView.setValidator(addressValidator);
532
533        mCcView.setAdapter(mAddressAdapterCc);
534        mCcView.setTokenizer(new Rfc822Tokenizer());
535        mCcView.setValidator(addressValidator);
536
537        mBccView.setAdapter(mAddressAdapterBcc);
538        mBccView.setTokenizer(new Rfc822Tokenizer());
539        mBccView.setValidator(addressValidator);
540
541        mSendButton.setOnClickListener(this);
542        mDiscardButton.setOnClickListener(this);
543        mSaveButton.setOnClickListener(this);
544
545        mSubjectView.setOnFocusChangeListener(this);
546        mMessageContentView.setOnFocusChangeListener(this);
547    }
548
549    /**
550     * Set up address auto-completion adapters.
551     */
552    @SuppressWarnings("all")
553    private void setupAddressAdapters() {
554        /* EXCHANGE-REMOVE-SECTION-START */
555        if (true) {
556            mAddressAdapterTo = new GalEmailAddressAdapter(this);
557            mAddressAdapterCc = new GalEmailAddressAdapter(this);
558            mAddressAdapterBcc = new GalEmailAddressAdapter(this);
559        } else {
560            /* EXCHANGE-REMOVE-SECTION-END */
561            mAddressAdapterTo = new EmailAddressAdapter(this);
562            mAddressAdapterCc = new EmailAddressAdapter(this);
563            mAddressAdapterBcc = new EmailAddressAdapter(this);
564            /* EXCHANGE-REMOVE-SECTION-START */
565        }
566        /* EXCHANGE-REMOVE-SECTION-END */
567    }
568
569    // TODO: is there any way to unify this with MessageView.LoadMessageTask?
570    private class LoadMessageTask extends AsyncTask<Long, Void, Object[]> {
571        @Override
572        protected Object[] doInBackground(Long... messageIds) {
573            synchronized (sSaveInProgressCondition) {
574                while (sSaveInProgress) {
575                    try {
576                        sSaveInProgressCondition.wait();
577                    } catch (InterruptedException e) {
578                        // ignore & retry loop
579                    }
580                }
581            }
582            Message message = Message.restoreMessageWithId(MessageCompose.this, messageIds[0]);
583            if (message == null) {
584                return new Object[] {null, null};
585            }
586            long accountId = message.mAccountKey;
587            Account account = Account.restoreAccountWithId(MessageCompose.this, accountId);
588            try {
589                // Body body = Body.restoreBodyWithMessageId(MessageCompose.this, message.mId);
590                message.mHtml = Body.restoreBodyHtmlWithMessageId(MessageCompose.this, message.mId);
591                message.mText = Body.restoreBodyTextWithMessageId(MessageCompose.this, message.mId);
592                boolean isEditDraft = ACTION_EDIT_DRAFT.equals(mAction);
593                // the reply fields are only filled/used for Drafts.
594                if (isEditDraft) {
595                    message.mHtmlReply =
596                        Body.restoreReplyHtmlWithMessageId(MessageCompose.this, message.mId);
597                    message.mTextReply =
598                        Body.restoreReplyTextWithMessageId(MessageCompose.this, message.mId);
599                    message.mIntroText =
600                        Body.restoreIntroTextWithMessageId(MessageCompose.this, message.mId);
601                    message.mSourceKey = Body.restoreBodySourceKey(MessageCompose.this,
602                                                                   message.mId);
603                } else {
604                    message.mHtmlReply = null;
605                    message.mTextReply = null;
606                    message.mIntroText = null;
607                }
608            } catch (RuntimeException e) {
609                Log.d(Email.LOG_TAG, "Exception while loading message body: " + e);
610                return new Object[] {null, null};
611            }
612            return new Object[]{message, account};
613        }
614
615        @Override
616        protected void onPostExecute(Object[] messageAndAccount) {
617            if (messageAndAccount == null) {
618                return;
619            }
620
621            final Message message = (Message) messageAndAccount[0];
622            final Account account = (Account) messageAndAccount[1];
623            if (message == null && account == null) {
624                // Something unexpected happened:
625                // the message or the body couldn't be loaded by SQLite.
626                // Bail out.
627                Toast.makeText(MessageCompose.this, R.string.error_loading_message_body,
628                               Toast.LENGTH_LONG).show();
629                finish();
630                return;
631            }
632
633            // Drafts and "forwards" need to include attachments from the original unless the
634            // account is marked as supporting smart forward
635            if (ACTION_EDIT_DRAFT.equals(mAction) || ACTION_FORWARD.equals(mAction)) {
636                final boolean draft = ACTION_EDIT_DRAFT.equals(mAction);
637                if (draft) {
638                    mDraft = message;
639                } else {
640                    mSource = message;
641                }
642                mLoadAttachmentsTask = new AsyncTask<Long, Void, Attachment[]>() {
643                    @Override
644                    protected Attachment[] doInBackground(Long... messageIds) {
645                        // TODO: When we finally allow the user to change the sending account,
646                        // we'll need a test to check whether the sending account is
647                        // the message's account
648                        boolean smartForward =
649                            (account.mFlags & Account.FLAGS_SUPPORTS_SMART_FORWARD) != 0;
650                        if (smartForward && !draft) {
651                            return null;
652                        }
653                        return Attachment.restoreAttachmentsWithMessageId(MessageCompose.this,
654                                messageIds[0]);
655                    }
656                    @Override
657                    protected void onPostExecute(Attachment[] attachments) {
658                        if (attachments == null) {
659                            return;
660                        }
661                        for (Attachment attachment : attachments) {
662                            addAttachment(attachment, true);
663                        }
664                    }
665                }.execute(message.mId);
666            } else if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction)) {
667                mSource = message;
668            } else if (Email.LOGD) {
669                Email.log("Action " + mAction + " has unexpected EXTRA_MESSAGE_ID");
670            }
671
672            setAccount(account);
673            processSourceMessageGuarded(message, mAccount);
674            mMessageLoaded = true;
675        }
676    }
677
678    public void onFocusChange(View view, boolean focused) {
679        if (focused) {
680            switch (view.getId()) {
681                case R.id.message_content:
682                    setMessageContentSelection((mAccount != null) ? mAccount.mSignature : null);
683            }
684        }
685    }
686
687    private void addAddresses(MultiAutoCompleteTextView view, Address[] addresses) {
688        if (addresses == null) {
689            return;
690        }
691        for (Address address : addresses) {
692            addAddress(view, address.toString());
693        }
694    }
695
696    private void addAddresses(MultiAutoCompleteTextView view, String[] addresses) {
697        if (addresses == null) {
698            return;
699        }
700        for (String oneAddress : addresses) {
701            addAddress(view, oneAddress);
702        }
703    }
704
705    private void addAddress(MultiAutoCompleteTextView view, String address) {
706        view.append(address + ", ");
707    }
708
709    private String getPackedAddresses(TextView view) {
710        Address[] addresses = Address.parse(view.getText().toString().trim());
711        return Address.pack(addresses);
712    }
713
714    private Address[] getAddresses(TextView view) {
715        Address[] addresses = Address.parse(view.getText().toString().trim());
716        return addresses;
717    }
718
719    /*
720     * Computes a short string indicating the destination of the message based on To, Cc, Bcc.
721     * If only one address appears, returns the friendly form of that address.
722     * Otherwise returns the friendly form of the first address appended with "and N others".
723     */
724    private String makeDisplayName(String packedTo, String packedCc, String packedBcc) {
725        Address first = null;
726        int nRecipients = 0;
727        for (String packed: new String[] {packedTo, packedCc, packedBcc}) {
728            Address[] addresses = Address.unpack(packed);
729            nRecipients += addresses.length;
730            if (first == null && addresses.length > 0) {
731                first = addresses[0];
732            }
733        }
734        if (nRecipients == 0) {
735            return "";
736        }
737        String friendly = first.toFriendly();
738        if (nRecipients == 1) {
739            return friendly;
740        }
741        return this.getString(R.string.message_compose_display_name, friendly, nRecipients - 1);
742    }
743
744    private ContentValues getUpdateContentValues(Message message) {
745        ContentValues values = new ContentValues();
746        values.put(MessageColumns.TIMESTAMP, message.mTimeStamp);
747        values.put(MessageColumns.FROM_LIST, message.mFrom);
748        values.put(MessageColumns.TO_LIST, message.mTo);
749        values.put(MessageColumns.CC_LIST, message.mCc);
750        values.put(MessageColumns.BCC_LIST, message.mBcc);
751        values.put(MessageColumns.SUBJECT, message.mSubject);
752        values.put(MessageColumns.DISPLAY_NAME, message.mDisplayName);
753        values.put(MessageColumns.FLAG_READ, message.mFlagRead);
754        values.put(MessageColumns.FLAG_LOADED, message.mFlagLoaded);
755        values.put(MessageColumns.FLAG_ATTACHMENT, message.mFlagAttachment);
756        values.put(MessageColumns.FLAGS, message.mFlags);
757        return values;
758    }
759
760    /**
761     * @param message The message to be updated.
762     * @param account the account (used to obtain From: address).
763     * @param bodyText the body text.
764     */
765    private void updateMessage(Message message, Account account, boolean hasAttachments) {
766        if (message.mMessageId == null || message.mMessageId.length() == 0) {
767            message.mMessageId = Utility.generateMessageId();
768        }
769        message.mTimeStamp = System.currentTimeMillis();
770        message.mFrom = new Address(account.getEmailAddress(), account.getSenderName()).pack();
771        message.mTo = getPackedAddresses(mToView);
772        message.mCc = getPackedAddresses(mCcView);
773        message.mBcc = getPackedAddresses(mBccView);
774        message.mSubject = mSubjectView.getText().toString();
775        message.mText = mMessageContentView.getText().toString();
776        message.mAccountKey = account.mId;
777        message.mDisplayName = makeDisplayName(message.mTo, message.mCc, message.mBcc);
778        message.mFlagRead = true;
779        message.mFlagLoaded = Message.FLAG_LOADED_COMPLETE;
780        message.mFlagAttachment = hasAttachments;
781        // Use the Intent to set flags saying this message is a reply or a forward and save the
782        // unique id of the source message
783        if (mSource != null && mQuotedTextBar.getVisibility() == View.VISIBLE) {
784            if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction)
785                    || ACTION_FORWARD.equals(mAction)) {
786                message.mSourceKey = mSource.mId;
787                // Get the body of the source message here
788                message.mHtmlReply = mSource.mHtml;
789                message.mTextReply = mSource.mText;
790            }
791
792            String fromAsString = Address.unpackToString(mSource.mFrom);
793            if (ACTION_FORWARD.equals(mAction)) {
794                message.mFlags |= Message.FLAG_TYPE_FORWARD;
795                String subject = mSource.mSubject;
796                String to = Address.unpackToString(mSource.mTo);
797                String cc = Address.unpackToString(mSource.mCc);
798                message.mIntroText =
799                    getString(R.string.message_compose_fwd_header_fmt, subject, fromAsString,
800                            to != null ? to : "", cc != null ? cc : "");
801            } else {
802                message.mFlags |= Message.FLAG_TYPE_REPLY;
803                message.mIntroText =
804                    getString(R.string.message_compose_reply_header_fmt, fromAsString);
805            }
806        }
807    }
808
809    private Attachment[] getAttachmentsFromUI() {
810        int count = mAttachments.getChildCount();
811        Attachment[] attachments = new Attachment[count];
812        for (int i = 0; i < count; ++i) {
813            attachments[i] = (Attachment) mAttachments.getChildAt(i).getTag();
814        }
815        return attachments;
816    }
817
818    /* This method does DB operations in UI thread because
819       the draftId is needed by onSaveInstanceState() which can't wait for it
820       to be saved in the background.
821       TODO: This will cause ANRs, so we need to find a better solution.
822    */
823    private long getOrCreateDraftId() {
824        synchronized (mDraft) {
825            if (mDraft.mId > 0) {
826                return mDraft.mId;
827            }
828            // don't save draft if the source message did not load yet
829            if (!mMessageLoaded) {
830                return -1;
831            }
832            final Attachment[] attachments = getAttachmentsFromUI();
833            updateMessage(mDraft, mAccount, attachments.length > 0);
834            mController.saveToMailbox(mDraft, EmailContent.Mailbox.TYPE_DRAFTS);
835            return mDraft.mId;
836        }
837    }
838
839    private class SendOrSaveMessageTask extends AsyncTask<Boolean, Void, Boolean> {
840
841        @Override
842        protected Boolean doInBackground(Boolean... params) {
843            boolean send = params[0];
844            final Attachment[] attachments = getAttachmentsFromUI();
845            updateMessage(mDraft, mAccount, attachments.length > 0);
846            synchronized (mDraft) {
847                ContentResolver resolver = getContentResolver();
848                if (mDraft.isSaved()) {
849                    // Update the message
850                    Uri draftUri =
851                        ContentUris.withAppendedId(Message.SYNCED_CONTENT_URI, mDraft.mId);
852                    resolver.update(draftUri, getUpdateContentValues(mDraft), null, null);
853                    // Update the body
854                    ContentValues values = new ContentValues();
855                    values.put(BodyColumns.TEXT_CONTENT, mDraft.mText);
856                    values.put(BodyColumns.TEXT_REPLY, mDraft.mTextReply);
857                    values.put(BodyColumns.HTML_REPLY, mDraft.mHtmlReply);
858                    values.put(BodyColumns.INTRO_TEXT, mDraft.mIntroText);
859                    values.put(BodyColumns.SOURCE_MESSAGE_KEY, mDraft.mSourceKey);
860                    Body.updateBodyWithMessageId(MessageCompose.this, mDraft.mId, values);
861                } else {
862                    // mDraft.mId is set upon return of saveToMailbox()
863                    mController.saveToMailbox(mDraft, EmailContent.Mailbox.TYPE_DRAFTS);
864                }
865                // For any unloaded attachment, set the flag saying we need it loaded
866                boolean hasUnloadedAttachments = false;
867                for (Attachment attachment : attachments) {
868                    if (attachment.mContentUri == null) {
869                        attachment.mFlags |= Attachment.FLAG_DOWNLOAD_FORWARD;
870                        hasUnloadedAttachments = true;
871                        if (Email.DEBUG){
872                            Log.d("MessageCompose",
873                                    "Requesting download of attachment #" + attachment.mId);
874                        }
875                    }
876                    if (!attachment.isSaved()) {
877                        // this attachment is new so save it to DB.
878                        attachment.mMessageKey = mDraft.mId;
879                        attachment.save(MessageCompose.this);
880                    } else {
881                        // We clone the attachment and save it again; otherwise, it will
882                        // continue to point to the source message.  From this point forward,
883                        // the attachments will be independent of the original message in the
884                        // database; however, we still need the message on the server in order
885                        // to retrieve unloaded attachments
886                        ContentValues cv = attachment.toContentValues();
887                        cv.put(Attachment.FLAGS, attachment.mFlags);
888                        cv.put(Attachment.MESSAGE_KEY, mDraft.mId);
889                        getContentResolver().insert(Attachment.CONTENT_URI, cv);
890                    }
891                }
892
893                if (send) {
894                    // Let the user know if message sending might be delayed by background
895                    // downlading of unloaded attachments
896                    if (hasUnloadedAttachments) {
897                        Utility.showToast(MessageCompose.this,
898                                R.string.message_view_attachment_background_load);
899                    }
900                    mController.sendMessage(mDraft.mId, mDraft.mAccountKey);
901                }
902                return send;
903            }
904        }
905
906        @Override
907        protected void onPostExecute(Boolean send) {
908            synchronized (sSaveInProgressCondition) {
909                sSaveInProgress = false;
910                sSaveInProgressCondition.notify();
911            }
912            if (isCancelled()) {
913                return;
914            }
915            // Don't display the toast if the user is just changing the orientation
916            if (!send && (getChangingConfigurations() & ActivityInfo.CONFIG_ORIENTATION) == 0) {
917                Toast.makeText(MessageCompose.this, R.string.message_saved_toast,
918                        Toast.LENGTH_LONG).show();
919            }
920        }
921    }
922
923    /**
924     * Send or save a message:
925     * - out of the UI thread
926     * - write to Drafts
927     * - if send, invoke Controller.sendMessage()
928     * - when operation is complete, display toast
929     */
930    private void sendOrSaveMessage(boolean send) {
931        if (!mMessageLoaded) {
932            // early save, before the message was loaded: do nothing
933            return;
934        }
935        synchronized (sSaveInProgressCondition) {
936            sSaveInProgress = true;
937        }
938        new SendOrSaveMessageTask().execute(send);
939   }
940
941    private void saveIfNeeded() {
942        if (!mDraftNeedsSaving) {
943            return;
944        }
945        setDraftNeedsSaving(false);
946        sendOrSaveMessage(false);
947    }
948
949    /**
950     * Checks whether all the email addresses listed in TO, CC, BCC are valid.
951     */
952    /* package */ boolean isAddressAllValid() {
953        for (TextView view : new TextView[]{mToView, mCcView, mBccView}) {
954            String addresses = view.getText().toString().trim();
955            if (!Address.isAllValid(addresses)) {
956                view.setError(getString(R.string.message_compose_error_invalid_email));
957                return false;
958            }
959        }
960        return true;
961    }
962
963    private void onSend() {
964        if (!isAddressAllValid()) {
965            Toast.makeText(this, getString(R.string.message_compose_error_invalid_email),
966                           Toast.LENGTH_LONG).show();
967        } else if (getAddresses(mToView).length == 0 &&
968                getAddresses(mCcView).length == 0 &&
969                getAddresses(mBccView).length == 0) {
970            mToView.setError(getString(R.string.message_compose_error_no_recipients));
971            Toast.makeText(this, getString(R.string.message_compose_error_no_recipients),
972                    Toast.LENGTH_LONG).show();
973        } else {
974            sendOrSaveMessage(true);
975            setDraftNeedsSaving(false);
976            finish();
977        }
978    }
979
980    private void onDiscard() {
981        if (mDraft.mId > 0) {
982            mController.deleteMessage(mDraft.mId, mDraft.mAccountKey);
983        }
984        Toast.makeText(this, getString(R.string.message_discarded_toast), Toast.LENGTH_LONG).show();
985        setDraftNeedsSaving(false);
986        finish();
987    }
988
989    private void onSave() {
990        saveIfNeeded();
991        finish();
992    }
993
994    private void onAddCcBcc() {
995        mCcView.setVisibility(View.VISIBLE);
996        mBccView.setVisibility(View.VISIBLE);
997    }
998
999    /**
1000     * Kick off a picker for whatever kind of MIME types we'll accept and let Android take over.
1001     */
1002    private void onAddAttachment() {
1003        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
1004        i.addCategory(Intent.CATEGORY_OPENABLE);
1005        i.setType(Email.ACCEPTABLE_ATTACHMENT_SEND_UI_TYPES[0]);
1006        startActivityForResult(
1007                Intent.createChooser(i, getString(R.string.choose_attachment_dialog_title)),
1008                ACTIVITY_REQUEST_PICK_ATTACHMENT);
1009    }
1010
1011    private Attachment loadAttachmentInfo(Uri uri) {
1012        int size = -1;
1013        String name = null;
1014        ContentResolver contentResolver = getContentResolver();
1015        Cursor metadataCursor = contentResolver.query(uri,
1016                ATTACHMENT_META_COLUMNS, null, null, null);
1017        if (metadataCursor != null) {
1018            try {
1019                if (metadataCursor.moveToFirst()) {
1020                    name = metadataCursor.getString(0);
1021                    size = metadataCursor.getInt(1);
1022                }
1023            } finally {
1024                metadataCursor.close();
1025            }
1026        }
1027        if (name == null) {
1028            name = uri.getLastPathSegment();
1029        }
1030
1031        String contentType = contentResolver.getType(uri);
1032        if (contentType == null) {
1033            contentType = "";
1034        }
1035
1036        Attachment attachment = new Attachment();
1037        attachment.mFileName = name;
1038        attachment.mContentUri = uri.toString();
1039        attachment.mSize = size;
1040        attachment.mMimeType = contentType;
1041        return attachment;
1042    }
1043
1044    private void addAttachment(Attachment attachment, boolean allowDelete) {
1045        // Before attaching the attachment, make sure it meets any other pre-attach criteria
1046        if (attachment.mSize > Email.MAX_ATTACHMENT_UPLOAD_SIZE) {
1047            Toast.makeText(this, R.string.message_compose_attachment_size, Toast.LENGTH_LONG)
1048                    .show();
1049            return;
1050        }
1051
1052        View view = getLayoutInflater().inflate(R.layout.message_compose_attachment,
1053                mAttachments, false);
1054        TextView nameView = (TextView)view.findViewById(R.id.attachment_name);
1055        ImageButton delete = (ImageButton)view.findViewById(R.id.attachment_delete);
1056        nameView.setText(attachment.mFileName);
1057        if (allowDelete) {
1058            delete.setOnClickListener(this);
1059            delete.setTag(view);
1060        } else {
1061            delete.setVisibility(View.INVISIBLE);
1062        }
1063        view.setTag(attachment);
1064        mAttachments.addView(view);
1065    }
1066
1067    private void addAttachment(Uri uri) {
1068        addAttachment(loadAttachmentInfo(uri), true);
1069    }
1070
1071    @Override
1072    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1073        if (data == null) {
1074            return;
1075        }
1076        addAttachment(data.getData());
1077        setDraftNeedsSaving(true);
1078    }
1079
1080    public void onClick(View view) {
1081        switch (view.getId()) {
1082            case R.id.send:
1083                onSend();
1084                break;
1085            case R.id.save:
1086                onSave();
1087                break;
1088            case R.id.discard:
1089                onDiscard();
1090                break;
1091            case R.id.attachment_delete:
1092                onDeleteAttachment(view);
1093                break;
1094            case R.id.quoted_text_delete:
1095                mQuotedTextBar.setVisibility(View.GONE);
1096                mQuotedText.setVisibility(View.GONE);
1097                mDraft.mIntroText = null;
1098                mDraft.mTextReply = null;
1099                mDraft.mHtmlReply = null;
1100                mDraft.mSourceKey = 0;
1101                setDraftNeedsSaving(true);
1102                break;
1103        }
1104    }
1105
1106    private void onDeleteAttachment(View delButtonView) {
1107        /*
1108         * The view is the delete button, and we have previously set the tag of
1109         * the delete button to the view that owns it. We don't use parent because the
1110         * view is very complex and could change in the future.
1111         */
1112        View attachmentView = (View) delButtonView.getTag();
1113        Attachment attachment = (Attachment) attachmentView.getTag();
1114        mAttachments.removeView(attachmentView);
1115        if (attachment.isSaved()) {
1116            // The following async task for deleting attachments:
1117            // - can be started multiple times in parallel (to delete multiple attachments).
1118            // - need not be interrupted on activity exit, instead should run to completion.
1119            new AsyncTask<Long, Void, Void>() {
1120                @Override
1121                protected Void doInBackground(Long... attachmentIds) {
1122                    mController.deleteAttachment(attachmentIds[0]);
1123                    return null;
1124                }
1125            }.execute(attachment.mId);
1126        }
1127        setDraftNeedsSaving(true);
1128    }
1129
1130    @Override
1131    public boolean onOptionsItemSelected(MenuItem item) {
1132        switch (item.getItemId()) {
1133            case R.id.send:
1134                onSend();
1135                break;
1136            case R.id.save:
1137                onSave();
1138                break;
1139            case R.id.discard:
1140                onDiscard();
1141                break;
1142            case R.id.add_cc_bcc:
1143                onAddCcBcc();
1144                break;
1145            case R.id.add_attachment:
1146                onAddAttachment();
1147                break;
1148            default:
1149                return super.onOptionsItemSelected(item);
1150        }
1151        return true;
1152    }
1153
1154    @Override
1155    public boolean onCreateOptionsMenu(Menu menu) {
1156        super.onCreateOptionsMenu(menu);
1157        getMenuInflater().inflate(R.menu.message_compose_option, menu);
1158        return true;
1159    }
1160
1161    /**
1162     * Set a message body and a signature when the Activity is launched.
1163     *
1164     * @param text the message body
1165     */
1166    /* package */ void setInitialComposeText(CharSequence text, String signature) {
1167        int textLength = 0;
1168        if (text != null) {
1169            mMessageContentView.append(text);
1170            textLength = text.length();
1171        }
1172        if (!TextUtils.isEmpty(signature)) {
1173            if (textLength == 0 || text.charAt(textLength - 1) != '\n') {
1174                mMessageContentView.append("\n");
1175            }
1176            mMessageContentView.append(signature);
1177        }
1178    }
1179
1180    /**
1181     * Fill all the widgets with the content found in the Intent Extra, if any.
1182     *
1183     * Note that we don't actually check the intent action  (typically VIEW, SENDTO, or SEND).
1184     * There is enough overlap in the definitions that it makes more sense to simply check for
1185     * all available data and use as much of it as possible.
1186     *
1187     * With one exception:  EXTRA_STREAM is defined as only valid for ACTION_SEND.
1188     *
1189     * @param intent the launch intent
1190     */
1191    /* package */ void initFromIntent(Intent intent) {
1192
1193        // First, add values stored in top-level extras
1194        String[] extraStrings = intent.getStringArrayExtra(Intent.EXTRA_EMAIL);
1195        if (extraStrings != null) {
1196            addAddresses(mToView, extraStrings);
1197        }
1198        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_CC);
1199        if (extraStrings != null) {
1200            addAddresses(mCcView, extraStrings);
1201        }
1202        extraStrings = intent.getStringArrayExtra(Intent.EXTRA_BCC);
1203        if (extraStrings != null) {
1204            addAddresses(mBccView, extraStrings);
1205        }
1206        String extraString = intent.getStringExtra(Intent.EXTRA_SUBJECT);
1207        if (extraString != null) {
1208            mSubjectView.setText(extraString);
1209        }
1210
1211        // Next, if we were invoked with a URI, try to interpret it
1212        // We'll take two courses here.  If it's mailto:, there is a specific set of rules
1213        // that define various optional fields.  However, for any other scheme, we'll simply
1214        // take the entire scheme-specific part and interpret it as a possible list of addresses.
1215        final Uri dataUri = intent.getData();
1216        if (dataUri != null) {
1217            if ("mailto".equals(dataUri.getScheme())) {
1218                initializeFromMailTo(dataUri.toString());
1219            } else {
1220                String toText = dataUri.getSchemeSpecificPart();
1221                if (toText != null) {
1222                    addAddresses(mToView, toText.split(","));
1223                }
1224            }
1225        }
1226
1227        // Next, fill in the plaintext (note, this will override mailto:?body=)
1228        CharSequence text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
1229        if (text != null) {
1230            setInitialComposeText(text, null);
1231        }
1232
1233        // Next, convert EXTRA_STREAM into an attachment
1234        if (Intent.ACTION_SEND.equals(mAction) && intent.hasExtra(Intent.EXTRA_STREAM)) {
1235            String type = intent.getType();
1236            Uri stream = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
1237            if (stream != null && type != null) {
1238                if (MimeUtility.mimeTypeMatches(type,
1239                        Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) {
1240                    addAttachment(stream);
1241                }
1242            }
1243        }
1244
1245        if (Intent.ACTION_SEND_MULTIPLE.equals(mAction)
1246                && intent.hasExtra(Intent.EXTRA_STREAM)) {
1247            ArrayList<Parcelable> list = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
1248            if (list != null) {
1249                for (Parcelable parcelable : list) {
1250                    Uri uri = (Uri) parcelable;
1251                    if (uri != null) {
1252                        Attachment attachment = loadAttachmentInfo(uri);
1253                        if (MimeUtility.mimeTypeMatches(attachment.mMimeType,
1254                                Email.ACCEPTABLE_ATTACHMENT_SEND_INTENT_TYPES)) {
1255                            addAttachment(attachment, true);
1256                        }
1257                    }
1258                }
1259            }
1260        }
1261
1262        // Finally - expose fields that were filled in but are normally hidden, and set focus
1263        if (mCcView.length() > 0) {
1264            mCcView.setVisibility(View.VISIBLE);
1265        }
1266        if (mBccView.length() > 0) {
1267            mBccView.setVisibility(View.VISIBLE);
1268        }
1269        setNewMessageFocus();
1270        setDraftNeedsSaving(false);
1271    }
1272
1273    /**
1274     * When we are launched with an intent that includes a mailto: URI, we can actually
1275     * gather quite a few of our message fields from it.
1276     *
1277     * @mailToString the href (which must start with "mailto:").
1278     */
1279    private void initializeFromMailTo(String mailToString) {
1280
1281        // Chop up everything between mailto: and ? to find recipients
1282        int index = mailToString.indexOf("?");
1283        int length = "mailto".length() + 1;
1284        String to;
1285        try {
1286            // Extract the recipient after mailto:
1287            if (index == -1) {
1288                to = decode(mailToString.substring(length));
1289            } else {
1290                to = decode(mailToString.substring(length, index));
1291            }
1292            addAddresses(mToView, to.split(" ,"));
1293        } catch (UnsupportedEncodingException e) {
1294            Log.e(Email.LOG_TAG, e.getMessage() + " while decoding '" + mailToString + "'");
1295        }
1296
1297        // Extract the other parameters
1298
1299        // We need to disguise this string as a URI in order to parse it
1300        Uri uri = Uri.parse("foo://" + mailToString);
1301
1302        List<String> cc = uri.getQueryParameters("cc");
1303        addAddresses(mCcView, cc.toArray(new String[cc.size()]));
1304
1305        List<String> otherTo = uri.getQueryParameters("to");
1306        addAddresses(mCcView, otherTo.toArray(new String[otherTo.size()]));
1307
1308        List<String> bcc = uri.getQueryParameters("bcc");
1309        addAddresses(mBccView, bcc.toArray(new String[bcc.size()]));
1310
1311        List<String> subject = uri.getQueryParameters("subject");
1312        if (subject.size() > 0) {
1313            mSubjectView.setText(subject.get(0));
1314        }
1315
1316        List<String> body = uri.getQueryParameters("body");
1317        if (body.size() > 0) {
1318            setInitialComposeText(body.get(0), (mAccount != null) ? mAccount.mSignature : null);
1319        }
1320    }
1321
1322    private String decode(String s) throws UnsupportedEncodingException {
1323        return URLDecoder.decode(s, "UTF-8");
1324    }
1325
1326    // used by processSourceMessage()
1327    private void displayQuotedText(String textBody, String htmlBody) {
1328        /* Use plain-text body if available, otherwise use HTML body.
1329         * This matches the desired behavior for IMAP/POP where we only send plain-text,
1330         * and for EAS which sends HTML and has no plain-text body.
1331         */
1332        boolean plainTextFlag = textBody != null;
1333        String text = plainTextFlag ? textBody : htmlBody;
1334        if (text != null) {
1335            text = plainTextFlag ? EmailHtmlUtil.escapeCharacterToDisplay(text) : text;
1336            // TODO: re-enable EmailHtmlUtil.resolveInlineImage() for HTML
1337            //    EmailHtmlUtil.resolveInlineImage(getContentResolver(), mAccount,
1338            //                                     text, message, 0);
1339            mQuotedTextBar.setVisibility(View.VISIBLE);
1340            if (mQuotedText != null) {
1341                mQuotedText.setVisibility(View.VISIBLE);
1342                mQuotedText.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
1343            }
1344        }
1345    }
1346
1347    /**
1348     * Given a packed address String, the address of our sending account, a view, and a list of
1349     * addressees already added to other addressing views, adds unique addressees that don't
1350     * match our address to the passed in view
1351     */
1352    private boolean safeAddAddresses(String addrs, String ourAddress,
1353            MultiAutoCompleteTextView view, ArrayList<Address> addrList) {
1354        boolean added = false;
1355        for (Address address : Address.unpack(addrs)) {
1356            // Don't send to ourselves or already-included addresses
1357            if (!address.getAddress().equalsIgnoreCase(ourAddress) && !addrList.contains(address)) {
1358                addrList.add(address);
1359                addAddress(view, address.toString());
1360                added = true;
1361            }
1362        }
1363        return added;
1364    }
1365
1366    /**
1367     * Set up the to and cc views properly for the "reply" and "replyAll" cases.  What's important
1368     * is that we not 1) send to ourselves, and 2) duplicate addressees.
1369     * @param message the message we're replying to
1370     * @param account the account we're sending from
1371     * @param toView the "To" view
1372     * @param ccView the "Cc" view
1373     * @param replyAll whether this is a replyAll (vs a reply)
1374     */
1375    /*package*/ void setupAddressViews(Message message, Account account,
1376            MultiAutoCompleteTextView toView, MultiAutoCompleteTextView ccView, boolean replyAll) {
1377        /*
1378         * If a reply-to was included with the message use that, otherwise use the from
1379         * or sender address.
1380         */
1381        Address[] replyToAddresses = Address.unpack(message.mReplyTo);
1382        if (replyToAddresses.length == 0) {
1383            replyToAddresses = Address.unpack(message.mFrom);
1384        }
1385        addAddresses(mToView, replyToAddresses);
1386
1387        if (replyAll) {
1388            // Keep a running list of addresses we're sending to
1389            ArrayList<Address> allAddresses = new ArrayList<Address>();
1390            String ourAddress = account.mEmailAddress;
1391
1392            for (Address address: replyToAddresses) {
1393                allAddresses.add(address);
1394            }
1395
1396            safeAddAddresses(message.mTo, ourAddress, mToView, allAddresses);
1397            if (safeAddAddresses(message.mCc, ourAddress, mCcView, allAddresses)) {
1398                mCcView.setVisibility(View.VISIBLE);
1399            }
1400        }
1401    }
1402
1403    void processSourceMessageGuarded(Message message, Account account) {
1404        // Make sure we only do this once (otherwise we'll duplicate addresses!)
1405        if (!mSourceMessageProcessed) {
1406            processSourceMessage(message, account);
1407            mSourceMessageProcessed = true;
1408        }
1409
1410        /* The quoted text is displayed in a WebView whose content is not automatically
1411         * saved/restored by onRestoreInstanceState(), so we need to *always* restore it here,
1412         * regardless of the value of mSourceMessageProcessed.
1413         * This only concerns EDIT_DRAFT because after a configuration change we're always
1414         * in EDIT_DRAFT.
1415         */
1416        if (ACTION_EDIT_DRAFT.equals(mAction)) {
1417            displayQuotedText(message.mTextReply, message.mHtmlReply);
1418        }
1419    }
1420
1421    /**
1422     * Pull out the parts of the now loaded source message and apply them to the new message
1423     * depending on the type of message being composed.
1424     * @param message
1425     */
1426    /* package */
1427    void processSourceMessage(Message message, Account account) {
1428        setDraftNeedsSaving(true);
1429        final String subject = message.mSubject;
1430        if (ACTION_REPLY.equals(mAction) || ACTION_REPLY_ALL.equals(mAction)) {
1431            setupAddressViews(message, account, mToView, mCcView,
1432                ACTION_REPLY_ALL.equals(mAction));
1433            if (subject != null && !subject.toLowerCase().startsWith("re:")) {
1434                mSubjectView.setText("Re: " + subject);
1435            } else {
1436                mSubjectView.setText(subject);
1437            }
1438            displayQuotedText(message.mText, message.mHtml);
1439            setInitialComposeText(null, (account != null) ? account.mSignature : null);
1440        } else if (ACTION_FORWARD.equals(mAction)) {
1441            mSubjectView.setText(subject != null && !subject.toLowerCase().startsWith("fwd:") ?
1442                    "Fwd: " + subject : subject);
1443            displayQuotedText(message.mText, message.mHtml);
1444            setInitialComposeText(null, (account != null) ? account.mSignature : null);
1445        } else if (ACTION_EDIT_DRAFT.equals(mAction)) {
1446            mSubjectView.setText(subject);
1447            addAddresses(mToView, Address.unpack(message.mTo));
1448            Address[] cc = Address.unpack(message.mCc);
1449            if (cc.length > 0) {
1450                addAddresses(mCcView, cc);
1451                mCcView.setVisibility(View.VISIBLE);
1452            }
1453            Address[] bcc = Address.unpack(message.mBcc);
1454            if (bcc.length > 0) {
1455                addAddresses(mBccView, bcc);
1456                mBccView.setVisibility(View.VISIBLE);
1457            }
1458
1459            mMessageContentView.setText(message.mText);
1460            // TODO: re-enable loadAttachments
1461            // loadAttachments(message, 0);
1462            setDraftNeedsSaving(false);
1463        }
1464        setNewMessageFocus();
1465    }
1466
1467    /**
1468     * Set a cursor to the end of a body except a signature
1469     */
1470    /* package */ void setMessageContentSelection(String signature) {
1471        // when selecting the message content, explicitly move IP to the end of the message,
1472        // so you can quickly resume typing into a draft
1473        int selection = mMessageContentView.length();
1474        if (!TextUtils.isEmpty(signature)) {
1475            int signatureLength = signature.length();
1476            int estimatedSelection = selection - signatureLength;
1477            if (estimatedSelection >= 0) {
1478                CharSequence text = mMessageContentView.getText();
1479                int i = 0;
1480                while (i < signatureLength
1481                       && text.charAt(estimatedSelection + i) == signature.charAt(i)) {
1482                    ++i;
1483                }
1484                if (i == signatureLength) {
1485                    selection = estimatedSelection;
1486                    while (selection > 0 && text.charAt(selection - 1) == '\n') {
1487                        --selection;
1488                    }
1489                }
1490            }
1491        }
1492        mMessageContentView.setSelection(selection, selection);
1493    }
1494
1495    /**
1496     * In order to accelerate typing, position the cursor in the first empty field,
1497     * or at the end of the body composition field if none are empty.  Typically, this will
1498     * play out as follows:
1499     *   Reply / Reply All - put cursor in the empty message body
1500     *   Forward - put cursor in the empty To field
1501     *   Edit Draft - put cursor in whatever field still needs entry
1502     */
1503    private void setNewMessageFocus() {
1504        if (mToView.length() == 0) {
1505            mToView.requestFocus();
1506        } else if (mSubjectView.length() == 0) {
1507            mSubjectView.requestFocus();
1508        } else {
1509            mMessageContentView.requestFocus();
1510            setMessageContentSelection((mAccount != null) ? mAccount.mSignature : null);
1511        }
1512    }
1513}
1514