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