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