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