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