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