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