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