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