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