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