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