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