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