CTS tests for AbsListView impl calls to jump drawables

Bug: 29978498
Change-Id: Id0ea485cbbbf11ee6068106b4f5075806180ce51
diff --git a/tests/tests/widget/src/android/widget/cts/ListViewTest.java b/tests/tests/widget/src/android/widget/cts/ListViewTest.java
index e89b3d3..a8d81da 100644
--- a/tests/tests/widget/src/android/widget/cts/ListViewTest.java
+++ b/tests/tests/widget/src/android/widget/cts/ListViewTest.java
@@ -16,44 +16,58 @@
 
 package android.widget.cts;
 
-import android.widget.BaseAdapter;
-import android.widget.LinearLayout;
-import android.widget.cts.R;
+import junit.framework.Assert;
 
 import org.xmlpull.v1.XmlPullParser;
 
+import android.app.ActionBar.LayoutParams;
 import android.app.Activity;
 import android.app.Instrumentation;
 import android.content.Context;
 import android.cts.util.PollingCheck;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.graphics.drawable.Drawable;
 import android.test.ActivityInstrumentationTestCase2;
 import android.test.UiThreadTest;
 import android.test.suitebuilder.annotation.MediumTest;
 import android.util.AttributeSet;
 import android.util.Pair;
+import android.util.SparseArray;
 import android.util.SparseBooleanArray;
 import android.util.Xml;
 import android.view.KeyEvent;
 import android.view.View;
+import android.view.View.MeasureSpec;
 import android.view.ViewGroup;
 import android.view.animation.LayoutAnimationController;
 import android.widget.AdapterView;
 import android.widget.AdapterView.OnItemClickListener;
 import android.widget.ArrayAdapter;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
 import android.widget.ListView;
 import android.widget.TextView;
+import android.widget.cts.R;
 import android.widget.cts.util.ViewTestUtils;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 
-import junit.framework.Assert;
-
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 
 public class ListViewTest extends ActivityInstrumentationTestCase2<ListViewCtsActivity> {
     private final String[] mCountryList = new String[] {
@@ -832,6 +846,94 @@
         assertEquals(childView2, listView.getChildAt(2));
     }
 
+    private static final int EXACTLY_500_PX = MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY);
+
+    @MediumTest
+    public void testJumpDrawables() {
+        FrameLayout layout = new FrameLayout(mActivity);
+        ListView listView = new ListView(mActivity);
+        ArrayAdapterWithMockDrawable adapter = new ArrayAdapterWithMockDrawable(mActivity);
+        for (int i = 0; i < 50; i++) {
+            adapter.add(Integer.toString(i));
+        }
+
+        // Initial state should jump exactly once during attach.
+        mInstrumentation.runOnMainSync(() -> {
+            listView.setAdapter(adapter);
+            layout.addView(listView, new LayoutParams(LayoutParams.MATCH_PARENT, 200));
+            mActivity.setContentView(layout);
+        });
+        mInstrumentation.waitForIdleSync();
+        assertTrue("List is not showing any children", listView.getChildCount() > 0);
+        Drawable firstBackground = listView.getChildAt(0).getBackground();
+        verify(firstBackground, times(1)).jumpToCurrentState();
+
+        // Lay out views without recycling. This should not jump again.
+        mInstrumentation.runOnMainSync(() -> listView.requestLayout());
+        mInstrumentation.waitForIdleSync();
+        assertSame(firstBackground, listView.getChildAt(0).getBackground());
+        verify(firstBackground, times(1)).jumpToCurrentState();
+
+        // If we're on a really big display, we might be in a position where
+        // the position we're going to scroll to is already visible, in which
+        // case we won't be able to test jump behavior when recycling.
+        int lastVisiblePosition = listView.getLastVisiblePosition();
+        int targetPosition = adapter.getCount() - 1;
+        if (targetPosition <= lastVisiblePosition) {
+            return;
+        }
+
+        // Reset the call counts before continuing, since the backgrounds may
+        // be recycled from either views that were on-screen or in the scrap
+        // heap, and those would have slightly different call counts.
+        adapter.resetMockBackgrounds();
+
+        // Scroll so that we have new views on screen. This should jump at
+        // least once when the view is recycled in a new position (but may be
+        // more if it was recycled from a view that was previously on-screen).
+        mInstrumentation.runOnMainSync(() -> listView.setSelection(targetPosition));
+        mInstrumentation.waitForIdleSync();
+
+        View lastChild = listView.getChildAt(listView.getChildCount() - 1);
+        verify(lastChild.getBackground(), atLeast(1)).jumpToCurrentState();
+
+        // Reset the call counts before continuing.
+        adapter.resetMockBackgrounds();
+
+        // Scroll back to the top. This should jump at least once when the view
+        // is recycled in a new position (but may be more if it was recycled
+        // from a view that was previously on-screen).
+        mInstrumentation.runOnMainSync(() -> listView.setSelection(0));
+        mInstrumentation.waitForIdleSync();
+
+        View firstChild = listView.getChildAt(0);
+        verify(firstChild.getBackground(), atLeast(1)).jumpToCurrentState();
+    }
+
+    private static class ArrayAdapterWithMockDrawable extends ArrayAdapter<String> {
+        private SparseArray<Drawable> mBackgrounds = new SparseArray<>();
+
+        public ArrayAdapterWithMockDrawable(Context context) {
+            super(context, android.R.layout.simple_list_item_1);
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final View view = super.getView(position, convertView, parent);
+            if (view.getBackground() == null) {
+                view.setBackground(spy(new ColorDrawable(Color.BLACK)));
+            }
+            return view;
+        }
+
+        public void resetMockBackgrounds() {
+            for (int i = 0; i < mBackgrounds.size(); i++) {
+                Drawable background = mBackgrounds.valueAt(i);
+                reset(background);
+            }
+        }
+    }
+
     private class TemporarilyDetachableMockView extends View {
 
         private boolean mIsDispatchingStartTemporaryDetach = false;