• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onUrlInMessageClicked(String url)215         public boolean onUrlInMessageClicked(String url);
216 
217         /**
218          * Called when the message specified doesn't exist, or is deleted/moved.
219          */
onMessageNotExists()220         public void onMessageNotExists();
221 
222         /** Called when it starts loading a message. */
onLoadMessageStarted()223         public void onLoadMessageStarted();
224 
225         /** Called when it successfully finishes loading a message. */
onLoadMessageFinished()226         public void onLoadMessageFinished();
227 
228         /** Called when an error occurred during loading a message. */
onLoadMessageError(String errorMessage)229         public void onLoadMessageError(String errorMessage);
230     }
231 
232     public static class EmptyCallback implements Callback {
233         public static final Callback INSTANCE = new EmptyCallback();
onLoadMessageError(String errorMessage)234         @Override public void onLoadMessageError(String errorMessage) {}
onLoadMessageFinished()235         @Override public void onLoadMessageFinished() {}
onLoadMessageStarted()236         @Override public void onLoadMessageStarted() {}
onMessageNotExists()237         @Override public void onMessageNotExists() {}
238         @Override
onUrlInMessageClicked(String url)239         public boolean onUrlInMessageClicked(String url) {
240             return false;
241         }
242     }
243 
244     private Callback mCallback = EmptyCallback.INSTANCE;
245 
246     @Override
onAttach(Activity activity)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
onCreate(Bundle savedInstanceState)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
onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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
onActivityCreated(Bundle savedInstanceState)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
onStart()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
onResume()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
onPause()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
onStop()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
onDestroyView()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 
cleanupDetachedViews()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
onDestroy()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
onDetach()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
onSaveInstanceState(Bundle outState)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 
restoreInstanceState(Bundle state)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 
setCallback(Callback callback)449     public void setCallback(Callback callback) {
450         mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
451     }
452 
cancelAllTasks()453     private void cancelAllTasks() {
454         mMessageObserver.unregister();
455         mTaskTracker.cancellAllInterrupt();
456     }
457 
getController()458     protected final Controller getController() {
459         return mController;
460     }
461 
getCallback()462     protected final Callback getCallback() {
463         return mCallback;
464     }
465 
getMessage()466     public final Message getMessage() {
467         return mMessage;
468     }
469 
isMessageOpen()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      */
getAccountId()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      */
showContent(boolean showContent, boolean showProgressWhenHidden)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.
resetView()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      */
getWebViewZoom()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 
initContactStatusViews()529     private void initContactStatusViews() {
530         mContactStatusState = CONTACT_STATUS_STATE_UNLOADED;
531         mQuickContactLookupUri = null;
532         showDefaultQuickContactBadgeImage();
533     }
534 
showDefaultQuickContactBadgeImage()535     private void showDefaultQuickContactBadgeImage() {
536         mFromBadge.setImageResource(R.drawable.ic_contact_picture);
537     }
538 
addTabFlags(int tabFlags)539     protected final void addTabFlags(int tabFlags) {
540         updateTabs(mTabFlags | tabFlags);
541     }
542 
clearTabFlags(int tabFlags)543     private final void clearTabFlags(int tabFlags) {
544         updateTabs(mTabFlags & ~tabFlags);
545     }
546 
setAttachmentCount(int count)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 
makeVisible(View v, boolean visible)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 
isVisible(View v)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      */
updateTabs(int tabFlags)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      */
setCurrentTab(int tab)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 
getTabViewForFlag(int tabFlag)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 
getTabContentViewForFlag(int tabFlag)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 
blockNetworkLoads(boolean block)647     private void blockNetworkLoads(boolean block) {
648         if (mMessageContentView != null) {
649             mMessageContentView.getSettings().setBlockNetworkLoads(block);
650         }
651     }
652 
setMessageHtml(String html)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      */
onClickSender()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 
ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment)705         public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) {
706             mFragment = fragment;
707         }
708 
createArguments(String emailAddress)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
onCreateLoader(int id, Bundle args)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
onLoadFinished(Loader<ContactStatusLoader.Result> loader, ContactStatusLoader.Result result)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
onLoaderReset(Loader<ContactStatusLoader.Result> loader)744         public void onLoaderReset(Loader<ContactStatusLoader.Result> loader) {
745         }
746     }
747 
onSaveAttachment(MessageViewAttachmentInfo info)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 
performAttachmentSave(MessageViewAttachmentInfo info)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 
onOpenAttachment(MessageViewAttachmentInfo info)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 
onInfoAttachment(final MessageViewAttachmentInfo attachment)847     private void onInfoAttachment(final MessageViewAttachmentInfo attachment) {
848         AttachmentInfoDialog dialog =
849             AttachmentInfoDialog.newInstance(getActivity(), attachment.mDenyFlags);
850         dialog.show(getActivity().getFragmentManager(), null);
851     }
852 
onLoadAttachment(final MessageViewAttachmentInfo attachment)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 
onCancelAttachment(MessageViewAttachmentInfo attachment)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      */
doFinishLoadAttachment(long attachmentId)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 
showPicturesInHtml()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 
showDetails()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 
hideDetails()943     private void hideDetails() {
944         mDetailsCollapsed.setVisibility(View.VISIBLE);
945         mDetailsExpanded.setVisibility(View.GONE);
946     }
947 
setDetailsRow(View root, String text, int textViewId, int rowViewId)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
onClick(View view)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      */
queryContactStatus()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      */
openMessageSync(Activity activity)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      */
reloadMessageSync(Activity activity)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          */
LoadMessageTask(boolean okToFetch)1054         public LoadMessageTask(boolean okToFetch) {
1055             super(mTaskTracker);
1056             mOkToFetch = okToFetch;
1057         }
1058 
1059         @Override
doInBackground(Void... params)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
onSuccess(Message message)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> {
ReloadMessageTask()1095         public ReloadMessageTask() {
1096             super(mTaskTracker);
1097         }
1098 
1099         @Override
doInBackground(Void... params)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
onSuccess(Message message)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      */
onMessageShown(long messageId, Mailbox mailbox)1124     protected void onMessageShown(long messageId, Mailbox mailbox) {
1125     }
1126 
1127     /**
1128      * Called when the message body is loaded.
1129      */
onPostLoadBody()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          */
LoadBodyTask(long messageId, boolean autoShowPictures)1145         public LoadBodyTask(long messageId, boolean autoShowPictures) {
1146             super(mTaskTracker);
1147             mId = messageId;
1148             mAutoShowPictures = autoShowPictures;
1149         }
1150 
1151         @Override
doInBackground(Void... params)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
onSuccess(String[] results)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[]> {
LoadAttachmentsTask()1192         public LoadAttachmentsTask() {
1193             super(mTaskTracker);
1194         }
1195 
1196         @Override
doInBackground(Long... messageIds)1197         protected Attachment[] doInBackground(Long... messageIds) {
1198             return Attachment.restoreAttachmentsWithMessageId(mContext, messageIds[0]);
1199         }
1200 
1201         @Override
onSuccess(Attachment[] attachments)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 
getPreviewIcon(Context context, AttachmentInfo attachment)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 
MessageViewAttachmentInfo(Context context, Attachment attachment, ProgressBar progressView)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          */
MessageViewAttachmentInfo(Context context, MessageViewAttachmentInfo oldInfo)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 
hideProgress()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 
showProgress(int progress)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 
showProgressIndeterminate()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          */
isFileSaved()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 
setSavedPath(String path)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          */
getSavedPath()1357         private String getSavedPath() {
1358             return sSavedFileInfos.get(this);
1359         }
1360 
1361         @Override
getUriForIntent(Context context, long accountId)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      */
updateAttachmentTab()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      */
updateAttachmentButtons(MessageViewAttachmentInfo attachmentInfo)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      */
addAttachment(Attachment attachment)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 
findAttachmentInfoFromView(long attachmentId)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      */
reloadUiFromMessage(Message message, boolean okToFetch)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 
updateHeaderView(Message message)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      */
formatDate(long millis, boolean withYear)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      */
reloadUiFromBody(String bodyText, String bodyHtml, boolean autoShowPictures)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
shouldOverrideUrlLoading(WebView view, String url)1730         public boolean shouldOverrideUrlLoading(WebView view, String url) {
1731             return mCallback.onUrlInMessageClicked(url);
1732         }
1733     }
1734 
findAttachmentView(long attachmentId)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 
findAttachmentInfo(long attachmentId)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 
setWaitForLoadMessageId(long messageId)1761         public void setWaitForLoadMessageId(long messageId) {
1762             mWaitForLoadMessageId = messageId;
1763         }
1764 
1765         @Override
loadMessageForViewCallback(MessagingException result, long accountId, long messageId, int progress)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
loadAttachmentCallback(MessagingException result, long accountId, long messageId, long attachmentId, int progress)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 
showAttachmentProgress(long attachmentId, int progress)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 
MessageObserver(Handler handler, Context context)1863         public MessageObserver(Handler handler, Context context) {
1864             super(handler);
1865             mContentResolver = context.getContentResolver();
1866             mThrottle = new Throttle("MessageObserver", this, handler);
1867         }
1868 
unregister()1869         public void unregister() {
1870             if (!mRegistered) {
1871                 return;
1872             }
1873             mThrottle.cancelScheduledCallback();
1874             mContentResolver.unregisterContentObserver(this);
1875             mRegistered = false;
1876         }
1877 
register(Uri notifyUri)1878         public void register(Uri notifyUri) {
1879             unregister();
1880             mContentResolver.registerContentObserver(notifyUri, true, this);
1881             mRegistered = true;
1882         }
1883 
1884         @Override
deliverSelfNotifications()1885         public boolean deliverSelfNotifications() {
1886             return true;
1887         }
1888 
1889         @Override
onChange(boolean selfChange)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
run()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 
updatePreviewIcon(MessageViewAttachmentInfo attachmentInfo)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 
UpdatePreviewIconTask(MessageViewAttachmentInfo attachmentInfo)1915         public UpdatePreviewIconTask(MessageViewAttachmentInfo attachmentInfo) {
1916             super(mTaskTracker);
1917             mContext = getActivity();
1918             mAttachmentInfo = attachmentInfo;
1919         }
1920 
1921         @Override
doInBackground(Void... params)1922         protected Bitmap doInBackground(Void... params) {
1923             return getPreviewIcon(mContext, mAttachmentInfo);
1924         }
1925 
1926         @Override
onSuccess(Bitmap result)1927         protected void onSuccess(Bitmap result) {
1928             if (result == null) {
1929                 return;
1930             }
1931             mAttachmentInfo.iconView.setImageBitmap(result);
1932         }
1933     }
1934 
shouldShowImagesFor(String senderEmail)1935     private boolean shouldShowImagesFor(String senderEmail) {
1936         return Preferences.getPreferences(getActivity()).shouldShowImagesFor(senderEmail);
1937     }
1938 
setShowImagesForSender()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 
isMessageLoadedForTest()1954     public boolean isMessageLoadedForTest() {
1955         return mIsMessageLoadedForTest;
1956     }
1957 
clearIsMessageLoadedForTest()1958     public void clearIsMessageLoadedForTest() {
1959         mIsMessageLoadedForTest = true;
1960     }
1961 }
1962