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