Merge "Release stretch overscroll when new items are added." into sc-v2-dev
diff --git a/tests/tests/widget/src/android/widget/cts/ListViewTest.java b/tests/tests/widget/src/android/widget/cts/ListViewTest.java
index f58eaa0..6cf81e3 100644
--- a/tests/tests/widget/src/android/widget/cts/ListViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ListViewTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
@@ -41,6 +42,7 @@
 import android.app.ActionBar.LayoutParams;
 import android.app.Activity;
 import android.app.Instrumentation;
+import android.app.UiAutomation;
 import android.content.Context;
 import android.graphics.Canvas;
 import android.graphics.Color;
@@ -48,6 +50,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.Parcelable;
+import android.os.SystemClock;
 import android.util.AttributeSet;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -1329,6 +1332,144 @@
         assertTrue(firstVisiblePositionAfterScroll > firstVisiblePositionBeforeScroll);
     }
 
+    @Test
+    public void testEdgeEffectAddToBottom() throws Throwable {
+        // Make sure that the view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        scrollToBottomOfStretch();
+
+        NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mListViewStretch.getContext());
+        mListViewStretch.mEdgeGlowBottom = edgeEffect;
+        edgeEffect.setPauseRelease(true);
+
+        executeWhileDragging(
+                -300,
+                () -> {
+                    assertFalse(edgeEffect.getOnReleaseCalled());
+                    try {
+                        mActivityRule.runOnUiThread(() -> {
+                            for (int color : mColorList) {
+                                mAdapterColors.addColor(Color.BLACK);
+                                mAdapterColors.addColor(color);
+                            }
+                        });
+                    } catch (Throwable e) {
+                    }
+                },
+                () -> {
+                    assertTrue(edgeEffect.getOnReleaseCalled());
+                    assertTrue(edgeEffect.getDistance() > 0);
+                }
+        );
+
+        edgeEffect.finish();
+        int firstVisible = mListViewStretch.getFirstVisiblePosition();
+
+        // We've turned off the release, so the distance won't change unless onPull() is called
+        executeWhileDragging(-300, () -> {}, () -> {});
+        assertTrue(edgeEffect.isFinished());
+        assertEquals(0f, edgeEffect.getDistance(), 0.01f);
+        assertNotEquals(firstVisible, mListViewStretch.getFirstVisiblePosition());
+    }
+
+    @Test
+    public void testEdgeEffectAddToTop() throws Throwable {
+        // Make sure that the view we care about is on screen and at the top:
+        showOnlyStretch();
+
+        NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mListViewStretch.getContext());
+        mListViewStretch.mEdgeGlowTop = edgeEffect;
+        edgeEffect.setPauseRelease(true);
+
+        executeWhileDragging(
+                300,
+                () -> {
+                    assertFalse(edgeEffect.getOnReleaseCalled());
+                    try {
+                        mActivityRule.runOnUiThread(() -> {
+                            for (int color : mColorList) {
+                                mAdapterColors.addColorAtStart(Color.BLACK);
+                                mAdapterColors.addColorAtStart(color);
+                            }
+                            mListViewStretch.setSelection(mColorList.length * 2);
+                        });
+                    } catch (Throwable e) {
+                    }
+                },
+                () -> {
+                    assertTrue(edgeEffect.getOnReleaseCalled());
+                    assertTrue(edgeEffect.getDistance() > 0);
+                }
+        );
+
+        edgeEffect.finish();
+        int firstVisible = mListViewStretch.getFirstVisiblePosition();
+
+        // We've turned off the release, so the distance won't change unless onPull() is called
+        executeWhileDragging(300, () -> {}, () -> {});
+        assertTrue(edgeEffect.isFinished());
+        assertEquals(0f, edgeEffect.getDistance(), 0.01f);
+        assertNotEquals(firstVisible, mListViewStretch.getFirstVisiblePosition());
+    }
+
+    private void executeWhileDragging(
+            int dragY,
+            Runnable duringDrag,
+            Runnable beforeUp
+    ) throws Throwable {
+        int[] locationOnScreen = new int[2];
+        mActivityRule.runOnUiThread(() -> {
+            mListViewStretch.getLocationOnScreen(locationOnScreen);
+        });
+
+        int screenX = locationOnScreen[0] + mListViewStretch.getWidth() / 2;
+        int screenY = locationOnScreen[1] + mListViewStretch.getHeight() / 2;
+        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
+        UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        long downTime = SystemClock.uptimeMillis();
+        StretchEdgeUtil.injectDownEvent(uiAutomation, downTime, screenX, screenY);
+
+        int middleY = screenY + (dragY / 2);
+        StretchEdgeUtil.injectMoveEventsForDrag(
+                uiAutomation,
+                downTime,
+                downTime,
+                screenX,
+                screenY,
+                screenX,
+                middleY,
+                5,
+                20
+        );
+
+        duringDrag.run();
+
+        int endY = screenY + dragY;
+
+        StretchEdgeUtil.injectMoveEventsForDrag(
+                uiAutomation,
+                downTime,
+                downTime + 25,
+                screenX,
+                middleY,
+                screenX,
+                endY,
+                5,
+                20
+        );
+
+        beforeUp.run();
+
+        StretchEdgeUtil.injectUpEvent(
+                uiAutomation,
+                downTime,
+                downTime + 50,
+                screenX,
+                endY
+        );
+    }
+
     private void showOnlyStretch() throws Throwable {
         mActivityRule.runOnUiThread(() -> {
             ViewGroup parent = (ViewGroup) mListViewStretch.getParent();
@@ -1498,6 +1639,7 @@
     private static class ColorAdapter extends BaseAdapter {
         private int[] mColors;
         private Context mContext;
+        private int mPositionOffset;
 
         ColorAdapter(Context context, int[] colors) {
             mContext = context;
@@ -1516,7 +1658,12 @@
 
         @Override
         public long getItemId(int position) {
-            return position;
+            return position - mPositionOffset;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
         }
 
         @NonNull
@@ -1532,6 +1679,23 @@
             view.setLayoutParams(new ViewGroup.LayoutParams(90, 50));
             return view;
         }
+
+        public void addColor(int color) {
+            int[] colors = new int[mColors.length + 1];
+            System.arraycopy(mColors, 0, colors, 0, mColors.length);
+            colors[mColors.length] = color;
+            mColors = colors;
+            notifyDataSetChanged();
+        }
+
+        public void addColorAtStart(int color) {
+            int[] colors = new int[mColors.length + 1];
+            System.arraycopy(mColors, 0, colors, 1, mColors.length);
+            colors[0] = color;
+            mColors = colors;
+            mPositionOffset++;
+            notifyDataSetChanged();
+        }
     }
 
     private static class ClickColorAdapter extends ColorAdapter {
diff --git a/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt b/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt
index b06c52a..6a03076 100644
--- a/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt
+++ b/tests/tests/widget/src/android/widget/cts/util/StretchEdgeUtil.kt
@@ -178,10 +178,13 @@
 /**
  * An [EdgeEffect] that does not release with [onRelease] unless [pauseRelease] is `false`.
  */
-class NoReleaseEdgeEffect(context: Context) : EdgeEffect(context) {
+open class NoReleaseEdgeEffect(context: Context) : EdgeEffect(context) {
     var pauseRelease = true
 
+    var onReleaseCalled = false
+
     override fun onRelease() {
+        onReleaseCalled = true
         if (!pauseRelease) {
             super.onRelease()
         }
@@ -232,7 +235,7 @@
     }
 }
 
-private fun injectMoveEventsForDrag(
+fun injectMoveEventsForDrag(
     uiAutomation: UiAutomation,
     downTime: Long,
     dragStartTime: Long = downTime,
@@ -265,7 +268,7 @@
  * @param yOnScreen The y screen coordinate to press on
  * sent.
  */
-private fun injectUpEvent(
+fun injectUpEvent(
     uiAutomation: UiAutomation,
     downTime: Long,
     upTime: Long,
@@ -281,7 +284,7 @@
  * @param yOnScreen The y screen coordinate to press on
  * sent.
  */
-private fun injectDownEvent(
+fun injectDownEvent(
     uiAutomation: UiAutomation,
     downTime: Long,
     xOnScreen: Int,