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