1 // Copyright 2020 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.feed.v2; 6 7 import android.animation.ObjectAnimator; 8 import android.animation.PropertyValuesHolder; 9 import android.app.Activity; 10 import android.content.Context; 11 import android.os.Handler; 12 import android.view.ContextThemeWrapper; 13 import android.view.View; 14 import android.view.ViewParent; 15 16 import androidx.annotation.Nullable; 17 import androidx.annotation.VisibleForTesting; 18 import androidx.recyclerview.widget.LinearLayoutManager; 19 import androidx.recyclerview.widget.RecyclerView; 20 import androidx.recyclerview.widget.RecyclerView.ItemAnimator.ItemAnimatorFinishedListener; 21 22 import org.chromium.base.Callback; 23 import org.chromium.base.Log; 24 import org.chromium.base.ObserverList; 25 import org.chromium.base.ThreadUtils; 26 import org.chromium.base.annotations.CalledByNative; 27 import org.chromium.base.annotations.JNINamespace; 28 import org.chromium.base.annotations.NativeMethods; 29 import org.chromium.base.supplier.Supplier; 30 import org.chromium.base.task.PostTask; 31 import org.chromium.chrome.R; 32 import org.chromium.chrome.browser.AppHooks; 33 import org.chromium.chrome.browser.feed.shared.ScrollTracker; 34 import org.chromium.chrome.browser.feed.shared.stream.Stream.ContentChangedListener; 35 import org.chromium.chrome.browser.feedback.HelpAndFeedbackLauncher; 36 import org.chromium.chrome.browser.flags.ChromeFeatureList; 37 import org.chromium.chrome.browser.native_page.NativePageNavigationDelegate; 38 import org.chromium.chrome.browser.ntp.NewTabPageUma; 39 import org.chromium.chrome.browser.offlinepages.OfflinePageBridge; 40 import org.chromium.chrome.browser.offlinepages.RequestCoordinatorBridge; 41 import org.chromium.chrome.browser.profiles.Profile; 42 import org.chromium.chrome.browser.signin.IdentityServicesProvider; 43 import org.chromium.chrome.browser.suggestions.NavigationRecorder; 44 import org.chromium.chrome.browser.suggestions.SuggestionsConfig; 45 import org.chromium.chrome.browser.tab.EmptyTabObserver; 46 import org.chromium.chrome.browser.tab.Tab; 47 import org.chromium.chrome.browser.ui.messages.snackbar.Snackbar; 48 import org.chromium.chrome.browser.ui.messages.snackbar.SnackbarManager; 49 import org.chromium.chrome.browser.xsurface.FeedActionsHandler; 50 import org.chromium.chrome.browser.xsurface.HybridListRenderer; 51 import org.chromium.chrome.browser.xsurface.ProcessScope; 52 import org.chromium.chrome.browser.xsurface.SurfaceActionsHandler; 53 import org.chromium.chrome.browser.xsurface.SurfaceScope; 54 import org.chromium.chrome.browser.xsurface.SurfaceScopeDependencyProvider; 55 import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent; 56 import org.chromium.components.browser_ui.bottomsheet.BottomSheetController; 57 import org.chromium.components.browser_ui.share.ShareHelper; 58 import org.chromium.components.browser_ui.share.ShareParams; 59 import org.chromium.components.browser_ui.widget.animation.Interpolators; 60 import org.chromium.components.feed.proto.FeedUiProto.SharedState; 61 import org.chromium.components.feed.proto.FeedUiProto.Slice; 62 import org.chromium.components.feed.proto.FeedUiProto.StreamUpdate; 63 import org.chromium.components.feed.proto.FeedUiProto.StreamUpdate.SliceUpdate; 64 import org.chromium.components.feed.proto.FeedUiProto.ZeroStateSlice; 65 import org.chromium.components.signin.base.CoreAccountInfo; 66 import org.chromium.components.signin.identitymanager.ConsentLevel; 67 import org.chromium.content_public.browser.LoadUrlParams; 68 import org.chromium.content_public.browser.UiThreadTaskTraits; 69 import org.chromium.content_public.common.Referrer; 70 import org.chromium.network.mojom.ReferrerPolicy; 71 import org.chromium.ui.base.PageTransition; 72 import org.chromium.ui.mojom.WindowOpenDisposition; 73 74 import java.util.ArrayList; 75 import java.util.HashMap; 76 import java.util.HashSet; 77 import java.util.List; 78 import java.util.Map; 79 80 /** 81 * Bridge class that lets Android code access native code for feed related functionalities. 82 * 83 * Created once for each StreamSurfaceMediator corresponding to each NTP/start surface. 84 */ 85 @JNINamespace("feed") 86 public class FeedStreamSurface implements SurfaceActionsHandler, FeedActionsHandler { 87 private static final String TAG = "FeedStreamSurface"; 88 89 private static final int SNACKBAR_DURATION_MS_SHORT = 4000; 90 private static final int SNACKBAR_DURATION_MS_LONG = 10000; 91 92 @VisibleForTesting 93 static final String FEEDBACK_REPORT_TYPE = 94 "com.google.chrome.feed.USER_INITIATED_FEEDBACK_REPORT"; 95 @VisibleForTesting 96 static final String FEEDBACK_CONTEXT = "mobile_browser"; 97 @VisibleForTesting 98 static final String XSURFACE_CARD_URL = "Card URL"; 99 100 private final long mNativeFeedStreamSurface; 101 private final FeedListContentManager mContentManager; 102 private final SurfaceScope mSurfaceScope; 103 @VisibleForTesting 104 RecyclerView mRootView; 105 private final HybridListRenderer mHybridListRenderer; 106 private final SnackbarManager mSnackbarManager; 107 private final Activity mActivity; 108 private final BottomSheetController mBottomSheetController; 109 @Nullable 110 private FeedSliceViewTracker mSliceViewTracker; 111 private final NativePageNavigationDelegate mPageNavigationDelegate; 112 private final HelpAndFeedbackLauncher mHelpAndFeedbackLauncher; 113 private final ScrollReporter mScrollReporter = new ScrollReporter(); 114 private final ObserverList<ContentChangedListener> mContentChangedListeners = 115 new ObserverList<ContentChangedListener>(); 116 private final RecyclerViewAnimationFinishDetector mRecyclerViewAnimationFinishDetector = 117 new RecyclerViewAnimationFinishDetector(); 118 // True after onSurfaceOpened(), and before onSurfaceClosed(). 119 private boolean mOpened; 120 private boolean mStreamContentVisible; 121 private boolean mStreamVisible; 122 private int mHeaderCount; 123 private BottomSheetContent mBottomSheetContent; 124 // If the bottom sheet was opened in response to an action on a slice, this is the slice ID. 125 private String mBottomSheetOriginatingSliceId; 126 private final int mLoadMoreTriggerLookahead; 127 private boolean mIsLoadingMoreContent; 128 private boolean mIsPlaceholderShown; 129 // TabSupplier for the current tab to share. 130 private final ShareHelperWrapper mShareHelper; 131 132 private static ProcessScope sXSurfaceProcessScope; 133 xSurfaceProcessScope()134 public static ProcessScope xSurfaceProcessScope() { 135 if (sXSurfaceProcessScope == null) { 136 sXSurfaceProcessScope = AppHooks.get().getExternalSurfaceProcessScope( 137 new FeedProcessScopeDependencyProvider()); 138 } 139 return sXSurfaceProcessScope; 140 } 141 142 // This must match the FeedSendFeedbackType enum in enums.xml. 143 public @interface FeedFeedbackType { 144 int FEEDBACK_TAPPED_ON_CARD = 0; 145 int FEEDBACK_TAPPED_ON_PAGE = 1; 146 int NUM_ENTRIES = 2; 147 } 148 149 // We avoid attaching surfaces until after |startup()| is called. This ensures that 150 // the correct sign-in state is used if attaching the surface triggers a fetch. 151 private static boolean sStartupCalled; 152 // Tracks all the instances of FeedStreamSurface. 153 @VisibleForTesting 154 static HashSet<FeedStreamSurface> sSurfaces; 155 startup()156 public static void startup() { 157 if (sStartupCalled) return; 158 sStartupCalled = true; 159 FeedServiceBridge.startup(); 160 if (sSurfaces != null) { 161 for (FeedStreamSurface surface : sSurfaces) { 162 surface.updateSurfaceOpenState(); 163 } 164 } 165 } 166 167 // Only called for cleanup during testing. 168 @VisibleForTesting shutdownForTesting()169 static void shutdownForTesting() { 170 sStartupCalled = false; 171 sSurfaces = null; 172 sXSurfaceProcessScope = null; 173 } 174 trackSurface(FeedStreamSurface surface)175 private static void trackSurface(FeedStreamSurface surface) { 176 if (sSurfaces == null) { 177 sSurfaces = new HashSet<FeedStreamSurface>(); 178 } 179 sSurfaces.add(surface); 180 } 181 untrackSurface(FeedStreamSurface surface)182 private static void untrackSurface(FeedStreamSurface surface) { 183 if (sSurfaces != null) { 184 sSurfaces.remove(surface); 185 } 186 } 187 188 /** 189 * Clear all the data related to all surfaces. 190 */ clearAll()191 public static void clearAll() { 192 if (sSurfaces == null) return; 193 194 ArrayList<FeedStreamSurface> openSurfaces = new ArrayList<FeedStreamSurface>(); 195 for (FeedStreamSurface surface : sSurfaces) { 196 if (surface.isOpened()) openSurfaces.add(surface); 197 } 198 for (FeedStreamSurface surface : openSurfaces) { 199 surface.onSurfaceClosed(); 200 } 201 202 ProcessScope processScope = xSurfaceProcessScope(); 203 if (processScope != null) { 204 processScope.resetAccount(); 205 } 206 207 for (FeedStreamSurface surface : openSurfaces) { 208 surface.updateSurfaceOpenState(); 209 } 210 } 211 212 /** 213 * Provides a wrapper around sharing methods. 214 * 215 * Makes it easier to test. 216 */ 217 public static class ShareHelperWrapper { 218 private Supplier<Tab> mTabSupplier; ShareHelperWrapper(Supplier<Tab> tabSupplier)219 public ShareHelperWrapper(Supplier<Tab> tabSupplier) { 220 mTabSupplier = tabSupplier; 221 } 222 223 /** 224 * Shares a url and title from Chrome to another app. 225 * Brings up the share sheet. 226 */ share(String url, String title)227 public void share(String url, String title) { 228 ShareParams params = 229 new ShareParams.Builder(mTabSupplier.get().getWindowAndroid(), url, title) 230 .build(); 231 ShareHelper.shareWithUi(params); 232 } 233 } 234 235 /** 236 * Provides activity and darkmode context for a single surface. 237 */ 238 private class FeedSurfaceScopeDependencyProvider implements SurfaceScopeDependencyProvider { 239 final Context mActivityContext; 240 final boolean mDarkMode; 241 FeedSurfaceScopeDependencyProvider(Context activityContext, boolean darkMode)242 FeedSurfaceScopeDependencyProvider(Context activityContext, boolean darkMode) { 243 mActivityContext = 244 FeedProcessScopeDependencyProvider.createFeedContext(activityContext); 245 mDarkMode = darkMode; 246 } 247 248 @Override getActivityContext()249 public Context getActivityContext() { 250 return mActivityContext; 251 } 252 253 @Override isDarkModeEnabled()254 public boolean isDarkModeEnabled() { 255 return mDarkMode; 256 } 257 258 @Override isActivityLoggingEnabled()259 public boolean isActivityLoggingEnabled() { 260 return FeedStreamSurfaceJni.get().isActivityLoggingEnabled( 261 mNativeFeedStreamSurface, FeedStreamSurface.this); 262 } 263 264 @Override getAccountName()265 public String getAccountName() { 266 // Don't return account name if there's a signed-out session ID. 267 if (!getSignedOutSessionId().isEmpty()) { 268 return ""; 269 } 270 assert ThreadUtils.runningOnUiThread(); 271 CoreAccountInfo primaryAccount = 272 IdentityServicesProvider.get() 273 .getIdentityManager(Profile.getLastUsedRegularProfile()) 274 .getPrimaryAccountInfo(ConsentLevel.NOT_REQUIRED); 275 return (primaryAccount == null) ? "" : primaryAccount.getEmail(); 276 } 277 278 @Override getExperimentIds()279 public int[] getExperimentIds() { 280 assert ThreadUtils.runningOnUiThread(); 281 return FeedStreamSurfaceJni.get().getExperimentIds(); 282 } 283 284 @Override getClientInstanceId()285 public String getClientInstanceId() { 286 // Don't return client instance id if there's a signed-out session ID. 287 if (!getSignedOutSessionId().isEmpty()) { 288 return ""; 289 } 290 assert ThreadUtils.runningOnUiThread(); 291 return FeedServiceBridge.getClientInstanceId(); 292 } 293 294 @Override getSignedOutSessionId()295 public String getSignedOutSessionId() { 296 assert ThreadUtils.runningOnUiThread(); 297 return FeedStreamSurfaceJni.get().getSessionId( 298 mNativeFeedStreamSurface, FeedStreamSurface.this); 299 } 300 } 301 302 /** 303 * A {@link TabObserver} that observes navigation related events that originate from Feed 304 * interactions. Calls reportPageLoaded when navigation completes. 305 */ 306 private class FeedTabNavigationObserver extends EmptyTabObserver { 307 private final boolean mInNewTab; 308 FeedTabNavigationObserver(boolean inNewTab)309 FeedTabNavigationObserver(boolean inNewTab) { 310 mInNewTab = inNewTab; 311 } 312 313 @Override onPageLoadFinished(Tab tab, String url)314 public void onPageLoadFinished(Tab tab, String url) { 315 // TODO(jianli): onPageLoadFinished is called on successful load, and if a user manually 316 // stops the page load. We should only capture successful page loads. 317 FeedStreamSurfaceJni.get().reportPageLoaded( 318 mNativeFeedStreamSurface, FeedStreamSurface.this, url, mInNewTab); 319 tab.removeObserver(this); 320 } 321 322 @Override onPageLoadFailed(Tab tab, int errorCode)323 public void onPageLoadFailed(Tab tab, int errorCode) { 324 tab.removeObserver(this); 325 } 326 327 @Override onCrash(Tab tab)328 public void onCrash(Tab tab) { 329 tab.removeObserver(this); 330 } 331 332 @Override onDestroyed(Tab tab)333 public void onDestroyed(Tab tab) { 334 tab.removeObserver(this); 335 } 336 } 337 338 /** 339 * Creates a {@link FeedStreamSurface} for creating native side bridge to access native feed 340 * client implementation. 341 */ FeedStreamSurface(Activity activity, boolean isBackgroundDark, SnackbarManager snackbarManager, NativePageNavigationDelegate pageNavigationDelegate, BottomSheetController bottomSheetController, HelpAndFeedbackLauncher helpAndFeedbackLauncher, boolean isPlaceholderShown, ShareHelperWrapper shareHelper)342 public FeedStreamSurface(Activity activity, boolean isBackgroundDark, 343 SnackbarManager snackbarManager, NativePageNavigationDelegate pageNavigationDelegate, 344 BottomSheetController bottomSheetController, 345 HelpAndFeedbackLauncher helpAndFeedbackLauncher, boolean isPlaceholderShown, 346 ShareHelperWrapper shareHelper) { 347 mNativeFeedStreamSurface = FeedStreamSurfaceJni.get().init(FeedStreamSurface.this); 348 mSnackbarManager = snackbarManager; 349 mActivity = activity; 350 mHelpAndFeedbackLauncher = helpAndFeedbackLauncher; 351 352 mPageNavigationDelegate = pageNavigationDelegate; 353 mBottomSheetController = bottomSheetController; 354 mLoadMoreTriggerLookahead = FeedServiceBridge.getLoadMoreTriggerLookahead(); 355 356 mContentManager = new FeedListContentManager(this, this); 357 358 mIsPlaceholderShown = isPlaceholderShown; 359 mShareHelper = shareHelper; 360 361 Context context = new ContextThemeWrapper( 362 activity, (isBackgroundDark ? R.style.Dark : R.style.Light)); 363 364 ProcessScope processScope = xSurfaceProcessScope(); 365 if (processScope != null) { 366 mSurfaceScope = processScope.obtainSurfaceScope( 367 new FeedSurfaceScopeDependencyProvider(context, isBackgroundDark)); 368 } else { 369 mSurfaceScope = null; 370 } 371 372 if (mSurfaceScope != null) { 373 mHybridListRenderer = mSurfaceScope.provideListRenderer(); 374 } else { 375 mHybridListRenderer = new NativeViewListRenderer(context); 376 } 377 378 if (mHybridListRenderer != null) { 379 // XSurface returns a View, but it should be a RecyclerView. 380 mRootView = (RecyclerView) mHybridListRenderer.bind(mContentManager); 381 382 mSliceViewTracker = 383 new FeedSliceViewTracker(mRootView, mContentManager, new ViewTrackerObserver()); 384 } else { 385 mRootView = null; 386 } 387 388 trackSurface(this); 389 } 390 391 /** 392 * Performs all necessary cleanups. 393 */ destroy()394 public void destroy() { 395 if (mOpened) onSurfaceClosed(); 396 untrackSurface(this); 397 if (mSliceViewTracker != null) { 398 mSliceViewTracker.destroy(); 399 mSliceViewTracker = null; 400 } 401 mHybridListRenderer.unbind(); 402 } 403 404 /** 405 * Puts a list of header views at the beginning. 406 */ setHeaderViews(List<View> headerViews)407 public void setHeaderViews(List<View> headerViews) { 408 ArrayList<FeedListContentManager.FeedContent> newContentList = 409 new ArrayList<FeedListContentManager.FeedContent>(); 410 411 // First add new header contents. Some of them may appear in the existing list. 412 for (int i = 0; i < headerViews.size(); ++i) { 413 View view = headerViews.get(i); 414 String key = "Header" + view.hashCode(); 415 FeedListContentManager.NativeViewContent headerContent = 416 new FeedListContentManager.NativeViewContent(key, view); 417 newContentList.add(headerContent); 418 } 419 420 // Then add all existing feed stream contents. 421 for (int i = mHeaderCount; i < mContentManager.getItemCount(); ++i) { 422 newContentList.add(mContentManager.getContent(i)); 423 } 424 425 updateContentsInPlace(newContentList); 426 427 mHeaderCount = headerViews.size(); 428 } 429 430 /** 431 * @return The android {@link View} that the surface is supposed to show. 432 */ getView()433 public View getView() { 434 return mRootView; 435 } 436 437 /** 438 * Attempts to load more content if it can be triggered. 439 * @return true if loading more content can be triggered. 440 */ maybeLoadMore()441 boolean maybeLoadMore() { 442 // Checks if loading more can be triggered. 443 boolean canLoadMore = false; 444 LinearLayoutManager layoutManager = (LinearLayoutManager) mRootView.getLayoutManager(); 445 if (layoutManager == null) { 446 return false; 447 } 448 int totalItemCount = layoutManager.getItemCount(); 449 int lastVisibleItem = layoutManager.findLastVisibleItemPosition(); 450 if (totalItemCount - lastVisibleItem > mLoadMoreTriggerLookahead) { 451 return false; 452 } 453 454 // Starts to load more content if not yet. 455 if (!mIsLoadingMoreContent) { 456 mIsLoadingMoreContent = true; 457 // The native loadMore() call may immediately result in onStreamUpdated(), which can 458 // result in a crash if maybeLoadMore() is being called in response to certain events. 459 // Use postTask to avoid this. 460 PostTask.postTask(UiThreadTaskTraits.DEFAULT, 461 () 462 -> FeedStreamSurfaceJni.get().loadMore(mNativeFeedStreamSurface, 463 FeedStreamSurface.this, 464 (Boolean success) -> { mIsLoadingMoreContent = false; })); 465 } 466 467 return true; 468 } 469 470 @VisibleForTesting getFeedListContentManagerForTesting()471 FeedListContentManager getFeedListContentManagerForTesting() { 472 return mContentManager; 473 } 474 475 /** 476 * Called when the stream update content is available. The content will get passed to UI 477 */ 478 @CalledByNative onStreamUpdated(byte[] data)479 void onStreamUpdated(byte[] data) { 480 // There should be no updates while the surface is closed. If the surface was recently 481 // closed, just ignore these. 482 if (!mOpened) return; 483 StreamUpdate streamUpdate; 484 try { 485 streamUpdate = StreamUpdate.parseFrom(data); 486 } catch (com.google.protobuf.InvalidProtocolBufferException e) { 487 Log.wtf(TAG, "Unable to parse StreamUpdate proto data", e); 488 return; 489 } 490 491 // Update using shared states. 492 for (SharedState state : streamUpdate.getNewSharedStatesList()) { 493 mHybridListRenderer.update(state.getXsurfaceSharedState().toByteArray()); 494 } 495 496 // Builds the new list containing: 497 // * existing headers 498 // * both new and existing contents 499 ArrayList<FeedListContentManager.FeedContent> newContentList = 500 new ArrayList<FeedListContentManager.FeedContent>(); 501 for (int i = 0; i < mHeaderCount; ++i) { 502 newContentList.add(mContentManager.getContent(i)); 503 } 504 for (SliceUpdate sliceUpdate : streamUpdate.getUpdatedSlicesList()) { 505 if (sliceUpdate.hasSlice()) { 506 FeedListContentManager.FeedContent content = 507 createContentFromSlice(sliceUpdate.getSlice()); 508 if (content != null) { 509 newContentList.add(content); 510 } 511 } else { 512 String existingSliceId = sliceUpdate.getSliceId(); 513 int position = mContentManager.findContentPositionByKey(existingSliceId); 514 if (position != -1) { 515 newContentList.add(mContentManager.getContent(position)); 516 } 517 } 518 } 519 520 updateContentsInPlace(newContentList); 521 } 522 523 @CalledByNative replaceDataStoreEntry(String key, byte[] data)524 void replaceDataStoreEntry(String key, byte[] data) { 525 if (mSurfaceScope != null) mSurfaceScope.replaceDataStoreEntry(key, data); 526 } 527 528 @CalledByNative removeDataStoreEntry(String key)529 void removeDataStoreEntry(String key) { 530 if (mSurfaceScope != null) mSurfaceScope.removeDataStoreEntry(key); 531 } 532 updateContentsInPlace( ArrayList<FeedListContentManager.FeedContent> newContentList)533 private void updateContentsInPlace( 534 ArrayList<FeedListContentManager.FeedContent> newContentList) { 535 boolean hasContentChange = false; 536 537 // 1) Builds the hash set based on keys of new contents. 538 HashSet<String> newContentKeySet = new HashSet<String>(); 539 for (int i = 0; i < newContentList.size(); ++i) { 540 hasContentChange = true; 541 newContentKeySet.add(newContentList.get(i).getKey()); 542 } 543 544 // 2) Builds the hash map of existing content list for fast look up by key. 545 HashMap<String, FeedListContentManager.FeedContent> existingContentMap = 546 new HashMap<String, FeedListContentManager.FeedContent>(); 547 for (int i = 0; i < mContentManager.getItemCount(); ++i) { 548 FeedListContentManager.FeedContent content = mContentManager.getContent(i); 549 existingContentMap.put(content.getKey(), content); 550 } 551 552 // 3) Removes those existing contents that do not appear in the new list. 553 for (int i = mContentManager.getItemCount() - 1; i >= 0; --i) { 554 String key = mContentManager.getContent(i).getKey(); 555 if (!newContentKeySet.contains(key)) { 556 hasContentChange = true; 557 mContentManager.removeContents(i, 1); 558 existingContentMap.remove(key); 559 } 560 } 561 562 // 4) Iterates through the new list to add the new content or move the existing content 563 // if needed. 564 int i = 0; 565 while (i < newContentList.size()) { 566 FeedListContentManager.FeedContent content = newContentList.get(i); 567 568 // If this is an existing content, moves it to new position. 569 if (existingContentMap.containsKey(content.getKey())) { 570 hasContentChange = true; 571 mContentManager.moveContent( 572 mContentManager.findContentPositionByKey(content.getKey()), i); 573 ++i; 574 continue; 575 } 576 577 // Otherwise, this is new content. Add it together with all adjacent new contents. 578 int startIndex = i++; 579 while (i < newContentList.size() 580 && !existingContentMap.containsKey(newContentList.get(i).getKey())) { 581 ++i; 582 } 583 hasContentChange = true; 584 mContentManager.addContents(startIndex, newContentList.subList(startIndex, i)); 585 } 586 587 if (hasContentChange) { 588 mRecyclerViewAnimationFinishDetector.asyncWait(); 589 } 590 } 591 notifyContentChanged()592 private void notifyContentChanged() { 593 for (ContentChangedListener listener : mContentChangedListeners) { 594 // For Feed v2, we only need to report if the content has changed. All other callbacks 595 // are not used at this point. 596 listener.onContentChanged(); 597 } 598 } 599 createContentFromSlice(Slice slice)600 private FeedListContentManager.FeedContent createContentFromSlice(Slice slice) { 601 String sliceId = slice.getSliceId(); 602 if (slice.hasXsurfaceSlice()) { 603 return new FeedListContentManager.ExternalViewContent( 604 sliceId, slice.getXsurfaceSlice().getXsurfaceFrame().toByteArray()); 605 } else if (slice.hasLoadingSpinnerSlice()) { 606 // If the placeholder is shown, spinner is not needed. 607 if (mIsPlaceholderShown) { 608 return null; 609 } 610 return new FeedListContentManager.NativeViewContent(sliceId, R.layout.feed_spinner); 611 } 612 assert slice.hasZeroStateSlice(); 613 if (slice.getZeroStateSlice().getType() == ZeroStateSlice.Type.CANT_REFRESH) { 614 return new FeedListContentManager.NativeViewContent(sliceId, R.layout.no_connection); 615 } 616 assert slice.getZeroStateSlice().getType() == ZeroStateSlice.Type.NO_CARDS_AVAILABLE; 617 return new FeedListContentManager.NativeViewContent(sliceId, R.layout.no_content_v2); 618 } 619 620 /** 621 * Returns the immediate child of parentView which contains descendentView. 622 * If descendentView is not in parentView's view heirarchy, this returns null. 623 * Note that the returned view may be descendentView, or descendentView.getParent(), 624 * or descendentView.getParent().getParent(), etc... 625 */ findChildViewContainingDescendent(View parentView, View descendentView)626 View findChildViewContainingDescendent(View parentView, View descendentView) { 627 if (parentView == null || descendentView == null) return null; 628 // Find the direct child of parentView which owns view. 629 if (parentView == descendentView.getParent()) { 630 return descendentView; 631 } else { 632 // One of the view's ancestors might be the child. 633 ViewParent p = descendentView.getParent(); 634 while (true) { 635 if (p == null) { 636 return null; 637 } 638 if (p.getParent() == parentView) { 639 if (p instanceof View) return (View) p; 640 return null; 641 } 642 p = p.getParent(); 643 } 644 } 645 } 646 647 @VisibleForTesting getSliceIdFromView(View view)648 String getSliceIdFromView(View view) { 649 View childOfRoot = findChildViewContainingDescendent(mRootView, view); 650 651 if (childOfRoot != null) { 652 // View is a child of the recycler view, find slice using the index. 653 int position = mRootView.getChildAdapterPosition(childOfRoot); 654 if (position >= 0 && position < mContentManager.getItemCount()) { 655 return mContentManager.getContent(position).getKey(); 656 } 657 } else if (mBottomSheetContent != null 658 && findChildViewContainingDescendent(mBottomSheetContent.getContentView(), view) 659 != null) { 660 // View is a child of the bottom sheet, return slice associated with the bottom sheet. 661 return mBottomSheetOriginatingSliceId; 662 } 663 return ""; 664 } 665 666 @Override navigateTab(String url, View actionSourceView)667 public void navigateTab(String url, View actionSourceView) { 668 assert ThreadUtils.runningOnUiThread(); 669 FeedStreamSurfaceJni.get().reportOpenAction(mNativeFeedStreamSurface, 670 FeedStreamSurface.this, getSliceIdFromView(actionSourceView)); 671 NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_SNIPPET); 672 673 openUrl(url, WindowOpenDisposition.CURRENT_TAB); 674 675 // Attempts to load more content if needed. 676 maybeLoadMore(); 677 } 678 679 @Override navigateNewTab(String url, View actionSourceView)680 public void navigateNewTab(String url, View actionSourceView) { 681 assert ThreadUtils.runningOnUiThread(); 682 FeedStreamSurfaceJni.get().reportOpenInNewTabAction(mNativeFeedStreamSurface, 683 FeedStreamSurface.this, getSliceIdFromView(actionSourceView)); 684 NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_SNIPPET); 685 686 openUrl(url, WindowOpenDisposition.NEW_BACKGROUND_TAB); 687 688 // Attempts to load more content if needed. 689 maybeLoadMore(); 690 } 691 692 @Override navigateIncognitoTab(String url)693 public void navigateIncognitoTab(String url) { 694 assert ThreadUtils.runningOnUiThread(); 695 FeedStreamSurfaceJni.get().reportOpenInNewIncognitoTabAction( 696 mNativeFeedStreamSurface, FeedStreamSurface.this); 697 NewTabPageUma.recordAction(NewTabPageUma.ACTION_OPENED_SNIPPET); 698 699 openUrl(url, WindowOpenDisposition.OFF_THE_RECORD); 700 701 // Attempts to load more content if needed. 702 maybeLoadMore(); 703 } 704 705 @Override downloadLink(String url)706 public void downloadLink(String url) { 707 assert ThreadUtils.runningOnUiThread(); 708 FeedStreamSurfaceJni.get().reportDownloadAction( 709 mNativeFeedStreamSurface, FeedStreamSurface.this); 710 RequestCoordinatorBridge.getForProfile(Profile.getLastUsedRegularProfile()) 711 .savePageLater( 712 url, OfflinePageBridge.NTP_SUGGESTIONS_NAMESPACE, true /* user requested*/); 713 } 714 715 @Override showBottomSheet(View view, View actionSourceView)716 public void showBottomSheet(View view, View actionSourceView) { 717 assert ThreadUtils.runningOnUiThread(); 718 dismissBottomSheet(); 719 720 FeedStreamSurfaceJni.get().reportContextMenuOpened( 721 mNativeFeedStreamSurface, FeedStreamSurface.this); 722 723 // Make a sheetContent with the view. 724 mBottomSheetContent = new CardMenuBottomSheetContent(view); 725 mBottomSheetOriginatingSliceId = getSliceIdFromView(actionSourceView); 726 mBottomSheetController.requestShowContent(mBottomSheetContent, true); 727 } 728 729 @Override dismissBottomSheet()730 public void dismissBottomSheet() { 731 assert ThreadUtils.runningOnUiThread(); 732 if (mBottomSheetContent != null) { 733 mBottomSheetController.hideContent(mBottomSheetContent, true); 734 } 735 mBottomSheetContent = null; 736 mBottomSheetOriginatingSliceId = null; 737 } 738 739 @Override recordActionManageInterests()740 public void recordActionManageInterests() { 741 assert ThreadUtils.runningOnUiThread(); 742 FeedStreamSurfaceJni.get().reportManageInterestsAction( 743 mNativeFeedStreamSurface, FeedStreamSurface.this); 744 } 745 746 @Override loadMore()747 public void loadMore() { 748 // TODO(jianli): Remove this from FeedActionsHandler interface. 749 } 750 751 @Override processThereAndBackAgainData(byte[] data)752 public void processThereAndBackAgainData(byte[] data) { 753 processThereAndBackAgainData(data, null); 754 } 755 756 @Override processThereAndBackAgainData(byte[] data, @Nullable View actionSourceView)757 public void processThereAndBackAgainData(byte[] data, @Nullable View actionSourceView) { 758 assert ThreadUtils.runningOnUiThread(); 759 FeedStreamSurfaceJni.get().processThereAndBackAgain( 760 mNativeFeedStreamSurface, FeedStreamSurface.this, data); 761 } 762 763 @Override processViewAction(byte[] data)764 public void processViewAction(byte[] data) { 765 // TODO(crbug.com/1117586): The caller should be calling on the Ui thread. 766 // assert ThreadUtils.runningOnUiThread(); 767 PostTask.postTask(UiThreadTaskTraits.DEFAULT, () -> { 768 FeedStreamSurfaceJni.get().processViewAction( 769 mNativeFeedStreamSurface, FeedStreamSurface.this, data); 770 }); 771 } 772 773 @Override sendFeedback(Map<String, String> productSpecificDataMap)774 public void sendFeedback(Map<String, String> productSpecificDataMap) { 775 assert ThreadUtils.runningOnUiThread(); 776 FeedStreamSurfaceJni.get().reportSendFeedbackAction( 777 mNativeFeedStreamSurface, FeedStreamSurface.this); 778 779 // Make sure the bottom sheet is dismissed before we take a snapshot. 780 dismissBottomSheet(); 781 782 Profile profile = Profile.getLastUsedRegularProfile(); 783 if (profile == null) { 784 return; 785 } 786 787 String url = productSpecificDataMap.get(XSURFACE_CARD_URL); 788 789 Map<String, String> feedContext = convertNameFormat(productSpecificDataMap); 790 791 // FEEDBACK_CONTEXT: This identifies this feedback as coming from Chrome for Android (as 792 // opposed to desktop). 793 // FEEDBACK_REPORT_TYPE: Reports for Chrome mobile must have a contextTag of the form 794 // com.chrome.feed.USER_INITIATED_FEEDBACK_REPORT, or they will be discarded for not 795 // matching an allow list rule. 796 mHelpAndFeedbackLauncher.showFeedback( 797 mActivity, profile, url, FEEDBACK_REPORT_TYPE, feedContext, FEEDBACK_CONTEXT); 798 } 799 800 // Since the XSurface client strings are slightly different than the Feed strings, convert the 801 // name from the XSurface format to the format that can be handled by the feedback system. Any 802 // new strings that are added on the XSurface side will need a code change here, and adding the 803 // PSD to the allow list. convertNameFormat(Map<String, String> xSurfaceMap)804 private Map<String, String> convertNameFormat(Map<String, String> xSurfaceMap) { 805 Map<String, String> feedbackNameConversionMap = new HashMap<>(); 806 feedbackNameConversionMap.put("Card URL", "CardUrl"); 807 feedbackNameConversionMap.put("Card Title", "CardTitle"); 808 feedbackNameConversionMap.put("Card Snippet", "CardSnippet"); 809 feedbackNameConversionMap.put("Card category", "CardCategory"); 810 feedbackNameConversionMap.put("Doc Creation Date", "DocCreationDate"); 811 812 // For each <name, value> entry in the input map, convert the name to the new name, and 813 // write the new <name, value> pair into the output map. 814 Map<String, String> feedbackMap = new HashMap<>(); 815 for (Map.Entry<String, String> entry : xSurfaceMap.entrySet()) { 816 String newName = feedbackNameConversionMap.get(entry.getKey()); 817 if (newName != null) { 818 feedbackMap.put(newName, entry.getValue()); 819 } else { 820 Log.v(TAG, "Found an entry with no conversion available."); 821 // We will put the entry into the map if untranslatable. It will be discarded 822 // unless it matches an allow list on the server, though. This way we can choose 823 // to allow it on the server if desired. 824 feedbackMap.put(entry.getKey(), entry.getValue()); 825 } 826 } 827 828 return feedbackMap; 829 } 830 831 @Override requestDismissal(byte[] data)832 public int requestDismissal(byte[] data) { 833 assert ThreadUtils.runningOnUiThread(); 834 return FeedStreamSurfaceJni.get().executeEphemeralChange( 835 mNativeFeedStreamSurface, FeedStreamSurface.this, data); 836 } 837 838 @Override commitDismissal(int changeId)839 public void commitDismissal(int changeId) { 840 assert ThreadUtils.runningOnUiThread(); 841 FeedStreamSurfaceJni.get().commitEphemeralChange( 842 mNativeFeedStreamSurface, FeedStreamSurface.this, changeId); 843 844 // Attempts to load more content if needed. 845 maybeLoadMore(); 846 } 847 848 @Override discardDismissal(int changeId)849 public void discardDismissal(int changeId) { 850 assert ThreadUtils.runningOnUiThread(); 851 FeedStreamSurfaceJni.get().discardEphemeralChange( 852 mNativeFeedStreamSurface, FeedStreamSurface.this, changeId); 853 } 854 855 @Override showSnackbar(String text, String actionLabel, FeedActionsHandler.SnackbarDuration duration, FeedActionsHandler.SnackbarController controller)856 public void showSnackbar(String text, String actionLabel, 857 FeedActionsHandler.SnackbarDuration duration, 858 FeedActionsHandler.SnackbarController controller) { 859 assert ThreadUtils.runningOnUiThread(); 860 int durationMs = SNACKBAR_DURATION_MS_SHORT; 861 if (duration == FeedActionsHandler.SnackbarDuration.LONG) { 862 durationMs = SNACKBAR_DURATION_MS_LONG; 863 } 864 865 mSnackbarManager.showSnackbar( 866 Snackbar.make(text, 867 new SnackbarManager.SnackbarController() { 868 @Override 869 public void onAction(Object actionData) { 870 controller.onAction(); 871 } 872 @Override 873 public void onDismissNoAction(Object actionData) { 874 controller.onDismissNoAction(); 875 } 876 }, 877 Snackbar.TYPE_ACTION, Snackbar.UMA_FEED_NTP_STREAM) 878 .setAction(actionLabel, /*actionData=*/null) 879 .setDuration(durationMs)); 880 } 881 882 @Override share(String url, String title)883 public void share(String url, String title) { 884 mShareHelper.share(url, title); 885 } 886 887 /** 888 * Informs whether or not feed content should be shown. 889 */ setStreamContentVisibility(boolean visible)890 public void setStreamContentVisibility(boolean visible) { 891 if (mStreamContentVisible == visible) return; 892 mStreamContentVisible = visible; 893 updateSurfaceOpenState(); 894 } 895 896 /** 897 * Informs FeedStreamSurface of the visibility of its parent stream. 898 */ setStreamVisibility(boolean visible)899 public void setStreamVisibility(boolean visible) { 900 if (mStreamVisible == visible) return; 901 mStreamVisible = visible; 902 updateSurfaceOpenState(); 903 } 904 updateSurfaceOpenState()905 private void updateSurfaceOpenState() { 906 boolean shouldOpen = sStartupCalled && mStreamContentVisible && mStreamVisible; 907 if (shouldOpen == mOpened) return; 908 if (shouldOpen) { 909 onSurfaceOpened(); 910 } else { 911 onSurfaceClosed(); 912 } 913 } 914 915 /** 916 * Called when the surface is considered opened. This happens when the feed should be visible 917 * and enabled on the screen. 918 */ onSurfaceOpened()919 private void onSurfaceOpened() { 920 assert (!mOpened); 921 assert (sStartupCalled); 922 assert (mStreamContentVisible); 923 // No feed content should exist. 924 assert (mContentManager.getItemCount() == mHeaderCount); 925 926 mOpened = true; 927 FeedStreamSurfaceJni.get().surfaceOpened(mNativeFeedStreamSurface, FeedStreamSurface.this); 928 mHybridListRenderer.onSurfaceOpened(); 929 } 930 931 /** 932 * Informs that the surface is closed. 933 */ onSurfaceClosed()934 private void onSurfaceClosed() { 935 assert (mOpened); 936 assert (sStartupCalled); 937 // Let the hybrid list renderer know that the surface has closed, so it doesn't 938 // interpret the removal of contents as related to actions otherwise initiated by 939 // the user. 940 mHybridListRenderer.onSurfaceClosed(); 941 942 // Remove Feed content from the content manager. 943 int feedCount = mContentManager.getItemCount() - mHeaderCount; 944 if (feedCount > 0) { 945 mContentManager.removeContents(mHeaderCount, feedCount); 946 mRecyclerViewAnimationFinishDetector.asyncWait(); 947 } 948 949 mScrollReporter.onUnbind(); 950 mSliceViewTracker.clear(); 951 952 FeedStreamSurfaceJni.get().surfaceClosed(mNativeFeedStreamSurface, FeedStreamSurface.this); 953 mOpened = false; 954 } 955 isOpened()956 public boolean isOpened() { 957 return mOpened; 958 } 959 openUrl(String url, int disposition)960 private void openUrl(String url, int disposition) { 961 LoadUrlParams params = new LoadUrlParams(url, PageTransition.AUTO_BOOKMARK); 962 params.setReferrer( 963 new Referrer(SuggestionsConfig.getReferrerUrl(ChromeFeatureList.INTEREST_FEED_V2), 964 // WARNING: ReferrerPolicy.ALWAYS is assumed by other Chrome code for NTP 965 // tiles to set consider_for_ntp_most_visited. 966 ReferrerPolicy.ALWAYS)); 967 Tab tab = mPageNavigationDelegate.openUrl(disposition, params); 968 969 boolean inNewTab = (disposition == WindowOpenDisposition.NEW_BACKGROUND_TAB 970 || disposition == WindowOpenDisposition.OFF_THE_RECORD); 971 972 FeedStreamSurfaceJni.get().reportNavigationStarted( 973 mNativeFeedStreamSurface, FeedStreamSurface.this); 974 if (tab != null) { 975 tab.addObserver(new FeedTabNavigationObserver(inNewTab)); 976 NavigationRecorder.record(tab, 977 visitData -> FeedServiceBridge.reportOpenVisitComplete(visitData.duration)); 978 } 979 } 980 addContentChangedListener(ContentChangedListener listener)981 public void addContentChangedListener(ContentChangedListener listener) { 982 mContentChangedListeners.addObserver(listener); 983 } 984 removeContentChangedListener(ContentChangedListener listener)985 public void removeContentChangedListener(ContentChangedListener listener) { 986 mContentChangedListeners.removeObserver(listener); 987 } 988 989 // Called when the stream is scrolled. streamScrolled(int dx, int dy)990 void streamScrolled(int dx, int dy) { 991 FeedStreamSurfaceJni.get().reportStreamScrollStart( 992 mNativeFeedStreamSurface, FeedStreamSurface.this); 993 mScrollReporter.trackScroll(dx, dy); 994 } 995 isPlaceholderShown()996 boolean isPlaceholderShown() { 997 return mIsPlaceholderShown; 998 } 999 1000 /** 1001 * Feed v2's background is set to be transparent in {@link FeedSurfaceCoordinator#createStream} 1002 * if the Feed placeholder is shown. After first batch of articles are loaded, set recyclerView 1003 * back to non-transparent. Since Feed v2 doesn't have fade-in animation, we add a fade-in 1004 * animation for Feed background to make the transition smooth. 1005 */ hidePlaceholder()1006 void hidePlaceholder() { 1007 if (!mIsPlaceholderShown) { 1008 return; 1009 } 1010 ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( 1011 mRootView.getBackground(), PropertyValuesHolder.ofInt("alpha", 255)); 1012 animator.setTarget(mRootView.getBackground()); 1013 animator.setDuration(mRootView.getItemAnimator().getAddDuration()) 1014 .setInterpolator(Interpolators.LINEAR_INTERPOLATOR); 1015 animator.start(); 1016 mIsPlaceholderShown = false; 1017 } 1018 1019 // Detects animation finishes in RecyclerView. 1020 // https://stackoverflow.com/questions/33710605/detect-animation-finish-in-androids-recyclerview 1021 private class RecyclerViewAnimationFinishDetector implements ItemAnimatorFinishedListener { 1022 private boolean mWaitingStarted; 1023 1024 /** Asynchronousy waits for the animation to finish. */ asyncWait()1025 public void asyncWait() { 1026 if (mWaitingStarted) { 1027 return; 1028 } 1029 mWaitingStarted = true; 1030 1031 // The RecyclerView has not started animating yet, so post a message to the 1032 // message queue that will be run after the RecyclerView has started animating. 1033 new Handler().post(() -> { checkFinish(); }); 1034 } 1035 checkFinish()1036 private void checkFinish() { 1037 if (mRootView.isAnimating()) { 1038 // The RecyclerView is still animating, try again when the animation has finished. 1039 mRootView.getItemAnimator().isRunning(this); 1040 return; 1041 } 1042 1043 // The RecyclerView has animated all it's views. 1044 onFinished(); 1045 } 1046 onFinished()1047 private void onFinished() { 1048 mWaitingStarted = false; 1049 1050 // This works around the bug that the out-of-screen toolbar is not brought back together 1051 // with the new tab page view when it slides down. This is because the RecyclerView 1052 // animation may not finish when content changed event is triggered and thus the new tab 1053 // page layout view may still be partially off screen. 1054 notifyContentChanged(); 1055 } 1056 1057 @Override onAnimationsFinished()1058 public void onAnimationsFinished() { 1059 // There might still be more items that will be animated after this one. 1060 new Handler().post(() -> { checkFinish(); }); 1061 } 1062 } 1063 1064 // Ingests scroll events and reports scroll completion back to native. 1065 private class ScrollReporter extends ScrollTracker { 1066 @Override onScrollEvent(int scrollAmount)1067 protected void onScrollEvent(int scrollAmount) { 1068 FeedStreamSurfaceJni.get().reportStreamScrolled( 1069 mNativeFeedStreamSurface, FeedStreamSurface.this, scrollAmount); 1070 } 1071 } 1072 1073 private class ViewTrackerObserver implements FeedSliceViewTracker.Observer { 1074 @Override sliceVisible(String sliceId)1075 public void sliceVisible(String sliceId) { 1076 FeedStreamSurfaceJni.get().reportSliceViewed( 1077 mNativeFeedStreamSurface, FeedStreamSurface.this, sliceId); 1078 } 1079 @Override feedContentVisible()1080 public void feedContentVisible() { 1081 FeedStreamSurfaceJni.get().reportFeedViewed( 1082 mNativeFeedStreamSurface, FeedStreamSurface.this); 1083 } 1084 } 1085 1086 @NativeMethods 1087 interface Natives { init(FeedStreamSurface caller)1088 long init(FeedStreamSurface caller); isActivityLoggingEnabled(long nativeFeedStreamSurface, FeedStreamSurface caller)1089 boolean isActivityLoggingEnabled(long nativeFeedStreamSurface, FeedStreamSurface caller); getExperimentIds()1090 int[] getExperimentIds(); getSessionId(long nativeFeedStreamSurface, FeedStreamSurface caller)1091 String getSessionId(long nativeFeedStreamSurface, FeedStreamSurface caller); reportFeedViewed(long nativeFeedStreamSurface, FeedStreamSurface caller)1092 void reportFeedViewed(long nativeFeedStreamSurface, FeedStreamSurface caller); reportSliceViewed( long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId)1093 void reportSliceViewed( 1094 long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId); reportNavigationStarted(long nativeFeedStreamSurface, FeedStreamSurface caller)1095 void reportNavigationStarted(long nativeFeedStreamSurface, FeedStreamSurface caller); reportPageLoaded(long nativeFeedStreamSurface, FeedStreamSurface caller, String url, boolean inNewTab)1096 void reportPageLoaded(long nativeFeedStreamSurface, FeedStreamSurface caller, String url, 1097 boolean inNewTab); reportOpenAction( long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId)1098 void reportOpenAction( 1099 long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId); reportOpenInNewTabAction( long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId)1100 void reportOpenInNewTabAction( 1101 long nativeFeedStreamSurface, FeedStreamSurface caller, String sliceId); reportOpenInNewIncognitoTabAction( long nativeFeedStreamSurface, FeedStreamSurface caller)1102 void reportOpenInNewIncognitoTabAction( 1103 long nativeFeedStreamSurface, FeedStreamSurface caller); reportSendFeedbackAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1104 void reportSendFeedbackAction(long nativeFeedStreamSurface, FeedStreamSurface caller); reportDownloadAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1105 void reportDownloadAction(long nativeFeedStreamSurface, FeedStreamSurface caller); reportContextMenuOpened(long nativeFeedStreamSurface, FeedStreamSurface caller)1106 void reportContextMenuOpened(long nativeFeedStreamSurface, FeedStreamSurface caller); reportManageInterestsAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1107 void reportManageInterestsAction(long nativeFeedStreamSurface, FeedStreamSurface caller); 1108 // TODO(crbug.com/1123044): Call these from the front end. reportTurnOnAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1109 void reportTurnOnAction(long nativeFeedStreamSurface, FeedStreamSurface caller); reportTurnOffAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1110 void reportTurnOffAction(long nativeFeedStreamSurface, FeedStreamSurface caller); 1111 1112 // TODO(crbug.com/1111101): These actions aren't visible to the client, so these functions 1113 // are never called. reportLearnMoreAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1114 void reportLearnMoreAction(long nativeFeedStreamSurface, FeedStreamSurface caller); reportRemoveAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1115 void reportRemoveAction(long nativeFeedStreamSurface, FeedStreamSurface caller); reportNotInterestedInAction(long nativeFeedStreamSurface, FeedStreamSurface caller)1116 void reportNotInterestedInAction(long nativeFeedStreamSurface, FeedStreamSurface caller); 1117 reportStreamScrolled( long nativeFeedStreamSurface, FeedStreamSurface caller, int distanceDp)1118 void reportStreamScrolled( 1119 long nativeFeedStreamSurface, FeedStreamSurface caller, int distanceDp); reportStreamScrollStart(long nativeFeedStreamSurface, FeedStreamSurface caller)1120 void reportStreamScrollStart(long nativeFeedStreamSurface, FeedStreamSurface caller); loadMore( long nativeFeedStreamSurface, FeedStreamSurface caller, Callback<Boolean> callback)1121 void loadMore( 1122 long nativeFeedStreamSurface, FeedStreamSurface caller, Callback<Boolean> callback); processThereAndBackAgain( long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data)1123 void processThereAndBackAgain( 1124 long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data); processViewAction(long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data)1125 void processViewAction(long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data); executeEphemeralChange( long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data)1126 int executeEphemeralChange( 1127 long nativeFeedStreamSurface, FeedStreamSurface caller, byte[] data); commitEphemeralChange( long nativeFeedStreamSurface, FeedStreamSurface caller, int changeId)1128 void commitEphemeralChange( 1129 long nativeFeedStreamSurface, FeedStreamSurface caller, int changeId); discardEphemeralChange( long nativeFeedStreamSurface, FeedStreamSurface caller, int changeId)1130 void discardEphemeralChange( 1131 long nativeFeedStreamSurface, FeedStreamSurface caller, int changeId); surfaceOpened(long nativeFeedStreamSurface, FeedStreamSurface caller)1132 void surfaceOpened(long nativeFeedStreamSurface, FeedStreamSurface caller); surfaceClosed(long nativeFeedStreamSurface, FeedStreamSurface caller)1133 void surfaceClosed(long nativeFeedStreamSurface, FeedStreamSurface caller); 1134 } 1135 } 1136