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