AbstractConversationViewFragment.java revision 022c7ea78f5ceb1b1633cf3df3c357a103eb3559
1/* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18package com.android.mail.ui; 19 20import android.animation.Animator; 21import android.animation.AnimatorInflater; 22import android.animation.AnimatorListenerAdapter; 23import android.app.Activity; 24import android.app.Fragment; 25import android.app.LoaderManager; 26import android.content.ActivityNotFoundException; 27import android.content.Context; 28import android.content.CursorLoader; 29import android.content.Intent; 30import android.content.Loader; 31import android.content.pm.ActivityInfo; 32import android.content.pm.PackageManager; 33import android.content.pm.ResolveInfo; 34import android.content.res.Resources; 35import android.database.Cursor; 36import android.database.DataSetObservable; 37import android.database.DataSetObserver; 38import android.net.Uri; 39import android.os.Bundle; 40import android.os.Handler; 41import android.provider.Browser; 42import android.util.Log; 43import android.view.Menu; 44import android.view.MenuInflater; 45import android.view.MenuItem; 46import android.view.View; 47import android.webkit.WebView; 48import android.webkit.WebViewClient; 49 50import com.android.mail.ContactInfo; 51import com.android.mail.ContactInfoSource; 52import com.android.mail.FormattedDateBuilder; 53import com.android.mail.R; 54import com.android.mail.SenderInfoLoader; 55import com.android.mail.browse.ConversationAccountController; 56import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks; 57import com.android.mail.browse.MessageCursor; 58import com.android.mail.browse.MessageCursor.ConversationController; 59import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks; 60import com.android.mail.providers.Account; 61import com.android.mail.providers.AccountObserver; 62import com.android.mail.providers.Address; 63import com.android.mail.providers.Conversation; 64import com.android.mail.providers.Folder; 65import com.android.mail.providers.ListParams; 66import com.android.mail.providers.UIProvider; 67import com.android.mail.providers.UIProvider.CursorStatus; 68import com.android.mail.utils.LogTag; 69import com.android.mail.utils.LogUtils; 70import com.android.mail.utils.Utils; 71import com.google.common.collect.ImmutableMap; 72 73import java.util.Arrays; 74import java.util.Collections; 75import java.util.HashMap; 76import java.util.List; 77import java.util.Map; 78import java.util.Set; 79 80public abstract class AbstractConversationViewFragment extends Fragment implements 81 ConversationController, ConversationAccountController, MessageHeaderViewCallbacks, 82 ConversationViewHeaderCallbacks { 83 84 private static final String ARG_ACCOUNT = "account"; 85 public static final String ARG_CONVERSATION = "conversation"; 86 private static final String ARG_FOLDER = "folder"; 87 private static final String LOG_TAG = LogTag.getLogTag(); 88 protected static final int MESSAGE_LOADER = 0; 89 protected static final int CONTACT_LOADER = 1; 90 private static int sMinDelay = -1; 91 private static int sMinShowTime = -1; 92 protected ControllableActivity mActivity; 93 private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks(); 94 protected FormattedDateBuilder mDateBuilder; 95 private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks(); 96 private MenuItem mChangeFoldersMenuItem; 97 protected Conversation mConversation; 98 protected Folder mFolder; 99 protected String mBaseUri; 100 protected Account mAccount; 101 /** 102 * Cache of email address strings to parsed Address objects. 103 * <p> 104 * Remember to synchronize on the map when reading or writing to this cache, because some 105 * instances use it off the UI thread (e.g. from WebView). 106 */ 107 protected final Map<String, Address> mAddressCache = Collections.synchronizedMap( 108 new HashMap<String, Address>()); 109 private MessageCursor mCursor; 110 private Context mContext; 111 /** 112 * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag, 113 * this flag is saved and restored. 114 */ 115 private boolean mUserVisible; 116 private View mProgressView; 117 private View mBackgroundView; 118 private final Handler mHandler = new Handler(); 119 /** True if we want to avoid marking the conversation as viewed and read. */ 120 private boolean mSuppressMarkingViewed; 121 /** 122 * Parcelable state of the conversation view. Can safely be used without null checking any time 123 * after {@link #onCreate(Bundle)}. 124 */ 125 protected ConversationViewState mViewState; 126 127 private long mLoadingShownTime = -1; 128 129 private boolean mIsDetached; 130 131 private boolean mHasConversationBeenTransformed; 132 private boolean mHasConversationTransformBeenReverted; 133 134 private final Runnable mDelayedShow = new FragmentRunnable("mDelayedShow") { 135 @Override 136 public void go() { 137 mLoadingShownTime = System.currentTimeMillis(); 138 mProgressView.setVisibility(View.VISIBLE); 139 } 140 }; 141 142 private final AccountObserver mAccountObserver = new AccountObserver() { 143 @Override 144 public void onChanged(Account newAccount) { 145 final Account oldAccount = mAccount; 146 mAccount = newAccount; 147 onAccountChanged(newAccount, oldAccount); 148 } 149 }; 150 151 private static final String BUNDLE_VIEW_STATE = 152 AbstractConversationViewFragment.class.getName() + "viewstate"; 153 /** 154 * We save the user visible flag so the various transitions that occur during rotation do not 155 * cause unnecessary visibility change. 156 */ 157 private static final String BUNDLE_USER_VISIBLE = 158 AbstractConversationViewFragment.class.getName() + "uservisible"; 159 160 private static final String BUNDLE_DETACHED = 161 AbstractConversationViewFragment.class.getName() + "detached"; 162 163 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED = 164 AbstractConversationViewFragment.class.getName() + "conversationtransformed"; 165 private static final String BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED = 166 AbstractConversationViewFragment.class.getName() + "conversationreverted"; 167 168 public static Bundle makeBasicArgs(Account account, Folder folder) { 169 Bundle args = new Bundle(); 170 args.putParcelable(ARG_ACCOUNT, account); 171 args.putParcelable(ARG_FOLDER, folder); 172 return args; 173 } 174 175 /** 176 * Constructor needs to be public to handle orientation changes and activity 177 * lifecycle events. 178 */ 179 public AbstractConversationViewFragment() { 180 super(); 181 } 182 183 /** 184 * Subclasses must override, since this depends on how many messages are 185 * shown in the conversation view. 186 */ 187 protected void markUnread() { 188 // Do not automatically mark this conversation viewed and read. 189 mSuppressMarkingViewed = true; 190 } 191 192 /** 193 * Subclasses must override this, since they may want to display a single or 194 * many messages related to this conversation. 195 */ 196 protected abstract void onMessageCursorLoadFinished(Loader<Cursor> loader, 197 MessageCursor newCursor, MessageCursor oldCursor); 198 199 /** 200 * Subclasses must override this, since they may want to display a single or 201 * many messages related to this conversation. 202 */ 203 @Override 204 public abstract void onConversationViewHeaderHeightChange(int newHeight); 205 206 public abstract void onUserVisibleHintChanged(); 207 208 /** 209 * Subclasses must override this. 210 */ 211 protected abstract void onAccountChanged(Account newAccount, Account oldAccount); 212 213 @Override 214 public void onCreate(Bundle savedState) { 215 super.onCreate(savedState); 216 217 final Bundle args = getArguments(); 218 mAccount = args.getParcelable(ARG_ACCOUNT); 219 mConversation = args.getParcelable(ARG_CONVERSATION); 220 mFolder = args.getParcelable(ARG_FOLDER); 221 222 // Since the uri specified in the conversation base uri may not be unique, we specify a 223 // base uri that us guaranteed to be unique for this conversation. 224 mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id; 225 226 LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this); 227 // Not really, we just want to get a crack to store a reference to the change_folder item 228 setHasOptionsMenu(true); 229 230 if (savedState != null) { 231 mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE); 232 mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE); 233 mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false); 234 mHasConversationBeenTransformed = 235 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, false); 236 mHasConversationTransformBeenReverted = 237 savedState.getBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, false); 238 } else { 239 mViewState = getNewViewState(); 240 mHasConversationBeenTransformed = false; 241 mHasConversationTransformBeenReverted = false; 242 } 243 } 244 245 @Override 246 public String toString() { 247 // log extra info at DEBUG level or finer 248 final String s = super.toString(); 249 if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) { 250 return s; 251 } 252 return "(" + s + " conv=" + mConversation + ")"; 253 } 254 255 protected abstract WebView getWebView(); 256 257 public void instantiateProgressIndicators(View rootView) { 258 mBackgroundView = rootView.findViewById(R.id.background_view); 259 mProgressView = rootView.findViewById(R.id.loading_progress); 260 } 261 262 protected void dismissLoadingStatus() { 263 dismissLoadingStatus(null); 264 } 265 266 /** 267 * Begin the fade-out animation to hide the Progress overlay, either immediately or after some 268 * timeout (to ensure that the progress minimum time elapses). 269 * 270 * @param doAfter an optional Runnable action to execute after the animation completes 271 */ 272 protected void dismissLoadingStatus(final Runnable doAfter) { 273 if (mLoadingShownTime == -1) { 274 // The runnable hasn't run yet, so just remove it. 275 mHandler.removeCallbacks(mDelayedShow); 276 dismiss(doAfter); 277 return; 278 } 279 final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime); 280 if (diff > sMinShowTime) { 281 dismiss(doAfter); 282 } else { 283 mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus") { 284 @Override 285 public void go() { 286 dismiss(doAfter); 287 } 288 }, Math.abs(sMinShowTime - diff)); 289 } 290 } 291 292 private void dismiss(final Runnable doAfter) { 293 // Reset loading shown time. 294 mLoadingShownTime = -1; 295 mProgressView.setVisibility(View.GONE); 296 if (mBackgroundView.getVisibility() == View.VISIBLE) { 297 animateDismiss(doAfter); 298 } else { 299 if (doAfter != null) { 300 doAfter.run(); 301 } 302 } 303 } 304 305 private void animateDismiss(final Runnable doAfter) { 306 // the animation can only work (and is only worth doing) if this fragment is added 307 // reasons it may not be added: fragment is being destroyed, or in the process of being 308 // restored 309 if (!isAdded()) { 310 mBackgroundView.setVisibility(View.GONE); 311 return; 312 } 313 314 Utils.enableHardwareLayer(mBackgroundView); 315 final Animator animator = AnimatorInflater.loadAnimator(getContext(), R.anim.fade_out); 316 animator.setTarget(mBackgroundView); 317 animator.addListener(new AnimatorListenerAdapter() { 318 @Override 319 public void onAnimationEnd(Animator animation) { 320 mBackgroundView.setVisibility(View.GONE); 321 mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null); 322 if (doAfter != null) { 323 doAfter.run(); 324 } 325 } 326 }); 327 animator.start(); 328 } 329 330 @Override 331 public void onActivityCreated(Bundle savedInstanceState) { 332 super.onActivityCreated(savedInstanceState); 333 final Activity activity = getActivity(); 334 if (!(activity instanceof ControllableActivity)) { 335 LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to" 336 + "create it. Cannot proceed."); 337 } 338 if (activity == null || activity.isFinishing()) { 339 // Activity is finishing, just bail. 340 return; 341 } 342 mActivity = (ControllableActivity) activity; 343 mContext = activity.getApplicationContext(); 344 mDateBuilder = new FormattedDateBuilder((Context) mActivity); 345 mAccount = mAccountObserver.initialize(mActivity.getAccountController()); 346 } 347 348 @Override 349 public ConversationUpdater getListController() { 350 final ControllableActivity activity = (ControllableActivity) getActivity(); 351 return activity != null ? activity.getConversationUpdater() : null; 352 } 353 354 355 protected void showLoadingStatus() { 356 if (!mUserVisible) { 357 return; 358 } 359 if (sMinDelay == -1) { 360 Resources res = getContext().getResources(); 361 sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay); 362 sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading); 363 } 364 // If the loading view isn't already showing, show it and remove any 365 // pending calls to show the loading screen. 366 mBackgroundView.setVisibility(View.VISIBLE); 367 mHandler.removeCallbacks(mDelayedShow); 368 mHandler.postDelayed(mDelayedShow, sMinDelay); 369 } 370 371 public Context getContext() { 372 return mContext; 373 } 374 375 @Override 376 public Conversation getConversation() { 377 return mConversation; 378 } 379 380 @Override 381 public MessageCursor getMessageCursor() { 382 return mCursor; 383 } 384 385 public Handler getHandler() { 386 return mHandler; 387 } 388 389 public MessageLoaderCallbacks getMessageLoaderCallbacks() { 390 return mMessageLoaderCallbacks; 391 } 392 393 public ContactLoaderCallbacks getContactInfoSource() { 394 return mContactLoaderCallbacks; 395 } 396 397 @Override 398 public Account getAccount() { 399 return mAccount; 400 } 401 402 @Override 403 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 404 super.onCreateOptionsMenu(menu, inflater); 405 mChangeFoldersMenuItem = menu.findItem(R.id.change_folder); 406 } 407 408 @Override 409 public boolean onOptionsItemSelected(MenuItem item) { 410 if (!isUserVisible()) { 411 // Unclear how this is happening. Current theory is that this fragment was scheduled 412 // to be removed, but the remove transaction failed. When the Activity is later 413 // restored, the FragmentManager restores this fragment, but Fragment.mMenuVisible is 414 // stuck at its initial value (true), which makes this zombie fragment eligible for 415 // menu item clicks. 416 // 417 // Work around this by relying on the (properly restored) extra user visible hint. 418 LogUtils.e(LOG_TAG, 419 "ACVF ignoring onOptionsItemSelected b/c userVisibleHint is false. f=%s", this); 420 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 421 Log.e(LOG_TAG, Utils.dumpFragment(this)); // the dump has '%' chars in it... 422 } 423 return false; 424 } 425 426 boolean handled = false; 427 switch (item.getItemId()) { 428 case R.id.inside_conversation_unread: 429 markUnread(); 430 handled = true; 431 break; 432 case R.id.show_original: 433 showUntransformedConversation(); 434 handled = true; 435 break; 436 } 437 return handled; 438 } 439 440 @Override 441 public void onPrepareOptionsMenu(Menu menu) { 442 // Only show option if we support message transforms and message has been transformed. 443 Utils.setMenuItemVisibility(menu, R.id.show_original, supportsMessageTransforms() && 444 mHasConversationBeenTransformed && !mHasConversationTransformBeenReverted); 445 } 446 447 // BEGIN conversation header callbacks 448 @Override 449 public void onFoldersClicked() { 450 if (mChangeFoldersMenuItem == null) { 451 LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation"); 452 return; 453 } 454 mActivity.onOptionsItemSelected(mChangeFoldersMenuItem); 455 } 456 // END conversation header callbacks 457 458 @Override 459 public void onSaveInstanceState(Bundle outState) { 460 if (mViewState != null) { 461 outState.putParcelable(BUNDLE_VIEW_STATE, mViewState); 462 } 463 outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible); 464 outState.putBoolean(BUNDLE_DETACHED, mIsDetached); 465 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_TRANSFORMED, 466 mHasConversationBeenTransformed); 467 outState.putBoolean(BUNDLE_KEY_HAS_CONVERSATION_BEEN_REVERTED, 468 mHasConversationTransformBeenReverted); 469 } 470 471 @Override 472 public void onDestroyView() { 473 super.onDestroyView(); 474 mAccountObserver.unregisterAndDestroy(); 475 } 476 477 /** 478 * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for 479 * reliability on older platforms. 480 */ 481 public void setExtraUserVisibleHint(boolean isVisibleToUser) { 482 LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this); 483 if (mUserVisible != isVisibleToUser) { 484 mUserVisible = isVisibleToUser; 485 MessageCursor cursor = getMessageCursor(); 486 if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) { 487 // Pop back to conversation list and show error. 488 onError(); 489 return; 490 } 491 onUserVisibleHintChanged(); 492 } 493 } 494 495 public boolean isUserVisible() { 496 return mUserVisible; 497 } 498 499 protected void timerMark(String msg) { 500 if (isUserVisible()) { 501 Utils.sConvLoadTimer.mark(msg); 502 } 503 } 504 505 private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> { 506 507 @Override 508 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 509 return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri); 510 } 511 512 @Override 513 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 514 // ignore truly duplicate results 515 // this can happen when restoring after rotation 516 if (mCursor == data) { 517 return; 518 } else { 519 final MessageCursor messageCursor = (MessageCursor) data; 520 521 // bind the cursor to this fragment so it can access to the current list controller 522 messageCursor.setController(AbstractConversationViewFragment.this); 523 524 if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) { 525 LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump()); 526 } 527 528 // We have no messages: exit conversation view. 529 if (messageCursor.getCount() == 0 530 && (!CursorStatus.isWaitingForResults(messageCursor.getStatus()) 531 || mIsDetached)) { 532 if (mUserVisible) { 533 onError(); 534 } else { 535 // we expect that the pager adapter will remove this 536 // conversation fragment on its own due to a separate 537 // conversation cursor update (we might get here if the 538 // message list update fires first. nothing to do 539 // because we expect to be torn down soon.) 540 LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update" 541 + " in anticipation of conv cursor update. c=%s", mConversation.uri); 542 } 543 // existing mCursor will imminently be closed, must stop referencing it 544 // since we expect to be kicked out soon, it doesn't matter what mCursor 545 // becomes 546 mCursor = null; 547 return; 548 } 549 550 // ignore cursors that are still loading results 551 if (!messageCursor.isLoaded()) { 552 // existing mCursor will imminently be closed, must stop referencing it 553 // in this case, the new cursor is also no good, and since don't expect to get 554 // here except in initial load situations, it's safest to just ensure the 555 // reference is null 556 mCursor = null; 557 return; 558 } 559 final MessageCursor oldCursor = mCursor; 560 mCursor = messageCursor; 561 onMessageCursorLoadFinished(loader, mCursor, oldCursor); 562 } 563 } 564 565 @Override 566 public void onLoaderReset(Loader<Cursor> loader) { 567 mCursor = null; 568 } 569 570 } 571 572 private void onError() { 573 // need to exit this view- conversation may have been 574 // deleted, or for whatever reason is now invalid (e.g. 575 // discard single draft) 576 // 577 // N.B. this may involve a fragment transaction, which 578 // FragmentManager will refuse to execute directly 579 // within onLoadFinished. Make sure the controller knows. 580 LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode"); 581 // TODO(mindyp): handle ERROR status by showing an error 582 // message to the user that there are no messages in 583 // this conversation 584 popOut(); 585 } 586 587 private void popOut() { 588 mHandler.post(new FragmentRunnable("popOut") { 589 @Override 590 public void go() { 591 mActivity.getListHandler() 592 .onConversationSelected(null, true /* inLoaderCallbacks */); 593 } 594 }); 595 } 596 597 protected void onConversationSeen() { 598 // Ignore unsafe calls made after a fragment is detached from an activity 599 final ControllableActivity activity = (ControllableActivity) getActivity(); 600 if (activity == null) { 601 LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id); 602 return; 603 } 604 605 mViewState.setInfoForConversation(mConversation); 606 607 // In most circumstances we want to mark the conversation as viewed and read, since the 608 // user has read it. However, if the user has already marked the conversation unread, we 609 // do not want a later mark-read operation to undo this. So we check this variable which 610 // is set in #markUnread() which suppresses automatic mark-read. 611 if (!mSuppressMarkingViewed) { 612 // mark viewed/read if not previously marked viewed by this conversation view, 613 // or if unread messages still exist in the message list cursor 614 // we don't want to keep marking viewed on rotation or restore 615 // but we do want future re-renders to mark read (e.g. "New message from X" case) 616 final MessageCursor cursor = getMessageCursor(); 617 if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) { 618 // Mark the conversation viewed and read. 619 activity.getConversationUpdater() 620 .markConversationsRead(Arrays.asList(mConversation), true, true); 621 622 // and update the Message objects in the cursor so the next time a cursor update 623 // happens with these messages marked read, we know to ignore it 624 if (cursor != null && !cursor.isClosed()) { 625 cursor.markMessagesRead(); 626 } 627 } 628 } 629 activity.getListHandler().onConversationSeen(mConversation); 630 } 631 632 protected ConversationViewState getNewViewState() { 633 return new ConversationViewState(); 634 } 635 636 private static class MessageLoader extends CursorLoader { 637 private boolean mDeliveredFirstResults = false; 638 639 public MessageLoader(Context c, Uri messageListUri) { 640 super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null); 641 } 642 643 @Override 644 public Cursor loadInBackground() { 645 return new MessageCursor(super.loadInBackground()); 646 } 647 648 @Override 649 public void deliverResult(Cursor result) { 650 // We want to deliver these results, and then we want to make sure 651 // that any subsequent 652 // queries do not hit the network 653 super.deliverResult(result); 654 655 if (!mDeliveredFirstResults) { 656 mDeliveredFirstResults = true; 657 Uri uri = getUri(); 658 659 // Create a ListParams that tells the provider to not hit the 660 // network 661 final ListParams listParams = new ListParams(ListParams.NO_LIMIT, 662 false /* useNetwork */); 663 664 // Build the new uri with this additional parameter 665 uri = uri 666 .buildUpon() 667 .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER, 668 listParams.serialize()).build(); 669 setUri(uri); 670 } 671 } 672 } 673 674 /** 675 * Inner class to to asynchronously load contact data for all senders in the conversation, 676 * and notify observers when the data is ready. 677 * 678 */ 679 protected class ContactLoaderCallbacks implements ContactInfoSource, 680 LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> { 681 682 private Set<String> mSenders; 683 private ImmutableMap<String, ContactInfo> mContactInfoMap; 684 private DataSetObservable mObservable = new DataSetObservable(); 685 686 public void setSenders(Set<String> emailAddresses) { 687 mSenders = emailAddresses; 688 } 689 690 @Override 691 public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) { 692 return new SenderInfoLoader(mActivity.getActivityContext(), mSenders); 693 } 694 695 @Override 696 public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader, 697 ImmutableMap<String, ContactInfo> data) { 698 mContactInfoMap = data; 699 mObservable.notifyChanged(); 700 } 701 702 @Override 703 public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) { 704 } 705 706 @Override 707 public ContactInfo getContactInfo(String email) { 708 if (mContactInfoMap == null) { 709 return null; 710 } 711 return mContactInfoMap.get(email); 712 } 713 714 @Override 715 public void registerObserver(DataSetObserver observer) { 716 mObservable.registerObserver(observer); 717 } 718 719 @Override 720 public void unregisterObserver(DataSetObserver observer) { 721 mObservable.unregisterObserver(observer); 722 } 723 } 724 725 protected class AbstractConversationWebViewClient extends WebViewClient { 726 @Override 727 public boolean shouldOverrideUrlLoading(WebView view, String url) { 728 final Activity activity = getActivity(); 729 if (activity == null) { 730 return false; 731 } 732 733 boolean result = false; 734 final Intent intent; 735 Uri uri = Uri.parse(url); 736 if (!Utils.isEmpty(mAccount.viewIntentProxyUri)) { 737 intent = generateProxyIntent(uri); 738 } else { 739 intent = new Intent(Intent.ACTION_VIEW, uri); 740 intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName()); 741 } 742 743 try { 744 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); 745 activity.startActivity(intent); 746 result = true; 747 } catch (ActivityNotFoundException ex) { 748 // If no application can handle the URL, assume that the 749 // caller can handle it. 750 } 751 752 return result; 753 } 754 755 private Intent generateProxyIntent(Uri uri) { 756 final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri); 757 intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri); 758 intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount); 759 760 final Context context = getContext(); 761 PackageManager manager = null; 762 // We need to catch the exception to make CanvasConversationHeaderView 763 // test pass. Bug: http://b/issue?id=3470653. 764 try { 765 manager = context.getPackageManager(); 766 } catch (UnsupportedOperationException e) { 767 LogUtils.e(LOG_TAG, e, "Error getting package manager"); 768 } 769 770 if (manager != null) { 771 // Try and resolve the intent, to find an activity from this package 772 final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities( 773 intent, PackageManager.MATCH_DEFAULT_ONLY); 774 775 final String packageName = context.getPackageName(); 776 777 // Now try and find one that came from this package, if one is not found, the UI 778 // provider must have specified an intent that is to be handled by a different apk. 779 // In that case, the class name will not be set on the intent, so the default 780 // intent resolution will be used. 781 for (ResolveInfo resolveInfo: resolvedActivities) { 782 final ActivityInfo activityInfo = resolveInfo.activityInfo; 783 if (packageName.equals(activityInfo.packageName)) { 784 intent.setClassName(activityInfo.packageName, activityInfo.name); 785 break; 786 } 787 } 788 } 789 790 return intent; 791 } 792 } 793 794 public abstract void onConversationUpdated(Conversation conversation); 795 796 /** 797 * Small Runnable-like wrapper that first checks that the Fragment is in a good state before 798 * doing any work. Ideal for use with a {@link Handler}. 799 */ 800 protected abstract class FragmentRunnable implements Runnable { 801 802 private final String mOpName; 803 804 public FragmentRunnable(String opName) { 805 mOpName = opName; 806 } 807 808 public abstract void go(); 809 810 @Override 811 public void run() { 812 if (!isAdded()) { 813 LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s", 814 mOpName, AbstractConversationViewFragment.this); 815 return; 816 } 817 go(); 818 } 819 820 } 821 822 public void onDetachedModeEntered() { 823 // If we have no messages, then we have nothing to display, so leave this view. 824 // Otherwise, just set the detached flag. 825 final Cursor messageCursor = getMessageCursor(); 826 827 if (messageCursor == null || messageCursor.getCount() == 0) { 828 popOut(); 829 } else { 830 mIsDetached = true; 831 } 832 } 833 834 /** 835 * Called when the JavaScript reports that it transformed a message. 836 * Sets a flag to true and invalidates the options menu so it will 837 * include the "Revert auto-sizing" menu option. 838 */ 839 public void onConversationTransformed() { 840 mHasConversationBeenTransformed = true; 841 mActivity.invalidateOptionsMenu(); 842 } 843 844 /** 845 * Called when the "Revert auto-sizing" option is selected. Default 846 * implementation simply sets a value on whether transforms should be 847 * applied. Derived classes should override this class and force a 848 * re-render so that the conversation renders without 849 */ 850 public void showUntransformedConversation() { 851 // must set the value to true so we don't show the options menu item again 852 mHasConversationTransformBeenReverted = true; 853 } 854 855 /** 856 * Returns {@code true} if the conversation should be transformed. {@code false}, otherwise. 857 * @return {@code true} if the conversation should be transformed. {@code false}, otherwise. 858 */ 859 public boolean shouldApplyTransforms() { 860 return !mHasConversationTransformBeenReverted; 861 } 862} 863