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