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