Backport of ListView.canScrollList in compat

Bug: 33100659
Test: ./gradlew support-compat:connectedCheck --info --daemon -Pandroid.testInstrumentationRunnerArguments.class=android.support.v4.widget.ListViewCompatTest
Test: ./gradlew support-core-ui:connectedCheck --info --daemon -Pandroid.testInstrumentationRunnerArguments.class=android.support.v4.widget.SwipeRefreshLayoutTest
Change-Id: If99bb26683671f5e722857d9c3600ea4d3d5fe53
diff --git a/api/26.0.0-SNAPSHOT.txt b/api/26.0.0-SNAPSHOT.txt
index f1040fe..b2a6003 100644
--- a/api/26.0.0-SNAPSHOT.txt
+++ b/api/26.0.0-SNAPSHOT.txt
@@ -9697,6 +9697,7 @@
   }
 
   public final class ListViewCompat {
+    method public static boolean canScrollList(android.widget.ListView, int);
     method public static void scrollListBy(android.widget.ListView, int);
   }
 
diff --git a/compat/java/android/support/v4/widget/ListViewCompat.java b/compat/java/android/support/v4/widget/ListViewCompat.java
index 1b1ec16..59ee741 100644
--- a/compat/java/android/support/v4/widget/ListViewCompat.java
+++ b/compat/java/android/support/v4/widget/ListViewCompat.java
@@ -22,8 +22,7 @@
 import android.widget.ListView;
 
 /**
- * Helper for accessing features in {@link ListView} introduced after API level
- * 4 in a backwards compatible fashion.
+ * Helper for accessing features in {@link ListView}
  */
 public final class ListViewCompat {
 
@@ -35,8 +34,10 @@
      */
     public static void scrollListBy(@NonNull ListView listView, int y) {
         if (Build.VERSION.SDK_INT >= 19) {
+            // Call the framework version directly
             listView.scrollListBy(y);
         } else {
+            // provide backport on earlier versions
             final int firstPosition = listView.getFirstVisiblePosition();
             if (firstPosition == ListView.INVALID_POSITION) {
                 return;
@@ -52,5 +53,38 @@
         }
     }
 
+    /**
+     * Check if the items in the list can be scrolled in a certain direction.
+     *
+     * @param direction Negative to check scrolling up, positive to check
+     *            scrolling down.
+     * @return true if the list can be scrolled in the specified direction,
+     *         false otherwise.
+     * @see #scrollListBy(ListView, int)
+     */
+    public static boolean canScrollList(@NonNull ListView listView, int direction) {
+        if (Build.VERSION.SDK_INT >= 19) {
+            // Call the framework version directly
+            return listView.canScrollList(direction);
+        } else {
+            // provide backport on earlier versions
+            final int childCount = listView.getChildCount();
+            if (childCount == 0) {
+                return false;
+            }
+
+            final int firstPosition = listView.getFirstVisiblePosition();
+            if (direction > 0) {
+                final int lastBottom = listView.getChildAt(childCount - 1).getBottom();
+                final int lastPosition = firstPosition + childCount;
+                return lastPosition < listView.getCount()
+                        || (lastBottom > listView.getHeight() - listView.getListPaddingBottom());
+            } else {
+                final int firstTop = listView.getChildAt(0).getTop();
+                return firstPosition > 0 || firstTop < listView.getListPaddingTop();
+            }
+        }
+    }
+
     private ListViewCompat() {}
 }
diff --git a/compat/tests/AndroidManifest.xml b/compat/tests/AndroidManifest.xml
index 5e9730c..ca75941 100644
--- a/compat/tests/AndroidManifest.xml
+++ b/compat/tests/AndroidManifest.xml
@@ -30,6 +30,8 @@
     <application
             android:supportsRtl="true"
             android:theme="@style/TestActivityTheme">
+        <activity android:name="android.support.v4.widget.ListViewTestActivity"/>
+
         <activity android:name="android.support.v4.widget.TextViewTestActivity"/>
 
         <activity android:name="android.support.v4.view.VpaActivity"/>
diff --git a/compat/tests/java/android/support/v4/widget/ListViewCompatTest.java b/compat/tests/java/android/support/v4/widget/ListViewCompatTest.java
new file mode 100644
index 0000000..4fc65ec
--- /dev/null
+++ b/compat/tests/java/android/support/v4/widget/ListViewCompatTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 android.support.v4.widget;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.compat.test.R;
+import android.support.test.filters.SmallTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.v4.BaseInstrumentationTestCase;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+@SmallTest
+public class ListViewCompatTest extends BaseInstrumentationTestCase<ListViewTestActivity> {
+    private ListView mListView;
+
+    public ListViewCompatTest() {
+        super(ListViewTestActivity.class);
+    }
+
+    @Before
+    public void setUp() throws Throwable {
+        mListView = (ListView) mActivityTestRule.getActivity().findViewById(R.id.content);
+        runOnMainAndLayoutSync(mActivityTestRule, mListView, new Runnable() {
+            @Override
+            public void run() {
+                mListView.setAdapter(new BaseAdapter() {
+                    @Override
+                    public int getCount() {
+                        return 500;
+                    }
+
+                    @Override
+                    public Object getItem(int position) {
+                        return null;
+                    }
+
+                    @Override
+                    public long getItemId(int position) {
+                        return position;
+                    }
+
+                    @Override
+                    public View getView(int position, View convertView, ViewGroup parent) {
+                        if (convertView == null) {
+                            convertView = LayoutInflater.from(mListView.getContext()).inflate(
+                                    R.layout.list_view_row, parent, false);
+                        }
+                        TextView result = (TextView) convertView;
+                        result.setText("row #" + (position + 1));
+                        return result;
+                    }
+                });
+            }
+        }, false);
+    }
+
+    private void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
+            @NonNull final View view, @Nullable final Runnable runner, final boolean forceLayout)
+            throws Throwable {
+        final View rootView = view.getRootView();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        activityTestRule.runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        rootView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+                        // countdown immediately since the layout we were waiting on has happened
+                        latch.countDown();
+                    }
+                };
+
+                rootView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
+
+                if (runner != null) {
+                    runner.run();
+                }
+
+                if (forceLayout) {
+                    rootView.requestLayout();
+                }
+            }
+        });
+
+        try {
+            Assert.assertTrue("Expected layout pass within 5 seconds",
+                    latch.await(5, TimeUnit.SECONDS));
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Test
+    public void testCanScroll() throws Throwable {
+        final int itemCount = mListView.getAdapter().getCount();
+
+        assertEquals(0, mListView.getFirstVisiblePosition());
+
+        // Verify that when we're at the top of the list, we can't scroll up but we can scroll
+        // down.
+        assertFalse(ListViewCompat.canScrollList(mListView, -1));
+        assertTrue(ListViewCompat.canScrollList(mListView, 1));
+
+        // Scroll down to the very end of the list
+        runOnMainAndLayoutSync(mActivityTestRule, mListView,
+                new Runnable() {
+                    @Override
+                    public void run() {
+                        mListView.setStackFromBottom(true);
+                    }
+                }, false);
+        assertEquals(itemCount - 1, mListView.getLastVisiblePosition());
+
+        // Verify that when we're at the bottom of the list, we can't scroll down but we can scroll
+        // up.
+        assertFalse(ListViewCompat.canScrollList(mListView, 1));
+        assertTrue(ListViewCompat.canScrollList(mListView, -1));
+    }
+}
diff --git a/compat/tests/java/android/support/v4/widget/ListViewTestActivity.java b/compat/tests/java/android/support/v4/widget/ListViewTestActivity.java
new file mode 100644
index 0000000..762889f
--- /dev/null
+++ b/compat/tests/java/android/support/v4/widget/ListViewTestActivity.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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 android.support.v4.widget;
+
+import android.support.compat.test.R;
+import android.support.v4.BaseTestActivity;
+
+public class ListViewTestActivity extends BaseTestActivity {
+    @Override
+    protected int getContentViewLayoutResId() {
+        return R.layout.list_view_activity;
+    }
+}
diff --git a/compat/tests/res/layout/list_view_activity.xml b/compat/tests/res/layout/list_view_activity.xml
new file mode 100644
index 0000000..1a8d2c6
--- /dev/null
+++ b/compat/tests/res/layout/list_view_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<ListView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/content"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:padding="48dp"
+    android:clipToPadding="false"/>
diff --git a/compat/tests/res/layout/list_view_row.xml b/compat/tests/res/layout/list_view_row.xml
new file mode 100644
index 0000000..f871960
--- /dev/null
+++ b/compat/tests/res/layout/list_view_row.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2017 The Android Open Source Project
+  ~
+  ~ 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.
+  -->
+
+<TextView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"/>
\ No newline at end of file
diff --git a/core-ui/java/android/support/v4/widget/SwipeRefreshLayout.java b/core-ui/java/android/support/v4/widget/SwipeRefreshLayout.java
index 6f0a179..983683f 100644
--- a/core-ui/java/android/support/v4/widget/SwipeRefreshLayout.java
+++ b/core-ui/java/android/support/v4/widget/SwipeRefreshLayout.java
@@ -40,6 +40,7 @@
 import android.view.animation.DecelerateInterpolator;
 import android.view.animation.Transformation;
 import android.widget.AbsListView;
+import android.widget.ListView;
 
 /**
  * The SwipeRefreshLayout should be used whenever the user can refresh the
@@ -657,6 +658,9 @@
         if (mChildScrollUpCallback != null) {
             return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
         }
+        if (mTarget instanceof ListView) {
+            return ListViewCompat.canScrollList((ListView) mTarget, -1);
+        }
         return ViewCompat.canScrollVertically(mTarget, -1);
     }