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