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