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