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