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