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