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