• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget.cts;
18 
19 import static android.widget.cts.util.StretchEdgeUtil.fling;
20 
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertFalse;
23 import static org.junit.Assert.assertNotEquals;
24 import static org.junit.Assert.assertNotNull;
25 import static org.junit.Assert.assertNull;
26 import static org.junit.Assert.assertSame;
27 import static org.junit.Assert.assertTrue;
28 import static org.mockito.Mockito.any;
29 import static org.mockito.Mockito.anyInt;
30 import static org.mockito.Mockito.anyLong;
31 import static org.mockito.Mockito.atLeast;
32 import static org.mockito.Mockito.atLeastOnce;
33 import static org.mockito.Mockito.mock;
34 import static org.mockito.Mockito.never;
35 import static org.mockito.Mockito.reset;
36 import static org.mockito.Mockito.spy;
37 import static org.mockito.Mockito.times;
38 import static org.mockito.Mockito.verify;
39 import static org.mockito.Mockito.verifyNoMoreInteractions;
40 
41 import android.animation.ValueAnimator;
42 import android.app.ActionBar.LayoutParams;
43 import android.app.Activity;
44 import android.app.Instrumentation;
45 import android.app.UiAutomation;
46 import android.content.Context;
47 import android.graphics.Canvas;
48 import android.graphics.Color;
49 import android.graphics.Rect;
50 import android.graphics.drawable.ColorDrawable;
51 import android.graphics.drawable.Drawable;
52 import android.os.Parcelable;
53 import android.os.SystemClock;
54 import android.util.AttributeSet;
55 import android.util.Pair;
56 import android.util.SparseArray;
57 import android.util.SparseBooleanArray;
58 import android.util.Xml;
59 import android.view.InputDevice;
60 import android.view.KeyEvent;
61 import android.view.MotionEvent;
62 import android.view.View;
63 import android.view.ViewGroup;
64 import android.view.animation.LayoutAnimationController;
65 import android.widget.AbsListView;
66 import android.widget.AdapterView;
67 import android.widget.AdapterView.OnItemClickListener;
68 import android.widget.ArrayAdapter;
69 import android.widget.BaseAdapter;
70 import android.widget.EdgeEffect;
71 import android.widget.FrameLayout;
72 import android.widget.ListView;
73 import android.widget.TextView;
74 import android.widget.cts.util.NoReleaseEdgeEffect;
75 import android.widget.cts.util.StretchEdgeUtil;
76 import android.widget.cts.util.TestUtils;
77 
78 import androidx.annotation.NonNull;
79 import androidx.annotation.Nullable;
80 import androidx.test.InstrumentationRegistry;
81 import androidx.test.annotation.UiThreadTest;
82 import androidx.test.filters.LargeTest;
83 import androidx.test.filters.MediumTest;
84 import androidx.test.filters.SmallTest;
85 import androidx.test.rule.ActivityTestRule;
86 import androidx.test.runner.AndroidJUnit4;
87 
88 import com.android.compatibility.common.util.CtsKeyEventUtil;
89 import com.android.compatibility.common.util.CtsTouchUtils;
90 import com.android.compatibility.common.util.PollingCheck;
91 import com.android.compatibility.common.util.WidgetTestUtils;
92 
93 import junit.framework.Assert;
94 
95 import org.junit.After;
96 import org.junit.Before;
97 import org.junit.Rule;
98 import org.junit.Test;
99 import org.junit.runner.RunWith;
100 import org.xmlpull.v1.XmlPullParser;
101 
102 import java.util.ArrayList;
103 import java.util.Arrays;
104 import java.util.List;
105 import java.util.concurrent.CountDownLatch;
106 import java.util.concurrent.TimeUnit;
107 
108 @SmallTest
109 @RunWith(AndroidJUnit4.class)
110 public class ListViewTest {
111     private final String[] mCountryList = new String[] {
112         "Argentina", "Australia", "China", "France", "Germany", "Italy", "Japan", "United States"
113     };
114     private final String[] mLongCountryList = new String[] {
115         "Argentina", "Australia", "Belize", "Botswana", "Brazil", "Cameroon", "China", "Cyprus",
116         "Denmark", "Djibouti", "Ethiopia", "Fiji", "Finland", "France", "Gabon", "Germany",
117         "Ghana", "Haiti", "Honduras", "Iceland", "India", "Indonesia", "Ireland", "Italy",
118         "Japan", "Kiribati", "Laos", "Lesotho", "Liberia", "Malaysia", "Mongolia", "Myanmar",
119         "Nauru", "Norway", "Oman", "Pakistan", "Philippines", "Portugal", "Romania", "Russia",
120         "Rwanda", "Singapore", "Slovakia", "Slovenia", "Somalia", "Swaziland", "Togo", "Tuvalu",
121         "Uganda", "Ukraine", "United States", "Vanuatu", "Venezuela", "Zimbabwe"
122     };
123     private final String[] mNameList = new String[] {
124         "Jacky", "David", "Kevin", "Michael", "Andy"
125     };
126     private final int[] mColorList = new int[] {
127         Color.BLUE, Color.CYAN, Color.GREEN, Color.YELLOW, Color.RED, Color.MAGENTA
128     };
129 
130     private Instrumentation mInstrumentation;
131     private CtsTouchUtils mCtsTouchUtils;
132     private CtsKeyEventUtil mCtsKeyEventUtil;
133     private Activity mActivity;
134     private ListView mListView;
135     private ListView mListViewStretch;
136     private TextView mTextView;
137     private TextView mSecondTextView;
138 
139     private AttributeSet mAttributeSet;
140     private ArrayAdapter<String> mAdapter_countries;
141     private ArrayAdapter<String> mAdapter_longCountries;
142     private ArrayAdapter<String> mAdapter_names;
143     private ColorAdapter mAdapterColors;
144     private float mPreviousDurationScale;
145 
146     @Rule
147     public ActivityTestRule<ListViewCtsActivity> mActivityRule =
148             new ActivityTestRule<>(ListViewCtsActivity.class);
149 
150     @Before
setup()151     public void setup() {
152         mPreviousDurationScale = ValueAnimator.getDurationScale();
153         ValueAnimator.setDurationScale(1.0f);
154         mInstrumentation = InstrumentationRegistry.getInstrumentation();
155         mCtsTouchUtils = new CtsTouchUtils(mInstrumentation.getTargetContext());
156         mCtsKeyEventUtil = new CtsKeyEventUtil(mInstrumentation.getTargetContext());
157         mActivity = mActivityRule.getActivity();
158         XmlPullParser parser = mActivity.getResources().getXml(R.layout.listview_layout);
159         mAttributeSet = Xml.asAttributeSet(parser);
160 
161         mAdapter_countries = new ArrayAdapter<>(mActivity,
162                 android.R.layout.simple_list_item_1, mCountryList);
163         mAdapter_longCountries = new ArrayAdapter<>(mActivity,
164                 android.R.layout.simple_list_item_1, mLongCountryList);
165         mAdapter_names = new ArrayAdapter<>(mActivity, android.R.layout.simple_list_item_1,
166                 mNameList);
167         mAdapterColors = new ColorAdapter(mActivity, mColorList);
168 
169         mListView = (ListView) mActivity.findViewById(R.id.listview_default);
170         mListViewStretch = (ListView) mActivity.findViewById(R.id.listview_stretch);
171     }
172 
173     @After
tearDown()174     public void tearDown() {
175         ValueAnimator.setDurationScale(mPreviousDurationScale);
176     }
177 
178     @Test
testConstructor()179     public void testConstructor() {
180         new ListView(mActivity);
181         new ListView(mActivity, mAttributeSet);
182         new ListView(mActivity, mAttributeSet, 0);
183     }
184 
185     @Test(expected=NullPointerException.class)
testConstructorNullContext1()186     public void testConstructorNullContext1() {
187         new ListView(null);
188     }
189 
190     @Test(expected=NullPointerException.class)
testConstructorNullContext2()191     public void testConstructorNullContext2() {
192         new ListView(null, null);
193     }
194 
195     @Test(expected=NullPointerException.class)
testConstructorNullContext3()196     public void testConstructorNullContext3() {
197         new ListView(null, null, -1);
198     }
199 
200     @Test
testGetMaxScrollAmount()201     public void testGetMaxScrollAmount() throws Throwable {
202         setAdapter(mAdapter_names);
203         int scrollAmount = mListView.getMaxScrollAmount();
204         assertTrue(scrollAmount > 0);
205 
206         mActivityRule.runOnUiThread(() -> {
207             mListView.getLayoutParams().height = 0;
208             mListView.requestLayout();
209         });
210         PollingCheck.waitFor(() -> mListView.getHeight() == 0);
211 
212         scrollAmount = mListView.getMaxScrollAmount();
213         assertEquals(0, scrollAmount);
214     }
215 
setAdapter(final ArrayAdapter<String> adapter)216     private void setAdapter(final ArrayAdapter<String> adapter) throws Throwable {
217         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
218                 () -> mListView.setAdapter(adapter));
219     }
220 
221     @Test
testAccessDividerHeight()222     public void testAccessDividerHeight() throws Throwable {
223         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
224                 () -> mListView.setAdapter(mAdapter_countries));
225 
226         Drawable d = mListView.getDivider();
227         final Rect r = d.getBounds();
228         PollingCheck.waitFor(() -> r.bottom - r.top > 0);
229 
230         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
231                 () -> mListView.setDividerHeight(20));
232 
233         assertEquals(20, mListView.getDividerHeight());
234         assertEquals(20, r.bottom - r.top);
235 
236         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
237                 () -> mListView.setDividerHeight(10));
238 
239         assertEquals(10, mListView.getDividerHeight());
240         assertEquals(10, r.bottom - r.top);
241     }
242 
243     @Test
testAccessItemsCanFocus()244     public void testAccessItemsCanFocus() {
245         mListView.setItemsCanFocus(true);
246         assertTrue(mListView.getItemsCanFocus());
247 
248         mListView.setItemsCanFocus(false);
249         assertFalse(mListView.getItemsCanFocus());
250 
251         // TODO: how to check?
252     }
253 
254     @Test
testAccessAdapter()255     public void testAccessAdapter() throws Throwable {
256         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
257                 () -> mListView.setAdapter(mAdapter_countries));
258 
259         assertSame(mAdapter_countries, mListView.getAdapter());
260         assertEquals(mCountryList.length, mListView.getCount());
261 
262         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
263                 () -> mListView.setAdapter(mAdapter_names));
264 
265         assertSame(mAdapter_names, mListView.getAdapter());
266         assertEquals(mNameList.length, mListView.getCount());
267     }
268 
269     @UiThreadTest
270     @Test
testAccessItemChecked()271     public void testAccessItemChecked() {
272         // NONE mode
273         mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
274         assertEquals(ListView.CHOICE_MODE_NONE, mListView.getChoiceMode());
275 
276         mListView.setItemChecked(1, true);
277         assertEquals(ListView.INVALID_POSITION, mListView.getCheckedItemPosition());
278         assertFalse(mListView.isItemChecked(1));
279 
280         // SINGLE mode
281         mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
282         assertEquals(ListView.CHOICE_MODE_SINGLE, mListView.getChoiceMode());
283 
284         mListView.setItemChecked(2, true);
285         assertEquals(2, mListView.getCheckedItemPosition());
286         assertTrue(mListView.isItemChecked(2));
287 
288         mListView.setItemChecked(3, true);
289         assertEquals(3, mListView.getCheckedItemPosition());
290         assertTrue(mListView.isItemChecked(3));
291         assertFalse(mListView.isItemChecked(2));
292 
293         // test attempt to uncheck a item that wasn't checked to begin with
294         mListView.setItemChecked(4, false);
295         // item three should still be checked
296         assertEquals(3, mListView.getCheckedItemPosition());
297         assertFalse(mListView.isItemChecked(4));
298         assertTrue(mListView.isItemChecked(3));
299         assertFalse(mListView.isItemChecked(2));
300 
301         mListView.setItemChecked(4, true);
302         assertTrue(mListView.isItemChecked(4));
303         mListView.clearChoices();
304         assertEquals(ListView.INVALID_POSITION, mListView.getCheckedItemPosition());
305         assertFalse(mListView.isItemChecked(4));
306 
307         // MULTIPLE mode
308         mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
309         assertEquals(ListView.CHOICE_MODE_MULTIPLE, mListView.getChoiceMode());
310 
311         mListView.setItemChecked(1, true);
312         assertEquals(ListView.INVALID_POSITION, mListView.getCheckedItemPosition());
313         SparseBooleanArray array = mListView.getCheckedItemPositions();
314         assertTrue(array.get(1));
315         assertFalse(array.get(2));
316         assertTrue(mListView.isItemChecked(1));
317         assertFalse(mListView.isItemChecked(2));
318 
319         mListView.setItemChecked(2, true);
320         mListView.setItemChecked(3, false);
321         mListView.setItemChecked(4, true);
322 
323         assertTrue(array.get(1));
324         assertTrue(array.get(2));
325         assertFalse(array.get(3));
326         assertTrue(array.get(4));
327         assertTrue(mListView.isItemChecked(1));
328         assertTrue(mListView.isItemChecked(2));
329         assertFalse(mListView.isItemChecked(3));
330         assertTrue(mListView.isItemChecked(4));
331 
332         mListView.clearChoices();
333         assertFalse(array.get(1));
334         assertFalse(array.get(2));
335         assertFalse(array.get(3));
336         assertFalse(array.get(4));
337         assertFalse(mListView.isItemChecked(1));
338         assertFalse(mListView.isItemChecked(2));
339         assertFalse(mListView.isItemChecked(3));
340         assertFalse(mListView.isItemChecked(4));
341     }
342 
343     @Test
testAccessFooterView()344     public void testAccessFooterView() throws Throwable {
345         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
346             mTextView = new TextView(mActivity);
347             mTextView.setText("footerview1");
348             mSecondTextView = new TextView(mActivity);
349             mSecondTextView.setText("footerview2");
350         });
351 
352         mActivityRule.runOnUiThread(() -> mListView.setFooterDividersEnabled(true));
353         assertTrue(mListView.areFooterDividersEnabled());
354         assertEquals(0, mListView.getFooterViewsCount());
355 
356         mActivityRule.runOnUiThread(() -> mListView.addFooterView(mTextView, null, true));
357         assertTrue(mListView.areFooterDividersEnabled());
358         assertEquals(1, mListView.getFooterViewsCount());
359 
360         mActivityRule.runOnUiThread(() -> {
361             mListView.setFooterDividersEnabled(false);
362             mListView.addFooterView(mSecondTextView);
363         });
364         assertFalse(mListView.areFooterDividersEnabled());
365         assertEquals(2, mListView.getFooterViewsCount());
366 
367         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
368                 () -> mListView.setAdapter(mAdapter_countries));
369 
370         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
371                 () -> mListView.removeFooterView(mTextView));
372         assertFalse(mListView.areFooterDividersEnabled());
373         assertEquals(1, mListView.getFooterViewsCount());
374 
375         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
376                 () -> mListView.removeFooterView(mSecondTextView));
377         assertFalse(mListView.areFooterDividersEnabled());
378         assertEquals(0, mListView.getFooterViewsCount());
379     }
380 
381     @UiThreadTest
382     @Test
testAccessHeaderView()383     public void testAccessHeaderView() {
384         final TextView headerView1 = (TextView) mActivity.findViewById(R.id.headerview1);
385         final TextView headerView2 = (TextView) mActivity.findViewById(R.id.headerview2);
386         ((ViewGroup) headerView1.getParent()).removeView(headerView1);
387         ((ViewGroup) headerView2.getParent()).removeView(headerView2);
388 
389         mListView.setHeaderDividersEnabled(true);
390         assertTrue(mListView.areHeaderDividersEnabled());
391         assertEquals(0, mListView.getHeaderViewsCount());
392 
393         mListView.addHeaderView(headerView2, null, true);
394         assertTrue(mListView.areHeaderDividersEnabled());
395         assertEquals(1, mListView.getHeaderViewsCount());
396 
397         mListView.setHeaderDividersEnabled(false);
398         mListView.addHeaderView(headerView1);
399         assertFalse(mListView.areHeaderDividersEnabled());
400         assertEquals(2, mListView.getHeaderViewsCount());
401 
402         mListView.removeHeaderView(headerView2);
403         assertFalse(mListView.areHeaderDividersEnabled());
404         assertEquals(1, mListView.getHeaderViewsCount());
405     }
406 
407     @Test
testHeaderFooterType()408     public void testHeaderFooterType() throws Throwable {
409         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
410                 () -> mTextView = new TextView(mActivity));
411         final List<Pair<View, View>> mismatch = new ArrayList<>();
412         final ArrayAdapter adapter = new ArrayAdapter<String>(mActivity,
413                 android.R.layout.simple_list_item_1, mNameList) {
414             @Override
415             public int getItemViewType(int position) {
416                 return position == 0 ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER :
417                         super.getItemViewType(position - 1);
418             }
419 
420             @Override
421             public View getView(int position, View convertView, ViewGroup parent) {
422                 if (position == 0) {
423                     if (convertView != null && convertView != mTextView) {
424                         mismatch.add(new Pair<>(mTextView, convertView));
425                     }
426                     return mTextView;
427                 } else {
428                     return super.getView(position - 1, convertView, parent);
429                 }
430             }
431 
432             @Override
433             public int getCount() {
434                 return super.getCount() + 1;
435             }
436         };
437 
438         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
439                 () -> mListView.setAdapter(adapter));
440 
441         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
442                 adapter::notifyDataSetChanged);
443 
444         assertEquals(0, mismatch.size());
445     }
446 
447     @Test
testAccessDivider()448     public void testAccessDivider() throws Throwable {
449         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
450                 () -> mListView.setAdapter(mAdapter_countries));
451 
452         Drawable defaultDrawable = mListView.getDivider();
453         final Rect r = defaultDrawable.getBounds();
454         PollingCheck.waitFor(() -> r.bottom - r.top > 0);
455 
456         final Drawable d = mActivity.getResources().getDrawable(R.drawable.scenery);
457 
458         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
459                 () -> mListView.setDivider(d));
460         assertSame(d, mListView.getDivider());
461         assertEquals(d.getBounds().height(), mListView.getDividerHeight());
462 
463         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
464                 () -> mListView.setDividerHeight(10));
465         assertEquals(10, mListView.getDividerHeight());
466         assertEquals(10, d.getBounds().height());
467     }
468 
469     @Test
testSetSelection()470     public void testSetSelection() throws Throwable {
471         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
472                 () -> mListView.setAdapter(mAdapter_countries));
473 
474         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
475                 () -> mListView.setSelection(1));
476         String item = (String) mListView.getSelectedItem();
477         assertEquals(mCountryList[1], item);
478 
479         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
480                 () -> mListView.setSelectionFromTop(5, 0));
481         item = (String) mListView.getSelectedItem();
482         assertEquals(mCountryList[5], item);
483 
484         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
485                 mListView::setSelectionAfterHeaderView);
486         item = (String) mListView.getSelectedItem();
487         assertEquals(mCountryList[0], item);
488     }
489 
490     @Test
testPerformItemClick()491     public void testPerformItemClick() throws Throwable {
492         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
493                 () -> mListView.setAdapter(mAdapter_countries));
494 
495         mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
496         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
497                 () -> mListView.setSelection(2));
498 
499         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
500                 () -> mTextView = (TextView) mAdapter_countries.getView(2, null, mListView));
501         assertNotNull(mTextView);
502         assertEquals(mCountryList[2], mTextView.getText().toString());
503         final long itemID = mAdapter_countries.getItemId(2);
504         assertEquals(2, itemID);
505 
506         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
507                 () -> mListView.performItemClick(mTextView, 2, itemID));
508 
509         OnItemClickListener onClickListener = mock(OnItemClickListener.class);
510         mListView.setOnItemClickListener(onClickListener);
511         verify(onClickListener, never()).onItemClick(any(AdapterView.class), any(View.class),
512                 anyInt(), anyLong());
513 
514         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
515                 () -> mListView.performItemClick(mTextView, 2, itemID));
516 
517         verify(onClickListener, times(1)).onItemClick(mListView, mTextView, 2, 2L);
518         verifyNoMoreInteractions(onClickListener);
519     }
520 
521     @UiThreadTest
522     @Test
testSaveAndRestoreInstanceState_positionIsRestored()523     public void testSaveAndRestoreInstanceState_positionIsRestored() {
524         mListView.setAdapter(mAdapter_countries);
525         assertEquals(0, mListView.getSelectedItemPosition());
526 
527         int positionToTest = mAdapter_countries.getCount() - 1;
528         mListView.setSelection(positionToTest);
529         assertEquals(positionToTest, mListView.getSelectedItemPosition());
530         Parcelable savedState = mListView.onSaveInstanceState();
531 
532         mListView.setSelection(positionToTest - 1);
533         assertEquals(positionToTest - 1, mListView.getSelectedItemPosition());
534 
535         mListView.onRestoreInstanceState(savedState);
536         int measureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
537         mListView.measure(measureSpec,measureSpec);
538         mListView.layout(0, 0, 100, 100);
539         assertEquals(positionToTest, mListView.getSelectedItemPosition());
540     }
541 
542     @Test
testDispatchKeyEvent()543     public void testDispatchKeyEvent() throws Throwable {
544         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
545                 () -> {
546                     mListView.setAdapter(mAdapter_countries);
547                     mListView.requestFocus();
548                 });
549         assertTrue(mListView.hasFocus());
550 
551         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
552                 () -> mListView.setSelection(1));
553         String item = (String) mListView.getSelectedItem();
554         assertEquals(mCountryList[1], item);
555 
556         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
557                 () ->  {
558                     KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A);
559                     mListView.dispatchKeyEvent(keyEvent);
560                 });
561 
562         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
563                 () -> {
564                     KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN,
565                             KeyEvent.KEYCODE_DPAD_DOWN);
566                     mListView.dispatchKeyEvent(keyEvent);
567                     mListView.dispatchKeyEvent(keyEvent);
568                     mListView.dispatchKeyEvent(keyEvent);
569                 });
570         item = (String)mListView.getSelectedItem();
571         assertEquals(mCountryList[4], item);
572     }
573 
574     @Test
testRequestChildRectangleOnScreen()575     public void testRequestChildRectangleOnScreen() throws Throwable {
576         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
577                 () -> mListView.setAdapter(mAdapter_countries));
578 
579         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
580                 () -> mTextView = (TextView) mAdapter_countries.getView(0, null, mListView));
581         assertNotNull(mTextView);
582         assertEquals(mCountryList[0], mTextView.getText().toString());
583 
584         Rect rect = new Rect(0, 0, 10, 10);
585         assertFalse(mListView.requestChildRectangleOnScreen(mTextView, rect, false));
586 
587         // TODO: how to check?
588     }
589 
590     @UiThreadTest
591     @Test
testCanAnimate()592     public void testCanAnimate() {
593         MyListView listView = new MyListView(mActivity, mAttributeSet);
594 
595         assertFalse(listView.canAnimate());
596         listView.setAdapter(mAdapter_countries);
597         assertFalse(listView.canAnimate());
598 
599         LayoutAnimationController controller = new LayoutAnimationController(
600                 mActivity, mAttributeSet);
601         listView.setLayoutAnimation(controller);
602 
603         assertTrue(listView.canAnimate());
604     }
605 
606 
607     @UiThreadTest
608     @Test
testFindViewTraversal()609     public void testFindViewTraversal() {
610         MyListView listView = new MyListView(mActivity, mAttributeSet);
611         TextView headerView = (TextView) mActivity.findViewById(R.id.headerview1);
612         ((ViewGroup) headerView.getParent()).removeView(headerView);
613 
614         assertNull(listView.findViewTraversal(R.id.headerview1));
615 
616         listView.addHeaderView(headerView);
617         assertNotNull(listView.findViewTraversal(R.id.headerview1));
618         assertSame(headerView, listView.findViewTraversal(R.id.headerview1));
619     }
620 
621     @UiThreadTest
622     @Test
testFindViewWithTagTraversal()623     public void testFindViewWithTagTraversal() {
624         MyListView listView = new MyListView(mActivity, mAttributeSet);
625         TextView headerView = (TextView) mActivity.findViewById(R.id.headerview1);
626         ((ViewGroup) headerView.getParent()).removeView(headerView);
627 
628         assertNull(listView.findViewWithTagTraversal("header"));
629 
630         headerView.setTag("header");
631         listView.addHeaderView(headerView);
632         assertNotNull(listView.findViewWithTagTraversal("header"));
633         assertSame(headerView, listView.findViewWithTagTraversal("header"));
634     }
635 
636     /**
637      * MyListView for test
638      */
639     private static class MyListView extends ListView {
MyListView(Context context, AttributeSet attrs)640         public MyListView(Context context, AttributeSet attrs) {
641             super(context, attrs);
642         }
643 
644         @Override
canAnimate()645         protected boolean canAnimate() {
646             return super.canAnimate();
647         }
648 
649         @Override
dispatchDraw(Canvas canvas)650         protected void dispatchDraw(Canvas canvas) {
651             super.dispatchDraw(canvas);
652         }
653 
654         @Override
findViewTraversal(int id)655         protected View findViewTraversal(int id) {
656             return super.findViewTraversal(id);
657         }
658 
659         @Override
findViewWithTagTraversal(Object tag)660         protected View findViewWithTagTraversal(Object tag) {
661             return super.findViewWithTagTraversal(tag);
662         }
663 
664         @Override
layoutChildren()665         protected void layoutChildren() {
666             super.layoutChildren();
667         }
668     }
669 
670     @MediumTest
671     @UiThreadTest
672     @Test
testRequestLayoutCallsMeasure()673     public void testRequestLayoutCallsMeasure() {
674         List<String> items = new ArrayList<>();
675         items.add("hello");
676         MockAdapter<String> adapter = new MockAdapter<>(mActivity, 0, items);
677         mListView.setAdapter(adapter);
678 
679         int measureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
680 
681         adapter.notifyDataSetChanged();
682         mListView.measure(measureSpec, measureSpec);
683         mListView.layout(0, 0, 100, 100);
684 
685         MockView childView = (MockView) mListView.getChildAt(0);
686 
687         childView.requestLayout();
688         childView.onMeasureCalled = false;
689         mListView.measure(measureSpec, measureSpec);
690         mListView.layout(0, 0, 100, 100);
691         Assert.assertTrue(childView.onMeasureCalled);
692     }
693 
694     @MediumTest
695     @UiThreadTest
696     @Test
testNoSelectableItems()697     public void testNoSelectableItems() throws Exception {
698         // We use a header as the unselectable item to remain after the selectable one is removed.
699         mListView.addHeaderView(new View(mActivity), null, false);
700         List<String> items = new ArrayList<>();
701         items.add("hello");
702         MockAdapter<String> adapter = new MockAdapter<>(mActivity, 0, items);
703         mListView.setAdapter(adapter);
704 
705         mListView.setSelection(1);
706 
707         int measureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
708 
709         adapter.notifyDataSetChanged();
710         mListView.measure(measureSpec, measureSpec);
711         mListView.layout(0, 0, 100, 100);
712 
713         items.remove(0);
714 
715         adapter.notifyDataSetChanged();
716         mListView.measure(measureSpec, measureSpec);
717         mListView.layout(0, 0, 100, 100);
718     }
719 
720     @MediumTest
721     @Test
testFullDetachHeaderViewOnScroll()722     public void testFullDetachHeaderViewOnScroll() throws Throwable {
723         final AttachDetachAwareView header = new AttachDetachAwareView(mActivity);
724         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
725             mListView.setAdapter(new DummyAdapter(1000));
726             mListView.addHeaderView(header);
727         });
728         assertEquals("test sanity", 1, header.mOnAttachCount);
729         assertEquals("test sanity", 0, header.mOnDetachCount);
730         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
731             mListView.scrollListBy(mListView.getHeight() * 3);
732         });
733         assertNull("test sanity, header should be removed", header.getParent());
734         assertEquals("header view should be detached", 1, header.mOnDetachCount);
735         assertFalse(header.isTemporarilyDetached());
736     }
737 
738     @MediumTest
739     @Test
testFullDetachHeaderViewOnRelayout()740     public void testFullDetachHeaderViewOnRelayout() throws Throwable {
741         final AttachDetachAwareView header = new AttachDetachAwareView(mActivity);
742         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
743             mListView.setAdapter(new DummyAdapter(1000));
744             mListView.addHeaderView(header);
745         });
746         assertEquals("test sanity", 1, header.mOnAttachCount);
747         assertEquals("test sanity", 0, header.mOnDetachCount);
748         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
749                 () -> mListView.setSelection(800));
750         assertNull("test sanity, header should be removed", header.getParent());
751         assertEquals("header view should be detached", 1, header.mOnDetachCount);
752         assertFalse(header.isTemporarilyDetached());
753     }
754 
755     @MediumTest
756     @Test
testFullDetachHeaderViewOnScrollForFocus()757     public void testFullDetachHeaderViewOnScrollForFocus() throws Throwable {
758         final AttachDetachAwareView header = new AttachDetachAwareView(mActivity);
759         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
760             mListView.setAdapter(new DummyAdapter(1000));
761             mListView.addHeaderView(header);
762         });
763         assertEquals("test sanity", 1, header.mOnAttachCount);
764         assertEquals("test sanity", 0, header.mOnDetachCount);
765         while (header.getParent() != null) {
766             assertEquals("header view should NOT be detached", 0, header.mOnDetachCount);
767             mCtsKeyEventUtil.sendKeys(mInstrumentation, mListView, KeyEvent.KEYCODE_DPAD_DOWN);
768             WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, null);
769         }
770         assertEquals("header view should be detached", 1, header.mOnDetachCount);
771         assertFalse(header.isTemporarilyDetached());
772     }
773 
774     @MediumTest
775     @Test
testFullyDetachUnusedViewOnScroll()776     public void testFullyDetachUnusedViewOnScroll() throws Throwable {
777         final AttachDetachAwareView theView = new AttachDetachAwareView(mActivity);
778         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
779                 () -> mListView.setAdapter(new DummyAdapter(1000, theView)));
780         assertEquals("test sanity", 1, theView.mOnAttachCount);
781         assertEquals("test sanity", 0, theView.mOnDetachCount);
782         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
783                 () -> mListView.scrollListBy(mListView.getHeight() * 2));
784         assertNull("test sanity, unused view should be removed", theView.getParent());
785         assertEquals("unused view should be detached", 1, theView.mOnDetachCount);
786         assertFalse(theView.isTemporarilyDetached());
787         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
788             mListView.scrollListBy(-mListView.getHeight() * 2);
789             // listview limits scroll to 1 page which is why we call it twice here.
790             mListView.scrollListBy(-mListView.getHeight() * 2);
791         });
792         assertNotNull("test sanity, view should be re-added", theView.getParent());
793         assertEquals("view should receive another attach call", 2, theView.mOnAttachCount);
794         assertEquals("view should not receive a detach call", 1, theView.mOnDetachCount);
795         assertFalse(theView.isTemporarilyDetached());
796     }
797 
798     @MediumTest
799     @Test
testFullyDetachUnusedViewOnReLayout()800     public void testFullyDetachUnusedViewOnReLayout() throws Throwable {
801         final AttachDetachAwareView theView = new AttachDetachAwareView(mActivity);
802         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
803                 () -> mListView.setAdapter(new DummyAdapter(1000, theView)));
804         assertEquals("test sanity", 1, theView.mOnAttachCount);
805         assertEquals("test sanity", 0, theView.mOnDetachCount);
806         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
807                 () -> mListView.setSelection(800));
808         assertNull("test sanity, unused view should be removed", theView.getParent());
809         assertEquals("unused view should be detached", 1, theView.mOnDetachCount);
810         assertFalse(theView.isTemporarilyDetached());
811         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
812                 () -> mListView.setSelection(0));
813         assertNotNull("test sanity, view should be re-added", theView.getParent());
814         assertEquals("view should receive another attach call", 2, theView.mOnAttachCount);
815         assertEquals("view should not receive a detach call", 1, theView.mOnDetachCount);
816         assertFalse(theView.isTemporarilyDetached());
817     }
818 
819     @MediumTest
820     @Test
testFullyDetachUnusedViewOnScrollForFocus()821     public void testFullyDetachUnusedViewOnScrollForFocus() throws Throwable {
822         final AttachDetachAwareView theView = new AttachDetachAwareView(mActivity);
823         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
824                 () -> mListView.setAdapter(new DummyAdapter(1000, theView)));
825         assertEquals("test sanity", 1, theView.mOnAttachCount);
826         assertEquals("test sanity", 0, theView.mOnDetachCount);
827         while(theView.getParent() != null) {
828             assertEquals("the view should NOT be detached", 0, theView.mOnDetachCount);
829             mCtsKeyEventUtil.sendKeys(mInstrumentation, mListView, KeyEvent.KEYCODE_DPAD_DOWN);
830             WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, null);
831         }
832         assertEquals("the view should be detached", 1, theView.mOnDetachCount);
833         assertFalse(theView.isTemporarilyDetached());
834         while(theView.getParent() == null) {
835             mCtsKeyEventUtil.sendKeys(mInstrumentation, mListView, KeyEvent.KEYCODE_DPAD_UP);
836             WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, null);
837         }
838         assertEquals("the view should be re-attached", 2, theView.mOnAttachCount);
839         assertEquals("the view should not recieve another detach", 1, theView.mOnDetachCount);
840         assertFalse(theView.isTemporarilyDetached());
841     }
842 
843     @MediumTest
844     @Test
testSetPadding()845     public void testSetPadding() throws Throwable {
846         View view = new View(mActivity);
847         view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
848                 ViewGroup.LayoutParams.WRAP_CONTENT));
849         view.setMinimumHeight(30);
850         final DummyAdapter adapter = new DummyAdapter(2, view);
851         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
852             mListView.setLayoutParams(new FrameLayout.LayoutParams(200, 100));
853             mListView.setAdapter(adapter);
854         });
855         assertEquals("test sanity", 200, mListView.getWidth());
856         assertEquals(200, view.getWidth());
857         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
858             mListView.setPadding(10, 0, 5, 0);
859             assertTrue(view.isLayoutRequested());
860         });
861         assertEquals(185, view.getWidth());
862         assertFalse(view.isLayoutRequested());
863         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
864             mListView.setPadding(10, 0, 5, 0);
865             assertFalse(view.isLayoutRequested());
866         });
867     }
868 
869     @MediumTest
870     @Test
testResolveRtlOnReAttach()871     public void testResolveRtlOnReAttach() throws Throwable {
872         View spacer = new View(mActivity);
873         spacer.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
874                 250));
875         final DummyAdapter adapter = new DummyAdapter(50, spacer);
876         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
877             mListView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
878             mListView.setLayoutParams(new FrameLayout.LayoutParams(200, 150));
879             mListView.setAdapter(adapter);
880         });
881         assertEquals("test sanity", 1, mListView.getChildCount());
882         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
883             // we scroll in pieces because list view caps scroll by its height
884             mListView.scrollListBy(100);
885             mListView.scrollListBy(100);
886             mListView.scrollListBy(60);
887         });
888         assertEquals("test sanity", 1, mListView.getChildCount());
889         assertEquals("test sanity", 1, mListView.getFirstVisiblePosition());
890         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
891             mListView.scrollListBy(-100);
892             mListView.scrollListBy(-100);
893             mListView.scrollListBy(-60);
894         });
895         assertEquals("test sanity", 1, mListView.getChildCount());
896         assertEquals("item 0 should be visible", 0, mListView.getFirstVisiblePosition());
897         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
898             mListView.scrollListBy(100);
899             mListView.scrollListBy(100);
900             mListView.scrollListBy(60);
901         });
902         assertEquals("test sanity", 1, mListView.getChildCount());
903         assertEquals("test sanity", 1, mListView.getFirstVisiblePosition());
904 
905         assertEquals("the view's RTL properties must be resolved",
906                 mListView.getChildAt(0).getLayoutDirection(), View.LAYOUT_DIRECTION_RTL);
907     }
908 
909     private class MockView extends View {
910 
911         public boolean onMeasureCalled = false;
912 
MockView(Context context)913         public MockView(Context context) {
914             super(context);
915         }
916 
917         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)918         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
919             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
920             onMeasureCalled = true;
921         }
922     }
923 
924     private class MockAdapter<T> extends ArrayAdapter<T> {
925 
MockAdapter(Context context, int resource, List<T> objects)926         public MockAdapter(Context context, int resource, List<T> objects) {
927             super(context, resource, objects);
928         }
929 
930         @Override
getView(int position, View convertView, ViewGroup parent)931         public View getView(int position, View convertView, ViewGroup parent) {
932             return new MockView(getContext());
933         }
934     }
935 
936     @MediumTest
937     @Test
testRequestLayoutWithTemporaryDetach()938     public void testRequestLayoutWithTemporaryDetach() throws Throwable {
939         List<String> items = new ArrayList<>();
940         items.add("0");
941         items.add("1");
942         items.add("2");
943         final TemporarilyDetachableMockViewAdapter<String> adapter =
944                 new TemporarilyDetachableMockViewAdapter<>(
945                         mActivity, android.R.layout.simple_list_item_1, items);
946         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
947                 () -> mListView.setAdapter(adapter));
948 
949         assertEquals(items.size(), mListView.getCount());
950         final TemporarilyDetachableMockView childView0 =
951                 (TemporarilyDetachableMockView) mListView.getChildAt(0);
952         final TemporarilyDetachableMockView childView1 =
953                 (TemporarilyDetachableMockView) mListView.getChildAt(1);
954         final TemporarilyDetachableMockView childView2 =
955                 (TemporarilyDetachableMockView) mListView.getChildAt(2);
956         assertNotNull(childView0);
957         assertNotNull(childView1);
958         assertNotNull(childView2);
959 
960         // Make sure that ListView#requestLayout() is optimized when nothing is changed.
961         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, mListView::requestLayout);
962         assertEquals(childView0, mListView.getChildAt(0));
963         assertEquals(childView1, mListView.getChildAt(1));
964         assertEquals(childView2, mListView.getChildAt(2));
965     }
966 
967     @MediumTest
968     @Test
testJumpDrawables()969     public void testJumpDrawables() throws Throwable {
970         FrameLayout layout = new FrameLayout(mActivity);
971         ListView listView = new ListView(mActivity);
972         ArrayAdapterWithMockDrawable adapter = new ArrayAdapterWithMockDrawable(mActivity);
973         for (int i = 0; i < 50; i++) {
974             adapter.add(Integer.toString(i));
975         }
976 
977         // Initial state should jump exactly once during attach.
978         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, () -> {
979             listView.setAdapter(adapter);
980             layout.addView(listView, new LayoutParams(LayoutParams.MATCH_PARENT, 200));
981             mActivity.setContentView(layout);
982         });
983         assertTrue("List is not showing any children", listView.getChildCount() > 0);
984         Drawable firstBackground = listView.getChildAt(0).getBackground();
985         verify(firstBackground, times(1)).jumpToCurrentState();
986 
987         // Lay out views without recycling. This should not jump again.
988         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, listView::requestLayout);
989         assertSame(firstBackground, listView.getChildAt(0).getBackground());
990         verify(firstBackground, times(1)).jumpToCurrentState();
991 
992         // If we're on a really big display, we might be in a position where
993         // the position we're going to scroll to is already visible, in which
994         // case we won't be able to test jump behavior when recycling.
995         int lastVisiblePosition = listView.getLastVisiblePosition();
996         int targetPosition = adapter.getCount() - 1;
997         if (targetPosition <= lastVisiblePosition) {
998             return;
999         }
1000 
1001         // Reset the call counts before continuing, since the backgrounds may
1002         // be recycled from either views that were on-screen or in the scrap
1003         // heap, and those would have slightly different call counts.
1004         adapter.resetMockBackgrounds();
1005 
1006         // Scroll so that we have new views on screen. This should jump at
1007         // least once when the view is recycled in a new position (but may be
1008         // more if it was recycled from a view that was previously on-screen).
1009         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
1010                 () -> listView.setSelection(targetPosition));
1011 
1012         View lastChild = listView.getChildAt(listView.getChildCount() - 1);
1013         verify(lastChild.getBackground(), atLeast(1)).jumpToCurrentState();
1014 
1015         // Reset the call counts before continuing.
1016         adapter.resetMockBackgrounds();
1017 
1018         // Scroll back to the top. This should jump at least once when the view
1019         // is recycled in a new position (but may be more if it was recycled
1020         // from a view that was previously on-screen).
1021         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
1022                 () -> listView.setSelection(0));
1023 
1024         View firstChild = listView.getChildAt(0);
1025         verify(firstChild.getBackground(), atLeast(1)).jumpToCurrentState();
1026     }
1027 
1028     private static class ArrayAdapterWithMockDrawable extends ArrayAdapter<String> {
1029         private SparseArray<Drawable> mBackgrounds = new SparseArray<>();
1030 
ArrayAdapterWithMockDrawable(Context context)1031         public ArrayAdapterWithMockDrawable(Context context) {
1032             super(context, android.R.layout.simple_list_item_1);
1033         }
1034 
1035         @Override
getView(int position, View convertView, ViewGroup parent)1036         public View getView(int position, View convertView, ViewGroup parent) {
1037             final View view = super.getView(position, convertView, parent);
1038             if (convertView == null) {
1039                 if (view.getBackground() == null) {
1040                     view.setBackground(spy(new ColorDrawable(Color.BLACK)));
1041                 } else {
1042                     view.setBackground(spy(view.getBackground()));
1043                 }
1044             }
1045             return view;
1046         }
1047 
resetMockBackgrounds()1048         public void resetMockBackgrounds() {
1049             for (int i = 0; i < mBackgrounds.size(); i++) {
1050                 Drawable background = mBackgrounds.valueAt(i);
1051                 reset(background);
1052             }
1053         }
1054     }
1055 
1056     private class TemporarilyDetachableMockView extends View {
1057 
1058         private boolean mIsDispatchingStartTemporaryDetach = false;
1059         private boolean mIsDispatchingFinishTemporaryDetach = false;
1060 
TemporarilyDetachableMockView(Context context)1061         public TemporarilyDetachableMockView(Context context) {
1062             super(context);
1063         }
1064 
1065         @Override
dispatchStartTemporaryDetach()1066         public void dispatchStartTemporaryDetach() {
1067             mIsDispatchingStartTemporaryDetach = true;
1068             super.dispatchStartTemporaryDetach();
1069             mIsDispatchingStartTemporaryDetach = false;
1070         }
1071 
1072         @Override
dispatchFinishTemporaryDetach()1073         public void dispatchFinishTemporaryDetach() {
1074             mIsDispatchingFinishTemporaryDetach = true;
1075             super.dispatchFinishTemporaryDetach();
1076             mIsDispatchingFinishTemporaryDetach = false;
1077         }
1078 
1079         @Override
onStartTemporaryDetach()1080         public void onStartTemporaryDetach() {
1081             super.onStartTemporaryDetach();
1082             if (!mIsDispatchingStartTemporaryDetach) {
1083                 throw new IllegalStateException("#onStartTemporaryDetach() must be indirectly"
1084                         + " called via #dispatchStartTemporaryDetach()");
1085             }
1086         }
1087 
1088         @Override
onFinishTemporaryDetach()1089         public void onFinishTemporaryDetach() {
1090             super.onFinishTemporaryDetach();
1091             if (!mIsDispatchingFinishTemporaryDetach) {
1092                 throw new IllegalStateException("#onStartTemporaryDetach() must be indirectly"
1093                         + " called via #dispatchFinishTemporaryDetach()");
1094             }
1095         }
1096     }
1097 
1098     private class TemporarilyDetachableMockViewAdapter<T> extends ArrayAdapter<T> {
1099         ArrayList<TemporarilyDetachableMockView> views = new ArrayList<>();
1100 
TemporarilyDetachableMockViewAdapter(Context context, int textViewResourceId, List<T> objects)1101         public TemporarilyDetachableMockViewAdapter(Context context, int textViewResourceId,
1102                 List<T> objects) {
1103             super(context, textViewResourceId, objects);
1104             for (int i = 0; i < objects.size(); i++) {
1105                 views.add(new TemporarilyDetachableMockView(context));
1106                 views.get(i).setFocusable(true);
1107             }
1108         }
1109 
1110         @Override
getCount()1111         public int getCount() {
1112             return views.size();
1113         }
1114 
1115         @Override
getItemId(int position)1116         public long getItemId(int position) {
1117             return position;
1118         }
1119 
1120         @Override
getView(int position, View convertView, ViewGroup parent)1121         public View getView(int position, View convertView, ViewGroup parent) {
1122             View result = views.get(position);
1123             ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
1124                     ViewGroup.LayoutParams.MATCH_PARENT, 40);
1125             result.setLayoutParams(lp);
1126             return result;
1127         }
1128     }
1129 
1130     @Test
testTransientStateUnstableIds()1131     public void testTransientStateUnstableIds() throws Throwable {
1132         final ListView listView = mListView;
1133         final ArrayList<String> items = new ArrayList<String>(Arrays.asList(mCountryList));
1134         final ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
1135                 android.R.layout.simple_list_item_1, items);
1136 
1137         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
1138                 () -> listView.setAdapter(adapter));
1139 
1140         final View oldItem = listView.getChildAt(2);
1141         final CharSequence oldText = ((TextView) oldItem.findViewById(android.R.id.text1))
1142                 .getText();
1143         oldItem.setHasTransientState(true);
1144 
1145         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
1146                 () -> {
1147                     adapter.remove(adapter.getItem(0));
1148                     adapter.notifyDataSetChanged();
1149                 });
1150 
1151         final View newItem = listView.getChildAt(2);
1152         final CharSequence newText = ((TextView) newItem.findViewById(android.R.id.text1))
1153                 .getText();
1154 
1155         Assert.assertFalse(oldText.equals(newText));
1156     }
1157 
1158     @Test
testTransientStateStableIds()1159     public void testTransientStateStableIds() throws Throwable {
1160         final ArrayList<String> items = new ArrayList<>(Arrays.asList(mCountryList));
1161         final StableArrayAdapter<String> adapter = new StableArrayAdapter<>(mActivity,
1162                 android.R.layout.simple_list_item_1, items);
1163 
1164         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1165                 () -> mListView.setAdapter(adapter));
1166 
1167         final Object tag = new Object();
1168         final View oldItem = mListView.getChildAt(2);
1169         final CharSequence oldText = ((TextView) oldItem.findViewById(android.R.id.text1))
1170                 .getText();
1171         oldItem.setHasTransientState(true);
1172         oldItem.setTag(tag);
1173 
1174         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1175                 () -> {
1176                     adapter.remove(adapter.getItem(0));
1177                     adapter.notifyDataSetChanged();
1178                 });
1179 
1180         final View newItem = mListView.getChildAt(1);
1181         final CharSequence newText = ((TextView) newItem.findViewById(android.R.id.text1))
1182                 .getText();
1183 
1184         Assert.assertTrue(newItem.hasTransientState());
1185         Assert.assertEquals(oldText, newText);
1186         Assert.assertEquals(tag, newItem.getTag());
1187     }
1188 
1189     @Test
testStretchAtTop()1190     public void testStretchAtTop() throws Throwable {
1191         // Make sure that the view we care about is on screen and at the top:
1192         showOnlyStretch();
1193 
1194         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mActivity);
1195         mListViewStretch.mEdgeGlowTop = edgeEffect;
1196         assertTrue(StretchEdgeUtil.dragStretches(
1197                 mActivityRule,
1198                 mListViewStretch,
1199                 edgeEffect,
1200                 0,
1201                 300
1202         ));
1203     }
1204 
1205     @Test
testStretchTopAndCatch()1206     public void testStretchTopAndCatch() throws Throwable {
1207         // Make sure that the view we care about is on screen and at the top:
1208         showOnlyStretch();
1209 
1210         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mActivity);
1211         mListViewStretch.mEdgeGlowTop = edgeEffect;
1212         assertTrue(StretchEdgeUtil.dragAndHoldKeepsStretch(
1213                 mActivityRule,
1214                 mListViewStretch,
1215                 edgeEffect,
1216                 0,
1217                 300
1218         ));
1219     }
1220 
scrollToBottomOfStretch()1221     private void scrollToBottomOfStretch() throws Throwable {
1222         do {
1223             mActivityRule.runOnUiThread(() -> {
1224                 mListViewStretch.scrollListBy(50);
1225             });
1226         } while (mListViewStretch.pointToPosition(0, 40) != mColorList.length - 1);
1227     }
1228 
1229     @Test
testStretchAtBottom()1230     public void testStretchAtBottom() throws Throwable {
1231         // Make sure that the view we care about is on screen and at the top:
1232         showOnlyStretch();
1233 
1234         scrollToBottomOfStretch();
1235         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mActivity);
1236         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1237         assertTrue(StretchEdgeUtil.dragStretches(
1238                 mActivityRule,
1239                 mListViewStretch,
1240                 edgeEffect,
1241                 0,
1242                 -300
1243         ));
1244     }
1245 
1246     @Test
testStretchBottomAndCatch()1247     public void testStretchBottomAndCatch() throws Throwable {
1248         // Make sure that the view we care about is on screen and at the top:
1249         showOnlyStretch();
1250 
1251         scrollToBottomOfStretch();
1252         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mActivity);
1253         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1254         assertTrue(StretchEdgeUtil.dragAndHoldKeepsStretch(
1255                 mActivityRule,
1256                 mListViewStretch,
1257                 edgeEffect,
1258                 0,
1259                 -300
1260         ));
1261     }
1262 
1263     @Test
testFlingWhileStretchedTop()1264     public void testFlingWhileStretchedTop() throws Throwable {
1265         // Make sure that the scroll view we care about is on screen and at the top:
1266         showOnlyStretch();
1267 
1268         ScrollViewTest.CaptureOnAbsorbEdgeEffect
1269                 edgeEffect = new ScrollViewTest.CaptureOnAbsorbEdgeEffect(mActivity);
1270         mListViewStretch.mEdgeGlowTop = edgeEffect;
1271         fling(mActivityRule, mListViewStretch, 0, 300);
1272         assertTrue(edgeEffect.onAbsorbVelocity > 0);
1273     }
1274 
1275     @Test
testFlingWhileStretchedBottom()1276     public void testFlingWhileStretchedBottom() throws Throwable {
1277         // Make sure that the scroll view we care about is on screen and at the top:
1278         showOnlyStretch();
1279 
1280         scrollToBottomOfStretch();
1281 
1282         ScrollViewTest.CaptureOnAbsorbEdgeEffect
1283                 edgeEffect = new ScrollViewTest.CaptureOnAbsorbEdgeEffect(mActivity);
1284         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1285         fling(mActivityRule, mListViewStretch, 0, -300);
1286         assertTrue(edgeEffect.onAbsorbVelocity > 0);
1287     }
1288 
1289     @Test
testScrollAfterStretch()1290     public void testScrollAfterStretch() throws Throwable {
1291         showOnlyStretch();
1292         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mActivity);
1293         mActivityRule.runOnUiThread(() -> {
1294             mListViewStretch.setAdapter(new ClickColorAdapter(mActivity, mColorList));
1295             mListViewStretch.mEdgeGlowTop = edgeEffect;
1296         });
1297         mActivityRule.runOnUiThread(() -> {});
1298 
1299         int[] locationOnScreen = new int[2];
1300         mActivityRule.runOnUiThread(() -> {
1301             mListViewStretch.getLocationOnScreen(locationOnScreen);
1302         });
1303 
1304         int screenX = locationOnScreen[0];
1305         int screenY = locationOnScreen[1];
1306 
1307         int lastVisiblePositionBeforeScroll = mListViewStretch.getLastVisiblePosition();
1308         int firstVisiblePositionBeforeScroll = mListViewStretch.getFirstVisiblePosition();
1309 
1310 
1311         // Cause a stretch
1312         mCtsTouchUtils.emulateDragGesture(
1313                 mInstrumentation,
1314                 mActivityRule,
1315                 screenX + mListViewStretch.getWidth() / 2,
1316                 screenY + mListViewStretch.getHeight() / 2,
1317                 0,
1318                 300,
1319                 300,
1320                 20,
1321                 false,
1322                 null
1323         );
1324 
1325         // Now scroll the other direction
1326         mCtsTouchUtils.emulateDragGesture(
1327                 mInstrumentation,
1328                 mActivityRule,
1329                 screenX + mListViewStretch.getWidth() / 2,
1330                 screenY + mListViewStretch.getHeight() / 2,
1331                 0,
1332                 -600,
1333                 160,
1334                 20,
1335                 false,
1336                 null
1337         );
1338 
1339         int lastVisiblePositionAfterScroll = mListViewStretch.getLastVisiblePosition();
1340         int firstVisiblePositionAfterScroll = mListViewStretch.getFirstVisiblePosition();
1341 
1342         assertTrue(lastVisiblePositionAfterScroll > lastVisiblePositionBeforeScroll);
1343         assertTrue(firstVisiblePositionAfterScroll > firstVisiblePositionBeforeScroll);
1344     }
1345 
1346     @Test
testEdgeEffectAddToBottom()1347     public void testEdgeEffectAddToBottom() throws Throwable {
1348         // Make sure that the view we care about is on screen and at the top:
1349         showOnlyStretch();
1350 
1351         scrollToBottomOfStretch();
1352 
1353         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mListViewStretch.getContext());
1354         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1355         edgeEffect.setPauseRelease(true);
1356 
1357         executeWhileDragging(
1358                 -300,
1359                 () -> {
1360                     assertFalse(edgeEffect.getOnReleaseCalled());
1361                     try {
1362                         mActivityRule.runOnUiThread(() -> {
1363                             for (int color : mColorList) {
1364                                 mAdapterColors.addColor(Color.BLACK);
1365                                 mAdapterColors.addColor(color);
1366                             }
1367                         });
1368                     } catch (Throwable e) {
1369                     }
1370                 },
1371                 () -> {
1372                     assertTrue(edgeEffect.getOnReleaseCalled());
1373                     assertTrue(edgeEffect.getDistance() > 0);
1374                 }
1375         );
1376 
1377         edgeEffect.finish();
1378         int firstVisible = mListViewStretch.getFirstVisiblePosition();
1379 
1380         // We've turned off the release, so the distance won't change unless onPull() is called
1381         executeWhileDragging(-300, () -> {}, () -> {});
1382         assertTrue(edgeEffect.isFinished());
1383         assertEquals(0f, edgeEffect.getDistance(), 0.01f);
1384         assertNotEquals(firstVisible, mListViewStretch.getFirstVisiblePosition());
1385     }
1386 
1387     @Test
testEdgeEffectAddToTop()1388     public void testEdgeEffectAddToTop() throws Throwable {
1389         // Make sure that the view we care about is on screen and at the top:
1390         showOnlyStretch();
1391 
1392         NoReleaseEdgeEffect edgeEffect = new NoReleaseEdgeEffect(mListViewStretch.getContext());
1393         mListViewStretch.mEdgeGlowTop = edgeEffect;
1394         edgeEffect.setPauseRelease(true);
1395 
1396         executeWhileDragging(
1397                 300,
1398                 () -> {
1399                     assertFalse(edgeEffect.getOnReleaseCalled());
1400                     try {
1401                         mActivityRule.runOnUiThread(() -> {
1402                             for (int color : mColorList) {
1403                                 mAdapterColors.addColorAtStart(Color.BLACK);
1404                                 mAdapterColors.addColorAtStart(color);
1405                             }
1406                             mListViewStretch.setSelection(mColorList.length * 2);
1407                         });
1408                     } catch (Throwable e) {
1409                     }
1410                 },
1411                 () -> {
1412                     assertTrue(edgeEffect.getOnReleaseCalled());
1413                     assertTrue(edgeEffect.getDistance() > 0);
1414                 }
1415         );
1416 
1417         edgeEffect.finish();
1418         int firstVisible = mListViewStretch.getFirstVisiblePosition();
1419 
1420         // We've turned off the release, so the distance won't change unless onPull() is called
1421         executeWhileDragging(300, () -> {}, () -> {});
1422         assertTrue(edgeEffect.isFinished());
1423         assertEquals(0f, edgeEffect.getDistance(), 0.01f);
1424         assertNotEquals(firstVisible, mListViewStretch.getFirstVisiblePosition());
1425     }
1426 
1427     @Test
scrollFromRotaryStretchesTop()1428     public void scrollFromRotaryStretchesTop() throws Throwable {
1429         showOnlyStretch();
1430 
1431         CaptureOnReleaseEdgeEffect
1432                 edgeEffect = new CaptureOnReleaseEdgeEffect(mActivity);
1433         mListViewStretch.mEdgeGlowTop = edgeEffect;
1434 
1435         mActivityRule.runOnUiThread(() -> {
1436             assertTrue(mListViewStretch.dispatchGenericMotionEvent(
1437                     createScrollEvent(2f, InputDevice.SOURCE_ROTARY_ENCODER)));
1438             assertFalse(edgeEffect.isFinished());
1439             assertTrue(edgeEffect.getDistance() > 0f);
1440             assertTrue(edgeEffect.onReleaseCalled);
1441         });
1442     }
1443 
1444     @Test
scrollFromMouseDoesNotStretchTop()1445     public void scrollFromMouseDoesNotStretchTop() throws Throwable {
1446         showOnlyStretch();
1447 
1448         CaptureOnReleaseEdgeEffect
1449                 edgeEffect = new CaptureOnReleaseEdgeEffect(mActivity);
1450         mListViewStretch.mEdgeGlowTop = edgeEffect;
1451 
1452         mActivityRule.runOnUiThread(() -> {
1453             assertFalse(mListViewStretch.dispatchGenericMotionEvent(
1454                     createScrollEvent(2f, InputDevice.SOURCE_MOUSE)));
1455             assertTrue(edgeEffect.isFinished());
1456             assertFalse(edgeEffect.onReleaseCalled);
1457         });
1458     }
1459 
1460     @Test
scrollFromRotaryStretchesBottom()1461     public void scrollFromRotaryStretchesBottom() throws Throwable {
1462         showOnlyStretch();
1463 
1464         scrollToBottomOfStretch();
1465 
1466         CaptureOnReleaseEdgeEffect
1467                 edgeEffect = new CaptureOnReleaseEdgeEffect(mActivity);
1468         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1469 
1470         mActivityRule.runOnUiThread(() -> {
1471             assertTrue(mListViewStretch.dispatchGenericMotionEvent(
1472                     createScrollEvent(-2f, InputDevice.SOURCE_ROTARY_ENCODER)));
1473             assertFalse(edgeEffect.isFinished());
1474             assertTrue(edgeEffect.getDistance() > 0f);
1475             assertTrue(edgeEffect.onReleaseCalled);
1476         });
1477     }
1478 
1479     @Test
scrollFromMouseDoesNotStretchBottom()1480     public void scrollFromMouseDoesNotStretchBottom() throws Throwable {
1481         showOnlyStretch();
1482 
1483         scrollToBottomOfStretch();
1484 
1485         CaptureOnReleaseEdgeEffect
1486                 edgeEffect = new CaptureOnReleaseEdgeEffect(mActivity);
1487         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1488 
1489         mActivityRule.runOnUiThread(() -> {
1490             assertFalse(mListViewStretch.dispatchGenericMotionEvent(
1491                     createScrollEvent(-2f, InputDevice.SOURCE_MOUSE)));
1492             assertTrue(edgeEffect.isFinished());
1493             assertFalse(edgeEffect.onReleaseCalled);
1494         });
1495     }
1496 
1497     @Test
flingUpWhileStretchedAtTop()1498     public void flingUpWhileStretchedAtTop() throws Throwable {
1499         showOnlyStretch();
1500 
1501         CaptureOnReleaseEdgeEffect edgeEffect = new CaptureOnReleaseEdgeEffect(mActivity);
1502         mListViewStretch.mEdgeGlowTop = edgeEffect;
1503 
1504         int[] scrollStateValue = new int[1];
1505 
1506         mListViewStretch.setOnScrollListener(new AbsListView.OnScrollListener() {
1507             @Override
1508             public void onScrollStateChanged(AbsListView view, int scrollState) {
1509                 scrollStateValue[0] = scrollState;
1510             }
1511 
1512             @Override
1513             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1514                     int totalItemCount) {
1515             }
1516         });
1517         executeWhileDragging(1000, () -> {}, () -> {
1518             assertFalse(edgeEffect.isFinished());
1519         });
1520         mActivityRule.runOnUiThread(() -> {
1521             edgeEffect.onReleaseCalled = false;
1522             mListViewStretch.fling(10000);
1523             assertFalse(edgeEffect.onReleaseCalled);
1524             assertFalse(edgeEffect.isFinished());
1525         });
1526         mActivityRule.runOnUiThread(() -> {
1527             assertEquals(AbsListView.OnScrollListener.SCROLL_STATE_FLING, scrollStateValue[0]);
1528         });
1529         long end = SystemClock.uptimeMillis() + 4000;
1530         while (scrollStateValue[0] == AbsListView.OnScrollListener.SCROLL_STATE_FLING
1531                 && SystemClock.uptimeMillis() < end) {
1532             // wait one frame
1533             mActivityRule.runOnUiThread(() -> {});
1534         }
1535         assertNotEquals(AbsListView.OnScrollListener.SCROLL_STATE_FLING, scrollStateValue[0]);
1536         mActivityRule.runOnUiThread(() -> {
1537             assertEquals(0f, edgeEffect.getDistance(), 0f);
1538             assertNotEquals(0, mListViewStretch.getFirstVisiblePosition());
1539         });
1540     }
1541 
1542     @Test
flingDownWhileStretchedAtBottom()1543     public void flingDownWhileStretchedAtBottom() throws Throwable {
1544         showOnlyStretch();
1545         scrollToBottomOfStretch();
1546 
1547         int bottomItem = mListViewStretch.getLastVisiblePosition();
1548 
1549         CaptureOnReleaseEdgeEffect edgeEffect = new CaptureOnReleaseEdgeEffect(mActivity);
1550         mListViewStretch.mEdgeGlowBottom = edgeEffect;
1551 
1552         int[] scrollStateValue = new int[1];
1553 
1554         mListViewStretch.setOnScrollListener(new AbsListView.OnScrollListener() {
1555             @Override
1556             public void onScrollStateChanged(AbsListView view, int scrollState) {
1557                 scrollStateValue[0] = scrollState;
1558             }
1559 
1560             @Override
1561             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1562                     int totalItemCount) {
1563             }
1564         });
1565         executeWhileDragging(-1000, () -> {}, () -> {
1566             assertFalse(edgeEffect.isFinished());
1567         });
1568         mActivityRule.runOnUiThread(() -> {
1569             edgeEffect.onReleaseCalled = false;
1570             mListViewStretch.fling(-10000);
1571             assertFalse(edgeEffect.onReleaseCalled);
1572             assertFalse(edgeEffect.isFinished());
1573         });
1574         mActivityRule.runOnUiThread(() -> {
1575             assertEquals(AbsListView.OnScrollListener.SCROLL_STATE_FLING, scrollStateValue[0]);
1576         });
1577         long end = SystemClock.uptimeMillis() + 4000;
1578         while (scrollStateValue[0] == AbsListView.OnScrollListener.SCROLL_STATE_FLING
1579                 && SystemClock.uptimeMillis() < end) {
1580             // wait one frame
1581             mActivityRule.runOnUiThread(() -> {});
1582         }
1583         assertNotEquals(AbsListView.OnScrollListener.SCROLL_STATE_FLING, scrollStateValue[0]);
1584         mActivityRule.runOnUiThread(() -> {
1585             assertEquals(0f, edgeEffect.getDistance(), 0f);
1586             assertNotEquals(bottomItem, mListViewStretch.getLastVisiblePosition());
1587         });
1588     }
1589 
createScrollEvent(float scrollAmount, int source)1590     private MotionEvent createScrollEvent(float scrollAmount, int source) {
1591         MotionEvent.PointerProperties pointerProperties = new MotionEvent.PointerProperties();
1592         pointerProperties.toolType = MotionEvent.TOOL_TYPE_MOUSE;
1593         MotionEvent.PointerCoords pointerCoords = new MotionEvent.PointerCoords();
1594         int axis = source == InputDevice.SOURCE_ROTARY_ENCODER ? MotionEvent.AXIS_SCROLL
1595                 : MotionEvent.AXIS_VSCROLL;
1596         pointerCoords.setAxisValue(axis, scrollAmount);
1597 
1598         return MotionEvent.obtain(
1599                 0, /* downTime */
1600                 0, /* eventTime */
1601                 MotionEvent.ACTION_SCROLL, /* action */
1602                 1, /* pointerCount */
1603                 new MotionEvent.PointerProperties[] { pointerProperties },
1604                 new MotionEvent.PointerCoords[] { pointerCoords },
1605                 0, /* metaState */
1606                 0, /* buttonState */
1607                 0f, /* xPrecision */
1608                 0f, /* yPrecision */
1609                 0, /* deviceId */
1610                 0, /* edgeFlags */
1611                 source, /* source */
1612                 0 /* flags */
1613         );
1614     }
1615 
executeWhileDragging( int dragY, Runnable duringDrag, Runnable beforeUp )1616     private void executeWhileDragging(
1617             int dragY,
1618             Runnable duringDrag,
1619             Runnable beforeUp
1620     ) throws Throwable {
1621         int[] locationOnScreen = new int[2];
1622         mActivityRule.runOnUiThread(() -> {
1623             mListViewStretch.getLocationOnScreen(locationOnScreen);
1624         });
1625 
1626         int screenX = locationOnScreen[0] + mListViewStretch.getWidth() / 2;
1627         int screenY = locationOnScreen[1] + mListViewStretch.getHeight() / 2;
1628         Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
1629         UiAutomation uiAutomation = instrumentation.getUiAutomation();
1630         long downTime = SystemClock.uptimeMillis();
1631         StretchEdgeUtil.injectDownEvent(uiAutomation, downTime, screenX, screenY);
1632 
1633         int middleY = screenY + (dragY / 2);
1634         StretchEdgeUtil.injectMoveEventsForDrag(
1635                 uiAutomation,
1636                 downTime,
1637                 downTime,
1638                 screenX,
1639                 screenY,
1640                 screenX,
1641                 middleY,
1642                 5,
1643                 20
1644         );
1645 
1646         duringDrag.run();
1647 
1648         int endY = screenY + dragY;
1649 
1650         StretchEdgeUtil.injectMoveEventsForDrag(
1651                 uiAutomation,
1652                 downTime,
1653                 downTime + 25,
1654                 screenX,
1655                 middleY,
1656                 screenX,
1657                 endY,
1658                 5,
1659                 20
1660         );
1661 
1662         beforeUp.run();
1663 
1664         StretchEdgeUtil.injectUpEvent(
1665                 uiAutomation,
1666                 downTime,
1667                 downTime + 50,
1668                 screenX,
1669                 endY
1670         );
1671     }
1672 
showOnlyStretch()1673     private void showOnlyStretch() throws Throwable {
1674         mActivityRule.runOnUiThread(() -> {
1675             ViewGroup parent = (ViewGroup) mListViewStretch.getParent();
1676             for (int i = 0; i < parent.getChildCount(); i++) {
1677                 View child = parent.getChildAt(i);
1678                 if (child != mListViewStretch) {
1679                     child.setVisibility(View.GONE);
1680                 }
1681             }
1682             mListViewStretch.setAdapter(mAdapterColors);
1683             mListViewStretch.setDivider(null);
1684             mListViewStretch.setDividerHeight(0);
1685         });
1686         // Give it an opportunity to finish layout.
1687         mActivityRule.runOnUiThread(() -> {});
1688     }
1689 
1690     private static class StableArrayAdapter<T> extends ArrayAdapter<T> {
StableArrayAdapter(Context context, int resource, List<T> objects)1691         public StableArrayAdapter(Context context, int resource, List<T> objects) {
1692             super(context, resource, objects);
1693         }
1694 
1695         @Override
getItemId(int position)1696         public long getItemId(int position) {
1697             return getItem(position).hashCode();
1698         }
1699 
1700         @Override
hasStableIds()1701         public boolean hasStableIds() {
1702             return true;
1703         }
1704     }
1705 
1706     @LargeTest
1707     @Test
testSmoothScrollByOffset()1708     public void testSmoothScrollByOffset() throws Throwable {
1709         final int itemCount = mLongCountryList.length;
1710 
1711         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1712                 () -> mListView.setAdapter(mAdapter_longCountries));
1713 
1714         assertEquals(0, mListView.getFirstVisiblePosition());
1715 
1716         // If we're on a really big display, we might be in a situation where the position
1717         // we're going to scroll to is already visible. In that case the logic in the rest
1718         // of this test will never fire off a listener callback and then fail the test.
1719         final int positionToScrollTo = itemCount - 10;
1720         final int lastVisiblePosition = mListView.getLastVisiblePosition();
1721         if (positionToScrollTo <= lastVisiblePosition) {
1722             return;
1723         }
1724 
1725         // Register a scroll listener on our ListView. The listener will notify our latch
1726         // when the "target" item comes into view. If that never happens, the latch will
1727         // time out and fail the test.
1728         final CountDownLatch latch = new CountDownLatch(1);
1729         mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
1730             @Override
1731             public void onScrollStateChanged(AbsListView view, int scrollState) {
1732             }
1733 
1734             @Override
1735             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1736                     int totalItemCount) {
1737                 if ((positionToScrollTo >= firstVisibleItem) &&
1738                         (positionToScrollTo <= (firstVisibleItem + visibleItemCount))) {
1739                     latch.countDown();
1740                 }
1741             }
1742         });
1743         int offset = positionToScrollTo - lastVisiblePosition;
1744         mActivityRule.runOnUiThread(() -> mListView.smoothScrollByOffset(offset));
1745 
1746         boolean result = false;
1747         try {
1748             result = latch.await(20, TimeUnit.SECONDS);
1749         } catch (InterruptedException e) {
1750             // ignore
1751         }
1752         assertTrue("Timed out while waiting for the target view to be scrolled into view", result);
1753     }
1754 
1755     private static class PositionArrayAdapter<T> extends ArrayAdapter<T> {
PositionArrayAdapter(Context context, int resource, List<T> objects)1756         public PositionArrayAdapter(Context context, int resource, List<T> objects) {
1757             super(context, resource, objects);
1758         }
1759 
1760         @Override
getItemId(int position)1761         public long getItemId(int position) {
1762             return position;
1763         }
1764 
1765         @Override
hasStableIds()1766         public boolean hasStableIds() {
1767             return true;
1768         }
1769     }
1770 
1771     @Test
testGetCheckItemIds()1772     public void testGetCheckItemIds() throws Throwable {
1773         final ArrayList<String> items = new ArrayList<>(Arrays.asList(mCountryList));
1774         final ArrayAdapter<String> adapter = new PositionArrayAdapter<>(mActivity,
1775                 android.R.layout.simple_list_item_1, items);
1776 
1777         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1778                 () -> mListView.setAdapter(adapter));
1779 
1780         mActivityRule.runOnUiThread(
1781                 () -> mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE));
1782         assertTrue(mListView.getCheckItemIds().length == 0);
1783 
1784         mActivityRule.runOnUiThread(() -> mListView.setItemChecked(2, true));
1785         TestUtils.assertIdentical(new long[] { 2 }, mListView.getCheckItemIds());
1786 
1787         mActivityRule.runOnUiThread(() -> mListView.setItemChecked(4, true));
1788         TestUtils.assertIdentical(new long[] { 2, 4 }, mListView.getCheckItemIds());
1789 
1790         mActivityRule.runOnUiThread(() -> mListView.setItemChecked(2, false));
1791         TestUtils.assertIdentical(new long[] { 4 }, mListView.getCheckItemIds());
1792 
1793         mActivityRule.runOnUiThread(() -> mListView.setItemChecked(4, false));
1794         assertTrue(mListView.getCheckItemIds().length == 0);
1795     }
1796 
1797     @Test
testAccessOverscrollHeader()1798     public void testAccessOverscrollHeader() throws Throwable {
1799         final Drawable overscrollHeaderDrawable = spy(new ColorDrawable(Color.YELLOW));
1800         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1801                 () -> {
1802                     mListView.setAdapter(mAdapter_longCountries);
1803                     mListView.setOverscrollHeader(overscrollHeaderDrawable);
1804                 });
1805 
1806         assertEquals(overscrollHeaderDrawable, mListView.getOverscrollHeader());
1807         verify(overscrollHeaderDrawable, never()).draw(any(Canvas.class));
1808 
1809         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1810                 () -> mListView.setScrollY(-mListView.getHeight() / 2));
1811 
1812         verify(overscrollHeaderDrawable, atLeastOnce()).draw(any(Canvas.class));
1813     }
1814 
1815     @Test
testAccessOverscrollFooter()1816     public void testAccessOverscrollFooter() throws Throwable {
1817         final Drawable overscrollFooterDrawable = spy(new ColorDrawable(Color.MAGENTA));
1818         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView, () -> {
1819             // Configure ListView to automatically scroll to the selected item
1820             mListView.setStackFromBottom(true);
1821             mListView.setTranscriptMode(AbsListView.TRANSCRIPT_MODE_ALWAYS_SCROLL);
1822 
1823             mListView.setAdapter(mAdapter_longCountries);
1824             mListView.setOverscrollFooter(overscrollFooterDrawable);
1825 
1826             // Set selection to the last item
1827             mListView.setSelection(mAdapter_longCountries.getCount() - 1);
1828         });
1829 
1830         assertEquals(overscrollFooterDrawable, mListView.getOverscrollFooter());
1831         verify(overscrollFooterDrawable, never()).draw(any(Canvas.class));
1832 
1833         WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, mListView,
1834                 () -> mListView.setScrollY(mListView.getHeight() / 2));
1835 
1836         verify(overscrollFooterDrawable, atLeastOnce()).draw(any(Canvas.class));
1837     }
1838 
1839     private static class ColorAdapter extends BaseAdapter {
1840         private int[] mColors;
1841         private Context mContext;
1842         private int mPositionOffset;
1843 
ColorAdapter(Context context, int[] colors)1844         ColorAdapter(Context context, int[] colors) {
1845             mContext = context;
1846             mColors = colors;
1847         }
1848 
1849         @Override
getCount()1850         public int getCount() {
1851             return mColors.length;
1852         }
1853 
1854         @Override
getItem(int position)1855         public Object getItem(int position) {
1856             return mColors[position];
1857         }
1858 
1859         @Override
getItemId(int position)1860         public long getItemId(int position) {
1861             return position - mPositionOffset;
1862         }
1863 
1864         @Override
hasStableIds()1865         public boolean hasStableIds() {
1866             return true;
1867         }
1868 
1869         @NonNull
1870         @Override
getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)1871         public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
1872             int color = mColors[position];
1873             if (convertView != null) {
1874                 convertView.setBackgroundColor(color);
1875                 return convertView;
1876             }
1877             View view = new View(mContext);
1878             view.setBackgroundColor(color);
1879             view.setLayoutParams(new ViewGroup.LayoutParams(90, 50));
1880             return view;
1881         }
1882 
addColor(int color)1883         public void addColor(int color) {
1884             int[] colors = new int[mColors.length + 1];
1885             System.arraycopy(mColors, 0, colors, 0, mColors.length);
1886             colors[mColors.length] = color;
1887             mColors = colors;
1888             notifyDataSetChanged();
1889         }
1890 
addColorAtStart(int color)1891         public void addColorAtStart(int color) {
1892             int[] colors = new int[mColors.length + 1];
1893             System.arraycopy(mColors, 0, colors, 1, mColors.length);
1894             colors[0] = color;
1895             mColors = colors;
1896             mPositionOffset++;
1897             notifyDataSetChanged();
1898         }
1899     }
1900 
1901     private static class ClickColorAdapter extends ColorAdapter {
ClickColorAdapter(Context context, int[] colors)1902         ClickColorAdapter(Context context, int[] colors) {
1903             super(context, colors);
1904         }
1905 
1906         @NonNull
1907         @Override
getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)1908         public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
1909             View view = super.getView(position, convertView, parent);
1910             view.setOnClickListener((v) -> { });
1911             return view;
1912         }
1913     }
1914 
1915     private static class CaptureOnReleaseEdgeEffect extends EdgeEffect {
1916         public boolean onReleaseCalled;
1917 
CaptureOnReleaseEdgeEffect(Context context)1918         CaptureOnReleaseEdgeEffect(Context context) {
1919             super(context);
1920         }
1921 
1922         @Override
onRelease()1923         public void onRelease() {
1924             onReleaseCalled = true;
1925             super.onRelease();
1926         }
1927     }
1928 }
1929