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