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