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