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