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