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