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