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