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