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