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