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