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