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