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