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