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