MessageViewFragmentBase.java revision 01bd33f318f03d5496b4d252e3a536856405f73c
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.AttachmentInfo; 20import com.android.email.Controller; 21import com.android.email.ControllerResultUiThreadWrapper; 22import com.android.email.Email; 23import com.android.email.Preferences; 24import com.android.email.R; 25import com.android.email.Throttle; 26import com.android.email.mail.internet.EmailHtmlUtil; 27import com.android.email.service.AttachmentDownloadService; 28import com.android.emailcommon.Logging; 29import com.android.emailcommon.mail.Address; 30import com.android.emailcommon.mail.MessagingException; 31import com.android.emailcommon.provider.EmailContent.Attachment; 32import com.android.emailcommon.provider.EmailContent.Body; 33import com.android.emailcommon.provider.EmailContent.Message; 34import com.android.emailcommon.provider.Mailbox; 35import com.android.emailcommon.utility.AttachmentUtilities; 36import com.android.emailcommon.utility.EmailAsyncTask; 37import com.android.emailcommon.utility.Utility; 38import com.google.common.collect.Maps; 39 40import org.apache.commons.io.IOUtils; 41 42import android.app.Activity; 43import android.app.DownloadManager; 44import android.app.Fragment; 45import android.app.LoaderManager.LoaderCallbacks; 46import android.content.ActivityNotFoundException; 47import android.content.ContentResolver; 48import android.content.ContentUris; 49import android.content.Context; 50import android.content.Intent; 51import android.content.Loader; 52import android.content.pm.PackageManager; 53import android.content.res.Resources; 54import android.database.ContentObserver; 55import android.graphics.Bitmap; 56import android.graphics.BitmapFactory; 57import android.media.MediaScannerConnection; 58import android.net.Uri; 59import android.os.Bundle; 60import android.os.Environment; 61import android.os.Handler; 62import android.provider.ContactsContract; 63import android.provider.ContactsContract.QuickContact; 64import android.text.SpannableStringBuilder; 65import android.text.TextUtils; 66import android.text.format.DateUtils; 67import android.util.Log; 68import android.util.Patterns; 69import android.view.LayoutInflater; 70import android.view.View; 71import android.view.ViewGroup; 72import android.webkit.WebSettings; 73import android.webkit.WebView; 74import android.webkit.WebViewClient; 75import android.widget.Button; 76import android.widget.ImageView; 77import android.widget.LinearLayout; 78import android.widget.ProgressBar; 79import android.widget.TextView; 80 81import java.io.File; 82import java.io.FileOutputStream; 83import java.io.IOException; 84import java.io.InputStream; 85import java.io.OutputStream; 86import java.util.Formatter; 87import java.util.Map; 88import java.util.regex.Matcher; 89import java.util.regex.Pattern; 90 91// TODO Better handling of config changes. 92// - Retain the content; don't kick 3 async tasks every time 93 94/** 95 * Base class for {@link MessageViewFragment} and {@link MessageFileViewFragment}. 96 * 97 * See {@link MessageViewBase} for the class relation diagram. 98 */ 99public abstract class MessageViewFragmentBase extends Fragment implements View.OnClickListener { 100 private static final String BUNDLE_KEY_CURRENT_TAB = "MessageViewFragmentBase.currentTab"; 101 private static final String BUNDLE_KEY_PICTURE_LOADED = "MessageViewFragmentBase.pictureLoaded"; 102 private static final int PHOTO_LOADER_ID = 1; 103 protected Context mContext; 104 105 // Regex that matches start of img tag. '<(?i)img\s+'. 106 private static final Pattern IMG_TAG_START_REGEX = Pattern.compile("<(?i)img\\s+"); 107 // Regex that matches Web URL protocol part as case insensitive. 108 private static final Pattern WEB_URL_PROTOCOL = Pattern.compile("(?i)http|https://"); 109 110 private static int PREVIEW_ICON_WIDTH = 62; 111 private static int PREVIEW_ICON_HEIGHT = 62; 112 113 private TextView mSubjectView; 114 private TextView mFromNameView; 115 private TextView mFromAddressView; 116 private TextView mDateTimeView; 117 private TextView mAddressesView; 118 private WebView mMessageContentView; 119 private LinearLayout mAttachments; 120 private View mTabSection; 121 private ImageView mFromBadge; 122 private ImageView mSenderPresenceView; 123 private View mMainView; 124 private View mLoadingProgress; 125 private Button mShowDetailsButton; 126 127 private TextView mMessageTab; 128 private TextView mAttachmentTab; 129 private TextView mInviteTab; 130 // It is not really a tab, but looks like one of them. 131 private TextView mShowPicturesTab; 132 133 private View mAttachmentsScroll; 134 private View mInviteScroll; 135 136 private long mAccountId = -1; 137 private long mMessageId = -1; 138 private Message mMessage; 139 140 private Controller mController; 141 private ControllerResultUiThreadWrapper<ControllerResults> mControllerCallback; 142 143 // contains the HTML body. Is used by LoadAttachmentTask to display inline images. 144 // is null most of the time, is used transiently to pass info to LoadAttachementTask 145 private String mHtmlTextRaw; 146 147 // contains the HTML content as set in WebView. 148 private String mHtmlTextWebView; 149 150 private boolean mResumed; 151 152 private boolean mIsMessageLoadedForTest; 153 154 private MessageObserver mMessageObserver; 155 156 private static final int CONTACT_STATUS_STATE_UNLOADED = 0; 157 private static final int CONTACT_STATUS_STATE_UNLOADED_TRIGGERED = 1; 158 private static final int CONTACT_STATUS_STATE_LOADED = 2; 159 160 private int mContactStatusState; 161 private Uri mQuickContactLookupUri; 162 163 /** Flag for {@link #mTabFlags}: Message has attachment(s) */ 164 protected static final int TAB_FLAGS_HAS_ATTACHMENT = 1; 165 166 /** 167 * Flag for {@link #mTabFlags}: Message contains invite. This flag is only set by 168 * {@link MessageViewFragment}. 169 */ 170 protected static final int TAB_FLAGS_HAS_INVITE = 2; 171 172 /** Flag for {@link #mTabFlags}: Message contains pictures */ 173 protected static final int TAB_FLAGS_HAS_PICTURES = 4; 174 175 /** Flag for {@link #mTabFlags}: "Show pictures" has already been pressed */ 176 protected static final int TAB_FLAGS_PICTURE_LOADED = 8; 177 178 /** 179 * Flags to control the tabs. 180 * @see #updateTabs(int) 181 */ 182 private int mTabFlags; 183 184 /** # of attachments in the current message */ 185 private int mAttachmentCount; 186 187 // Use (random) large values, to avoid confusion with TAB_FLAGS_* 188 protected static final int TAB_MESSAGE = 101; 189 protected static final int TAB_INVITE = 102; 190 protected static final int TAB_ATTACHMENT = 103; 191 private static final int TAB_NONE = 0; 192 193 /** Current tab */ 194 private int mCurrentTab = TAB_NONE; 195 /** 196 * Tab that was selected in the previous activity instance. 197 * Used to restore the current tab after screen rotation. 198 */ 199 private int mRestoredTab = TAB_NONE; 200 201 private boolean mRestoredPictureLoaded; 202 203 private final EmailAsyncTask.Tracker mTaskTracker = new EmailAsyncTask.Tracker(); 204 205 /** 206 * Zoom scales for webview. Values correspond to {@link Preferences#TEXT_ZOOM_TINY}.. 207 * {@link Preferences#TEXT_ZOOM_HUGE}. 208 */ 209 private static final float[] ZOOM_SCALE_ARRAY = new float[] {0.8f, 0.9f, 1.0f, 1.2f, 1.5f}; 210 211 public interface Callback { 212 /** Called when the fragment is about to show up, or show a different message. */ 213 public void onMessageViewShown(int mailboxType); 214 215 /** Called when the fragment is about to be destroyed. */ 216 public void onMessageViewGone(); 217 218 /** 219 * Called when a link in a message is clicked. 220 * 221 * @param url link url that's clicked. 222 * @return true if handled, false otherwise. 223 */ 224 public boolean onUrlInMessageClicked(String url); 225 226 /** 227 * Called when the message specified doesn't exist, or is deleted/moved. 228 */ 229 public void onMessageNotExists(); 230 231 /** Called when it starts loading a message. */ 232 public void onLoadMessageStarted(); 233 234 /** Called when it successfully finishes loading a message. */ 235 public void onLoadMessageFinished(); 236 237 /** Called when an error occurred during loading a message. */ 238 public void onLoadMessageError(String errorMessage); 239 } 240 241 public static class EmptyCallback implements Callback { 242 public static final Callback INSTANCE = new EmptyCallback(); 243 @Override public void onMessageViewShown(int mailboxType) {} 244 @Override public void onMessageViewGone() {} 245 @Override public void onLoadMessageError(String errorMessage) {} 246 @Override public void onLoadMessageFinished() {} 247 @Override public void onLoadMessageStarted() {} 248 @Override public void onMessageNotExists() {} 249 @Override 250 public boolean onUrlInMessageClicked(String url) { 251 return false; 252 } 253 } 254 255 private Callback mCallback = EmptyCallback.INSTANCE; 256 257 @Override 258 public void onCreate(Bundle savedInstanceState) { 259 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 260 Log.d(Logging.LOG_TAG, this + " onCreate"); 261 } 262 super.onCreate(savedInstanceState); 263 264 mContext = getActivity().getApplicationContext(); 265 266 mControllerCallback = new ControllerResultUiThreadWrapper<ControllerResults>( 267 new Handler(), new ControllerResults()); 268 269 mController = Controller.getInstance(mContext); 270 mMessageObserver = new MessageObserver(new Handler(), mContext); 271 272 if (savedInstanceState != null) { 273 restoreInstanceState(savedInstanceState); 274 } 275 } 276 277 @Override 278 public View onCreateView( 279 LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 280 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 281 Log.d(Logging.LOG_TAG, this + " onCreateView"); 282 } 283 final View view = inflater.inflate(R.layout.message_view_fragment, container, false); 284 285 mSubjectView = (TextView) UiUtilities.getView(view, R.id.subject); 286 mFromNameView = (TextView) UiUtilities.getView(view, R.id.from_name); 287 mFromAddressView = (TextView) UiUtilities.getView(view, R.id.from_address); 288 mAddressesView = (TextView) UiUtilities.getView(view, R.id.addresses); 289 mDateTimeView = (TextView) UiUtilities.getView(view, R.id.datetime); 290 mMessageContentView = (WebView) UiUtilities.getView(view, R.id.message_content); 291 mAttachments = (LinearLayout) UiUtilities.getView(view, R.id.attachments); 292 mTabSection = UiUtilities.getView(view, R.id.message_tabs_section); 293 mFromBadge = (ImageView) UiUtilities.getView(view, R.id.badge); 294 mSenderPresenceView = (ImageView) UiUtilities.getView(view, R.id.presence); 295 mMainView = UiUtilities.getView(view, R.id.main_panel); 296 mLoadingProgress = UiUtilities.getView(view, R.id.loading_progress); 297 mShowDetailsButton = (Button) UiUtilities.getView(view, R.id.show_details); 298 299 mFromNameView.setOnClickListener(this); 300 mFromAddressView.setOnClickListener(this); 301 mFromBadge.setOnClickListener(this); 302 mSenderPresenceView.setOnClickListener(this); 303 304 mMessageTab = (TextView) UiUtilities.getView(view, R.id.show_message); 305 mAttachmentTab = (TextView) UiUtilities.getView(view, R.id.show_attachments); 306 mShowPicturesTab = (TextView) UiUtilities.getView(view, R.id.show_pictures); 307 // Invite is only used in MessageViewFragment, but visibility is controlled here. 308 mInviteTab = (TextView) UiUtilities.getView(view, R.id.show_invite); 309 310 mMessageTab.setOnClickListener(this); 311 mAttachmentTab.setOnClickListener(this); 312 mShowPicturesTab.setOnClickListener(this); 313 mInviteTab.setOnClickListener(this); 314 mShowDetailsButton.setOnClickListener(this); 315 316 mAttachmentsScroll = UiUtilities.getView(view, R.id.attachments_scroll); 317 mInviteScroll = UiUtilities.getView(view, R.id.invite_scroll); 318 319 WebSettings webSettings = mMessageContentView.getSettings(); 320 boolean supportMultiTouch = mContext.getPackageManager() 321 .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH); 322 webSettings.setDisplayZoomControls(!supportMultiTouch); 323 webSettings.setSupportZoom(true); 324 webSettings.setBuiltInZoomControls(true); 325 mMessageContentView.setWebViewClient(new CustomWebViewClient()); 326 return view; 327 } 328 329 @Override 330 public void onActivityCreated(Bundle savedInstanceState) { 331 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 332 Log.d(Logging.LOG_TAG, this + " onActivityCreated"); 333 } 334 super.onActivityCreated(savedInstanceState); 335 mController.addResultCallback(mControllerCallback); 336 337 resetView(); 338 new LoadMessageTask(true).executeParallel(); 339 } 340 341 @Override 342 public void onStart() { 343 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 344 Log.d(Logging.LOG_TAG, this + " onStart"); 345 } 346 super.onStart(); 347 } 348 349 @Override 350 public void onResume() { 351 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 352 Log.d(Logging.LOG_TAG, this + " onResume"); 353 } 354 super.onResume(); 355 356 mResumed = true; 357 358 // We might have comes back from other full-screen activities. If so, we need to update 359 // the attachment tab as system settings may have been updated that affect which 360 // options are available to the user. 361 updateAttachmentTab(); 362 } 363 364 @Override 365 public void onPause() { 366 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 367 Log.d(Logging.LOG_TAG, this + " onPause"); 368 } 369 mResumed = false; 370 super.onPause(); 371 } 372 373 @Override 374 public void onStop() { 375 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 376 Log.d(Logging.LOG_TAG, this + " onStop"); 377 } 378 super.onStop(); 379 } 380 381 @Override 382 public void onDestroy() { 383 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 384 Log.d(Logging.LOG_TAG, this + " onDestroy"); 385 } 386 mCallback.onMessageViewGone(); 387 mController.removeResultCallback(mControllerCallback); 388 cancelAllTasks(); 389 mMessageContentView.destroy(); 390 mMessageContentView = null; 391 super.onDestroy(); 392 } 393 394 @Override 395 public void onSaveInstanceState(Bundle outState) { 396 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 397 Log.d(Logging.LOG_TAG, this + " onSaveInstanceState"); 398 } 399 super.onSaveInstanceState(outState); 400 outState.putInt(BUNDLE_KEY_CURRENT_TAB, mCurrentTab); 401 outState.putBoolean(BUNDLE_KEY_PICTURE_LOADED, (mTabFlags & TAB_FLAGS_PICTURE_LOADED) != 0); 402 } 403 404 private void restoreInstanceState(Bundle state) { 405 if (Logging.DEBUG_LIFECYCLE && Email.DEBUG) { 406 Log.d(Logging.LOG_TAG, this + " restoreInstanceState"); 407 } 408 // At this point (in onCreate) no tabs are visible (because we don't know if the message has 409 // an attachment or invite before loading it). We just remember the tab here. 410 // We'll make it current when the tab first becomes visible in updateTabs(). 411 mRestoredTab = state.getInt(BUNDLE_KEY_CURRENT_TAB); 412 mRestoredPictureLoaded = state.getBoolean(BUNDLE_KEY_PICTURE_LOADED); 413 } 414 415 public void setCallback(Callback callback) { 416 mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback; 417 } 418 419 private void cancelAllTasks() { 420 mMessageObserver.unregister(); 421 mTaskTracker.cancellAllInterrupt(); 422 } 423 424 protected final Controller getController() { 425 return mController; 426 } 427 428 protected final Callback getCallback() { 429 return mCallback; 430 } 431 432 protected final Message getMessage() { 433 return mMessage; 434 } 435 436 protected final boolean isMessageOpen() { 437 return mMessage != null; 438 } 439 440 /** 441 * Returns the account id of the current message, or -1 if unknown (message not open yet, or 442 * viewing an EML message). 443 */ 444 public long getAccountId() { 445 return mAccountId; 446 } 447 448 449 /** 450 * Show/hide the content. We hide all the content (except for the bottom buttons) when loading, 451 * to avoid flicker. 452 */ 453 private void showContent(boolean showContent, boolean showProgressWhenHidden) { 454 makeVisible(mMainView, showContent); 455 makeVisible(mLoadingProgress, !showContent && showProgressWhenHidden); 456 } 457 458 protected void resetView() { 459 showContent(false, false); 460 updateTabs(0); 461 setCurrentTab(TAB_MESSAGE); 462 if (mMessageContentView != null) { 463 blockNetworkLoads(true); 464 mMessageContentView.scrollTo(0, 0); 465 mMessageContentView.clearView(); 466 467 // Dynamic configuration of WebView 468 final WebSettings settings = mMessageContentView.getSettings(); 469 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL); 470 mMessageContentView.setInitialScale(getWebViewZoom()); 471 } 472 mAttachmentsScroll.scrollTo(0, 0); 473 mInviteScroll.scrollTo(0, 0); 474 mAttachments.removeAllViews(); 475 mAttachments.setVisibility(View.GONE); 476 initContactStatusViews(); 477 } 478 479 /** 480 * Returns the zoom scale (in percent) which is a combination of the user setting 481 * (tiny, small, normal, large, huge) and the device density. The intention 482 * is for the text to be physically equal in size over different density 483 * screens. 484 */ 485 private int getWebViewZoom() { 486 float density = mContext.getResources().getDisplayMetrics().density; 487 int zoom = Preferences.getPreferences(mContext).getTextZoom(); 488 return (int) (ZOOM_SCALE_ARRAY[zoom] * density * 100); 489 } 490 491 private void initContactStatusViews() { 492 mContactStatusState = CONTACT_STATUS_STATE_UNLOADED; 493 mQuickContactLookupUri = null; 494 mSenderPresenceView.setImageResource(ContactStatusLoader.PRESENCE_UNKNOWN_RESOURCE_ID); 495 showDefaultQuickContactBadgeImage(); 496 } 497 498 private void showDefaultQuickContactBadgeImage() { 499 mFromBadge.setImageResource(R.drawable.ic_contact_picture); 500 } 501 502 protected final void addTabFlags(int tabFlags) { 503 updateTabs(mTabFlags | tabFlags); 504 } 505 506 private final void clearTabFlags(int tabFlags) { 507 updateTabs(mTabFlags & ~tabFlags); 508 } 509 510 private void setAttachmentCount(int count) { 511 mAttachmentCount = count; 512 if (mAttachmentCount > 0) { 513 addTabFlags(TAB_FLAGS_HAS_ATTACHMENT); 514 } else { 515 clearTabFlags(TAB_FLAGS_HAS_ATTACHMENT); 516 } 517 } 518 519 private static void makeVisible(View v, boolean visible) { 520 final int visibility = visible ? View.VISIBLE : View.GONE; 521 if ((v != null) && (v.getVisibility() != visibility)) { 522 v.setVisibility(visibility); 523 } 524 } 525 526 private static boolean isVisible(View v) { 527 return (v != null) && (v.getVisibility() == View.VISIBLE); 528 } 529 530 /** 531 * Update the visual of the tabs. (visibility, text, etc) 532 */ 533 private void updateTabs(int tabFlags) { 534 mTabFlags = tabFlags; 535 boolean messageTabVisible = (tabFlags & (TAB_FLAGS_HAS_INVITE | TAB_FLAGS_HAS_ATTACHMENT)) 536 != 0; 537 makeVisible(mMessageTab, messageTabVisible); 538 makeVisible(mInviteTab, (tabFlags & TAB_FLAGS_HAS_INVITE) != 0); 539 makeVisible(mAttachmentTab, (tabFlags & TAB_FLAGS_HAS_ATTACHMENT) != 0); 540 541 final boolean hasPictures = (tabFlags & TAB_FLAGS_HAS_PICTURES) != 0; 542 final boolean pictureLoaded = (tabFlags & TAB_FLAGS_PICTURE_LOADED) != 0; 543 makeVisible(mShowPicturesTab, hasPictures && !pictureLoaded); 544 545 mAttachmentTab.setText(mContext.getResources().getQuantityString( 546 R.plurals.message_view_show_attachments_action, 547 mAttachmentCount, mAttachmentCount)); 548 549 // Hide the entire section if no tabs are visible. 550 makeVisible(mTabSection, isVisible(mMessageTab) || isVisible(mInviteTab) 551 || isVisible(mAttachmentTab) || isVisible(mShowPicturesTab)); 552 553 // Restore previously selected tab after rotation 554 if (mRestoredTab != TAB_NONE && isVisible(getTabViewForFlag(mRestoredTab))) { 555 setCurrentTab(mRestoredTab); 556 mRestoredTab = TAB_NONE; 557 } 558 } 559 560 /** 561 * Set the current tab. 562 * 563 * @param tab any of {@link #TAB_MESSAGE}, {@link #TAB_ATTACHMENT} or {@link #TAB_INVITE}. 564 */ 565 private void setCurrentTab(int tab) { 566 mCurrentTab = tab; 567 568 // Hide & unselect all tabs 569 makeVisible(getTabContentViewForFlag(TAB_MESSAGE), false); 570 makeVisible(getTabContentViewForFlag(TAB_ATTACHMENT), false); 571 makeVisible(getTabContentViewForFlag(TAB_INVITE), false); 572 getTabViewForFlag(TAB_MESSAGE).setSelected(false); 573 getTabViewForFlag(TAB_ATTACHMENT).setSelected(false); 574 getTabViewForFlag(TAB_INVITE).setSelected(false); 575 576 makeVisible(getTabContentViewForFlag(mCurrentTab), true); 577 getTabViewForFlag(mCurrentTab).setSelected(true); 578 } 579 580 private View getTabViewForFlag(int tabFlag) { 581 switch (tabFlag) { 582 case TAB_MESSAGE: 583 return mMessageTab; 584 case TAB_ATTACHMENT: 585 return mAttachmentTab; 586 case TAB_INVITE: 587 return mInviteTab; 588 } 589 throw new IllegalArgumentException(); 590 } 591 592 private View getTabContentViewForFlag(int tabFlag) { 593 switch (tabFlag) { 594 case TAB_MESSAGE: 595 return mMessageContentView; 596 case TAB_ATTACHMENT: 597 return mAttachmentsScroll; 598 case TAB_INVITE: 599 return mInviteScroll; 600 } 601 throw new IllegalArgumentException(); 602 } 603 604 private void blockNetworkLoads(boolean block) { 605 if (mMessageContentView != null) { 606 mMessageContentView.getSettings().setBlockNetworkLoads(block); 607 } 608 } 609 610 private void setMessageHtml(String html) { 611 if (html == null) { 612 html = ""; 613 } 614 if (mMessageContentView != null) { 615 mMessageContentView.loadDataWithBaseURL("email://", html, "text/html", "utf-8", null); 616 } 617 } 618 619 /** 620 * Handle clicks on sender, which shows {@link QuickContact} or prompts to add 621 * the sender as a contact. 622 */ 623 private void onClickSender() { 624 if (!isMessageOpen()) return; 625 final Address senderEmail = Address.unpackFirst(mMessage.mFrom); 626 if (senderEmail == null) return; 627 628 if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED) { 629 // Status not loaded yet. 630 mContactStatusState = CONTACT_STATUS_STATE_UNLOADED_TRIGGERED; 631 return; 632 } 633 if (mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED) { 634 return; // Already clicked, and waiting for the data. 635 } 636 637 if (mQuickContactLookupUri != null) { 638 QuickContact.showQuickContact(mContext, mFromBadge, mQuickContactLookupUri, 639 QuickContact.MODE_LARGE, null); 640 } else { 641 // No matching contact, ask user to create one 642 final Uri mailUri = Uri.fromParts("mailto", senderEmail.getAddress(), null); 643 final Intent intent = new Intent(ContactsContract.Intents.SHOW_OR_CREATE_CONTACT, 644 mailUri); 645 646 // Pass along full E-mail string for possible create dialog 647 intent.putExtra(ContactsContract.Intents.EXTRA_CREATE_DESCRIPTION, 648 senderEmail.toString()); 649 650 // Only provide personal name hint if we have one 651 final String senderPersonal = senderEmail.getPersonal(); 652 if (!TextUtils.isEmpty(senderPersonal)) { 653 intent.putExtra(ContactsContract.Intents.Insert.NAME, senderPersonal); 654 } 655 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 656 657 startActivity(intent); 658 } 659 } 660 661 private static class ContactStatusLoaderCallbacks 662 implements LoaderCallbacks<ContactStatusLoader.Result> { 663 private static final String BUNDLE_EMAIL_ADDRESS = "email"; 664 private final MessageViewFragmentBase mFragment; 665 666 public ContactStatusLoaderCallbacks(MessageViewFragmentBase fragment) { 667 mFragment = fragment; 668 } 669 670 public static Bundle createArguments(String emailAddress) { 671 Bundle b = new Bundle(); 672 b.putString(BUNDLE_EMAIL_ADDRESS, emailAddress); 673 return b; 674 } 675 676 @Override 677 public Loader<ContactStatusLoader.Result> onCreateLoader(int id, Bundle args) { 678 return new ContactStatusLoader(mFragment.mContext, 679 args.getString(BUNDLE_EMAIL_ADDRESS)); 680 } 681 682 @Override 683 public void onLoadFinished(Loader<ContactStatusLoader.Result> loader, 684 ContactStatusLoader.Result result) { 685 boolean triggered = 686 (mFragment.mContactStatusState == CONTACT_STATUS_STATE_UNLOADED_TRIGGERED); 687 mFragment.mContactStatusState = CONTACT_STATUS_STATE_LOADED; 688 mFragment.mQuickContactLookupUri = result.mLookupUri; 689 mFragment.mSenderPresenceView.setImageResource(result.mPresenceResId); 690 if (result.mPhoto != null) { // photo will be null if unknown. 691 mFragment.mFromBadge.setImageBitmap(result.mPhoto); 692 } 693 if (triggered) { 694 mFragment.onClickSender(); 695 } 696 } 697 698 @Override 699 public void onLoaderReset(Loader<ContactStatusLoader.Result> loader) { 700 } 701 } 702 703 private void onSaveAttachment(MessageViewAttachmentInfo info) { 704 if (!Utility.isExternalStorageMounted()) { 705 /* 706 * Abort early if there's no place to save the attachment. We don't want to spend 707 * the time downloading it and then abort. 708 */ 709 Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved); 710 return; 711 } 712 713 if (info.isFileSaved()) { 714 // Nothing to do - we have the file saved. 715 return; 716 } 717 718 File savedFile = performAttachmentSave(info); 719 if (savedFile != null) { 720 Utility.showToast(getActivity(), String.format( 721 mContext.getString(R.string.message_view_status_attachment_saved), 722 savedFile.getName())); 723 } else { 724 Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved); 725 } 726 } 727 728 private File performAttachmentSave(MessageViewAttachmentInfo info) { 729 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, info.mId); 730 Uri attachmentUri = AttachmentUtilities.getAttachmentUri(mAccountId, attachment.mId); 731 732 try { 733 File downloads = Environment.getExternalStoragePublicDirectory( 734 Environment.DIRECTORY_DOWNLOADS); 735 downloads.mkdirs(); 736 File file = Utility.createUniqueFile(downloads, attachment.mFileName); 737 Uri contentUri = AttachmentUtilities.resolveAttachmentIdToContentUri( 738 mContext.getContentResolver(), attachmentUri); 739 InputStream in = mContext.getContentResolver().openInputStream(contentUri); 740 OutputStream out = new FileOutputStream(file); 741 IOUtils.copy(in, out); 742 out.flush(); 743 out.close(); 744 in.close(); 745 746 String absolutePath = file.getAbsolutePath(); 747 748 // Although the download manager can scan media files, scanning only happens after the 749 // user clicks on the item in the Downloads app. So, we run the attachment through 750 // the media scanner ourselves so it gets added to gallery / music immediately. 751 MediaScannerConnection.scanFile(mContext, new String[] {absolutePath}, 752 null, null); 753 754 DownloadManager dm = 755 (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE); 756 dm.addCompletedDownload(info.mName, info.mName, 757 false /* do not use media scanner */, 758 info.mContentType, absolutePath, info.mSize, 759 true /* show notification */); 760 761 // Cache the stored file information. 762 info.setSavedPath(absolutePath); 763 764 // Update our buttons. 765 updateAttachmentButtons(info); 766 767 return file; 768 769 } catch (IOException ioe) { 770 // Ignore. Callers will handle it from the return code. 771 } 772 773 return null; 774 } 775 776 private void onOpenAttachment(MessageViewAttachmentInfo info) { 777 if (info.mAllowInstall) { 778 // The package installer is unable to install files from a content URI; it must be 779 // given a file path. Therefore, we need to save it first in order to proceed 780 if (!info.mAllowSave || !Utility.isExternalStorageMounted()) { 781 Utility.showToast(getActivity(), R.string.message_view_status_attachment_not_saved); 782 return; 783 } 784 785 if (!info.isFileSaved()) { 786 if (performAttachmentSave(info) == null) { 787 // Saving failed for some reason - bail. 788 Utility.showToast( 789 getActivity(), R.string.message_view_status_attachment_not_saved); 790 return; 791 } 792 } 793 } 794 try { 795 Intent intent = info.getAttachmentIntent(mContext, mAccountId); 796 startActivity(intent); 797 } catch (ActivityNotFoundException e) { 798 Utility.showToast(getActivity(), R.string.message_view_display_attachment_toast); 799 } 800 } 801 802 private void onInfoAttachment(final MessageViewAttachmentInfo attachment) { 803 AttachmentInfoDialog dialog = 804 AttachmentInfoDialog.newInstance(getActivity(), attachment.mDenyFlags); 805 dialog.show(getActivity().getFragmentManager(), null); 806 } 807 808 private void onLoadAttachment(final MessageViewAttachmentInfo attachment) { 809 attachment.loadButton.setVisibility(View.GONE); 810 // If there's nothing in the download queue, we'll probably start right away so wait a 811 // second before showing the cancel button 812 if (AttachmentDownloadService.getQueueSize() == 0) { 813 // Set to invisible; if the button is still in this state one second from now, we'll 814 // assume the download won't start right away, and we make the cancel button visible 815 attachment.cancelButton.setVisibility(View.GONE); 816 // Create the timed task that will change the button state 817 new EmailAsyncTask<Void, Void, Void>(mTaskTracker) { 818 @Override 819 protected Void doInBackground(Void... params) { 820 try { 821 Thread.sleep(1000L); 822 } catch (InterruptedException e) { } 823 return null; 824 } 825 @Override 826 protected void onPostExecute(Void result) { 827 // If the timeout completes and the attachment has not loaded, show cancel 828 if (!attachment.loaded) { 829 attachment.cancelButton.setVisibility(View.VISIBLE); 830 } 831 } 832 }.executeParallel(); 833 } else { 834 attachment.cancelButton.setVisibility(View.VISIBLE); 835 } 836 attachment.showProgressIndeterminate(); 837 mController.loadAttachment(attachment.mId, mMessageId, mAccountId); 838 } 839 840 private void onCancelAttachment(MessageViewAttachmentInfo attachment) { 841 // Don't change button states if we couldn't cancel the download 842 if (AttachmentDownloadService.cancelQueuedAttachment(attachment.mId)) { 843 attachment.loadButton.setVisibility(View.VISIBLE); 844 attachment.cancelButton.setVisibility(View.GONE); 845 attachment.hideProgress(); 846 } 847 } 848 849 /** 850 * Called by ControllerResults. Show the "View" and "Save" buttons; hide "Load" and "Stop" 851 * 852 * @param attachmentId the attachment that was just downloaded 853 */ 854 private void doFinishLoadAttachment(long attachmentId) { 855 MessageViewAttachmentInfo info = findAttachmentInfo(attachmentId); 856 if (info != null) { 857 info.loaded = true; 858 updateAttachmentButtons(info); 859 } 860 } 861 862 private void onShowPicturesInHtml() { 863 if (mMessageContentView != null) { 864 blockNetworkLoads(false); 865 setMessageHtml(mHtmlTextWebView); 866 addTabFlags(TAB_FLAGS_PICTURE_LOADED); 867 } 868 } 869 870 private void onShowDetails() { 871 if (!isMessageOpen()) return; 872 String subject = mMessage.mSubject; 873 String date = formatDate(mMessage.mTimeStamp, true); 874 875 final String SEPARATOR = "\n"; 876 String from = Address.toString(Address.unpack(mMessage.mFrom), SEPARATOR); 877 String to = Address.toString(Address.unpack(mMessage.mTo), SEPARATOR); 878 String cc = Address.toString(Address.unpack(mMessage.mCc), SEPARATOR); 879 String bcc = Address.toString(Address.unpack(mMessage.mBcc), SEPARATOR); 880 MessageViewMessageDetailsDialog dialog = MessageViewMessageDetailsDialog.newInstance( 881 getActivity(), subject, date, from, to, cc, bcc); 882 dialog.show(getActivity().getFragmentManager(), null); 883 } 884 885 @Override 886 public void onClick(View view) { 887 if (!isMessageOpen()) { 888 return; // Ignore. 889 } 890 switch (view.getId()) { 891 case R.id.from_name: 892 case R.id.from_address: 893 case R.id.badge: 894 case R.id.presence: 895 onClickSender(); 896 break; 897 case R.id.load: 898 onLoadAttachment((MessageViewAttachmentInfo) view.getTag()); 899 break; 900 case R.id.info: 901 onInfoAttachment((MessageViewAttachmentInfo) view.getTag()); 902 break; 903 case R.id.save: 904 onSaveAttachment((MessageViewAttachmentInfo) view.getTag()); 905 break; 906 case R.id.open: 907 onOpenAttachment((MessageViewAttachmentInfo) view.getTag()); 908 break; 909 case R.id.cancel: 910 onCancelAttachment((MessageViewAttachmentInfo) view.getTag()); 911 break; 912 case R.id.show_message: 913 setCurrentTab(TAB_MESSAGE); 914 break; 915 case R.id.show_invite: 916 setCurrentTab(TAB_INVITE); 917 break; 918 case R.id.show_attachments: 919 setCurrentTab(TAB_ATTACHMENT); 920 break; 921 case R.id.show_pictures: 922 onShowPicturesInHtml(); 923 break; 924 case R.id.show_details: 925 onShowDetails(); 926 break; 927 } 928 } 929 930 /** 931 * Start loading contact photo and presence. 932 */ 933 private void queryContactStatus() { 934 if (!isMessageOpen()) return; 935 initContactStatusViews(); // Initialize the state, just in case. 936 937 // Find the sender email address, and start presence check. 938 Address sender = Address.unpackFirst(mMessage.mFrom); 939 if (sender != null) { 940 String email = sender.getAddress(); 941 if (email != null) { 942 getLoaderManager().restartLoader(PHOTO_LOADER_ID, 943 ContactStatusLoaderCallbacks.createArguments(email), 944 new ContactStatusLoaderCallbacks(this)); 945 } 946 } 947 } 948 949 /** 950 * Called by {@link LoadMessageTask} and {@link ReloadMessageTask} to load a message in a 951 * subclass specific way. 952 * 953 * NOTE This method is called on a worker thread! Implementations must properly synchronize 954 * when accessing members. 955 * 956 * @param activity the parent activity. Subclass use it as a context, and to show a toast. 957 */ 958 protected abstract Message openMessageSync(Activity activity); 959 960 /** 961 * Async task for loading a single message outside of the UI thread 962 */ 963 private class LoadMessageTask extends EmailAsyncTask<Void, Void, Message> { 964 965 private final boolean mOkToFetch; 966 private int mMailboxType; 967 968 /** 969 * Special constructor to cache some local info 970 */ 971 public LoadMessageTask(boolean okToFetch) { 972 super(mTaskTracker); 973 mOkToFetch = okToFetch; 974 } 975 976 @Override 977 protected Message doInBackground(Void... params) { 978 Activity activity = getActivity(); 979 Message message = null; 980 if (activity != null) { 981 message = openMessageSync(activity); 982 } 983 if (message != null) { 984 mMailboxType = Mailbox.getMailboxType(mContext, message.mMailboxKey); 985 if (mMailboxType == -1) { 986 message = null; // mailbox removed?? 987 } 988 } 989 return message; 990 } 991 992 @Override 993 protected void onPostExecute(Message message) { 994 if (isCancelled()) { 995 return; 996 } 997 if (message == null) { 998 resetView(); 999 mCallback.onMessageNotExists(); 1000 return; 1001 } 1002 mMessageId = message.mId; 1003 1004 reloadUiFromMessage(message, mOkToFetch); 1005 queryContactStatus(); 1006 onMessageShown(mMessageId, mMailboxType); 1007 } 1008 } 1009 1010 /** 1011 * Kicked by {@link MessageObserver}. Reload the message and update the views. 1012 */ 1013 private class ReloadMessageTask extends EmailAsyncTask<Void, Void, Message> { 1014 public ReloadMessageTask() { 1015 super(mTaskTracker); 1016 } 1017 1018 @Override 1019 protected Message doInBackground(Void... params) { 1020 Activity activity = getActivity(); 1021 if (activity == null) { 1022 return null; 1023 } else { 1024 return openMessageSync(activity); 1025 } 1026 } 1027 1028 @Override 1029 protected void onPostExecute(Message message) { 1030 if (message == null || message.mMailboxKey != mMessage.mMailboxKey) { 1031 // Message deleted or moved. 1032 mCallback.onMessageNotExists(); 1033 return; 1034 } 1035 mMessage = message; 1036 updateHeaderView(mMessage); 1037 } 1038 } 1039 1040 /** 1041 * Called when a message is shown to the user. 1042 */ 1043 protected void onMessageShown(long messageId, int mailboxType) { 1044 mCallback.onMessageViewShown(mailboxType); 1045 } 1046 1047 /** 1048 * Called when the message body is loaded. 1049 */ 1050 protected void onPostLoadBody() { 1051 } 1052 1053 /** 1054 * Async task for loading a single message body outside of the UI thread 1055 */ 1056 private class LoadBodyTask extends EmailAsyncTask<Void, Void, String[]> { 1057 1058 private final long mId; 1059 private boolean mErrorLoadingMessageBody; 1060 1061 /** 1062 * Special constructor to cache some local info 1063 */ 1064 public LoadBodyTask(long messageId) { 1065 super(mTaskTracker); 1066 mId = messageId; 1067 } 1068 1069 @Override 1070 protected String[] doInBackground(Void... params) { 1071 try { 1072 String text = null; 1073 String html = Body.restoreBodyHtmlWithMessageId(mContext, mId); 1074 if (html == null) { 1075 text = Body.restoreBodyTextWithMessageId(mContext, mId); 1076 } 1077 return new String[] { text, html }; 1078 } catch (RuntimeException re) { 1079 // This catches SQLiteException as well as other RTE's we've seen from the 1080 // database calls, such as IllegalStateException 1081 Log.d(Logging.LOG_TAG, "Exception while loading message body", re); 1082 mErrorLoadingMessageBody = true; 1083 return null; 1084 } 1085 } 1086 1087 @Override 1088 protected void onPostExecute(String[] results) { 1089 if (results == null || isCancelled()) { 1090 if (mErrorLoadingMessageBody) { 1091 Utility.showToast(getActivity(), R.string.error_loading_message_body); 1092 } 1093 resetView(); 1094 return; 1095 } 1096 reloadUiFromBody(results[0], results[1]); // text, html 1097 onPostLoadBody(); 1098 } 1099 } 1100 1101 /** 1102 * Async task for loading attachments 1103 * 1104 * Note: This really should only be called when the message load is complete - or, we should 1105 * leave open a listener so the attachments can fill in as they are discovered. In either case, 1106 * this implementation is incomplete, as it will fail to refresh properly if the message is 1107 * partially loaded at this time. 1108 */ 1109 private class LoadAttachmentsTask extends EmailAsyncTask<Long, Void, Attachment[]> { 1110 public LoadAttachmentsTask() { 1111 super(mTaskTracker); 1112 } 1113 1114 @Override 1115 protected Attachment[] doInBackground(Long... messageIds) { 1116 return Attachment.restoreAttachmentsWithMessageId(mContext, messageIds[0]); 1117 } 1118 1119 @Override 1120 protected void onPostExecute(Attachment[] attachments) { 1121 try { 1122 if (isCancelled() || attachments == null) { 1123 return; 1124 } 1125 boolean htmlChanged = false; 1126 int numDisplayedAttachments = 0; 1127 for (Attachment attachment : attachments) { 1128 if (mHtmlTextRaw != null && attachment.mContentId != null 1129 && attachment.mContentUri != null) { 1130 // for html body, replace CID for inline images 1131 // Regexp which matches ' src="cid:contentId"'. 1132 String contentIdRe = 1133 "\\s+(?i)src=\"cid(?-i):\\Q" + attachment.mContentId + "\\E\""; 1134 String srcContentUri = " src=\"" + attachment.mContentUri + "\""; 1135 mHtmlTextRaw = mHtmlTextRaw.replaceAll(contentIdRe, srcContentUri); 1136 htmlChanged = true; 1137 } else { 1138 addAttachment(attachment); 1139 numDisplayedAttachments++; 1140 } 1141 } 1142 setAttachmentCount(numDisplayedAttachments); 1143 mHtmlTextWebView = mHtmlTextRaw; 1144 mHtmlTextRaw = null; 1145 if (htmlChanged) { 1146 setMessageHtml(mHtmlTextWebView); 1147 } 1148 } finally { 1149 showContent(true, false); 1150 } 1151 } 1152 } 1153 1154 private static Bitmap getPreviewIcon(Context context, AttachmentInfo attachment) { 1155 try { 1156 return BitmapFactory.decodeStream( 1157 context.getContentResolver().openInputStream( 1158 AttachmentUtilities.getAttachmentThumbnailUri( 1159 attachment.mAccountKey, attachment.mId, 1160 PREVIEW_ICON_WIDTH, 1161 PREVIEW_ICON_HEIGHT))); 1162 } catch (Exception e) { 1163 Log.d(Logging.LOG_TAG, "Attachment preview failed with exception " + e.getMessage()); 1164 return null; 1165 } 1166 } 1167 1168 /** 1169 * Subclass of AttachmentInfo which includes our views and buttons related to attachment 1170 * handling, as well as our determination of suitability for viewing (based on availability of 1171 * a viewer app) and saving (based upon the presence of external storage) 1172 */ 1173 private static class MessageViewAttachmentInfo extends AttachmentInfo { 1174 private Button openButton; 1175 private Button saveButton; 1176 private Button loadButton; 1177 private Button infoButton; 1178 private Button cancelButton; 1179 private ImageView iconView; 1180 1181 private static final Map<AttachmentInfo, String> sSavedFileInfos = Maps.newHashMap(); 1182 1183 // Don't touch it directly from the outer class. 1184 private final ProgressBar mProgressView; 1185 private boolean loaded; 1186 1187 private MessageViewAttachmentInfo(Context context, Attachment attachment, 1188 ProgressBar progressView) { 1189 super(context, attachment); 1190 mProgressView = progressView; 1191 } 1192 1193 /** 1194 * Create a new attachment info based upon an existing attachment info. Display 1195 * related fields (such as views and buttons) are copied from old to new. 1196 */ 1197 private MessageViewAttachmentInfo(Context context, MessageViewAttachmentInfo oldInfo) { 1198 super(context, oldInfo); 1199 openButton = oldInfo.openButton; 1200 saveButton = oldInfo.saveButton; 1201 loadButton = oldInfo.loadButton; 1202 infoButton = oldInfo.infoButton; 1203 cancelButton = oldInfo.cancelButton; 1204 iconView = oldInfo.iconView; 1205 mProgressView = oldInfo.mProgressView; 1206 loaded = oldInfo.loaded; 1207 } 1208 1209 public void hideProgress() { 1210 // Don't use GONE, which'll break the layout. 1211 if (mProgressView.getVisibility() != View.INVISIBLE) { 1212 mProgressView.setVisibility(View.INVISIBLE); 1213 } 1214 } 1215 1216 public void showProgress(int progress) { 1217 if (mProgressView.getVisibility() != View.VISIBLE) { 1218 mProgressView.setVisibility(View.VISIBLE); 1219 } 1220 if (mProgressView.isIndeterminate()) { 1221 mProgressView.setIndeterminate(false); 1222 } 1223 mProgressView.setProgress(progress); 1224 } 1225 1226 public void showProgressIndeterminate() { 1227 if (mProgressView.getVisibility() != View.VISIBLE) { 1228 mProgressView.setVisibility(View.VISIBLE); 1229 } 1230 if (!mProgressView.isIndeterminate()) { 1231 mProgressView.setIndeterminate(true); 1232 } 1233 } 1234 1235 /** 1236 * Determines whether or not this attachment has a saved file in the external storage. That 1237 * is, the user has at some point clicked "save" for this attachment. 1238 * 1239 * Note: this is an approximation and uses an in-memory cache that can get wiped when the 1240 * process dies, and so is somewhat conservative. Additionally, the user can modify the file 1241 * after saving, and so the file may not be the same (though this is unlikely). 1242 */ 1243 public boolean isFileSaved() { 1244 String path = getSavedPath(); 1245 if (path == null) { 1246 return false; 1247 } 1248 boolean savedFileExists = new File(path).exists(); 1249 if (!savedFileExists) { 1250 // Purge the cache entry. 1251 setSavedPath(null); 1252 } 1253 return savedFileExists; 1254 } 1255 1256 private void setSavedPath(String path) { 1257 if (path == null) { 1258 sSavedFileInfos.remove(this); 1259 } else { 1260 sSavedFileInfos.put(this, path); 1261 } 1262 } 1263 1264 /** 1265 * Returns an absolute file path for the given attachment if it has been saved. If one is 1266 * not found, {@code null} is returned. 1267 * 1268 * Clients are expected to validate that the file at the given path is still valid. 1269 */ 1270 private String getSavedPath() { 1271 return sSavedFileInfos.get(this); 1272 } 1273 1274 @Override 1275 protected Uri getUriForIntent(Context context, long accountId) { 1276 // Prefer to act on the saved file for intents. 1277 String path = getSavedPath(); 1278 return (path != null) 1279 ? Uri.parse("file://" + getSavedPath()) 1280 : super.getUriForIntent(context, accountId); 1281 } 1282 } 1283 1284 /** 1285 * Updates all current attachments on the attachment tab. 1286 */ 1287 private void updateAttachmentTab() { 1288 for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { 1289 View view = mAttachments.getChildAt(i); 1290 MessageViewAttachmentInfo oldInfo = (MessageViewAttachmentInfo)view.getTag(); 1291 MessageViewAttachmentInfo newInfo = 1292 new MessageViewAttachmentInfo(getActivity(), oldInfo); 1293 updateAttachmentButtons(newInfo); 1294 view.setTag(newInfo); 1295 } 1296 } 1297 1298 /** 1299 * Updates the attachment buttons. Adjusts the visibility of the buttons as well 1300 * as updating any tag information associated with the buttons. 1301 */ 1302 private void updateAttachmentButtons(MessageViewAttachmentInfo attachmentInfo) { 1303 ImageView attachmentIcon = attachmentInfo.iconView; 1304 Button openButton = attachmentInfo.openButton; 1305 Button saveButton = attachmentInfo.saveButton; 1306 Button loadButton = attachmentInfo.loadButton; 1307 Button infoButton = attachmentInfo.infoButton; 1308 Button cancelButton = attachmentInfo.cancelButton; 1309 1310 if (!attachmentInfo.mAllowView) { 1311 openButton.setVisibility(View.GONE); 1312 } 1313 if (!attachmentInfo.mAllowSave) { 1314 saveButton.setVisibility(View.GONE); 1315 } 1316 1317 if (!attachmentInfo.mAllowView && !attachmentInfo.mAllowSave) { 1318 // This attachment may never be viewed or saved, so block everything 1319 attachmentInfo.hideProgress(); 1320 openButton.setVisibility(View.GONE); 1321 saveButton.setVisibility(View.GONE); 1322 loadButton.setVisibility(View.GONE); 1323 cancelButton.setVisibility(View.GONE); 1324 infoButton.setVisibility(View.VISIBLE); 1325 } else if (attachmentInfo.loaded) { 1326 // If the attachment is loaded, show 100% progress 1327 // Note that for POP3 messages, the user will only see "Open" and "Save", 1328 // because the entire message is loaded before being shown. 1329 // Hide "Load" and "Info", show "View" and "Save" 1330 attachmentInfo.showProgress(100); 1331 if (attachmentInfo.mAllowSave) { 1332 saveButton.setVisibility(View.VISIBLE); 1333 1334 boolean isFileSaved = attachmentInfo.isFileSaved(); 1335 saveButton.setEnabled(!isFileSaved); 1336 if (!isFileSaved) { 1337 saveButton.setText(R.string.message_view_attachment_save_action); 1338 } else { 1339 saveButton.setText(R.string.message_view_attachment_saved); 1340 } 1341 } 1342 if (attachmentInfo.mAllowView) { 1343 // Set the attachment action button text accordingly 1344 if (attachmentInfo.mContentType.startsWith("audio/") || 1345 attachmentInfo.mContentType.startsWith("video/")) { 1346 openButton.setText(R.string.message_view_attachment_play_action); 1347 } else if (attachmentInfo.mAllowInstall) { 1348 openButton.setText(R.string.message_view_attachment_install_action); 1349 } else { 1350 openButton.setText(R.string.message_view_attachment_view_action); 1351 } 1352 openButton.setVisibility(View.VISIBLE); 1353 } 1354 if (attachmentInfo.mDenyFlags == AttachmentInfo.ALLOW) { 1355 infoButton.setVisibility(View.GONE); 1356 } else { 1357 infoButton.setVisibility(View.VISIBLE); 1358 } 1359 loadButton.setVisibility(View.GONE); 1360 cancelButton.setVisibility(View.GONE); 1361 1362 updatePreviewIcon(attachmentInfo); 1363 } else { 1364 // The attachment is not loaded, so present UI to start downloading it 1365 1366 // Show "Load"; hide "View", "Save" and "Info" 1367 saveButton.setVisibility(View.GONE); 1368 openButton.setVisibility(View.GONE); 1369 infoButton.setVisibility(View.GONE); 1370 1371 // If the attachment is queued, show the indeterminate progress bar. From this point,. 1372 // any progress changes will cause this to be replaced by the normal progress bar 1373 if (AttachmentDownloadService.isAttachmentQueued(attachmentInfo.mId)) { 1374 attachmentInfo.showProgressIndeterminate(); 1375 loadButton.setVisibility(View.GONE); 1376 cancelButton.setVisibility(View.VISIBLE); 1377 } else { 1378 loadButton.setVisibility(View.VISIBLE); 1379 cancelButton.setVisibility(View.GONE); 1380 } 1381 } 1382 openButton.setTag(attachmentInfo); 1383 saveButton.setTag(attachmentInfo); 1384 loadButton.setTag(attachmentInfo); 1385 infoButton.setTag(attachmentInfo); 1386 cancelButton.setTag(attachmentInfo); 1387 } 1388 1389 /** 1390 * Copy data from a cursor-refreshed attachment into the UI. Called from UI thread. 1391 * 1392 * @param attachment A single attachment loaded from the provider 1393 */ 1394 private void addAttachment(Attachment attachment) { 1395 LayoutInflater inflater = getActivity().getLayoutInflater(); 1396 View view = inflater.inflate(R.layout.message_view_attachment, null); 1397 1398 TextView attachmentName = (TextView) UiUtilities.getView(view, R.id.attachment_name); 1399 TextView attachmentInfoView = (TextView) UiUtilities.getView(view, R.id.attachment_info); 1400 ImageView attachmentIcon = (ImageView) UiUtilities.getView(view, R.id.attachment_icon); 1401 Button openButton = (Button) UiUtilities.getView(view, R.id.open); 1402 Button saveButton = (Button) UiUtilities.getView(view, R.id.save); 1403 Button loadButton = (Button) UiUtilities.getView(view, R.id.load); 1404 Button infoButton = (Button) UiUtilities.getView(view, R.id.info); 1405 Button cancelButton = (Button) UiUtilities.getView(view, R.id.cancel); 1406 ProgressBar attachmentProgress = (ProgressBar) UiUtilities.getView(view, R.id.progress); 1407 1408 MessageViewAttachmentInfo attachmentInfo = new MessageViewAttachmentInfo( 1409 mContext, attachment, attachmentProgress); 1410 1411 // Check whether the attachment already exists 1412 if (Utility.attachmentExists(mContext, attachment)) { 1413 attachmentInfo.loaded = true; 1414 } 1415 1416 attachmentInfo.openButton = openButton; 1417 attachmentInfo.saveButton = saveButton; 1418 attachmentInfo.loadButton = loadButton; 1419 attachmentInfo.infoButton = infoButton; 1420 attachmentInfo.cancelButton = cancelButton; 1421 attachmentInfo.iconView = attachmentIcon; 1422 1423 updateAttachmentButtons(attachmentInfo); 1424 1425 view.setTag(attachmentInfo); 1426 openButton.setOnClickListener(this); 1427 saveButton.setOnClickListener(this); 1428 loadButton.setOnClickListener(this); 1429 infoButton.setOnClickListener(this); 1430 cancelButton.setOnClickListener(this); 1431 1432 attachmentName.setText(attachmentInfo.mName); 1433 attachmentInfoView.setText(UiUtilities.formatSize(mContext, attachmentInfo.mSize)); 1434 1435 mAttachments.addView(view); 1436 mAttachments.setVisibility(View.VISIBLE); 1437 } 1438 1439 private MessageViewAttachmentInfo findAttachmentInfoFromView(long attachmentId) { 1440 for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { 1441 MessageViewAttachmentInfo attachmentInfo = 1442 (MessageViewAttachmentInfo) mAttachments.getChildAt(i).getTag(); 1443 if (attachmentInfo.mId == attachmentId) { 1444 return attachmentInfo; 1445 } 1446 } 1447 return null; 1448 } 1449 1450 /** 1451 * Reload the UI from a provider cursor. {@link LoadMessageTask#onPostExecute} calls it. 1452 * 1453 * Update the header views, and start loading the body. 1454 * 1455 * @param message A copy of the message loaded from the database 1456 * @param okToFetch If true, and message is not fully loaded, it's OK to fetch from 1457 * the network. Use false to prevent looping here. 1458 */ 1459 protected void reloadUiFromMessage(Message message, boolean okToFetch) { 1460 mMessage = message; 1461 mAccountId = message.mAccountKey; 1462 1463 mMessageObserver.register(ContentUris.withAppendedId(Message.CONTENT_URI, mMessage.mId)); 1464 1465 updateHeaderView(mMessage); 1466 1467 // Handle partially-loaded email, as follows: 1468 // 1. Check value of message.mFlagLoaded 1469 // 2. If != LOADED, ask controller to load it 1470 // 3. Controller callback (after loaded) should trigger LoadBodyTask & LoadAttachmentsTask 1471 // 4. Else start the loader tasks right away (message already loaded) 1472 if (okToFetch && message.mFlagLoaded != Message.FLAG_LOADED_COMPLETE) { 1473 mControllerCallback.getWrappee().setWaitForLoadMessageId(message.mId); 1474 mController.loadMessageForView(message.mId); 1475 } else { 1476 mControllerCallback.getWrappee().setWaitForLoadMessageId(-1); 1477 // Ask for body 1478 new LoadBodyTask(message.mId).executeParallel(); 1479 } 1480 } 1481 1482 protected void updateHeaderView(Message message) { 1483 mSubjectView.setText(message.mSubject); 1484 final Address from = Address.unpackFirst(message.mFrom); 1485 1486 // Set sender address/display name 1487 // Note we set " " for empty field, so TextView's won't get squashed. 1488 // Otherwise their height will be 0, which breaks the layout. 1489 if (from != null) { 1490 final String fromFriendly = from.toFriendly(); 1491 final String fromAddress = from.getAddress(); 1492 mFromNameView.setText(fromFriendly); 1493 mFromAddressView.setText(fromFriendly.equals(fromAddress) ? " " : fromAddress); 1494 } else { 1495 mFromNameView.setText(" "); 1496 mFromAddressView.setText(" "); 1497 } 1498 mDateTimeView.setText(formatDate(message.mTimeStamp, false)); 1499 1500 // To/Cc/Bcc 1501 final Resources res = mContext.getResources(); 1502 final SpannableStringBuilder ssb = new SpannableStringBuilder(); 1503 final String friendlyTo = Address.toFriendly(Address.unpack(message.mTo)); 1504 final String friendlyCc = Address.toFriendly(Address.unpack(message.mCc)); 1505 final String friendlyBcc = Address.toFriendly(Address.unpack(message.mBcc)); 1506 1507 if (!TextUtils.isEmpty(friendlyTo)) { 1508 Utility.appendBold(ssb, res.getString(R.string.message_view_to_label)); 1509 ssb.append(" "); 1510 ssb.append(friendlyTo); 1511 } 1512 if (!TextUtils.isEmpty(friendlyCc)) { 1513 ssb.append(" "); 1514 Utility.appendBold(ssb, res.getString(R.string.message_view_cc_label)); 1515 ssb.append(" "); 1516 ssb.append(friendlyCc); 1517 } 1518 if (!TextUtils.isEmpty(friendlyBcc)) { 1519 ssb.append(" "); 1520 Utility.appendBold(ssb, res.getString(R.string.message_view_bcc_label)); 1521 ssb.append(" "); 1522 ssb.append(friendlyBcc); 1523 } 1524 mAddressesView.setText(ssb); 1525 } 1526 1527 private String formatDate(long millis, boolean withYear) { 1528 StringBuilder sb = new StringBuilder(); 1529 Formatter formatter = new Formatter(sb); 1530 DateUtils.formatDateRange(mContext, formatter, millis, millis, 1531 DateUtils.FORMAT_SHOW_DATE 1532 | DateUtils.FORMAT_ABBREV_ALL 1533 | DateUtils.FORMAT_SHOW_TIME 1534 | (withYear ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR)); 1535 return sb.toString(); 1536 } 1537 1538 /** 1539 * Reload the body from the provider cursor. This must only be called from the UI thread. 1540 * 1541 * @param bodyText text part 1542 * @param bodyHtml html part 1543 * 1544 * TODO deal with html vs text and many other issues <- WHAT DOES IT MEAN?? 1545 */ 1546 private void reloadUiFromBody(String bodyText, String bodyHtml) { 1547 String text = null; 1548 mHtmlTextRaw = null; 1549 boolean hasImages = false; 1550 1551 if (bodyHtml == null) { 1552 text = bodyText; 1553 /* 1554 * Convert the plain text to HTML 1555 */ 1556 StringBuffer sb = new StringBuffer("<html><body>"); 1557 if (text != null) { 1558 // Escape any inadvertent HTML in the text message 1559 text = EmailHtmlUtil.escapeCharacterToDisplay(text); 1560 // Find any embedded URL's and linkify 1561 Matcher m = Patterns.WEB_URL.matcher(text); 1562 while (m.find()) { 1563 int start = m.start(); 1564 /* 1565 * WEB_URL_PATTERN may match domain part of email address. To detect 1566 * this false match, the character just before the matched string 1567 * should not be '@'. 1568 */ 1569 if (start == 0 || text.charAt(start - 1) != '@') { 1570 String url = m.group(); 1571 Matcher proto = WEB_URL_PROTOCOL.matcher(url); 1572 String link; 1573 if (proto.find()) { 1574 // This is work around to force URL protocol part be lower case, 1575 // because WebView could follow only lower case protocol link. 1576 link = proto.group().toLowerCase() + url.substring(proto.end()); 1577 } else { 1578 // Patterns.WEB_URL matches URL without protocol part, 1579 // so added default protocol to link. 1580 link = "http://" + url; 1581 } 1582 String href = String.format("<a href=\"%s\">%s</a>", link, url); 1583 m.appendReplacement(sb, href); 1584 } 1585 else { 1586 m.appendReplacement(sb, "$0"); 1587 } 1588 } 1589 m.appendTail(sb); 1590 } 1591 sb.append("</body></html>"); 1592 text = sb.toString(); 1593 } else { 1594 text = bodyHtml; 1595 mHtmlTextRaw = bodyHtml; 1596 hasImages = IMG_TAG_START_REGEX.matcher(text).find(); 1597 } 1598 1599 // TODO this is not really accurate. 1600 // - Images aren't the only network resources. (e.g. CSS) 1601 // - If images are attached to the email and small enough, we download them at once, 1602 // and won't need network access when they're shown. 1603 if (hasImages) { 1604 if (mRestoredPictureLoaded) { 1605 blockNetworkLoads(false); 1606 addTabFlags(TAB_FLAGS_PICTURE_LOADED); // Set for next onSaveInstanceState 1607 1608 // Make sure to reset the flag -- otherwise this will keep taking effect even after 1609 // moving to another message. 1610 mRestoredPictureLoaded = false; 1611 } else { 1612 addTabFlags(TAB_FLAGS_HAS_PICTURES); 1613 } 1614 } 1615 setMessageHtml(text); 1616 1617 // Ask for attachments after body 1618 new LoadAttachmentsTask().executeParallel(mMessage.mId); 1619 1620 mIsMessageLoadedForTest = true; 1621 } 1622 1623 /** 1624 * Overrides for WebView behaviors. 1625 */ 1626 private class CustomWebViewClient extends WebViewClient { 1627 @Override 1628 public boolean shouldOverrideUrlLoading(WebView view, String url) { 1629 return mCallback.onUrlInMessageClicked(url); 1630 } 1631 } 1632 1633 private View findAttachmentView(long attachmentId) { 1634 for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) { 1635 View view = mAttachments.getChildAt(i); 1636 MessageViewAttachmentInfo attachment = (MessageViewAttachmentInfo) view.getTag(); 1637 if (attachment.mId == attachmentId) { 1638 return view; 1639 } 1640 } 1641 return null; 1642 } 1643 1644 private MessageViewAttachmentInfo findAttachmentInfo(long attachmentId) { 1645 View view = findAttachmentView(attachmentId); 1646 if (view != null) { 1647 return (MessageViewAttachmentInfo)view.getTag(); 1648 } 1649 return null; 1650 } 1651 1652 /** 1653 * Controller results listener. We wrap it with {@link ControllerResultUiThreadWrapper}, 1654 * so all methods are called on the UI thread. 1655 */ 1656 private class ControllerResults extends Controller.Result { 1657 private long mWaitForLoadMessageId; 1658 1659 public void setWaitForLoadMessageId(long messageId) { 1660 mWaitForLoadMessageId = messageId; 1661 } 1662 1663 @Override 1664 public void loadMessageForViewCallback(MessagingException result, long accountId, 1665 long messageId, int progress) { 1666 if (messageId != mWaitForLoadMessageId) { 1667 // We are not waiting for this message to load, so exit quickly 1668 return; 1669 } 1670 if (result == null) { 1671 switch (progress) { 1672 case 0: 1673 mCallback.onLoadMessageStarted(); 1674 // Loading from network -- show the progress icon. 1675 showContent(false, true); 1676 break; 1677 case 100: 1678 mWaitForLoadMessageId = -1; 1679 mCallback.onLoadMessageFinished(); 1680 // reload UI and reload everything else too 1681 // pass false to LoadMessageTask to prevent looping here 1682 cancelAllTasks(); 1683 new LoadMessageTask(false).executeParallel(); 1684 break; 1685 default: 1686 // do nothing - we don't have a progress bar at this time 1687 break; 1688 } 1689 } else { 1690 mWaitForLoadMessageId = -1; 1691 String error = mContext.getString(R.string.status_network_error); 1692 mCallback.onLoadMessageError(error); 1693 resetView(); 1694 } 1695 } 1696 1697 @Override 1698 public void loadAttachmentCallback(MessagingException result, long accountId, 1699 long messageId, long attachmentId, int progress) { 1700 if (messageId == mMessageId) { 1701 if (result == null) { 1702 showAttachmentProgress(attachmentId, progress); 1703 switch (progress) { 1704 case 100: 1705 final MessageViewAttachmentInfo attachmentInfo = 1706 findAttachmentInfoFromView(attachmentId); 1707 if (attachmentInfo != null) { 1708 updatePreviewIcon(attachmentInfo); 1709 } 1710 doFinishLoadAttachment(attachmentId); 1711 break; 1712 default: 1713 // do nothing - we don't have a progress bar at this time 1714 break; 1715 } 1716 } else { 1717 MessageViewAttachmentInfo attachment = findAttachmentInfo(attachmentId); 1718 if (attachment == null) { 1719 // Called before LoadAttachmentsTask finishes. 1720 // (Possible if you quickly close & re-open a message) 1721 return; 1722 } 1723 attachment.cancelButton.setVisibility(View.GONE); 1724 attachment.loadButton.setVisibility(View.VISIBLE); 1725 attachment.hideProgress(); 1726 1727 final String error; 1728 if (result.getCause() instanceof IOException) { 1729 error = mContext.getString(R.string.status_network_error); 1730 } else { 1731 error = mContext.getString( 1732 R.string.message_view_load_attachment_failed_toast, 1733 attachment.mName); 1734 } 1735 mCallback.onLoadMessageError(error); 1736 } 1737 } 1738 } 1739 1740 private void showAttachmentProgress(long attachmentId, int progress) { 1741 MessageViewAttachmentInfo attachment = findAttachmentInfo(attachmentId); 1742 if (attachment != null) { 1743 if (progress == 0) { 1744 attachment.cancelButton.setVisibility(View.GONE); 1745 } 1746 attachment.showProgress(progress); 1747 } 1748 } 1749 } 1750 1751 /** 1752 * Class to detect update on the current message (e.g. toggle star). When it gets content 1753 * change notifications, it kicks {@link ReloadMessageTask}. 1754 */ 1755 private class MessageObserver extends ContentObserver implements Runnable { 1756 private final Throttle mThrottle; 1757 private final ContentResolver mContentResolver; 1758 1759 private boolean mRegistered; 1760 1761 public MessageObserver(Handler handler, Context context) { 1762 super(handler); 1763 mContentResolver = context.getContentResolver(); 1764 mThrottle = new Throttle("MessageObserver", this, handler); 1765 } 1766 1767 public void unregister() { 1768 if (!mRegistered) { 1769 return; 1770 } 1771 mThrottle.cancelScheduledCallback(); 1772 mContentResolver.unregisterContentObserver(this); 1773 mRegistered = false; 1774 } 1775 1776 public void register(Uri notifyUri) { 1777 unregister(); 1778 mContentResolver.registerContentObserver(notifyUri, true, this); 1779 mRegistered = true; 1780 } 1781 1782 @Override 1783 public boolean deliverSelfNotifications() { 1784 return true; 1785 } 1786 1787 @Override 1788 public void onChange(boolean selfChange) { 1789 mThrottle.onEvent(); 1790 } 1791 1792 /** This method is delay-called by {@link Throttle} on the UI thread. */ 1793 @Override 1794 public void run() { 1795 // This method is delay-called, so need to make sure if it's still registered. 1796 if (mRegistered) { 1797 new ReloadMessageTask().cancelPreviousAndExecuteParallel(); 1798 } 1799 } 1800 } 1801 1802 private void updatePreviewIcon(MessageViewAttachmentInfo attachmentInfo) { 1803 new UpdatePreviewIconTask(attachmentInfo).executeParallel(); 1804 } 1805 1806 private class UpdatePreviewIconTask extends EmailAsyncTask<Void, Void, Bitmap> { 1807 @SuppressWarnings("hiding") 1808 private final Context mContext; 1809 private final MessageViewAttachmentInfo mAttachmentInfo; 1810 1811 public UpdatePreviewIconTask(MessageViewAttachmentInfo attachmentInfo) { 1812 super(mTaskTracker); 1813 mContext = getActivity(); 1814 mAttachmentInfo = attachmentInfo; 1815 } 1816 1817 @Override 1818 protected Bitmap doInBackground(Void... params) { 1819 return getPreviewIcon(mContext, mAttachmentInfo); 1820 } 1821 1822 @Override 1823 protected void onPostExecute(Bitmap result) { 1824 if (result == null) { 1825 return; 1826 } 1827 mAttachmentInfo.iconView.setImageBitmap(result); 1828 } 1829 } 1830 1831 public boolean isMessageLoadedForTest() { 1832 return mIsMessageLoadedForTest; 1833 } 1834 1835 public void clearIsMessageLoadedForTest() { 1836 mIsMessageLoadedForTest = true; 1837 } 1838} 1839