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