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