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