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