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