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