MessageViewFragmentBase.java revision 2ac95b550df19617eace2e7436049926da5c606b
1/*
2 * Copyright (C) 2010 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.ControllerResultUiThreadWrapper;
21import com.android.email.Email;
22import com.android.email.Preferences;
23import com.android.email.R;
24import com.android.email.Throttle;
25import com.android.email.Utility;
26import com.android.email.mail.Address;
27import com.android.email.mail.MessagingException;
28import com.android.email.mail.internet.EmailHtmlUtil;
29import com.android.email.mail.internet.MimeUtility;
30import com.android.email.provider.AttachmentProvider;
31import com.android.email.provider.EmailContent.Attachment;
32import com.android.email.provider.EmailContent.Body;
33import com.android.email.provider.EmailContent.Mailbox;
34import com.android.email.provider.EmailContent.Message;
35import com.android.email.service.AttachmentDownloadService;
36
37import org.apache.commons.io.IOUtils;
38
39import android.app.Activity;
40import android.app.Fragment;
41import android.app.LoaderManager.LoaderCallbacks;
42import android.content.ActivityNotFoundException;
43import android.content.ContentResolver;
44import android.content.ContentUris;
45import android.content.Context;
46import android.content.Intent;
47import android.content.Loader;
48import android.database.ContentObserver;
49import android.graphics.Bitmap;
50import android.graphics.BitmapFactory;
51import android.graphics.drawable.ColorDrawable;
52import android.graphics.drawable.Drawable;
53import android.net.Uri;
54import android.os.AsyncTask;
55import android.os.Bundle;
56import android.os.Environment;
57import android.os.Handler;
58import android.provider.ContactsContract;
59import android.provider.ContactsContract.QuickContact;
60import android.text.TextUtils;
61import android.util.Log;
62import android.util.Patterns;
63import android.view.LayoutInflater;
64import android.view.View;
65import android.view.ViewGroup;
66import android.webkit.WebSettings;
67import android.webkit.WebView;
68import android.webkit.WebViewClient;
69import android.widget.Button;
70import android.widget.ImageView;
71import android.widget.LinearLayout;
72import android.widget.ProgressBar;
73import android.widget.TextView;
74
75import java.io.File;
76import java.io.FileOutputStream;
77import java.io.IOException;
78import java.io.InputStream;
79import java.io.OutputStream;
80import java.util.Date;
81import java.util.regex.Matcher;
82import java.util.regex.Pattern;
83
84// TODO Better handling of config changes.
85// - Restore "Show pictures" state, scroll position and current tab
86// - Retain the content; don't kick 3 async tasks every time
87
88/**
89 * Base class for {@link MessageViewFragment} and {@link MessageFileViewFragment}.
90 *
91 * See {@link MessageViewBase} for the class relation diagram.
92 */
93public abstract class MessageViewFragmentBase extends Fragment implements View.OnClickListener {
94    private static final int PHOTO_LOADER_ID = 1;
95    private Context mContext;
96
97    // Regex that matches start of img tag. '<(?i)img\s+'.
98    private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+");
99    // Regex that matches Web URL protocol part as case insensitive.
100    private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://");
101
102    private static int PREVIEW_ICON_WIDTH = 62;
103    private static int PREVIEW_ICON_HEIGHT = 62;
104
105    private TextView mSubjectView;
106    private TextView mFromNameView;
107    private TextView mFromAddressView;
108    private TextView mDateTimeView;
109    private TextView mToView;
110    private TextView mCcView;
111    private View mCcContainerView;
112    private TextView mBccView;
113    private View mBccContainerView;
114    private WebView mMessageContentView;
115    private LinearLayout mAttachments;
116    private View mTabSection;
117    private ImageView mFromBadge;
118    private ImageView mSenderPresenceView;
119
120    private TextView mMessageTab;
121    private TextView mAttachmentTab;
122    private TextView mInviteTab;
123    // It is not really a tab, but looks like one of them.
124    private TextView mShowPicturesTab;
125
126    private View mAttachmentsScroll;
127    private View mInviteScroll;
128
129    private long mAccountId = -1;
130    private long mMessageId = -1;
131    private Message mMessage;
132
133    private LoadMessageTask mLoadMessageTask;
134    private ReloadMessageTask mReloadMessageTask;
135    private LoadBodyTask mLoadBodyTask;
136    private LoadAttachmentsTask mLoadAttachmentsTask;
137
138    private java.text.DateFormat mDateFormat;
139    private java.text.DateFormat mTimeFormat;
140
141    private Controller mController;
142    private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback;
143
144    // contains the HTML body. Is used by LoadAttachmentTask to display inline images.
145    // is null most of the time, is used transiently to pass info to LoadAttachementTask
146    private String mHtmlTextRaw;
147
148    // contains the HTML content as set in WebView.
149    private String mHtmlTextWebView;
150
151    private boolean mResumed;
152    private boolean mLoadWhenResumed;
153
154    private boolean mIsMessageLoadedForTest;
155
156    private MessageObserver mMessageObserver;
157
158    private static final int CONTACT_STATUS_STATE_UNLOADED = 0;
159    private static final int CONTACT_STATUS_STATE_UNLOADED_TRIGGERED = 1;
160    private static final int CONTACT_STATUS_STATE_LOADED = 2;
161
162    private int mContactStatusState;
163    private Uri mQuickContactLookupUri;
164
165    /** Flag for {@link #mTabFlags}: Message has attachment(s) */
166    protected static final int TAB_FLAGS_HAS_ATTACHMENT = 1;
167
168    /**
169     * Flag for {@link #mTabFlags}: Message contains invite.  This flag is only set by
170     * {@link MessageViewFragment}.
171     */
172    protected static final int TAB_FLAGS_HAS_INVITE = 2;
173
174    /** Flag for {@link #mTabFlags}: Message contains pictures */
175    protected static final int TAB_FLAGS_HAS_PICTURES = 4;
176
177    /** Flag for {@link #mTabFlags}: "Show pictures" has already been pressed */
178    protected static final int TAB_FLAGS_PICTURE_LOADED = 8;
179
180    /**
181     * Flags to control the tabs.
182     * @see #updateTabFlags(int)
183     */
184    private int mTabFlags;
185
186    /** # of attachments in the current message */
187    private int mAttachmentCount;
188
189    // Use (random) large values, to avoid confusion with TAB_FLAGS_*
190    protected static final int TAB_MESSAGE = 101;
191    protected static final int TAB_INVITE = 102;
192    protected static final int TAB_ATTACHMENT = 103;
193
194    /**
195     * Currently visible tab.  Any of {@link #TAB_MESSAGE}, {@link #TAB_INVITE} or
196     * {@link #TAB_ATTACHMENT}.
197     *
198     * Note we don't retain this value through configuration changes, as restoring the current tab
199     * would be clumsy with the current implementation where we load Message/Body/Attachments
200     * separately.  (e.g. # of attachments can't be obtained quickly enough to update the UI
201     * after screen rotation.)
202     */
203    private int mCurrentTab;
204
205    /**
206     * Encapsulates known information about a single attachment.
207     */
208    private static class AttachmentInfo {
209        public String name;
210        public String contentType;
211        public long size;
212        public long attachmentId;
213        public Button viewButton;
214        public Button saveButton;
215        public Button loadButton;
216        public Button cancelButton;
217        public ImageView iconView;
218        public ProgressBar progressView;
219    }
220
221    public interface Callback {
222        /** Called when the fragment is about to show up, or show a different message. */
223        public void onMessageViewShown(int mailboxType);
224
225        /** Called when the fragment is about to be destroyed. */
226        public void onMessageViewGone();
227
228        /**
229         * Called when a link in a message is clicked.
230         *
231         * @param url link url that's clicked.
232         * @return true if handled, false otherwise.
233         */
234        public boolean onUrlInMessageClicked(String url);
235
236        /**
237         * Called when the message specified doesn't exist, or is deleted/moved.
238         */
239        public void onMessageNotExists();
240
241        /** Called when it starts loading a message. */
242        public void onLoadMessageStarted();
243
244        /** Called when it successfully finishes loading a message. */
245        public void onLoadMessageFinished();
246
247        /** Called when an error occurred during loading a message. */
248        public void onLoadMessageError(String errorMessage);
249    }
250
251    public static class EmptyCallback implements Callback {
252        public static final Callback INSTANCE = new EmptyCallback();
253        @Override public void onMessageViewShown(int mailboxType) {}
254        @Override public void onMessageViewGone() {}
255        @Override public void onLoadMessageError(String errorMessage) {}
256        @Override public void onLoadMessageFinished() {}
257        @Override public void onLoadMessageStarted() {}
258        @Override public void onMessageNotExists() {}
259        @Override
260        public boolean onUrlInMessageClicked(String url) {
261            return false;
262        }
263    }
264
265    private Callback mCallback = EmptyCallback.INSTANCE;
266
267    @Override
268    public void onCreate(Bundle savedInstanceState) {
269        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
270            Log.d(Email.LOG_TAG, "MessageViewFragment onCreate");
271        }
272        super.onCreate(savedInstanceState);
273
274        mContext = getActivity().getApplicationContext();
275
276        mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>(
277                new Handler(), new ControllerResults());
278
279        mDateFormat = android.text.format.DateFormat.getDateFormat(mContext); // short format
280        mTimeFormat = android.text.format.DateFormat.getTimeFormat(mContext); // 12/24 date format
281
282        mController = Controller.getInstance(mContext);
283        mMessageObserver = new MessageObserver(new Handler(), mContext);
284    }
285
286    @Override
287    public View onCreateView(
288            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
289        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
290            Log.d(Email.LOG_TAG, "MessageViewFragment onCreateView");
291        }
292        final View view = inflater.inflate(R.layout.message_view_fragment, container, false);
293
294        mSubjectView = (TextView) view.findViewById(R.id.subject);
295        mFromNameView = (TextView) view.findViewById(R.id.from_name);
296        mFromAddressView = (TextView) view.findViewById(R.id.from_address);
297        mToView = (TextView) view.findViewById(R.id.to);
298        mCcView = (TextView) view.findViewById(R.id.cc);
299        mCcContainerView = view.findViewById(R.id.cc_container);
300        mBccView = (TextView) view.findViewById(R.id.bcc);
301        mBccContainerView = view.findViewById(R.id.bcc_container);
302        mDateTimeView = (TextView) view.findViewById(R.id.datetime);
303        mMessageContentView = (WebView) view.findViewById(R.id.message_content);
304        mAttachments = (LinearLayout) view.findViewById(R.id.attachments);
305        mTabSection = view.findViewById(R.id.message_tabs_section);
306        mFromBadge = (ImageView) view.findViewById(R.id.badge);
307        mSenderPresenceView = (ImageView) view.findViewById(R.id.presence);
308
309        mFromNameView.setOnClickListener(this);
310        mFromAddressView.setOnClickListener(this);
311        mFromBadge.setOnClickListener(this);
312        mSenderPresenceView.setOnClickListener(this);
313
314        mMessageTab = (TextView) view.findViewById(R.id.show_message);
315        mAttachmentTab = (TextView) view.findViewById(R.id.show_attachments);
316        mShowPicturesTab = (TextView) view.findViewById(R.id.show_pictures);
317        // Invite is only used in MessageViewFragment, but visibility is controlled here.
318        mInviteTab = (TextView) view.findViewById(R.id.show_invite);
319
320        mMessageTab.setOnClickListener(this);
321        mAttachmentTab.setOnClickListener(this);
322        mShowPicturesTab.setOnClickListener(this);
323        mInviteTab.setOnClickListener(this);
324
325        mAttachmentsScroll = view.findViewById(R.id.attachments_scroll);
326        mInviteScroll = view.findViewById(R.id.invite_scroll);
327
328        mMessageContentView.setVerticalScrollBarEnabled(false);
329        WebSettings webSettings = mMessageContentView.getSettings();
330        webSettings.setBlockNetworkLoads(true);
331        webSettings.setSupportZoom(true);
332        webSettings.setBuiltInZoomControls(true);
333        mMessageContentView.setWebViewClient(new CustomWebViewClient());
334        return view;
335    }
336
337    @Override
338    public void onActivityCreated(Bundle savedInstanceState) {
339        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
340            Log.d(Email.LOG_TAG, "MessageViewFragment onActivityCreated");
341        }
342        super.onActivityCreated(savedInstanceState);
343        mController.addResultCallback(mControllerCallback);
344    }
345
346    @Override
347    public void onStart() {
348        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
349            Log.d(Email.LOG_TAG, "MessageViewFragment onStart");
350        }
351        super.onStart();
352    }
353
354    @Override
355    public void onResume() {
356        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
357            Log.d(Email.LOG_TAG, "MessageViewFragment onResume");
358        }
359        super.onResume();
360
361        mResumed = true;
362        if (isMessageSpecified()) {
363            if (mLoadWhenResumed) {
364                loadMessageIfResumed();
365            } else {
366                // This means, the user comes back from other (full-screen) activities.
367                // In this case we've already loaded the content, so don't load it again,
368                // which results in resetting all view state, including WebView zoom/pan
369                // and the current tab.
370            }
371        }
372    }
373
374    @Override
375    public void onPause() {
376        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
377            Log.d(Email.LOG_TAG, "MessageViewFragment onPause");
378        }
379        mResumed = false;
380        super.onPause();
381    }
382
383    @Override
384    public void onStop() {
385        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
386            Log.d(Email.LOG_TAG, "MessageViewFragment onStop");
387        }
388        super.onStop();
389    }
390
391    @Override
392    public void onDestroy() {
393        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
394            Log.d(Email.LOG_TAG, "MessageViewFragment onDestroy");
395        }
396        mCallback.onMessageViewGone();
397        mController.removeResultCallback(mControllerCallback);
398        clearContent();
399        mMessageContentView.destroy();
400        mMessageContentView = null;
401        super.onDestroy();
402    }
403
404    @Override
405    public void onSaveInstanceState(Bundle outState) {
406        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
407            Log.d(Email.LOG_TAG, "MessageViewFragment onSaveInstanceState");
408        }
409        super.onSaveInstanceState(outState);
410    }
411
412    public void setCallback(Callback callback) {
413        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
414    }
415
416    private void cancelAllTasks() {
417        mMessageObserver.unregister();
418        Utility.cancelTaskInterrupt(mLoadMessageTask);
419        mLoadMessageTask = null;
420        Utility.cancelTaskInterrupt(mReloadMessageTask);
421        mReloadMessageTask = null;
422        Utility.cancelTaskInterrupt(mLoadBodyTask);
423        mLoadBodyTask = null;
424        Utility.cancelTaskInterrupt(mLoadAttachmentsTask);
425        mLoadAttachmentsTask = null;
426    }
427
428    /**
429     * Subclass returns true if which message to open is already specified by the activity.
430     */
431    protected abstract boolean isMessageSpecified();
432
433    protected final Controller getController() {
434        return mController;
435    }
436
437    protected final Callback getCallback() {
438        return mCallback;
439    }
440
441    protected final Message getMessage() {
442        return mMessage;
443    }
444
445    protected final boolean isMessageOpen() {
446        return mMessage != null;
447    }
448
449    /**
450     * Returns the account id of the current message, or -1 if unknown (message not open yet, or
451     * viewing an EML message).
452     */
453    public long getAccountId() {
454        return mAccountId;
455    }
456
457    /**
458     * Clear all the content -- should be called when the fragment is hidden.
459     */
460    public void clearContent() {
461        cancelAllTasks();
462        resetView();
463    }
464
465    protected final void loadMessageIfResumed() {
466        if (!mResumed) {
467            mLoadWhenResumed = true;
468            return;
469        }
470        mLoadWhenResumed = false;
471        cancelAllTasks();
472        resetView();
473        mLoadMessageTask = new LoadMessageTask(true);
474        mLoadMessageTask.execute();
475    }
476
477    protected void resetView() {
478        setCurrentTab(TAB_MESSAGE);
479        updateTabFlags(0);
480        if (mMessageContentView != null) {
481            mMessageContentView.getSettings().setBlockNetworkLoads(true);
482            mMessageContentView.scrollTo(0, 0);
483            mMessageContentView.loadUrl("file:///android_asset/empty.html");
484
485            // Dynamic configuration of WebView
486            WebSettings.TextSize textZoom;
487            switch (Preferences.getPreferences(mContext).getTextZoom()) {
488                case Preferences.TEXT_ZOOM_TINY:    textZoom = WebSettings.TextSize.SMALLEST; break;
489                case Preferences.TEXT_ZOOM_SMALL:   textZoom = WebSettings.TextSize.SMALLER; break;
490                case Preferences.TEXT_ZOOM_NORMAL:  textZoom = WebSettings.TextSize.NORMAL; break;
491                case Preferences.TEXT_ZOOM_LARGE:   textZoom = WebSettings.TextSize.LARGER; break;
492                case Preferences.TEXT_ZOOM_HUGE:    textZoom = WebSettings.TextSize.LARGEST; break;
493                default:                            textZoom = WebSettings.TextSize.NORMAL; break;
494            }
495            mMessageContentView.getSettings().setTextSize(textZoom);
496        }
497        mAttachmentsScroll.scrollTo(0, 0);
498        mInviteScroll.scrollTo(0, 0);
499        mAttachments.removeAllViews();
500        mAttachments.setVisibility(View.GONE);
501        initContactStatusViews();
502    }
503
504    private void initContactStatusViews() {
505        mContactStatusState = CONTACT_STATUS_STATE_UNLOADED;
506        mQuickContactLookupUri = null;
507        mSenderPresenceView.setImageResource(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID);
508        showDefaultQuickContactBadgeImage();
509    }
510
511    private static final Drawable sEmptyBadgeDrawable = new ColorDrawable(0xFF808080);
512
513    private void showDefaultQuickContactBadgeImage() {
514        // STOPSHIP Show the default 'Andy' icon.
515        mFromBadge.setImageDrawable(sEmptyBadgeDrawable);
516    }
517
518    protected final void addTabFlags(int tabFlags) {
519        updateTabFlags(mTabFlags | tabFlags);
520    }
521
522    private final void clearTabFlags(int tabFlags) {
523        updateTabFlags(mTabFlags & ~tabFlags);
524    }
525
526    private void setAttachmentCount(int count) {
527        mAttachmentCount = count;
528        if (mAttachmentCount > 0) {
529            addTabFlags(TAB_FLAGS_HAS_ATTACHMENT);
530        } else {
531            clearTabFlags(TAB_FLAGS_HAS_ATTACHMENT);
532        }
533    }
534
535    private static void makeVisible(View v, boolean visible) {
536        v.setVisibility(visible ? View.VISIBLE : View.GONE);
537    }
538
539    /**
540     * Update the visual of the tabs.  (visibility, text, etc)
541     */
542    private void updateTabFlags(int tabFlags) {
543        mTabFlags = tabFlags;
544        mTabSection.setVisibility(tabFlags == 0 ? View.GONE : View.VISIBLE);
545        if (tabFlags == 0) {
546            return;
547        }
548        boolean messageTabVisible = (tabFlags & (TAB_FLAGS_HAS_INVITE | TAB_FLAGS_HAS_ATTACHMENT))
549                != 0;
550        makeVisible(mMessageTab, messageTabVisible);
551        makeVisible(mInviteTab, (tabFlags & TAB_FLAGS_HAS_INVITE) != 0);
552        makeVisible(mAttachmentTab, (tabFlags & TAB_FLAGS_HAS_ATTACHMENT) != 0);
553        makeVisible(mShowPicturesTab, (tabFlags & TAB_FLAGS_HAS_PICTURES) != 0);
554        mShowPicturesTab.setEnabled((tabFlags & TAB_FLAGS_PICTURE_LOADED) == 0);
555
556        mAttachmentTab.setText(mContext.getResources().getQuantityString(
557                R.plurals.message_view_show_attachments_action,
558                mAttachmentCount, mAttachmentCount));
559    }
560
561    /**
562     * Set the current tab.
563     *
564     * @param tab any of {@link #TAB_MESSAGE}, {@link #TAB_ATTACHMENT} or {@link #TAB_INVITE}.
565     */
566    private void setCurrentTab(int tab) {
567        mCurrentTab = tab;
568        makeVisible(mMessageContentView, tab == TAB_MESSAGE);
569        mMessageTab.setSelected(tab == TAB_MESSAGE);
570
571        makeVisible(mAttachmentsScroll, tab == TAB_ATTACHMENT);
572        mAttachmentTab.setSelected(tab == TAB_ATTACHMENT);
573
574        makeVisible(mInviteScroll, tab == TAB_INVITE);
575        mInviteTab.setSelected(tab == TAB_INVITE);
576    }
577
578    /**
579     * Handle clicks on sender, which shows {@link QuickContact} or prompts to add
580     * the sender as a contact.
581     */
582    private void onClickSender() {
583        final Address senderEmail = Address.unpackFirst(mMessage.mFrom);
584        if (senderEmail == null) return;
585
586        if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED) {
587            // Status not loaded yet.
588            mContactStatusState = CONTACT_STATUS_STATE_UNLOADED_TRIGGERED;
589            return;
590        }
591        if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED) {
592            return; // Already clicked, and waiting for the data.
593        }
594
595        if (mQuickContactLookupUri != null) {
596            QuickContact.showQuickContact(mContext, mFromBadge, mQuickContactLookupUri,
597                        QuickContact.MODE_LARGE, null);
598        } else {
599            // No matching contact, ask user to create one
600            final Uri mailUri = Uri.fromParts("mailto", senderEmail.getAddress(), null);
601            final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT,
602                    mailUri);
603
604            // Pass along full E-mail string for possible create dialog
605            intent.putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION,
606                    senderEmail.toString());
607
608            // Only provide personal name hint if we have one
609            final String senderPersonal = senderEmail.getPersonal();
610            if (!TextUtils.isEmpty(senderPersonal)) {
611                intent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal);
612            }
613            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
614
615            startActivity(intent);
616        }
617    }
618
619    private static class ContactStatusLoaderCallbacks
620            implements LoaderCallbacks<ContactStatusLoader.Result> {
621        private static final String BUNDLE_EMAIL_ADDRESS = "email";
622        private final MessageViewFragmentBase mFragment;
623
624        public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) {
625            mFragment = fragment;
626        }
627
628        public static Bundle createArguments(String emailAddress) {
629            Bundle b = new Bundle();
630            b.putString(BUNDLE_EMAIL_ADDRESS, emailAddress);
631            return b;
632        }
633
634        @Override
635        public Loader<ContactStatusLoader.Result> onCreateLoader(int id, Bundle args) {
636            return new ContactStatusLoader(mFragment.mContext,
637                    args.getString(BUNDLE_EMAIL_ADDRESS));
638        }
639
640        @Override
641        public void onLoadFinished(Loader<ContactStatusLoader.Result> loader,
642                ContactStatusLoader.Result result) {
643            boolean triggered =
644                    (mFragment.mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED);
645            mFragment.mContactStatusState = CONTACT_STATUS_STATE_LOADED;
646            mFragment.mQuickContactLookupUri = result.mLookupUri;
647            mFragment.mSenderPresenceView.setImageResource(result.mPresenceResId);
648            if (result.mPhoto != null) { // photo will be null if unknown.
649                mFragment.mFromBadge.setImageBitmap(result.mPhoto);
650            }
651            if (triggered) {
652                mFragment.onClickSender();
653            }
654        }
655    }
656
657    private void onSaveAttachment(AttachmentInfo info) {
658        if (!Utility.isExternalStorageMounted()) {
659            /*
660             * Abort early if there's no place to save the attachment. We don't want to spend
661             * the time downloading it and then abort.
662             */
663            Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved);
664            return;
665        }
666        Attachment attachment = Attachment.restoreAttachmentWithId(mContext, info.attachmentId);
667        Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, attachment.mId);
668
669        try {
670            File file = Utility.createUniqueFile(Environment.getExternalStorageDirectory(),
671                    attachment.mFileName);
672            Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(
673                    mContext.getContentResolver(), attachmentUri);
674            InputStream in = mContext.getContentResolver().openInputStream(contentUri);
675            OutputStream out = new FileOutputStream(file);
676            IOUtils.copy(in, out);
677            out.flush();
678            out.close();
679            in.close();
680
681            Utility.showToast(getActivity(), String.format(
682                    mContext.getString(R.string.message_view_status_attachment_saved),
683                    file.getName()));
684            MediaOpener.scanAndOpen(getActivity(), file);
685        } catch (IOException ioe) {
686            Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved);
687        }
688    }
689
690    private void onViewAttachment(AttachmentInfo info) {
691        Uri attachmentUri = AttachmentProvider.getAttachmentUri(mAccountId, info.attachmentId);
692        Uri contentUri = AttachmentProvider.resolveAttachmentIdToContentUri(
693                mContext.getContentResolver(), attachmentUri);
694        try {
695            Intent intent = new Intent(Intent.ACTION_VIEW);
696            intent.setData(contentUri);
697            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
698                            | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
699            startActivity(intent);
700        } catch (ActivityNotFoundException e) {
701            Utility.showToast(getActivity(), R.string.message_view_display_attachment_toast);
702            // TODO: Add a proper warning message (and lots of upstream cleanup to prevent
703            // it from happening) in the next release.
704        }
705    }
706
707    private void onLoadAttachment(final AttachmentInfo attachment) {
708        attachment.loadButton.setVisibility(View.GONE);
709        // If there's nothing in the download queue, we'll probably start right away so wait a
710        // second before showing the cancel button
711        if (AttachmentDownloadService.getQueueSize() == 0) {
712            // Set to invisible; if the button is still in this state one second from now, we'll
713            // assume the download won't start right away, and we make the cancel button visible
714            attachment.cancelButton.setVisibility(View.INVISIBLE);
715            // Create the timed task that will change the button state
716            new AsyncTask<Void, Void, Void>() {
717                @Override
718                protected Void doInBackground(Void... params) {
719                    try {
720                        Thread.sleep(1000L);
721                    } catch (InterruptedException e) { }
722                    return null;
723                }
724                @Override
725                protected void onPostExecute(Void result) {
726                    if (attachment.cancelButton.getVisibility() == View.INVISIBLE) {
727                        attachment.cancelButton.setVisibility(View.VISIBLE);
728                    }
729                }
730            }.execute();
731        } else {
732            attachment.cancelButton.setVisibility(View.VISIBLE);
733        }
734        ProgressBar bar = attachment.progressView;
735        bar.setVisibility(View.VISIBLE);
736        bar.setIndeterminate(true);
737        mController.loadAttachment(attachment.attachmentId, mMessageId, mAccountId);
738    }
739
740    private void onCancelAttachment(AttachmentInfo attachment) {
741        // Don't change button states if we couldn't cancel the download
742        if (AttachmentDownloadService.cancelQueuedAttachment(attachment.attachmentId)) {
743            attachment.loadButton.setVisibility(View.VISIBLE);
744            attachment.cancelButton.setVisibility(View.GONE);
745            ProgressBar bar = attachment.progressView;
746            bar.setVisibility(View.GONE);
747        }
748    }
749
750    /**
751     * Called by ControllerResults. Show the "View" and "Save" buttons; hide "Load"
752     *
753     * @param attachmentId the attachment that was just downloaded
754     */
755    private void doFinishLoadAttachment(long attachmentId) {
756        AttachmentInfo info = findAttachmentInfo(attachmentId);
757        if (info != null) {
758            info.loadButton.setVisibility(View.INVISIBLE);
759            info.loadButton.setVisibility(View.GONE);
760            if (!TextUtils.isEmpty(info.name)) {
761                info.saveButton.setVisibility(View.VISIBLE);
762            }
763            info.viewButton.setVisibility(View.VISIBLE);
764        }
765    }
766
767    private void onShowPicturesInHtml() {
768        if (mMessageContentView != null) {
769            mMessageContentView.getSettings().setBlockNetworkLoads(false);
770            if (mHtmlTextWebView != null) {
771                mMessageContentView.loadDataWithBaseURL("email://", mHtmlTextWebView,
772                                                        "text/html", "utf-8", null);
773            }
774            addTabFlags(TAB_FLAGS_PICTURE_LOADED);
775        }
776    }
777
778    @Override
779    public void onClick(View view) {
780        if (!isMessageOpen()) {
781            return; // Ignore.
782        }
783        switch (view.getId()) {
784            case R.id.from_name:
785            case R.id.from_address:
786            case R.id.badge:
787            case R.id.presence:
788                onClickSender();
789                break;
790            case R.id.load:
791                onLoadAttachment((AttachmentInfo) view.getTag());
792                break;
793            case R.id.save:
794                onSaveAttachment((AttachmentInfo) view.getTag());
795                break;
796            case R.id.view:
797                onViewAttachment((AttachmentInfo) view.getTag());
798                break;
799            case R.id.cancel:
800                onCancelAttachment((AttachmentInfo) view.getTag());
801                break;
802            case R.id.show_message:
803                setCurrentTab(TAB_MESSAGE);
804                break;
805            case R.id.show_invite:
806                setCurrentTab(TAB_INVITE);
807                break;
808            case R.id.show_attachments:
809                setCurrentTab(TAB_ATTACHMENT);
810                break;
811            case R.id.show_pictures:
812                onShowPicturesInHtml();
813                break;
814        }
815    }
816
817    /**
818     * Start loading contact photo and presence.
819     */
820    private void queryContactStatus() {
821        initContactStatusViews(); // Initialize the state, just in case.
822
823        // Find the sender email address, and start presence check.
824        if (mMessage != null) {
825            Address sender = Address.unpackFirst(mMessage.mFrom);
826            if (sender != null) {
827                String email = sender.getAddress();
828                if (email != null) {
829                    getLoaderManager().restartLoader(PHOTO_LOADER_ID,
830                            ContactStatusLoaderCallbacks.createArguments(email),
831                            new ContactStatusLoaderCallbacks(this));
832                }
833            }
834        }
835    }
836
837    /**
838     * Called by {@link LoadMessageTask} and {@link ReloadMessageTask} to load a message in a
839     * subclass specific way.
840     *
841     * NOTE This method is called on a worker thread!  Implementations must properly synchronize
842     * when accessing members.  This method may be called after or even at the same time as
843     * {@link #clearContent()}.
844     *
845     * @param activity the parent activity.  Subclass use it as a context, and to show a toast.
846     */
847    protected abstract Message openMessageSync(Activity activity);
848
849    /**
850     * Async task for loading a single message outside of the UI thread
851     */
852    private class LoadMessageTask extends AsyncTask<Void, Void, Message> {
853
854        private final boolean mOkToFetch;
855        private int mMailboxType;
856
857        /**
858         * Special constructor to cache some local info
859         */
860        public LoadMessageTask(boolean okToFetch) {
861            mOkToFetch = okToFetch;
862        }
863
864        @Override
865        protected Message doInBackground(Void... params) {
866            Activity activity = getActivity();
867            Message message = null;
868            if (activity != null) {
869                message = openMessageSync(activity);
870            }
871            if (message != null) {
872                mMailboxType = Mailbox.getMailboxType(mContext, message.mMailboxKey);
873                if (mMailboxType == -1) {
874                    message = null; // mailbox removed??
875                }
876            }
877            return message;
878        }
879
880        @Override
881        protected void onPostExecute(Message message) {
882            if (isCancelled()) {
883                return;
884            }
885            if (message == null) {
886                mCallback.onMessageNotExists();
887                return;
888            }
889            mMessageId = message.mId;
890
891            reloadUiFromMessage(message, mOkToFetch);
892            queryContactStatus();
893            onMessageShown(mMessageId, mMailboxType);
894        }
895    }
896
897    /**
898     * Kicked by {@link MessageObserver}.  Reload the message and update the views.
899     */
900    private class ReloadMessageTask extends AsyncTask<Void, Void, Message> {
901        @Override
902        protected Message doInBackground(Void... params) {
903            if (!isMessageSpecified()) { // just in case
904                return null;
905            }
906            Activity activity = getActivity();
907            if (activity == null) {
908                return null;
909            } else {
910                return openMessageSync(activity);
911            }
912        }
913
914        @Override
915        protected void onPostExecute(Message message) {
916            if (isCancelled()) {
917                return;
918            }
919            if (message == null || message.mMailboxKey != mMessage.mMailboxKey) {
920                // Message deleted or moved.
921                mCallback.onMessageNotExists();
922                return;
923            }
924            mMessage = message;
925            updateHeaderView(mMessage);
926        }
927    }
928
929    /**
930     * Called when a message is shown to the user.
931     */
932    protected void onMessageShown(long messageId, int mailboxType) {
933        mCallback.onMessageViewShown(mailboxType);
934    }
935
936    /**
937     * Called when the message body is loaded.
938     */
939    protected void onPostLoadBody() {
940    }
941
942    /**
943     * Async task for loading a single message body outside of the UI thread
944     */
945    private class LoadBodyTask extends AsyncTask<Void, Void, String[]> {
946
947        private long mId;
948        private boolean mErrorLoadingMessageBody;
949
950        /**
951         * Special constructor to cache some local info
952         */
953        public LoadBodyTask(long messageId) {
954            mId = messageId;
955        }
956
957        @Override
958        protected String[] doInBackground(Void... params) {
959            try {
960                String text = null;
961                String html = Body.restoreBodyHtmlWithMessageId(mContext, mId);
962                if (html == null) {
963                    text = Body.restoreBodyTextWithMessageId(mContext, mId);
964                }
965                return new String[] { text, html };
966            } catch (RuntimeException re) {
967                // This catches SQLiteException as well as other RTE's we've seen from the
968                // database calls, such as IllegalStateException
969                Log.d(Email.LOG_TAG, "Exception while loading message body: " + re.toString());
970                mErrorLoadingMessageBody = true;
971                return null;
972            }
973        }
974
975        @Override
976        protected void onPostExecute(String[] results) {
977            if (results == null || isCancelled()) {
978                if (mErrorLoadingMessageBody) {
979                    Utility.showToast(getActivity(), R.string.error_loading_message_body);
980                }
981                return;
982            }
983            reloadUiFromBody(results[0], results[1]);    // text, html
984            onPostLoadBody();
985        }
986    }
987
988    /**
989     * Async task for loading attachments
990     *
991     * Note:  This really should only be called when the message load is complete - or, we should
992     * leave open a listener so the attachments can fill in as they are discovered.  In either case,
993     * this implementation is incomplete, as it will fail to refresh properly if the message is
994     * partially loaded at this time.
995     */
996    private class LoadAttachmentsTask extends AsyncTask<Long, Void, Attachment[]> {
997        @Override
998        protected Attachment[] doInBackground(Long... messageIds) {
999            return Attachment.restoreAttachmentsWithMessageId(mContext, messageIds[0]);
1000        }
1001
1002        @Override
1003        protected void onPostExecute(Attachment[] attachments) {
1004            if (isCancelled() || attachments == null) {
1005                return;
1006            }
1007            boolean htmlChanged = false;
1008            setAttachmentCount(attachments.length);
1009            for (Attachment attachment : attachments) {
1010                if (mHtmlTextRaw != null && attachment.mContentId != null
1011                        && attachment.mContentUri != null) {
1012                    // for html body, replace CID for inline images
1013                    // Regexp which matches ' src="cid:contentId"'.
1014                    String contentIdRe =
1015                        "\\s+(?i)src=\"cid(?-i):\\Q" + attachment.mContentId + "\\E\"";
1016                    String srcContentUri = " src=\"" + attachment.mContentUri + "\"";
1017                    mHtmlTextRaw = mHtmlTextRaw.replaceAll(contentIdRe, srcContentUri);
1018                    htmlChanged = true;
1019                } else {
1020                    addAttachment(attachment);
1021                }
1022            }
1023            mHtmlTextWebView = mHtmlTextRaw;
1024            mHtmlTextRaw = null;
1025            if (htmlChanged && mMessageContentView != null) {
1026                mMessageContentView.loadDataWithBaseURL("email://", mHtmlTextWebView,
1027                                                        "text/html", "utf-8", null);
1028            }
1029        }
1030    }
1031
1032    private Bitmap getPreviewIcon(AttachmentInfo attachment) {
1033        try {
1034            return BitmapFactory.decodeStream(
1035                    mContext.getContentResolver().openInputStream(
1036                            AttachmentProvider.getAttachmentThumbnailUri(
1037                                    mAccountId, attachment.attachmentId,
1038                                    PREVIEW_ICON_WIDTH,
1039                                    PREVIEW_ICON_HEIGHT)));
1040        } catch (Exception e) {
1041            Log.d(Email.LOG_TAG, "Attachment preview failed with exception " + e.getMessage());
1042            return null;
1043        }
1044    }
1045
1046    private void updateAttachmentThumbnail(long attachmentId) {
1047        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1048            AttachmentInfo attachment = (AttachmentInfo) mAttachments.getChildAt(i).getTag();
1049            if (attachment.attachmentId == attachmentId) {
1050                Bitmap previewIcon = getPreviewIcon(attachment);
1051                if (previewIcon != null) {
1052                    attachment.iconView.setImageBitmap(previewIcon);
1053                }
1054                return;
1055            }
1056        }
1057    }
1058
1059    /**
1060     * Copy data from a cursor-refreshed attachment into the UI.  Called from UI thread.
1061     *
1062     * @param attachment A single attachment loaded from the provider
1063     */
1064    private void addAttachment(Attachment attachment) {
1065        AttachmentInfo attachmentInfo = new AttachmentInfo();
1066        attachmentInfo.size = attachment.mSize;
1067        attachmentInfo.contentType =
1068                AttachmentProvider.inferMimeType(attachment.mFileName, attachment.mMimeType);
1069        attachmentInfo.name = attachment.mFileName;
1070        attachmentInfo.attachmentId = attachment.mId;
1071
1072        LayoutInflater inflater = getActivity().getLayoutInflater();
1073        View view = inflater.inflate(R.layout.message_view_attachment, null);
1074
1075        TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name);
1076        TextView attachmentInfoView = (TextView)view.findViewById(R.id.attachment_info);
1077        ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon);
1078        Button attachmentView = (Button)view.findViewById(R.id.view);
1079        Button attachmentSave = (Button)view.findViewById(R.id.save);
1080        Button attachmentLoad = (Button)view.findViewById(R.id.load);
1081        Button attachmentCancel = (Button)view.findViewById(R.id.cancel);
1082        ProgressBar attachmentProgress = (ProgressBar)view.findViewById(R.id.progress);
1083
1084        // TODO: Remove this test (acceptable types = everything; unacceptable = nothing)
1085        if ((!MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
1086                Email.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
1087                || (MimeUtility.mimeTypeMatches(attachmentInfo.contentType,
1088                        Email.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
1089            attachmentView.setVisibility(View.GONE);
1090        }
1091
1092        if (attachmentInfo.size > Email.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
1093            attachmentView.setVisibility(View.GONE);
1094            attachmentSave.setVisibility(View.GONE);
1095        }
1096
1097        attachmentInfo.viewButton = attachmentView;
1098        attachmentInfo.saveButton = attachmentSave;
1099        attachmentInfo.loadButton = attachmentLoad;
1100        attachmentInfo.cancelButton = attachmentCancel;
1101        attachmentInfo.iconView = attachmentIcon;
1102        attachmentInfo.progressView = attachmentProgress;
1103
1104        // If the attachment is loaded, show 100% progress
1105        // Note that for POP3 messages, the user will only see "Open" and "Save" since the entire
1106        // message is loaded before being shown.
1107        if (Utility.attachmentExists(mContext, attachment)) {
1108            // Hide "Load", show "View" and "Save"
1109            attachmentProgress.setVisibility(View.VISIBLE);
1110            attachmentProgress.setProgress(100);
1111            attachmentSave.setVisibility(View.VISIBLE);
1112            attachmentView.setVisibility(View.VISIBLE);
1113            attachmentLoad.setVisibility(View.INVISIBLE);
1114            attachmentCancel.setVisibility(View.GONE);
1115        } else {
1116            // Show "Load"; hide "View" and "Save"
1117            attachmentSave.setVisibility(View.INVISIBLE);
1118            attachmentView.setVisibility(View.INVISIBLE);
1119            // If the attachment is queued, show the indeterminate progress bar.  From this point,.
1120            // any progress changes will cause this to be replaced by the normal progress bar
1121            if (AttachmentDownloadService.isAttachmentQueued(attachment.mId)){
1122                attachmentProgress.setVisibility(View.VISIBLE);
1123                attachmentProgress.setIndeterminate(true);
1124                attachmentLoad.setVisibility(View.GONE);
1125                attachmentCancel.setVisibility(View.VISIBLE);
1126            } else {
1127                attachmentLoad.setVisibility(View.VISIBLE);
1128                attachmentCancel.setVisibility(View.GONE);
1129            }
1130        }
1131
1132        // Don't enable the "save" button if we've got no place to save the file
1133        if (!Utility.isExternalStorageMounted()) {
1134            attachmentSave.setEnabled(false);
1135        }
1136
1137        view.setTag(attachmentInfo);
1138        attachmentView.setOnClickListener(this);
1139        attachmentView.setTag(attachmentInfo);
1140        attachmentSave.setOnClickListener(this);
1141        attachmentSave.setTag(attachmentInfo);
1142        attachmentLoad.setOnClickListener(this);
1143        attachmentLoad.setTag(attachmentInfo);
1144        attachmentCancel.setOnClickListener(this);
1145        attachmentCancel.setTag(attachmentInfo);
1146
1147        attachmentName.setText(attachmentInfo.name);
1148        attachmentInfoView.setText(Utility.formatSize(mContext, attachmentInfo.size));
1149
1150        Bitmap previewIcon = getPreviewIcon(attachmentInfo);
1151        if (previewIcon != null) {
1152            attachmentIcon.setImageBitmap(previewIcon);
1153        }
1154
1155        mAttachments.addView(view);
1156        mAttachments.setVisibility(View.VISIBLE);
1157    }
1158
1159    /**
1160     * Reload the UI from a provider cursor.  {@link LoadMessageTask#onPostExecute} calls it.
1161     *
1162     * Update the header views, and start loading the body.
1163     *
1164     * @param message A copy of the message loaded from the database
1165     * @param okToFetch If true, and message is not fully loaded, it's OK to fetch from
1166     * the network.  Use false to prevent looping here.
1167     */
1168    protected void reloadUiFromMessage(Message message, boolean okToFetch) {
1169        mMessage = message;
1170        mAccountId = message.mAccountKey;
1171
1172        mMessageObserver.register(ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId));
1173
1174        updateHeaderView(mMessage);
1175
1176        // Handle partially-loaded email, as follows:
1177        // 1. Check value of message.mFlagLoaded
1178        // 2. If != LOADED, ask controller to load it
1179        // 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask
1180        // 4. Else start the loader tasks right away (message already loaded)
1181        if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) {
1182            mControllerCallback.getWrappee().setWaitForLoadMessageId(message.mId);
1183            mController.loadMessageForView(message.mId);
1184        } else {
1185            mControllerCallback.getWrappee().setWaitForLoadMessageId(-1);
1186            // Ask for body
1187            mLoadBodyTask = new LoadBodyTask(message.mId);
1188            mLoadBodyTask.execute();
1189        }
1190    }
1191
1192    protected void updateHeaderView(Message message) {
1193        mSubjectView.setText(message.mSubject);
1194        final Address from = Address.unpackFirst(message.mFrom);
1195
1196        // Set sender address/display name
1197        // Note we set " " for empty field, so TextView's won't get squashed.
1198        // Otherwise their height will be 0, which breaks the layout.
1199        if (from != null) {
1200            final String fromFriendly = from.toFriendly();
1201            final String fromAddress = from.getAddress();
1202            mFromNameView.setText(fromFriendly);
1203            mFromAddressView.setText(fromFriendly.equals(fromAddress) ? " " : fromAddress);
1204        } else {
1205            mFromNameView.setText(" ");
1206            mFromAddressView.setText(" ");
1207        }
1208        Date date = new Date(message.mTimeStamp);
1209        // STOPSHIP Use the same format as MessageListItem uses
1210        mDateTimeView.setText(mTimeFormat.format(date));
1211        mToView.setText(Address.toFriendly(Address.unpack(message.mTo)));
1212        String friendlyCc = Address.toFriendly(Address.unpack(message.mCc));
1213        mCcView.setText(friendlyCc);
1214        mCcContainerView.setVisibility((friendlyCc != null) ? View.VISIBLE : View.GONE);
1215        String friendlyBcc = Address.toFriendly(Address.unpack(message.mBcc));
1216        mBccView.setText(friendlyBcc);
1217        mBccContainerView.setVisibility((friendlyBcc != null) ? View.VISIBLE : View.GONE);
1218    }
1219
1220    /**
1221     * Reload the body from the provider cursor.  This must only be called from the UI thread.
1222     *
1223     * @param bodyText text part
1224     * @param bodyHtml html part
1225     *
1226     * TODO deal with html vs text and many other issues <- WHAT DOES IT MEAN??
1227     */
1228    private void reloadUiFromBody(String bodyText, String bodyHtml) {
1229        String text = null;
1230        mHtmlTextRaw = null;
1231        boolean hasImages = false;
1232
1233        if (bodyHtml == null) {
1234            text = bodyText;
1235            /*
1236             * Convert the plain text to HTML
1237             */
1238            StringBuffer sb = new StringBuffer("<html><body>");
1239            if (text != null) {
1240                // Escape any inadvertent HTML in the text message
1241                text = EmailHtmlUtil.escapeCharacterToDisplay(text);
1242                // Find any embedded URL's and linkify
1243                Matcher m = Patterns.WEB_URL.matcher(text);
1244                while (m.find()) {
1245                    int start = m.start();
1246                    /*
1247                     * WEB_URL_PATTERN may match domain part of email address. To detect
1248                     * this false match, the character just before the matched string
1249                     * should not be '@'.
1250                     */
1251                    if (start == 0 || text.charAt(start - 1) != '@') {
1252                        String url = m.group();
1253                        Matcher proto = WEB_URL_PROTOCOL.matcher(url);
1254                        String link;
1255                        if (proto.find()) {
1256                            // This is work around to force URL protocol part be lower case,
1257                            // because WebView could follow only lower case protocol link.
1258                            link = proto.group().toLowerCase() + url.substring(proto.end());
1259                        } else {
1260                            // Patterns.WEB_URL matches URL without protocol part,
1261                            // so added default protocol to link.
1262                            link = "http://" + url;
1263                        }
1264                        String href = String.format("<a href=\"%s\">%s</a>", link, url);
1265                        m.appendReplacement(sb, href);
1266                    }
1267                    else {
1268                        m.appendReplacement(sb, "$0");
1269                    }
1270                }
1271                m.appendTail(sb);
1272            }
1273            sb.append("</body></html>");
1274            text = sb.toString();
1275        } else {
1276            text = bodyHtml;
1277            mHtmlTextRaw = bodyHtml;
1278            hasImages = IMG_TAG_START_REGEX.matcher(text).find();
1279        }
1280
1281        // TODO this is not really accurate.
1282        // - Images aren't the only network resources.  (e.g. CSS)
1283        // - If images are attached to the email and small enough, we download them at once,
1284        //   and won't need network access when they're shown.
1285        if (hasImages) {
1286            addTabFlags(TAB_FLAGS_HAS_PICTURES);
1287        }
1288        if (mMessageContentView != null) {
1289            mMessageContentView.loadDataWithBaseURL("email://", text, "text/html", "utf-8", null);
1290        }
1291
1292        // Ask for attachments after body
1293        mLoadAttachmentsTask = new LoadAttachmentsTask();
1294        mLoadAttachmentsTask.execute(mMessage.mId);
1295
1296        mIsMessageLoadedForTest = true;
1297    }
1298
1299    /**
1300     * Overrides for WebView behaviors.
1301     */
1302    private class CustomWebViewClient extends WebViewClient {
1303        @Override
1304        public boolean shouldOverrideUrlLoading(WebView view, String url) {
1305            return mCallback.onUrlInMessageClicked(url);
1306        }
1307    }
1308
1309    private View findAttachmentView(long attachmentId) {
1310        for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
1311            View view = mAttachments.getChildAt(i);
1312            AttachmentInfo attachment = (AttachmentInfo) view.getTag();
1313            if (attachment.attachmentId == attachmentId) {
1314                return view;
1315            }
1316        }
1317        return null;
1318    }
1319
1320    private AttachmentInfo findAttachmentInfo(long attachmentId) {
1321        View view = findAttachmentView(attachmentId);
1322        if (view != null) {
1323            return (AttachmentInfo)view.getTag();
1324        }
1325        return null;
1326    }
1327
1328    /**
1329     * Controller results listener.  We wrap it with {@link ControllerResultUiThreadWrapper},
1330     * so all methods are called on the UI thread.
1331     */
1332    private class ControllerResults extends Controller.Result {
1333        private long mWaitForLoadMessageId;
1334
1335        public void setWaitForLoadMessageId(long messageId) {
1336            mWaitForLoadMessageId = messageId;
1337        }
1338
1339        @Override
1340        public void loadMessageForViewCallback(MessagingException result, long messageId,
1341                int progress) {
1342            if (messageId != mWaitForLoadMessageId) {
1343                // We are not waiting for this message to load, so exit quickly
1344                return;
1345            }
1346            if (result == null) {
1347                switch (progress) {
1348                    case 0:
1349                        mCallback.onLoadMessageStarted();
1350                        loadBodyContent("file:///android_asset/loading.html");
1351                        break;
1352                    case 100:
1353                        mWaitForLoadMessageId = -1;
1354                        mCallback.onLoadMessageFinished();
1355                        // reload UI and reload everything else too
1356                        // pass false to LoadMessageTask to prevent looping here
1357                        cancelAllTasks();
1358                        mLoadMessageTask = new LoadMessageTask(false);
1359                        mLoadMessageTask.execute();
1360                        break;
1361                    default:
1362                        // do nothing - we don't have a progress bar at this time
1363                        break;
1364                }
1365            } else {
1366                mWaitForLoadMessageId = -1;
1367                String error = mContext.getString(R.string.status_network_error);
1368                mCallback.onLoadMessageError(error);
1369                loadBodyContent("file:///android_asset/empty.html");
1370            }
1371        }
1372
1373        private void loadBodyContent(String uri) {
1374            if (mMessageContentView != null) {
1375                mMessageContentView.loadUrl(uri);
1376            }
1377        }
1378
1379        @Override
1380        public void loadAttachmentCallback(MessagingException result, long messageId,
1381                long attachmentId, int progress) {
1382            if (messageId == mMessageId) {
1383                if (result == null) {
1384                    showAttachmentProgress(attachmentId, progress);
1385                    switch (progress) {
1386                        case 100:
1387                            updateAttachmentThumbnail(attachmentId);
1388                            doFinishLoadAttachment(attachmentId);
1389                            break;
1390                        default:
1391                            // do nothing - we don't have a progress bar at this time
1392                            break;
1393                    }
1394                } else {
1395                    AttachmentInfo attachment = findAttachmentInfo(attachmentId);
1396                    attachment.cancelButton.setVisibility(View.GONE);
1397                    attachment.loadButton.setVisibility(View.VISIBLE);
1398                    attachment.progressView.setVisibility(View.INVISIBLE);
1399
1400                    final String error;
1401                    if (result.getCause() instanceof IOException) {
1402                        error = mContext.getString(R.string.status_network_error);
1403                    } else {
1404                        error = mContext.getString(
1405                                R.string.message_view_load_attachment_failed_toast,
1406                                attachment.name);
1407                    }
1408                    mCallback.onLoadMessageError(error);
1409                }
1410            }
1411        }
1412
1413        private void showAttachmentProgress(long attachmentId, int progress) {
1414            AttachmentInfo attachment = findAttachmentInfo(attachmentId);
1415            if (attachment != null) {
1416                ProgressBar bar = attachment.progressView;
1417                if (progress == 0) {
1418                    // When the download starts, we can get rid of the indeterminate bar
1419                    bar.setVisibility(View.VISIBLE);
1420                    bar.setIndeterminate(false);
1421                    // And we're not implementing stop of in-progress downloads
1422                    attachment.cancelButton.setVisibility(View.GONE);
1423                }
1424                bar.setProgress(progress);
1425            }
1426        }
1427    }
1428
1429    /**
1430     * Class to detect update on the current message (e.g. toggle star).  When it gets content
1431     * change notifications, it kicks {@link ReloadMessageTask}.
1432     *
1433     * TODO Use the new Throttle class.
1434     */
1435    private class MessageObserver extends ContentObserver implements Runnable {
1436        private final Throttle mThrottle;
1437        private final ContentResolver mContentResolver;
1438
1439        private boolean mRegistered;
1440
1441        public MessageObserver(Handler handler, Context context) {
1442            super(handler);
1443            mContentResolver = context.getContentResolver();
1444            mThrottle = new Throttle("MessageObserver", this, handler);
1445        }
1446
1447        public void unregister() {
1448            if (!mRegistered) {
1449                return;
1450            }
1451            mThrottle.cancelScheduledCallback();
1452            mContentResolver.unregisterContentObserver(this);
1453            mRegistered = false;
1454        }
1455
1456        public void register(Uri notifyUri) {
1457            unregister();
1458            mContentResolver.registerContentObserver(notifyUri, true, this);
1459            mRegistered = true;
1460        }
1461
1462        @Override
1463        public boolean deliverSelfNotifications() {
1464            return true;
1465        }
1466
1467        @Override
1468        public void onChange(boolean selfChange) {
1469            mThrottle.onEvent();
1470        }
1471
1472        /**
1473         * This method is delay-called by {@link Throttle} on the UI thread.  Need to make
1474         * sure if the fragment is still valid.  (i.e. don't reload if clearContent() has been
1475         * called.)
1476         */
1477        @Override
1478        public void run() {
1479            if (!isMessageSpecified()) {
1480                return;
1481            }
1482            Utility.cancelTaskInterrupt(mReloadMessageTask);
1483            mReloadMessageTask = new ReloadMessageTask();
1484            mReloadMessageTask.execute();
1485        }
1486    }
1487
1488    public boolean isMessageLoadedForTest() {
1489        return mIsMessageLoadedForTest;
1490    }
1491
1492    public void clearIsMessageLoadedForTest() {
1493        mIsMessageLoadedForTest = true;
1494    }
1495}
1496