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