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