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