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