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