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