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