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