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