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