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