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