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