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