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