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