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