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