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