Switch basicstream to use sharedstream ScrollObserver and remove ScrollListener.

PiperOrigin-RevId: 246882624
Change-Id: Ibf20706d9be41422ba5a5a55a000c5dc7967581a
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/BUILD b/src/main/java/com/google/android/libraries/feed/basicstream/BUILD
index 11f6615..720a497 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/BUILD
@@ -38,6 +38,7 @@
         "//src/main/java/com/google/android/libraries/feed/sharedstream/offlinemonitor",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/menumeasurer",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
         "//src/main/proto/com/google/android/libraries/feed/basicstream/internal/proto:stream_saved_instance_state_java_proto_lite",
         "//src/main/proto/com/google/android/libraries/feed/internalapi/proto:client_feed_java_proto_lite",
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/BasicStream.java b/src/main/java/com/google/android/libraries/feed/basicstream/BasicStream.java
index 56a5e3e..7c003bd 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/BasicStream.java
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/BasicStream.java
@@ -40,6 +40,7 @@
 import com.google.android.libraries.feed.basicstream.internal.StreamRecyclerViewAdapter;
 import com.google.android.libraries.feed.basicstream.internal.StreamSavedInstanceStateProto.StreamSavedInstanceState;
 import com.google.android.libraries.feed.basicstream.internal.drivers.StreamDriver;
+import com.google.android.libraries.feed.basicstream.internal.scroll.BasicStreamScrollMonitor;
 import com.google.android.libraries.feed.basicstream.internal.scroll.ScrollRestorer;
 import com.google.android.libraries.feed.basicstream.internal.viewloggingupdater.ViewLoggingUpdater;
 import com.google.android.libraries.feed.common.concurrent.CancelableTask;
@@ -87,7 +88,8 @@
 import com.google.android.libraries.feed.sharedstream.proto.ScrollStateProto.ScrollState;
 import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason;
 import com.google.android.libraries.feed.sharedstream.publicapi.menumeasurer.MenuMeasurer;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.scroll.ScrollListenerNotifier;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
 import java.util.List;
@@ -127,13 +129,14 @@
   private final TooltipApi tooltipApi;
   private final UiSessionRequestLogger uiSessionRequestLogger;
   private final StreamConfiguration streamConfiguration;
+  private final BasicStreamScrollMonitor scrollMonitor;
 
   private RecyclerView recyclerView;
   private ContextMenuManager contextMenuManager;
   private List<Header> headers;
   private StreamItemTouchCallbacks itemTouchCallbacks;
   private StreamRecyclerViewAdapter adapter;
-  private StreamScrollMonitor streamScrollMonitor;
+  private ScrollListenerNotifier scrollListenerNotifier;
   private ScrollRestorer scrollRestorer;
   private long sessionStartTimestamp;
   private long initialLoadingSpinnerStartTime;
@@ -216,6 +219,7 @@
             isBackgroundDark);
     this.context =
         new ContextThemeWrapper(context, (isBackgroundDark ? R.style.Dark : R.style.Light));
+    this.scrollMonitor = new BasicStreamScrollMonitor(clock);
   }
 
   @VisibleForTesting
@@ -262,7 +266,8 @@
     setupRecyclerView();
 
     if (savedInstanceState == null) {
-      scrollRestorer = createScrollRestorer(configuration, recyclerView, streamScrollMonitor, null);
+      scrollRestorer =
+          createScrollRestorer(configuration, recyclerView, scrollListenerNotifier, null);
       return;
     }
 
@@ -278,11 +283,12 @@
           createScrollRestorer(
               configuration,
               recyclerView,
-              streamScrollMonitor,
+              scrollListenerNotifier,
               streamSavedInstanceState.getScrollState());
     } catch (IllegalArgumentException | InvalidProtocolBufferException e) {
       Logger.wtf(TAG, "Could not parse saved instance state String.");
-      scrollRestorer = createScrollRestorer(configuration, recyclerView, streamScrollMonitor, null);
+      scrollRestorer =
+          createScrollRestorer(configuration, recyclerView, scrollListenerNotifier, null);
     }
   }
 
@@ -454,12 +460,12 @@
 
   @Override
   public void addScrollListener(ScrollListener listener) {
-    streamScrollMonitor.addScrollListener(listener);
+    scrollListenerNotifier.addScrollListener(listener);
   }
 
   @Override
   public void removeScrollListener(ScrollListener listener) {
-    streamScrollMonitor.removeScrollListener(listener);
+    scrollListenerNotifier.removeScrollListener(listener);
   }
 
   @Override
@@ -508,8 +514,9 @@
 
   private void setupRecyclerView() {
     recyclerView = new RecyclerView(context);
-    streamScrollMonitor = createStreamScrollMonitor(streamContentChangedListener, mainThreadRunner);
-    recyclerView.addOnScrollListener(streamScrollMonitor);
+    scrollListenerNotifier =
+        createScrollListenerNotifier(streamContentChangedListener, scrollMonitor, mainThreadRunner);
+    recyclerView.addOnScrollListener(scrollMonitor);
     adapter =
         createRecyclerViewAdapter(
             context,
@@ -517,7 +524,7 @@
             pietManager,
             deepestContentTracker,
             streamContentChangedListener,
-            streamScrollMonitor,
+            scrollMonitor,
             configuration,
             new PietEventLogger(basicLoggingApi));
     adapter.setHeaders(headers);
@@ -577,7 +584,7 @@
             mainThreadRunner,
             tooltipApi,
             uiRefreshReason,
-            streamScrollMonitor);
+            scrollListenerNotifier);
 
     uiRefreshReason = UiRefreshReason.getDefaultInstance();
 
@@ -757,7 +764,8 @@
       ScrollRestorer initialScrollRestorer =
           modelProvider.getCurrentState() == State.READY
               ? scrollRestorer
-              : createNonRestoringScrollRestorer(configuration, recyclerView, streamScrollMonitor);
+              : createNonRestoringScrollRestorer(
+                  configuration, recyclerView, scrollListenerNotifier);
 
       streamDriver =
           createStreamDriver(
@@ -781,7 +789,7 @@
               mainThreadRunner,
               tooltipApi,
               UiRefreshReason.getDefaultInstance(),
-              streamScrollMonitor);
+              scrollListenerNotifier);
 
       showSpinnerWithDelay();
       adapter.setDriver(streamDriver);
@@ -826,7 +834,7 @@
       MainThreadRunner mainThreadRunner,
       TooltipApi tooltipApi,
       UiRefreshReason uiRefreshReason,
-      StreamScrollMonitor streamScrollMonitor) {
+      ScrollListenerNotifier scrollListenerNotifier) {
     return new StreamDriver(
         actionApi,
         actionManager,
@@ -849,7 +857,7 @@
         viewLoggingUpdater,
         tooltipApi,
         uiRefreshReason,
-        streamScrollMonitor);
+        scrollMonitor);
   }
 
   @VisibleForTesting
@@ -859,7 +867,7 @@
       PietManager pietManager,
       DeepestContentTracker deepestContentTracker,
       StreamContentChangedListener streamContentChangedListener,
-      StreamScrollMonitor streamScrollMonitor,
+      ScrollObservable scrollObservable,
       Configuration configuration,
       PietEventLogger pietEventLogger) {
     return new StreamRecyclerViewAdapter(
@@ -869,15 +877,17 @@
         pietManager,
         deepestContentTracker,
         streamContentChangedListener,
-        streamScrollMonitor,
+        scrollObservable,
         configuration,
         pietEventLogger);
   }
 
   @VisibleForTesting
-  StreamScrollMonitor createStreamScrollMonitor(
-      ContentChangedListener contentChangedListener, MainThreadRunner mainThreadRunner) {
-    return new StreamScrollMonitor(contentChangedListener, mainThreadRunner);
+  ScrollListenerNotifier createScrollListenerNotifier(
+      ContentChangedListener contentChangedListener,
+      BasicStreamScrollMonitor scrollMonitor,
+      MainThreadRunner mainThreadRunner) {
+    return new ScrollListenerNotifier(contentChangedListener, scrollMonitor, mainThreadRunner);
   }
 
   @VisibleForTesting
@@ -895,17 +905,17 @@
   ScrollRestorer createScrollRestorer(
       Configuration configuration,
       RecyclerView recyclerView,
-      StreamScrollMonitor streamScrollMonitor,
+      ScrollListenerNotifier scrollListenerNotifier,
       /*@Nullable*/ ScrollState scrollState) {
-    return new ScrollRestorer(configuration, recyclerView, streamScrollMonitor, scrollState);
+    return new ScrollRestorer(configuration, recyclerView, scrollListenerNotifier, scrollState);
   }
 
   @VisibleForTesting
   ScrollRestorer createNonRestoringScrollRestorer(
       Configuration configuration,
       RecyclerView recyclerView,
-      StreamScrollMonitor streamScrollMonitor) {
-    return ScrollRestorer.nonRestoringRestorer(configuration, recyclerView, streamScrollMonitor);
+      ScrollListenerNotifier scrollListenerNotifier) {
+    return ScrollRestorer.nonRestoringRestorer(configuration, recyclerView, scrollListenerNotifier);
   }
 
   @VisibleForTesting
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/BUILD b/src/main/java/com/google/android/libraries/feed/basicstream/internal/BUILD
index df77de5..3c4c505 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/BUILD
@@ -9,14 +9,13 @@
         "//src/main/java/com/google/android/libraries/feed/api/stream",
         "//src/main/java/com/google/android/libraries/feed/basicstream/internal/drivers",
         "//src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders",
-        "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/logging",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/stream",
         "//src/main/java/com/google/android/libraries/feed/piet",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/deepestcontenttracker",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
-        "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "@com_google_code_findbugs_jsr305//jar",
         "@maven//:com_android_support_interpolator",
         "@maven//:com_android_support_recyclerview_v7",
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapter.java b/src/main/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapter.java
index cdac0fb..335757d 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapter.java
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapter.java
@@ -48,7 +48,7 @@
 import com.google.android.libraries.feed.piet.PietManager;
 import com.google.android.libraries.feed.sharedstream.deepestcontenttracker.DeepestContentTracker;
 import com.google.android.libraries.feed.sharedstream.piet.PietEventLogger;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -75,7 +75,7 @@
   private boolean shown;
 
   /*@Nullable*/ private StreamDriver streamDriver;
-  private final StreamScrollMonitor streamScrollMonitor;
+  private final ScrollObservable scrollObservable;
 
   // Suppress initialization warnings for calling setHasStableIds on RecyclerView.Adapter
   @SuppressWarnings("initialization")
@@ -86,7 +86,7 @@
       PietManager pietManager,
       DeepestContentTracker deepestContentTracker,
       ContentChangedListener contentChangedListener,
-      StreamScrollMonitor streamScrollMonitor,
+      ScrollObservable scrollObservable,
       Configuration configuration,
       PietEventLogger eventLogger) {
     this.context = context;
@@ -101,7 +101,7 @@
     boundViewHolderToLeafFeatureDriverMap = new HashMap<>();
     streamContentVisible = true;
     this.deepestContentTracker = deepestContentTracker;
-    this.streamScrollMonitor = streamScrollMonitor;
+    this.scrollObservable = scrollObservable;
     this.eventLogger = eventLogger;
   }
 
@@ -125,7 +125,7 @@
         cardConfiguration,
         frameLayout,
         pietManager,
-        streamScrollMonitor,
+        scrollObservable,
         viewport,
         context,
         configuration,
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriver.java b/src/main/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriver.java
index b530bd6..dee2e83 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriver.java
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriver.java
@@ -22,6 +22,7 @@
 import com.google.android.libraries.feed.api.stream.ContentChangedListener;
 import com.google.android.libraries.feed.basicstream.internal.drivers.ContinuationDriver.CursorChangedListener;
 import com.google.android.libraries.feed.basicstream.internal.pendingdismiss.PendingDismissHandler;
+import com.google.android.libraries.feed.basicstream.internal.scroll.BasicStreamScrollMonitor;
 import com.google.android.libraries.feed.basicstream.internal.scroll.BasicStreamScrollTracker;
 import com.google.android.libraries.feed.basicstream.internal.scroll.ScrollRestorer;
 import com.google.android.libraries.feed.basicstream.internal.viewloggingupdater.ViewLoggingUpdater;
@@ -55,7 +56,6 @@
 import com.google.android.libraries.feed.sharedstream.removetrackingfactory.StreamRemoveTrackingFactory;
 import com.google.android.libraries.feed.sharedstream.scroll.ScrollLogger;
 import com.google.android.libraries.feed.sharedstream.scroll.ScrollTracker;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
 import com.google.search.now.ui.action.FeedActionProto.UndoAction;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -119,7 +119,7 @@
       ViewLoggingUpdater viewLoggingUpdater,
       TooltipApi tooltipApi,
       UiRefreshReason uiRefreshReason,
-      StreamScrollMonitor scrollMonitor) {
+      BasicStreamScrollMonitor scrollMonitor) {
     this.actionApi = actionApi;
     this.actionManager = actionManager;
     this.actionParserFactory = actionParserFactory;
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD
index 8575b43..3957133 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD
@@ -6,13 +6,13 @@
     name = "scroll",
     srcs = glob(["*.java"]),
     deps = [
-        "//src/main/java/com/google/android/libraries/feed/api/stream",
         "//src/main/java/com/google/android/libraries/feed/common",
         "//src/main/java/com/google/android/libraries/feed/common/concurrent",
         "//src/main/java/com/google/android/libraries/feed/common/logging",
         "//src/main/java/com/google/android/libraries/feed/common/time",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
         "//src/main/proto/com/google/android/libraries/feed/sharedstream/proto:scroll_state_java_proto_lite",
         "@com_google_code_findbugs_jsr305//jar",
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollMonitor.java b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollMonitor.java
new file mode 100644
index 0000000..9829c21
--- /dev/null
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollMonitor.java
@@ -0,0 +1,81 @@
+// Copyright 2018 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.basicstream.internal.scroll;
+
+import android.support.v7.widget.RecyclerView;
+import com.google.android.libraries.feed.common.time.Clock;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObserver;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Monitors and dispatches scroll events to the registered listeners.
+ *
+ * <p>Note: This is intentionally unscoped, because we want features to be able to manage their
+ * scroll locally, instead of having a singleton ScrollMonitor.
+ */
+public class BasicStreamScrollMonitor extends RecyclerView.OnScrollListener
+    implements ScrollObservable {
+
+  private final Set<ScrollObserver> scrollObservers;
+  private final Clock clock;
+
+  private int currentScrollState = RecyclerView.SCROLL_STATE_IDLE;
+
+  public BasicStreamScrollMonitor(Clock clock) {
+    this.clock = clock;
+    scrollObservers = Collections.newSetFromMap(new ConcurrentHashMap<>());
+  }
+
+  @Override
+  public void addScrollObserver(ScrollObserver scrollObserver) {
+    scrollObservers.add(scrollObserver);
+  }
+
+  @Override
+  public void removeScrollObserver(ScrollObserver scrollObserver) {
+    scrollObservers.remove(scrollObserver);
+  }
+
+  @Override
+  public int getCurrentScrollState() {
+    return currentScrollState;
+  }
+
+  public int getObserverCount() {
+    return scrollObservers.size();
+  }
+
+  /**
+   * Notify the monitor of a scroll state change event that should be dispatched to the observers.
+   */
+  @Override
+  public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+    currentScrollState = newState;
+    for (ScrollObserver observer : scrollObservers) {
+      observer.onScrollStateChanged(recyclerView, "", newState, clock.currentTimeMillis());
+    }
+  }
+
+  /** Notify the monitor of a scroll event that should be dispatched to its observers. */
+  @Override
+  public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+    for (ScrollObserver observer : scrollObservers) {
+      observer.onScroll(recyclerView, "", dx, dy);
+    }
+  }
+}
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTracker.java b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTracker.java
index 6c95b12..8b83189 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTracker.java
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTracker.java
@@ -14,31 +14,31 @@
 
 package com.google.android.libraries.feed.basicstream.internal.scroll;
 
-import com.google.android.libraries.feed.api.stream.ScrollListener;
-import com.google.android.libraries.feed.api.stream.ScrollListener.ScrollState;
+import android.view.View;
 import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.time.Clock;
 import com.google.android.libraries.feed.host.logging.ScrollType;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObserver;
 import com.google.android.libraries.feed.sharedstream.scroll.ScrollLogger;
 import com.google.android.libraries.feed.sharedstream.scroll.ScrollTracker;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
 
 /** A @link{ScrollTracker} used by BasicStream */
 public class BasicStreamScrollTracker extends ScrollTracker {
   private final ScrollLogger scrollLogger;
-  private final ScrollListener scrollListener;
-  private final StreamScrollMonitor scrollMonitor;
+  private final ScrollObserver scrollObserver;
+  private final ScrollObservable scrollObservable;
 
   public BasicStreamScrollTracker(
       MainThreadRunner mainThreadRunner,
       ScrollLogger scrollLogger,
       Clock clock,
-      StreamScrollMonitor scrollMonitor) {
+      ScrollObservable scrollObservable) {
     super(mainThreadRunner, clock);
     this.scrollLogger = scrollLogger;
-    this.scrollMonitor = scrollMonitor;
-    this.scrollListener = new BasicStreamScrollListener();
-    scrollMonitor.addScrollListener(scrollListener);
+    this.scrollObservable = scrollObservable;
+    this.scrollObserver = new BasicStreamScrollObserver();
+    scrollObservable.addScrollObserver(scrollObserver);
   }
 
   @Override
@@ -49,16 +49,16 @@
   @Override
   public void onUnbind() {
     super.onUnbind();
-    scrollMonitor.removeScrollListener(scrollListener);
+    scrollObservable.removeScrollObserver(scrollObserver);
   }
 
-  private class BasicStreamScrollListener implements ScrollListener {
+  private class BasicStreamScrollObserver implements ScrollObserver {
 
     @Override
-    public void onScrollStateChanged(@ScrollState int state) {}
+    public void onScrollStateChanged(View view, String featureId, int newState, long timestamp) {}
 
     @Override
-    public void onScrolled(int dx, int dy) {
+    public void onScroll(View view, String featureId, int dx, int dy) {
       trackScroll(dx, dy);
     }
   }
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorer.java b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorer.java
index a769af6..e23ea16 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorer.java
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorer.java
@@ -22,8 +22,8 @@
 import com.google.android.libraries.feed.common.logging.Logger;
 import com.google.android.libraries.feed.host.config.Configuration;
 import com.google.android.libraries.feed.sharedstream.proto.ScrollStateProto.ScrollState;
+import com.google.android.libraries.feed.sharedstream.scroll.ScrollListenerNotifier;
 import com.google.android.libraries.feed.sharedstream.scroll.ScrollRestoreHelper;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
 
 /**
  * Class which is able to save a scroll position for a RecyclerView and restore to that scroll
@@ -35,7 +35,7 @@
 
   private final Configuration configuration;
   private final RecyclerView recyclerView;
-  private final StreamScrollMonitor streamScrollMonitor;
+  private final ScrollListenerNotifier scrollListenerNotifier;
   private final int scrollPosition;
   private final int scrollOffset;
 
@@ -44,11 +44,11 @@
   public ScrollRestorer(
       Configuration configuration,
       RecyclerView recyclerView,
-      StreamScrollMonitor streamScrollMonitor,
+      ScrollListenerNotifier scrollListenerNotifier,
       /*@Nullable*/ ScrollState scrollState) {
     this.configuration = configuration;
     this.recyclerView = recyclerView;
-    this.streamScrollMonitor = streamScrollMonitor;
+    this.scrollListenerNotifier = scrollListenerNotifier;
 
     if (scrollState != null) {
       canRestore = true;
@@ -63,11 +63,11 @@
   private ScrollRestorer(
       Configuration configuration,
       RecyclerView recyclerView,
-      StreamScrollMonitor streamScrollMonitor) {
+      ScrollListenerNotifier scrollListenerNotifier) {
     canRestore = false;
     this.configuration = configuration;
     this.recyclerView = recyclerView;
-    this.streamScrollMonitor = streamScrollMonitor;
+    this.scrollListenerNotifier = scrollListenerNotifier;
     scrollPosition = 0;
     scrollOffset = 0;
   }
@@ -79,8 +79,8 @@
   public static ScrollRestorer nonRestoringRestorer(
       Configuration configuration,
       RecyclerView recyclerView,
-      StreamScrollMonitor streamScrollMonitor) {
-    return new ScrollRestorer(configuration, recyclerView, streamScrollMonitor);
+      ScrollListenerNotifier scrollListenerNotifier) {
+    return new ScrollRestorer(configuration, recyclerView, scrollListenerNotifier);
   }
 
   /**
@@ -102,7 +102,7 @@
     }
     Logger.d(TAG, "Restoring scroll");
     getLayoutManager().scrollToPositionWithOffset(scrollPosition, scrollOffset);
-    streamScrollMonitor.onProgrammaticScroll(recyclerView);
+    scrollListenerNotifier.onProgrammaticScroll(recyclerView);
     canRestore = false;
   }
 
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD b/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD
index 7fddbd8..facca52 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD
@@ -20,6 +20,7 @@
         "//src/main/java/com/google/android/libraries/feed/piet/host",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/logging",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/ui",  # buildcleaner: keep
         "//src/main/proto/search/now/ui/action:feed_action_payload_java_proto_lite",
diff --git a/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolder.java b/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolder.java
index ab0ce8f..7ecaddd 100644
--- a/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolder.java
+++ b/src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolder.java
@@ -18,12 +18,12 @@
 
 import android.content.Context;
 import android.support.annotation.VisibleForTesting;
+import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.LayoutParams;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.FrameLayout;
-import com.google.android.libraries.feed.api.stream.ScrollListener;
 import com.google.android.libraries.feed.common.logging.Logger;
 import com.google.android.libraries.feed.common.ui.LayoutUtils;
 import com.google.android.libraries.feed.host.action.StreamActionApi;
@@ -38,7 +38,9 @@
 import com.google.android.libraries.feed.sharedstream.logging.LoggingListener;
 import com.google.android.libraries.feed.sharedstream.logging.VisibilityMonitor;
 import com.google.android.libraries.feed.sharedstream.piet.PietEventLogger;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObserver;
+import com.google.android.libraries.feed.sharedstream.scroll.ScrollListenerNotifier;
 import com.google.search.now.ui.action.FeedActionPayloadProto.FeedActionPayload;
 import com.google.search.now.ui.piet.PietProto.Frame;
 import com.google.search.now.ui.piet.PietProto.PietSharedState;
@@ -53,7 +55,7 @@
   private static final String TAG = "PietViewHolder";
   private final CardConfiguration cardConfiguration;
   private final FrameLayout cardView;
-  private final StreamScrollMonitor streamScrollMonitor;
+  private final ScrollObservable scrollObservable;
   private final FrameAdapter frameAdapter;
   private final VisibilityMonitor visibilityMonitor;
   private final View viewport;
@@ -63,13 +65,13 @@
   /*@Nullable*/ private LoggingListener loggingListener;
   /*@Nullable*/ private StreamActionApi streamActionApi;
   /*@Nullable*/ private FeedActionPayload swipeAction;
-  /*@Nullable*/ private PietViewActionScrollListener scrollListener;
+  /*@Nullable*/ private PietViewActionScrollObserver scrollObserver;
 
   public PietViewHolder(
       CardConfiguration cardConfiguration,
       FrameLayout cardView,
       PietManager pietManager,
-      StreamScrollMonitor streamScrollMonitor,
+      ScrollObservable scrollObservable,
       View viewport,
       Context context,
       Configuration configuration,
@@ -77,7 +79,7 @@
     super(cardView);
     this.cardConfiguration = cardConfiguration;
     this.cardView = cardView;
-    this.streamScrollMonitor = streamScrollMonitor;
+    this.scrollObservable = scrollObservable;
     this.viewport = viewport;
     cardView.setId(R.id.feed_content_card);
     this.frameAdapter =
@@ -121,7 +123,7 @@
     this.streamActionApi = streamActionApi;
     this.swipeAction = swipeAction;
     this.actionParser = actionParser;
-    scrollListener = new PietViewActionScrollListener(frameAdapter, viewport, loggingListener);
+    scrollObserver = new PietViewActionScrollObserver(frameAdapter, viewport, loggingListener);
     // Need to reset padding here.  Setting a background can affect padding so if we switch from
     // a background which has padding to one that does not, then the padding needs to be removed.
     cardView.setPadding(0, 0, 0, 0);
@@ -148,8 +150,8 @@
         0, // TODO: set the frame width here
         null,
         pietSharedStates);
-    if (scrollListener != null) {
-      streamScrollMonitor.addScrollListener(scrollListener);
+    if (scrollObserver != null) {
+      scrollObservable.addScrollObserver(scrollObserver);
     }
 
     bound = true;
@@ -167,8 +169,8 @@
     streamActionApi = null;
     swipeAction = null;
     visibilityMonitor.setListener(null);
-    if (scrollListener != null) {
-      streamScrollMonitor.removeScrollListener(scrollListener);
+    if (scrollObserver != null) {
+      scrollObservable.removeScrollObserver(scrollObserver);
     }
     bound = false;
   }
@@ -215,12 +217,12 @@
         actionParser, "Action parser can only be retrieved once view holder has been bound");
   }
 
-  static class PietViewActionScrollListener implements ScrollListener {
+  static class PietViewActionScrollObserver implements ScrollObserver {
     private final FrameAdapter frameAdapter;
     private final View viewport;
     private final LoggingListener loggingListener;
 
-    PietViewActionScrollListener(
+    PietViewActionScrollObserver(
         FrameAdapter frameAdapter, View viewport, LoggingListener loggingListener) {
       this.frameAdapter = frameAdapter;
       this.viewport = viewport;
@@ -228,19 +230,20 @@
     }
 
     @Override
-    public void onScrollStateChanged(@ScrollState int state) {
+    public void onScrollStateChanged(View view, String featureId, int newState, long timestamp) {
       // There was logic in here previously ensuring that the state had changed; however, you can
       // have a case where the feed is idle and you fling the feed, and it goes to idle again
       // without triggering the observer during scrolling. Just triggering this every time the feed
       // comes to rest appears to have the desired behavior. We can think about also triggering
       // while the feed is scrolling; not sure how frequently the observer triggers during scroll.
-      if (state == ScrollState.IDLE) {
+      if (newState == RecyclerView.SCROLL_STATE_IDLE) {
         frameAdapter.triggerViewActions(viewport);
       }
-      loggingListener.onScrollStateChanged(state);
+      loggingListener.onScrollStateChanged(
+          ScrollListenerNotifier.convertRecyclerViewScrollStateToListenerState(newState));
     }
 
     @Override
-    public void onScrolled(int dx, int dy) {}
+    public void onScroll(View view, String featureId, int dx, int dy) {}
   }
 }
diff --git a/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD b/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD
index def854e..7ba5625 100644
--- a/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD
+++ b/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD
@@ -12,6 +12,7 @@
         "//src/main/java/com/google/android/libraries/feed/common/time",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/proto/com/google/android/libraries/feed/sharedstream/proto:scroll_state_java_proto_lite",
         "@com_google_code_findbugs_jsr305//jar",
         "@maven//:com_android_support_recyclerview_v7",
diff --git a/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/StreamScrollMonitor.java b/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/ScrollListenerNotifier.java
similarity index 79%
rename from src/main/java/com/google/android/libraries/feed/sharedstream/scroll/StreamScrollMonitor.java
rename to src/main/java/com/google/android/libraries/feed/sharedstream/scroll/ScrollListenerNotifier.java
index 1036b35..83b42f2 100644
--- a/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/StreamScrollMonitor.java
+++ b/src/main/java/com/google/android/libraries/feed/sharedstream/scroll/ScrollListenerNotifier.java
@@ -16,18 +16,20 @@
 
 import static com.google.android.libraries.feed.api.stream.ScrollListener.UNKNOWN_SCROLL_DELTA;
 
-import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.RecyclerView;
+import android.view.View;
 import com.google.android.libraries.feed.api.stream.ContentChangedListener;
 import com.google.android.libraries.feed.api.stream.ScrollListener;
 import com.google.android.libraries.feed.api.stream.ScrollListener.ScrollState;
 import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
 import com.google.android.libraries.feed.common.logging.Logger;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObserver;
 import java.util.HashSet;
 import java.util.Set;
 
 /** Class which monitors scrolls and notifies listeners on changes. */
-public class StreamScrollMonitor extends RecyclerView.OnScrollListener {
+public class ScrollListenerNotifier implements ScrollObserver {
 
   private static final String TAG = "StreamScrollMonitor";
 
@@ -35,16 +37,18 @@
   private final Set<ScrollListener> scrollListeners;
   private final ContentChangedListener contentChangedListener;
 
-  // Nullness checker doesn't like adding listeners in constructor.  This is OK as RecyclerView will
-  // not call listener when it is added.
+  // Doesn't like adding itself to the scrollobservable
   @SuppressWarnings("initialization")
-  public StreamScrollMonitor(
+  public ScrollListenerNotifier(
       ContentChangedListener childChangeListener,
+      ScrollObservable scrollObservable,
       MainThreadRunner mainThreadRunner) {
     this.contentChangedListener = childChangeListener;
     this.mainThreadRunner = mainThreadRunner;
 
     scrollListeners = new HashSet<>();
+
+    scrollObservable.addScrollObserver(this);
   }
 
   public void addScrollListener(ScrollListener listener) {
@@ -64,32 +68,12 @@
         TAG + " onProgrammaticScroll",
         () -> {
           // Post scroll as this allows users of scroll to retrieve new heights/widths of change.
-          onScrolled(recyclerView, UNKNOWN_SCROLL_DELTA, UNKNOWN_SCROLL_DELTA);
+          onScroll(recyclerView, "", UNKNOWN_SCROLL_DELTA, UNKNOWN_SCROLL_DELTA);
         });
   }
 
-  @Override
-  public void onScrollStateChanged(RecyclerView recyclerView, int state) {
-    if (state == RecyclerView.SCROLL_STATE_IDLE) {
-      contentChangedListener.onContentChanged();
-    }
-
-    int scrollState = convertRecyclerViewScrollStateToListenerState(state);
-    for (ScrollListener scrollListener : scrollListeners) {
-      scrollListener.onScrollStateChanged(scrollState);
-    }
-  }
-
-  @Override
-  public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
-    for (ScrollListener scrollListener : scrollListeners) {
-      scrollListener.onScrolled(dx, dy);
-    }
-  }
-
   @ScrollState
-  @VisibleForTesting
-  static int convertRecyclerViewScrollStateToListenerState(int state) {
+  public static int convertRecyclerViewScrollStateToListenerState(int state) {
     switch (state) {
       case RecyclerView.SCROLL_STATE_DRAGGING:
         return ScrollState.DRAGGING;
@@ -103,8 +87,22 @@
     }
   }
 
-  @VisibleForTesting
-  public int getListenerCount() {
-    return scrollListeners.size();
+  @Override
+  public void onScrollStateChanged(View view, String featureId, int newState, long timestamp) {
+    if (newState == RecyclerView.SCROLL_STATE_IDLE) {
+      contentChangedListener.onContentChanged();
+    }
+
+    int scrollState = convertRecyclerViewScrollStateToListenerState(newState);
+    for (ScrollListener scrollListener : scrollListeners) {
+      scrollListener.onScrollStateChanged(scrollState);
+    }
+  }
+
+  @Override
+  public void onScroll(View view, String featureId, int dx, int dy) {
+    for (ScrollListener scrollListener : scrollListeners) {
+      scrollListener.onScrolled(dx, dy);
+    }
   }
 }
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/BUILD b/src/test/java/com/google/android/libraries/feed/basicstream/BUILD
index ae73e40..83187ad 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/BUILD
@@ -41,6 +41,7 @@
         "//src/main/java/com/google/android/libraries/feed/sharedstream/offlinemonitor",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/menumeasurer",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
         "//src/main/java/com/google/android/libraries/feed/testing/shadows",
         "//src/main/proto/com/google/android/libraries/feed/basicstream/internal/proto:stream_saved_instance_state_java_proto_lite",
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/BasicStreamTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/BasicStreamTest.java
index 8352a91..0a3c992 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/BasicStreamTest.java
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/BasicStreamTest.java
@@ -21,8 +21,8 @@
 import static com.google.android.libraries.feed.common.testing.RunnableSubject.assertThatRunnable;
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -50,6 +50,7 @@
 import com.google.android.libraries.feed.basicstream.internal.StreamRecyclerViewAdapter;
 import com.google.android.libraries.feed.basicstream.internal.StreamSavedInstanceStateProto.StreamSavedInstanceState;
 import com.google.android.libraries.feed.basicstream.internal.drivers.StreamDriver;
+import com.google.android.libraries.feed.basicstream.internal.scroll.BasicStreamScrollMonitor;
 import com.google.android.libraries.feed.basicstream.internal.scroll.ScrollRestorer;
 import com.google.android.libraries.feed.basicstream.internal.viewloggingupdater.ViewLoggingUpdater;
 import com.google.android.libraries.feed.common.concurrent.MainThreadRunner;
@@ -93,7 +94,8 @@
 import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason;
 import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason.Reason;
 import com.google.android.libraries.feed.sharedstream.publicapi.menumeasurer.MenuMeasurer;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.scroll.ScrollListenerNotifier;
 import com.google.android.libraries.feed.testing.shadows.ShadowRecycledViewPool;
 import com.google.protobuf.InvalidProtocolBufferException;
 import com.google.search.now.feed.client.StreamDataProto.UiContext;
@@ -148,7 +150,7 @@
   @Mock private SnackbarApi snackbarApi;
   @Mock private StreamDriver streamDriver;
   @Mock private StreamRecyclerViewAdapter adapter;
-  @Mock private StreamScrollMonitor streamScrollMonitor;
+  @Mock private ScrollListenerNotifier scrollListenerNotifier;
   @Mock private ScrollRestorer nonRestoringScrollRestorer;
   @Mock private ScrollRestorer scrollRestorer;
   @Mock private BasicLoggingApi basicLoggingApi;
@@ -763,14 +765,14 @@
   public void testAddScrollListener() {
     ScrollListener scrollListener = mock(ScrollListener.class);
     basicStream.addScrollListener(scrollListener);
-    verify(streamScrollMonitor).addScrollListener(scrollListener);
+    verify(scrollListenerNotifier).addScrollListener(scrollListener);
   }
 
   @Test
   public void testRemoveScrollListener() {
     ScrollListener scrollListener = mock(ScrollListener.class);
     basicStream.removeScrollListener(scrollListener);
-    verify(streamScrollMonitor).removeScrollListener(scrollListener);
+    verify(scrollListenerNotifier).removeScrollListener(scrollListener);
   }
 
   @Test
@@ -1308,7 +1310,7 @@
         MainThreadRunner mainThreadRunner,
         TooltipApi tooltipApi,
         UiRefreshReason uiRefreshReason,
-        StreamScrollMonitor streamScrollMonitor) {
+        ScrollListenerNotifier scrollListenerNotifier) {
       streamDriverScrollRestorer = scrollRestorer;
       streamDriverRestoring = restoring;
       this.streamDriverUiRefreshReason = uiRefreshReason;
@@ -1328,16 +1330,18 @@
         PietManager pietManager,
         DeepestContentTracker deepestContentTracker,
         StreamContentChangedListener streamContentChangedListener,
-        StreamScrollMonitor streamScrollMonitor,
+        ScrollObservable scrollObservable,
         Configuration configuration,
         PietEventLogger pietEventLogger) {
       return adapter;
     }
 
     @Override
-    StreamScrollMonitor createStreamScrollMonitor(
-        ContentChangedListener contentChangedListener, MainThreadRunner mainThreadRunner) {
-      return streamScrollMonitor;
+    ScrollListenerNotifier createScrollListenerNotifier(
+        ContentChangedListener contentChangedListener,
+        BasicStreamScrollMonitor scrollMonitor,
+        MainThreadRunner mainThreadRunner) {
+      return scrollListenerNotifier;
     }
 
     @Override
@@ -1349,7 +1353,7 @@
     ScrollRestorer createScrollRestorer(
         Configuration configuration,
         RecyclerView recyclerView,
-        StreamScrollMonitor streamScrollMonitor,
+        ScrollListenerNotifier scrollListenerNotifier,
         /*@Nullable*/ ScrollState scrollState) {
       return scrollRestorer;
     }
@@ -1358,7 +1362,7 @@
     ScrollRestorer createNonRestoringScrollRestorer(
         Configuration configuration,
         RecyclerView recyclerView,
-        StreamScrollMonitor streamScrollMonitor) {
+        ScrollListenerNotifier scrollListenerNotifier) {
       return nonRestoringScrollRestorer;
     }
 
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/BUILD b/src/test/java/com/google/android/libraries/feed/basicstream/internal/BUILD
index 849cb5c..25aa053 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/BUILD
@@ -59,7 +59,7 @@
         "//src/main/java/com/google/android/libraries/feed/piet/host",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/deepestcontenttracker",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
-        "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/ui",
         "//third_party:robolectric",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapterTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapterTest.java
index 533b14c..af6cbf8 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapterTest.java
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/StreamRecyclerViewAdapterTest.java
@@ -53,7 +53,7 @@
 import com.google.android.libraries.feed.piet.host.EventLogger;
 import com.google.android.libraries.feed.sharedstream.deepestcontenttracker.DeepestContentTracker;
 import com.google.android.libraries.feed.sharedstream.piet.PietEventLogger;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
 import com.google.android.libraries.feed.sharedstream.ui.R;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Lists;
@@ -93,7 +93,7 @@
   @Mock private DeepestContentTracker deepestContentTracker;
   @Mock private Header header1;
   @Mock private Header header2;
-  @Mock private StreamScrollMonitor streamScrollMonitor;
+  @Mock private ScrollObservable scrollObservable;
   @Mock private PietEventLogger pietEventLogger;
 
   private Context context;
@@ -137,7 +137,7 @@
             pietManager,
             deepestContentTracker,
             contentChangedListener,
-            streamScrollMonitor,
+            scrollObservable,
             CONFIGURATION,
             pietEventLogger);
     streamRecyclerViewAdapter.setHeaders(headers);
@@ -371,7 +371,7 @@
             pietManager,
             deepestContentTracker,
             contentChangedListener,
-            streamScrollMonitor,
+            scrollObservable,
             CONFIGURATION,
             pietEventLogger) {
           @Override
@@ -401,7 +401,7 @@
             pietManager,
             deepestContentTracker,
             contentChangedListener,
-            streamScrollMonitor,
+            scrollObservable,
             CONFIGURATION,
             pietEventLogger) {
           @Override
@@ -494,7 +494,7 @@
             pietManager,
             deepestContentTracker,
             contentChangedListener,
-            streamScrollMonitor,
+            scrollObservable,
             CONFIGURATION,
             pietEventLogger) {
           @Override
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriverTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriverTest.java
index 7fd2634..6ec8d88 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriverTest.java
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/drivers/StreamDriverTest.java
@@ -35,6 +35,7 @@
 import com.google.android.libraries.feed.basicstream.internal.drivers.StreamDriver.StreamContentListener;
 import com.google.android.libraries.feed.basicstream.internal.drivers.testing.FakeFeatureDriver;
 import com.google.android.libraries.feed.basicstream.internal.drivers.testing.FakeLeafFeatureDriver;
+import com.google.android.libraries.feed.basicstream.internal.scroll.BasicStreamScrollMonitor;
 import com.google.android.libraries.feed.basicstream.internal.scroll.ScrollRestorer;
 import com.google.android.libraries.feed.basicstream.internal.viewloggingupdater.ViewLoggingUpdater;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
@@ -63,7 +64,6 @@
 import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason;
 import com.google.android.libraries.feed.sharedstream.proto.UiRefreshReasonProto.UiRefreshReason.Reason;
 import com.google.android.libraries.feed.sharedstream.removetrackingfactory.StreamRemoveTrackingFactory;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
 import com.google.android.libraries.feed.testing.modelprovider.FakeModelChild;
 import com.google.android.libraries.feed.testing.modelprovider.FakeModelCursor;
 import com.google.android.libraries.feed.testing.modelprovider.FakeModelFeature;
@@ -130,7 +130,7 @@
   @Mock private StreamOfflineMonitor streamOfflineMonitor;
   @Mock private PendingDismissCallback pendingDismissCallback;
   @Mock private TooltipApi tooltipApi;
-  @Mock private StreamScrollMonitor scrollMonitor;
+  @Mock private BasicStreamScrollMonitor scrollMonitor;
 
   @Captor private ArgumentCaptor<List<LeafFeatureDriver>> featureDriversCaptor;
 
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD
index d8e9bce..e73baa5 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BUILD
@@ -3,6 +3,26 @@
 licenses(["notice"])  # Apache 2
 
 android_local_test(
+    name = "BasicStreamScrollMonitorTest",
+    size = "small",
+    timeout = "moderate",
+    srcs = ["BasicStreamScrollMonitorTest.java"],
+    aapt_version = "aapt2",
+    manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
+    deps = [
+        "//src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
+        "//third_party:robolectric",
+        "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@maven//:com_android_support_recyclerview_v7",
+        "@maven//:com_google_truth_truth",
+        "@maven//:org_mockito_mockito_core",
+        "@robolectric//bazel:android-all",
+    ],
+)
+
+android_local_test(
     name = "BasicStreamScrollTrackerTest",
     size = "small",
     timeout = "moderate",
@@ -10,11 +30,11 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
-        "//src/main/java/com/google/android/libraries/feed/api/stream",
         "//src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll",
         "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
         "//third_party:robolectric",
         "@com_google_protobuf_javalite//:protobuf_java_lite",
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollMonitorTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollMonitorTest.java
new file mode 100644
index 0000000..44cfea7
--- /dev/null
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollMonitorTest.java
@@ -0,0 +1,137 @@
+// Copyright 2018 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.basicstream.internal.scroll;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.Activity;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObserver;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link BasicStreamScrollMonitor}. */
+@RunWith(RobolectricTestRunner.class)
+public final class BasicStreamScrollMonitorTest {
+
+  private static final String FEATURE_ID = "";
+  private static final long TIMESTAMP = 150000L;
+
+  @Mock private ScrollObserver scrollObserver1;
+  @Mock private ScrollObserver scrollObserver2;
+
+  private RecyclerView view;
+  private FakeClock clock;
+  private BasicStreamScrollMonitor scrollMonitor;
+
+  @Before
+  public void setUp() {
+    initMocks(this);
+    view = new RecyclerView(Robolectric.buildActivity(Activity.class).get());
+    clock = new FakeClock();
+    clock.set(TIMESTAMP);
+    scrollMonitor = new BasicStreamScrollMonitor(clock);
+  }
+
+  @Test
+  public void testCallbacks_onScroll() {
+    scrollMonitor.onScrolled(view, 0, 0);
+
+    scrollMonitor.addScrollObserver(scrollObserver1);
+
+    scrollMonitor.onScrolled(view, 1, 1);
+    scrollMonitor.onScrolled(view, 2, 2);
+
+    scrollMonitor.addScrollObserver(scrollObserver2);
+
+    scrollMonitor.onScrolled(view, 3, 3);
+    scrollMonitor.onScrolled(view, 4, 4);
+
+    scrollMonitor.removeScrollObserver(scrollObserver1);
+
+    scrollMonitor.onScrolled(view, 5, 5);
+    scrollMonitor.onScrolled(view, 6, 6);
+
+    InOrder inOrder1 = inOrder(scrollObserver1);
+    inOrder1.verify(scrollObserver1).onScroll(view, FEATURE_ID, 1, 1);
+    inOrder1.verify(scrollObserver1).onScroll(view, FEATURE_ID, 2, 2);
+    inOrder1.verify(scrollObserver1).onScroll(view, FEATURE_ID, 3, 3);
+    inOrder1.verify(scrollObserver1).onScroll(view, FEATURE_ID, 4, 4);
+
+    InOrder inOrder2 = inOrder(scrollObserver2);
+    inOrder2.verify(scrollObserver2).onScroll(view, FEATURE_ID, 3, 3);
+    inOrder2.verify(scrollObserver2).onScroll(view, FEATURE_ID, 4, 4);
+    inOrder2.verify(scrollObserver2).onScroll(view, FEATURE_ID, 5, 5);
+    inOrder2.verify(scrollObserver2).onScroll(view, FEATURE_ID, 6, 6);
+  }
+
+  @Test
+  public void testCallbacks_onScrollStateChanged() {
+    scrollMonitor.onScrollStateChanged(view, 0);
+
+    scrollMonitor.addScrollObserver(scrollObserver1);
+
+    scrollMonitor.onScrollStateChanged(view, 1);
+
+    scrollMonitor.addScrollObserver(scrollObserver2);
+
+    scrollMonitor.onScrollStateChanged(view, 2);
+
+    scrollMonitor.removeScrollObserver(scrollObserver1);
+
+    scrollMonitor.onScrollStateChanged(view, 1);
+    scrollMonitor.onScrollStateChanged(view, 0);
+
+    InOrder inOrder1 = inOrder(scrollObserver1);
+    inOrder1.verify(scrollObserver1).onScrollStateChanged(view, FEATURE_ID, 1, TIMESTAMP);
+    inOrder1.verify(scrollObserver1).onScrollStateChanged(view, FEATURE_ID, 2, TIMESTAMP);
+
+    InOrder inOrder2 = inOrder(scrollObserver2);
+    inOrder2.verify(scrollObserver2).onScrollStateChanged(view, FEATURE_ID, 2, TIMESTAMP);
+    inOrder2.verify(scrollObserver2).onScrollStateChanged(view, FEATURE_ID, 1, TIMESTAMP);
+    inOrder2.verify(scrollObserver2).onScrollStateChanged(view, FEATURE_ID, 0, TIMESTAMP);
+  }
+
+  @Test
+  public void testCallbacks_afterEvent() {
+    scrollMonitor.onScrolled(view, 1, 1);
+    scrollMonitor.onScrolled(view, 2, 2);
+
+    scrollMonitor.addScrollObserver(scrollObserver1);
+
+    verify(scrollObserver1, never()).onScroll(any(View.class), anyString(), anyInt(), anyInt());
+  }
+
+  @Test
+  public void testGetCurrentScrollState() {
+    assertThat(scrollMonitor.getCurrentScrollState()).isEqualTo(RecyclerView.SCROLL_STATE_IDLE);
+    scrollMonitor.onScrollStateChanged(view, RecyclerView.SCROLL_STATE_DRAGGING);
+    assertThat(scrollMonitor.getCurrentScrollState()).isEqualTo(RecyclerView.SCROLL_STATE_DRAGGING);
+  }
+}
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTrackerTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTrackerTest.java
index ab9e350..9bae5c6 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTrackerTest.java
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/BasicStreamScrollTrackerTest.java
@@ -14,16 +14,16 @@
 
 package com.google.android.libraries.feed.basicstream.internal.scroll;
 
-import static org.mockito.Matchers.any;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.MockitoAnnotations.initMocks;
 
-import com.google.android.libraries.feed.api.stream.ScrollListener;
 import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
 import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.host.logging.ScrollType;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObserver;
 import com.google.android.libraries.feed.sharedstream.scroll.ScrollLogger;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,7 +35,7 @@
 public class BasicStreamScrollTrackerTest {
 
   @Mock private ScrollLogger logger;
-  @Mock private StreamScrollMonitor scrollMonitor;
+  @Mock private ScrollObservable scrollObservable;
   private final FakeClock clock = new FakeClock();
 
   private final FakeMainThreadRunner mainThreadRunner = FakeMainThreadRunner.queueAllTasks();
@@ -44,14 +44,14 @@
   @Before
   public void setUp() {
     initMocks(this);
-    scrollTracker = new BasicStreamScrollTracker(mainThreadRunner, logger, clock, scrollMonitor);
-    verify(scrollMonitor).addScrollListener(any(ScrollListener.class));
+    scrollTracker = new BasicStreamScrollTracker(mainThreadRunner, logger, clock, scrollObservable);
+    verify(scrollObservable).addScrollObserver(any(ScrollObserver.class));
   }
 
   @Test
   public void onUnbind_removedScrollListener() {
     scrollTracker.onUnbind();
-    verify(scrollMonitor).removeScrollListener(any(ScrollListener.class));
+    verify(scrollObservable).removeScrollObserver(any(ScrollObserver.class));
   }
 
   @Test
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorerTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorerTest.java
index 9ca6821..7958091 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorerTest.java
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/scroll/ScrollRestorerTest.java
@@ -30,7 +30,7 @@
 import com.google.android.libraries.feed.host.config.Configuration;
 import com.google.android.libraries.feed.host.config.Configuration.ConfigKey;
 import com.google.android.libraries.feed.sharedstream.proto.ScrollStateProto.ScrollState;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
+import com.google.android.libraries.feed.sharedstream.scroll.ScrollListenerNotifier;
 import com.google.android.libraries.feed.testing.android.LinearLayoutManagerForTest;
 import org.junit.Before;
 import org.junit.Test;
@@ -51,7 +51,7 @@
   private static final ScrollState SCROLL_STATE =
       ScrollState.newBuilder().setPosition(TOP_POSITION).setOffset(OFFSET).build();
 
-  @Mock private StreamScrollMonitor streamScrollMonitor;
+  @Mock private ScrollListenerNotifier scrollListenerNotifier;
   @Mock private Configuration configuration;
 
   private RecyclerView recyclerView;
@@ -78,10 +78,10 @@
   @Test
   public void testRestoreScroll() {
     ScrollRestorer scrollRestorer =
-        new ScrollRestorer(configuration, recyclerView, streamScrollMonitor, SCROLL_STATE);
+        new ScrollRestorer(configuration, recyclerView, scrollListenerNotifier, SCROLL_STATE);
     scrollRestorer.maybeRestoreScroll();
 
-    verify(streamScrollMonitor).onProgrammaticScroll(recyclerView);
+    verify(scrollListenerNotifier).onProgrammaticScroll(recyclerView);
     assertThat(layoutManager.scrolledPosition).isEqualTo(TOP_POSITION);
     assertThat(layoutManager.scrolledOffset).isEqualTo(OFFSET);
   }
@@ -90,13 +90,13 @@
   public void testRestoreScroll_repeatedCall() {
     ScrollRestorer scrollRestorer =
         new ScrollRestorer(
-            configuration, recyclerView, streamScrollMonitor, createScrollState(10, 20));
+            configuration, recyclerView, scrollListenerNotifier, createScrollState(10, 20));
     scrollRestorer.maybeRestoreScroll();
 
     layoutManager.scrollToPositionWithOffset(TOP_POSITION, OFFSET);
     scrollRestorer.maybeRestoreScroll();
 
-    verify(streamScrollMonitor).onProgrammaticScroll(recyclerView);
+    verify(scrollListenerNotifier).onProgrammaticScroll(recyclerView);
     assertThat(layoutManager.scrolledPosition).isEqualTo(TOP_POSITION);
     assertThat(layoutManager.scrolledOffset).isEqualTo(OFFSET);
   }
@@ -107,7 +107,7 @@
 
     ScrollRestorer scrollRestorer =
         new ScrollRestorer(
-            configuration, recyclerView, streamScrollMonitor, createScrollState(10, 20));
+            configuration, recyclerView, scrollListenerNotifier, createScrollState(10, 20));
     scrollRestorer.abandonRestoringScroll();
     scrollRestorer.maybeRestoreScroll();
 
@@ -123,7 +123,7 @@
     view.setTop(OFFSET);
     layoutManager.addChildToPosition(TOP_POSITION, view);
     ScrollState restorerScrollState =
-        nonRestoringRestorer(configuration, recyclerView, streamScrollMonitor)
+        nonRestoringRestorer(configuration, recyclerView, scrollListenerNotifier)
             .getScrollStateForScrollRestore(HEADER_COUNT);
     assertThat(restorerScrollState).isEqualTo(SCROLL_STATE);
   }
@@ -131,7 +131,7 @@
   @Test
   public void testGetBundleForScrollPosition_invalidPosition() {
     assertThat(
-            nonRestoringRestorer(configuration, recyclerView, streamScrollMonitor)
+            nonRestoringRestorer(configuration, recyclerView, scrollListenerNotifier)
                 .getScrollStateForScrollRestore(10))
         .isNull();
   }
@@ -148,7 +148,7 @@
     view.setTop(OFFSET);
     layoutManager.addChildToPosition(TOP_POSITION, view);
     ScrollState restorerScrollState =
-        nonRestoringRestorer(configuration, recyclerView, streamScrollMonitor)
+        nonRestoringRestorer(configuration, recyclerView, scrollListenerNotifier)
             .getScrollStateForScrollRestore(HEADER_COUNT);
     assertThat(restorerScrollState).isEqualTo(SCROLL_STATE);
   }
@@ -164,7 +164,7 @@
     view.setTop(OFFSET);
     layoutManager.addChildToPosition(TOP_POSITION, view);
     ScrollState restorerScrollState =
-        nonRestoringRestorer(configuration, recyclerView, streamScrollMonitor)
+        nonRestoringRestorer(configuration, recyclerView, scrollListenerNotifier)
             .getScrollStateForScrollRestore(HEADER_COUNT);
     assertThat(restorerScrollState).isNull();
   }
@@ -181,7 +181,7 @@
     view.setTop(OFFSET);
     layoutManager.addChildToPosition(TOP_POSITION, view);
     ScrollState restorerScrollState =
-        nonRestoringRestorer(configuration, recyclerView, streamScrollMonitor)
+        nonRestoringRestorer(configuration, recyclerView, scrollListenerNotifier)
             .getScrollStateForScrollRestore(HEADER_COUNT);
     assertThat(restorerScrollState).isNull();
   }
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD b/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD
index b6743ed..468fb47 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/BUILD
@@ -67,11 +67,11 @@
     aapt_version = "aapt2",
     manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
     deps = [
-        "//src/main/java/com/google/android/libraries/feed/api/stream",
         "//src/main/java/com/google/android/libraries/feed/basicstream/internal/actions",
+        "//src/main/java/com/google/android/libraries/feed/basicstream/internal/scroll",
         "//src/main/java/com/google/android/libraries/feed/basicstream/internal/viewholders",
-        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
         "//src/main/java/com/google/android/libraries/feed/common/functional",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
         "//src/main/java/com/google/android/libraries/feed/host/config",
         "//src/main/java/com/google/android/libraries/feed/host/logging",
         "//src/main/java/com/google/android/libraries/feed/host/stream",
@@ -81,7 +81,6 @@
         "//src/main/java/com/google/android/libraries/feed/piet/testing",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/logging",
         "//src/main/java/com/google/android/libraries/feed/sharedstream/piet",
-        "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
         "//src/main/java/com/google/android/libraries/feed/testing/host/stream",
         "//src/main/proto/search/now/ui/action:feed_action_java_proto_lite",
         "//src/main/proto/search/now/ui/action:feed_action_payload_java_proto_lite",
diff --git a/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolderTest.java b/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolderTest.java
index c73bae3..b88bb12 100644
--- a/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolderTest.java
+++ b/src/test/java/com/google/android/libraries/feed/basicstream/internal/viewholders/PietViewHolderTest.java
@@ -15,9 +15,8 @@
 package com.google.android.libraries.feed.basicstream.internal.viewholders;
 
 import static com.google.common.truth.Truth.assertThat;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -33,10 +32,10 @@
 import android.view.ViewGroup.MarginLayoutParams;
 import android.widget.FrameLayout;
 import android.widget.TextView;
-import com.google.android.libraries.feed.api.stream.ContentChangedListener;
 import com.google.android.libraries.feed.basicstream.internal.actions.StreamActionApiImpl;
-import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
+import com.google.android.libraries.feed.basicstream.internal.scroll.BasicStreamScrollMonitor;
 import com.google.android.libraries.feed.common.functional.Supplier;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
 import com.google.android.libraries.feed.host.config.Configuration;
 import com.google.android.libraries.feed.host.logging.BasicLoggingApi;
 import com.google.android.libraries.feed.host.stream.CardConfiguration;
@@ -51,7 +50,6 @@
 import com.google.android.libraries.feed.sharedstream.logging.LoggingListener;
 import com.google.android.libraries.feed.sharedstream.logging.VisibilityMonitor;
 import com.google.android.libraries.feed.sharedstream.piet.PietEventLogger;
-import com.google.android.libraries.feed.sharedstream.scroll.StreamScrollMonitor;
 import com.google.android.libraries.feed.testing.host.stream.FakeCardConfiguration;
 import com.google.search.now.ui.action.FeedActionPayloadProto.FeedActionPayload;
 import com.google.search.now.ui.action.FeedActionProto.FeedAction;
@@ -90,7 +88,7 @@
   @Mock private VisibilityMonitor visibilityMonitor;
   @Mock private BasicLoggingApi basicLoggingApi;
 
-  private StreamScrollMonitor streamScrollMonitor;
+  private BasicStreamScrollMonitor streamScrollMonitor;
   private FakeFrameAdapter frameAdapter;
   private ActionHandler actionHandler;
   private Configuration configuration;
@@ -104,9 +102,7 @@
   @Before
   public void setUp() {
     initMocks(this);
-    streamScrollMonitor =
-        new StreamScrollMonitor(
-            mock(ContentChangedListener.class), FakeMainThreadRunner.runTasksImmediately());
+    streamScrollMonitor = new BasicStreamScrollMonitor(new FakeClock());
     cardConfiguration = new FakeCardConfiguration();
     context = Robolectric.buildActivity(Activity.class).get();
     frameLayout = new FrameLayout(context);
@@ -269,7 +265,7 @@
         loggingListener,
         actionParser);
 
-    assertThat(streamScrollMonitor.getListenerCount()).isEqualTo(1);
+    assertThat(streamScrollMonitor.getObserverCount()).isEqualTo(1);
   }
 
   @Test
@@ -314,7 +310,7 @@
 
     pietViewHolder.unbind();
 
-    assertThat(streamScrollMonitor.getListenerCount()).isEqualTo(0);
+    assertThat(streamScrollMonitor.getObserverCount()).isEqualTo(0);
   }
 
   @Test
diff --git a/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD b/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD
index f8dd032..92467a5 100644
--- a/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD
+++ b/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/BUILD
@@ -3,6 +3,28 @@
 licenses(["notice"])  # Apache 2
 
 android_local_test(
+    name = "ScrollListenerNotifierTest",
+    size = "small",
+    timeout = "moderate",
+    srcs = ["ScrollListenerNotifierTest.java"],
+    aapt_version = "aapt2",
+    manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
+    deps = [
+        "//src/main/java/com/google/android/libraries/feed/api/stream",
+        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/testing",
+        "//src/main/java/com/google/android/libraries/feed/common/time/testing",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/publicapi/scroll",
+        "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
+        "//third_party:robolectric",
+        "@com_google_protobuf_javalite//:protobuf_java_lite",
+        "@maven//:com_android_support_recyclerview_v7",
+        "@maven//:org_mockito_mockito_core",
+        "@robolectric//bazel:android-all",
+    ],
+)
+
+android_local_test(
     name = "ScrollLoggerTest",
     size = "small",
     timeout = "moderate",
@@ -57,23 +79,3 @@
         "@robolectric//bazel:android-all",
     ],
 )
-
-android_local_test(
-    name = "StreamScrollMonitorTest",
-    size = "small",
-    timeout = "moderate",
-    srcs = ["StreamScrollMonitorTest.java"],
-    aapt_version = "aapt2",
-    manifest_values = DEFAULT_ANDROID_LOCAL_TEST_MANIFEST,
-    deps = [
-        "//src/main/java/com/google/android/libraries/feed/api/stream",
-        "//src/main/java/com/google/android/libraries/feed/common/concurrent/testing",
-        "//src/main/java/com/google/android/libraries/feed/common/testing",
-        "//src/main/java/com/google/android/libraries/feed/sharedstream/scroll",
-        "//third_party:robolectric",
-        "@com_google_protobuf_javalite//:protobuf_java_lite",
-        "@maven//:com_android_support_recyclerview_v7",
-        "@maven//:org_mockito_mockito_core",
-        "@robolectric//bazel:android-all",
-    ],
-)
diff --git a/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/ScrollListenerNotifierTest.java b/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/ScrollListenerNotifierTest.java
new file mode 100644
index 0000000..66973a6
--- /dev/null
+++ b/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/ScrollListenerNotifierTest.java
@@ -0,0 +1,174 @@
+// Copyright 2018 The Feed Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.android.libraries.feed.sharedstream.scroll;
+
+import static com.google.android.libraries.feed.api.stream.ScrollListener.UNKNOWN_SCROLL_DELTA;
+import static com.google.android.libraries.feed.common.testing.RunnableSubject.assertThatRunnable;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import com.google.android.libraries.feed.api.stream.ContentChangedListener;
+import com.google.android.libraries.feed.api.stream.ScrollListener;
+import com.google.android.libraries.feed.api.stream.ScrollListener.ScrollState;
+import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
+import com.google.android.libraries.feed.common.time.testing.FakeClock;
+import com.google.android.libraries.feed.sharedstream.publicapi.scroll.ScrollObservable;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+/** Tests for {@link ScrollListenerNotifier}. */
+@RunWith(RobolectricTestRunner.class)
+public class ScrollListenerNotifierTest {
+
+  private static final String FEATURE_ID = "feature";
+  private static final long TIME = 12345L;
+
+  @Mock private ScrollListener scrollListener1;
+  @Mock private ScrollListener scrollListener2;
+  @Mock private ContentChangedListener contentChangedListener;
+  @Mock private ScrollObservable scrollObservable;
+
+  private ScrollListenerNotifier scrollListenerNotifier;
+  private RecyclerView recyclerView;
+  private FakeMainThreadRunner mainThreadRunner;
+  private FakeClock clock;
+
+  @Before
+  public void setUp() {
+    initMocks(this);
+
+    Context context = Robolectric.buildActivity(Activity.class).get();
+    recyclerView = new RecyclerView(context);
+    mainThreadRunner = FakeMainThreadRunner.queueAllTasks();
+    clock = new FakeClock();
+    clock.set(TIME);
+
+    scrollListenerNotifier =
+        new ScrollListenerNotifier(contentChangedListener, scrollObservable, mainThreadRunner);
+    scrollListenerNotifier.addScrollListener(scrollListener1);
+  }
+
+  @Test
+  public void testConstructor() {
+    ScrollListenerNotifier notifier =
+        new ScrollListenerNotifier(contentChangedListener, scrollObservable, mainThreadRunner);
+
+    verify(scrollObservable).addScrollObserver(notifier);
+  }
+
+  @Test
+  public void testScrollStateOutputs() {
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_IDLE, TIME);
+    verify(scrollListener1).onScrollStateChanged(ScrollState.IDLE);
+
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_DRAGGING, TIME);
+    verify(scrollListener1).onScrollStateChanged(ScrollState.DRAGGING);
+
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_SETTLING, TIME);
+    verify(scrollListener1).onScrollStateChanged(ScrollState.SETTLING);
+
+    assertThatRunnable(
+            () -> scrollListenerNotifier.onScrollStateChanged(recyclerView, FEATURE_ID, -42, TIME))
+        .throwsAnExceptionOfType(RuntimeException.class);
+  }
+
+  @Test
+  public void testOnScrollStateChanged() {
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_IDLE, TIME);
+
+    verify(scrollListener1).onScrollStateChanged(ScrollState.IDLE);
+  }
+
+  @Test
+  public void testOnScrollStateChanged_notifiesObserverOnIdle() {
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_IDLE, TIME);
+
+    verify(contentChangedListener).onContentChanged();
+  }
+
+  @Test
+  public void testOnScrollStateChanged_multipleListeners() {
+    scrollListenerNotifier.addScrollListener(scrollListener2);
+
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_IDLE, TIME);
+
+    verify(scrollListener1).onScrollStateChanged(ScrollState.IDLE);
+    verify(scrollListener2).onScrollStateChanged(ScrollState.IDLE);
+  }
+
+  @Test
+  public void testOnScrollStateChanged_removedListener() {
+    scrollListenerNotifier.addScrollListener(scrollListener2);
+    scrollListenerNotifier.removeScrollListener(scrollListener1);
+
+    scrollListenerNotifier.onScrollStateChanged(
+        recyclerView, FEATURE_ID, RecyclerView.SCROLL_STATE_IDLE, TIME);
+
+    verify(scrollListener1, never()).onScrollStateChanged(anyInt());
+    verify(scrollListener2).onScrollStateChanged(ScrollState.IDLE);
+  }
+
+  @Test
+  public void testOnScrolled() {
+    scrollListenerNotifier.onScroll(recyclerView, FEATURE_ID, 1, 2);
+
+    verify(scrollListener1).onScrolled(1, 2);
+  }
+
+  @Test
+  public void testOnScrolled_multipleListeners() {
+    scrollListenerNotifier.addScrollListener(scrollListener2);
+    scrollListenerNotifier.onScroll(recyclerView, FEATURE_ID, 1, 2);
+
+    verify(scrollListener1).onScrolled(1, 2);
+    verify(scrollListener2).onScrolled(1, 2);
+  }
+
+  @Test
+  public void testOnScrolled_removedListener() {
+    scrollListenerNotifier.addScrollListener(scrollListener2);
+    scrollListenerNotifier.removeScrollListener(scrollListener1);
+
+    scrollListenerNotifier.onScroll(recyclerView, FEATURE_ID, 1, 2);
+
+    verify(scrollListener1, never()).onScrolled(anyInt(), anyInt());
+    verify(scrollListener2).onScrolled(1, 2);
+  }
+
+  @Test
+  public void onProgrammaticScroll() {
+    scrollListenerNotifier.onProgrammaticScroll(recyclerView);
+
+    verify(scrollListener1, never()).onScrolled(anyInt(), anyInt());
+
+    mainThreadRunner.runAllTasks();
+    verify(scrollListener1).onScrolled(UNKNOWN_SCROLL_DELTA, UNKNOWN_SCROLL_DELTA);
+  }
+}
diff --git a/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/StreamScrollMonitorTest.java b/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/StreamScrollMonitorTest.java
deleted file mode 100644
index d3a002f..0000000
--- a/src/test/java/com/google/android/libraries/feed/sharedstream/scroll/StreamScrollMonitorTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright 2018 The Feed Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.android.libraries.feed.sharedstream.scroll;
-
-import static com.google.android.libraries.feed.api.stream.ScrollListener.UNKNOWN_SCROLL_DELTA;
-import static com.google.android.libraries.feed.common.testing.RunnableSubject.assertThatRunnable;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.MockitoAnnotations.initMocks;
-
-import android.app.Activity;
-import android.content.Context;
-import android.support.v7.widget.RecyclerView;
-import com.google.android.libraries.feed.api.stream.ContentChangedListener;
-import com.google.android.libraries.feed.api.stream.ScrollListener;
-import com.google.android.libraries.feed.api.stream.ScrollListener.ScrollState;
-import com.google.android.libraries.feed.common.concurrent.testing.FakeMainThreadRunner;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.robolectric.Robolectric;
-import org.robolectric.RobolectricTestRunner;
-
-/** Tests for {@link StreamScrollMonitor}. */
-@RunWith(RobolectricTestRunner.class)
-public class StreamScrollMonitorTest {
-
-  @Mock private ScrollListener scrollListener1;
-  @Mock private ScrollListener scrollListener2;
-  @Mock private ContentChangedListener contentChangedListener;
-
-  private StreamScrollMonitor streamScrollMonitor;
-  private RecyclerView recyclerView;
-  private FakeMainThreadRunner mainThreadRunner;
-
-  @Before
-  public void setUp() {
-    initMocks(this);
-
-    Context context = Robolectric.buildActivity(Activity.class).get();
-    recyclerView = new RecyclerView(context);
-    mainThreadRunner = FakeMainThreadRunner.queueAllTasks();
-
-    streamScrollMonitor = new StreamScrollMonitor(contentChangedListener, mainThreadRunner);
-    streamScrollMonitor.addScrollListener(scrollListener1);
-  }
-
-  @Test
-  public void testScrollStateOutputs() {
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_IDLE);
-    verify(scrollListener1).onScrollStateChanged(ScrollState.IDLE);
-
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_DRAGGING);
-    verify(scrollListener1).onScrollStateChanged(ScrollState.DRAGGING);
-
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_SETTLING);
-    verify(scrollListener1).onScrollStateChanged(ScrollState.SETTLING);
-
-    assertThatRunnable(() -> streamScrollMonitor.onScrollStateChanged(recyclerView, -42))
-        .throwsAnExceptionOfType(RuntimeException.class);
-  }
-
-  @Test
-  public void testOnScrollStateChanged() {
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_IDLE);
-
-    verify(scrollListener1).onScrollStateChanged(ScrollState.IDLE);
-  }
-
-  @Test
-  public void testOnScrollStateChanged_notifiesObserverOnIdle() {
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_IDLE);
-
-    verify(contentChangedListener).onContentChanged();
-  }
-
-  @Test
-  public void testOnScrollStateChanged_multipleListeners() {
-    streamScrollMonitor.addScrollListener(scrollListener2);
-
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_IDLE);
-
-    verify(scrollListener1).onScrollStateChanged(ScrollState.IDLE);
-    verify(scrollListener2).onScrollStateChanged(ScrollState.IDLE);
-  }
-
-  @Test
-  public void testOnScrollStateChanged_removedListener() {
-    streamScrollMonitor.addScrollListener(scrollListener2);
-    streamScrollMonitor.removeScrollListener(scrollListener1);
-
-    streamScrollMonitor.onScrollStateChanged(recyclerView, RecyclerView.SCROLL_STATE_IDLE);
-
-    verify(scrollListener1, never()).onScrollStateChanged(anyInt());
-    verify(scrollListener2).onScrollStateChanged(ScrollState.IDLE);
-  }
-
-  @Test
-  public void testOnScrolled() {
-    streamScrollMonitor.onScrolled(recyclerView, 1, 2);
-
-    verify(scrollListener1).onScrolled(1, 2);
-  }
-
-  @Test
-  public void testOnScrolled_multipleListeners() {
-    streamScrollMonitor.addScrollListener(scrollListener2);
-    streamScrollMonitor.onScrolled(recyclerView, 1, 2);
-
-    verify(scrollListener1).onScrolled(1, 2);
-    verify(scrollListener2).onScrolled(1, 2);
-  }
-
-  @Test
-  public void testOnScrolled_removedListener() {
-    streamScrollMonitor.addScrollListener(scrollListener2);
-    streamScrollMonitor.removeScrollListener(scrollListener1);
-
-    streamScrollMonitor.onScrolled(recyclerView, 1, 2);
-
-    verify(scrollListener1, never()).onScrolled(anyInt(), anyInt());
-    verify(scrollListener2).onScrolled(1, 2);
-  }
-
-  @Test
-  public void onProgrammaticScroll() {
-    streamScrollMonitor.onProgrammaticScroll(recyclerView);
-
-    verify(scrollListener1, never()).onScrolled(anyInt(), anyInt());
-
-    mainThreadRunner.runAllTasks();
-    verify(scrollListener1).onScrolled(UNKNOWN_SCROLL_DELTA, UNKNOWN_SCROLL_DELTA);
-  }
-}