Add tests for edge view to the LinearLayoutManagerSnappingTest

Also, stabilize the Linear- and GridLayoutManagerSnappingTest
as they seemed to have a flaky setup.

Bug:31399980

Change-Id:If7780a5cc863a53cf4df946d186dfd5ae28274cc
diff --git a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
index 915556e..37197e4 100644
--- a/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
+++ b/v7/recyclerview/src/android/support/v7/widget/SnapHelper.java
@@ -53,6 +53,7 @@
                     }
                 }
 
+                @Override
                 public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                     if (dx != 0 || dy != 0) {
                         mScrolled = true;
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
index 3a72152..9109c87 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseGridLayoutManagerTest.java
@@ -16,6 +16,7 @@
 package android.support.v7.widget;
 
 import android.content.Context;
+import android.graphics.Rect;
 import android.view.View;
 import android.view.ViewGroup;
 
@@ -152,6 +153,8 @@
 
         CountDownLatch prefetchLatch;
 
+        OrientationHelper mSecondaryOrientation;
+
         List<GridLayoutManagerTest.Callback>
                 mCallbacks = new ArrayList<GridLayoutManagerTest.Callback>();
 
@@ -182,6 +185,26 @@
         }
 
         @Override
+        public void setOrientation(int orientation) {
+            super.setOrientation(orientation);
+            mSecondaryOrientation = null;
+        }
+
+        @Override
+        void ensureLayoutState() {
+            super.ensureLayoutState();
+            if (mSecondaryOrientation == null) {
+                if (getOrientation() == RecyclerView.HORIZONTAL) {
+                    mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
+                        RecyclerView.VERTICAL);
+                } else {
+                    mSecondaryOrientation = OrientationHelper.createOrientationHelper(this,
+                        RecyclerView.HORIZONTAL);
+                }
+            }
+        }
+
+        @Override
         public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
             try {
                 for (GridLayoutManagerTest.Callback callback : mCallbacks) {
@@ -213,6 +236,23 @@
             };
         }
 
+        Rect getViewBounds(View view) {
+            if (getOrientation() == HORIZONTAL) {
+                return new Rect(
+                    mOrientationHelper.getDecoratedStart(view),
+                    mSecondaryOrientation.getDecoratedStart(view),
+                    mOrientationHelper.getDecoratedEnd(view),
+                    mSecondaryOrientation.getDecoratedEnd(view));
+            } else {
+                return new Rect(
+                    mSecondaryOrientation.getDecoratedStart(view),
+                    mOrientationHelper.getDecoratedStart(view),
+                    mSecondaryOrientation.getDecoratedEnd(view),
+                    mOrientationHelper.getDecoratedEnd(view));
+            }
+
+        }
+
         public void expectLayout(int layoutCount) {
             mLayoutLatch = new CountDownLatch(layoutCount);
         }
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
index 50c9ba3..54190f1 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseLinearLayoutManagerTest.java
@@ -21,12 +21,14 @@
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import android.content.Context;
 import android.graphics.Rect;
+import android.support.annotation.Nullable;
 import android.support.v4.util.Pair;
 import android.util.Log;
 import android.view.View;
@@ -83,14 +85,21 @@
     }
 
     void setupByConfig(Config config, boolean waitForFirstLayout) throws Throwable {
+        setupByConfig(config, waitForFirstLayout, null, null);
+    }
+
+    void setupByConfig(Config config, boolean waitForFirstLayout,
+        @Nullable RecyclerView.LayoutParams childLayoutParams,
+        @Nullable RecyclerView.LayoutParams parentLayoutParams) throws Throwable {
         mRecyclerView = inflateWrappedRV();
 
         mRecyclerView.setHasFixedSize(true);
-        mTestAdapter = config.mTestAdapter == null ? new TestAdapter(config.mItemCount)
+        mTestAdapter = config.mTestAdapter == null
+                ? new TestAdapter(config.mItemCount, childLayoutParams)
                 : config.mTestAdapter;
         mRecyclerView.setAdapter(mTestAdapter);
         mLayoutManager = new WrappedLinearLayoutManager(getActivity(), config.mOrientation,
-                config.mReverseLayout);
+            config.mReverseLayout);
         mLayoutManager.setStackFromEnd(config.mStackFromEnd);
         mLayoutManager.setRecycleChildrenOnDetach(config.mRecycleChildrenOnDetach);
         mRecyclerView.setLayoutManager(mLayoutManager);
@@ -102,6 +111,10 @@
                     )
             );
         }
+        if (parentLayoutParams != null) {
+            mRecyclerView.setLayoutParams(parentLayoutParams);
+        }
+
         if (waitForFirstLayout) {
             waitForFirstLayout();
         }
@@ -342,15 +355,26 @@
     class WrappedLinearLayoutManager extends LinearLayoutManager {
 
         CountDownLatch layoutLatch;
-
         CountDownLatch snapLatch;
-
         CountDownLatch prefetchLatch;
+        CountDownLatch callbackLatch;
 
         OrientationHelper mSecondaryOrientation;
 
         OnLayoutListener mOnLayoutListener;
 
+        RecyclerView.OnScrollListener mCallbackListener = new RecyclerView.OnScrollListener() {
+
+            @Override
+            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
+                super.onScrollStateChanged(recyclerView, newState);
+                callbackLatch.countDown();
+                if (callbackLatch.getCount() == 0L) {
+                    removeOnScrollListener(this);
+                }
+            }
+        };
+
         public WrappedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
             super(context, orientation, reverseLayout);
         }
@@ -359,6 +383,15 @@
             layoutLatch = new CountDownLatch(count);
         }
 
+        public void expectCallbacks(int count) throws Throwable {
+            callbackLatch = new CountDownLatch(count);
+            mRecyclerView.addOnScrollListener(mCallbackListener);
+        }
+
+        private void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
+            mRecyclerView.removeOnScrollListener(listener);
+        }
+
         public void waitForLayout(int seconds) throws Throwable {
             layoutLatch.await(seconds * (DEBUG ? 100 : 1), SECONDS);
             checkForMainThreadException();
@@ -372,6 +405,13 @@
             });
         }
 
+        public void assertNoCallbacks(String msg, long timeout) throws Throwable {
+            callbackLatch.await(timeout, TimeUnit.SECONDS);
+            long latchCount = callbackLatch.getCount();
+            assertFalse(msg + " :" + latchCount, latchCount == 0);
+            removeOnScrollListener(mCallbackListener);
+        }
+
         public void expectPrefetch(int count) {
             prefetchLatch = new CountDownLatch(count);
         }
@@ -413,8 +453,7 @@
             // use a runnable to ensure RV layout is finished
             getInstrumentation().runOnMainSync(new Runnable() {
                 @Override
-                public void run() {
-                }
+                public void run() {}
             });
         }
 
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
index 7a7a4d4..0373251 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/BaseRecyclerViewInstrumentationTest.java
@@ -27,6 +27,7 @@
 import android.app.Instrumentation;
 import android.graphics.Rect;
 import android.os.Looper;
+import android.support.annotation.Nullable;
 import android.support.test.InstrumentationRegistry;
 import android.support.test.rule.ActivityTestRule;
 import android.support.v4.view.ViewCompat;
@@ -791,10 +792,16 @@
 
         ViewAttachDetachCounter mAttachmentCounter = new ViewAttachDetachCounter();
         List<Item> mItems;
+        final @Nullable RecyclerView.LayoutParams mLayoutParams;
 
         public TestAdapter(int count) {
+            this(count, null);
+        }
+
+        public TestAdapter(int count, @Nullable RecyclerView.LayoutParams layoutParams) {
             mItems = new ArrayList<Item>(count);
             addItems(0, count, DEFAULT_ITEM_PREFIX);
+            mLayoutParams = layoutParams;
         }
 
         private void addItems(int pos, int count, String prefix) {
@@ -848,6 +855,9 @@
             final Item item = mItems.get(position);
             ((TextView) (holder.itemView)).setText(item.mText + "(" + item.mId + ")");
             holder.mBoundItem = item;
+            if (mLayoutParams != null) {
+                holder.itemView.setLayoutParams(new RecyclerView.LayoutParams(mLayoutParams));
+            }
         }
 
         public Item getItemAt(int position) {
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java
index 0ba8fed..6dc80bd 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/GridLayoutManagerSnappingTest.java
@@ -168,7 +168,11 @@
         mGlm.waitForLayout(2);
 
         View view = findCenterView(mGlm);
-        int scrollDistance = (getViewDimension(view) / 2) + 10;
+        int scrollDistance = distFromCenter(view) / 2;
+        if (scrollDistance == 0) {
+            return;
+        }
+
         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
 
         mGlm.expectIdleState(2);
@@ -208,6 +212,14 @@
         }
     }
 
+    private int distFromCenter(View view) {
+        if (mGlm.canScrollHorizontally()) {
+            return Math.abs(mRecyclerView.getWidth() / 2 - mGlm.getViewBounds(view).centerX());
+        } else {
+            return Math.abs(mRecyclerView.getHeight() / 2 - mGlm.getViewBounds(view).centerY());
+        }
+    }
+
     private boolean fling(final int velocityX, final int velocityY)
             throws Throwable {
         final AtomicBoolean didStart = new AtomicBoolean(false);
diff --git a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java
index ae19fc1..3d978da 100644
--- a/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java
+++ b/v7/recyclerview/tests/src/android/support/v7/widget/LinearLayoutManagerSnappingTest.java
@@ -59,6 +59,32 @@
 
     @MediumTest
     @Test
+    public void snapOnScrollSameViewEdge() throws Throwable {
+        final Config config = (Config) mConfig.clone();
+        // Ensure that the views are big enough to reach the pathological case when the view closest
+        // to the center is an edge view, but it cannot scroll further in order to snap.
+        setupByConfig(config, true, new RecyclerView.LayoutParams(1000, 1000),
+            new RecyclerView.LayoutParams(1500, 1500));
+        SnapHelper snapHelper = new LinearSnapHelper();
+        mLayoutManager.expectIdleState(1);
+        snapHelper.attachToRecyclerView(mRecyclerView);
+        mLayoutManager.waitForSnap(10);
+
+        // Record the current center view.
+        View view = findCenterView(mLayoutManager);
+
+        int scrollDistance = (getViewDimension(view) / 2) - 1;
+        int scrollDist = config.mStackFromEnd == config.mReverseLayout
+            ? -scrollDistance : scrollDistance;
+        mLayoutManager.expectIdleState(1);
+        smoothScrollBy(scrollDist);
+        mLayoutManager.waitForSnap(10);
+        mLayoutManager.expectCallbacks(5);
+        mLayoutManager.assertNoCallbacks("There should be no callbacks after some time", 3);
+    }
+
+    @MediumTest
+    @Test
     public void snapOnScrollSameView() throws Throwable {
         final Config config = (Config) mConfig.clone();
         setupByConfig(config, true);
@@ -76,7 +102,7 @@
 
         // Views have not changed
         View viewAfterFling = findCenterView(mLayoutManager);
-        assertSame("The view should have scrolled", view, viewAfterFling);
+        assertSame("The view should NOT have scrolled", view, viewAfterFling);
         assertCenterAligned(viewAfterFling);
     }
 
@@ -165,7 +191,11 @@
         mLayoutManager.waitForLayout(2);
 
         View view = findCenterView(mLayoutManager);
-        int scrollDistance = (getViewDimension(view) / 2) + 10;
+        int scrollDistance = distFromCenter(view) / 2;
+        if (scrollDistance == 0) {
+            return;
+        }
+
         int scrollDist = mReverseScroll ? -scrollDistance : scrollDistance;
 
         mLayoutManager.expectIdleState(2);
@@ -201,6 +231,16 @@
         }
     }
 
+    private int distFromCenter(View view) {
+        if (mLayoutManager.canScrollHorizontally()) {
+            return Math.abs(mRecyclerView.getWidth() / 2 -
+                mLayoutManager.getViewBounds(view).centerX());
+        } else {
+            return Math.abs(mRecyclerView.getHeight() / 2 -
+                mLayoutManager.getViewBounds(view).centerY());
+        }
+    }
+
     private boolean fling(final int velocityX, final int velocityY) throws Throwable {
         final AtomicBoolean didStart = new AtomicBoolean(false);
         runTestOnUiThread(new Runnable() {