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