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