• 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 junit.framework.Assert;
20 
21 import org.xmlpull.v1.XmlPullParser;
22 
23 import android.app.ActionBar.LayoutParams;
24 import android.app.Activity;
25 import android.app.Instrumentation;
26 import android.content.Context;
27 import android.cts.util.PollingCheck;
28 import android.graphics.Canvas;
29 import android.graphics.Color;
30 import android.graphics.Rect;
31 import android.graphics.drawable.ColorDrawable;
32 import android.graphics.drawable.Drawable;
33 import android.test.ActivityInstrumentationTestCase2;
34 import android.test.UiThreadTest;
35 import android.test.suitebuilder.annotation.MediumTest;
36 import android.util.AttributeSet;
37 import android.util.Pair;
38 import android.util.SparseArray;
39 import android.util.SparseBooleanArray;
40 import android.util.Xml;
41 import android.view.KeyEvent;
42 import android.view.View;
43 import android.view.View.MeasureSpec;
44 import android.view.ViewGroup;
45 import android.view.animation.LayoutAnimationController;
46 import android.widget.AdapterView;
47 import android.widget.AdapterView.OnItemClickListener;
48 import android.widget.ArrayAdapter;
49 import android.widget.FrameLayout;
50 import android.widget.LinearLayout;
51 import android.widget.ListView;
52 import android.widget.TextView;
53 import android.widget.cts.R;
54 import android.widget.cts.util.ViewTestUtils;
55 
56 import java.util.ArrayList;
57 import java.util.Arrays;
58 import java.util.List;
59 
60 import static org.mockito.Mockito.any;
61 import static org.mockito.Mockito.anyInt;
62 import static org.mockito.Mockito.anyLong;
63 import static org.mockito.Mockito.atLeast;
64 import static org.mockito.Mockito.mock;
65 import static org.mockito.Mockito.never;
66 import static org.mockito.Mockito.reset;
67 import static org.mockito.Mockito.spy;
68 import static org.mockito.Mockito.times;
69 import static org.mockito.Mockito.verify;
70 import static org.mockito.Mockito.verifyNoMoreInteractions;
71 
72 public class ListViewTest extends ActivityInstrumentationTestCase2<ListViewCtsActivity> {
73     private final String[] mCountryList = new String[] {
74         "Argentina", "Australia", "China", "France", "Germany", "Italy", "Japan", "United States"
75     };
76     private final String[] mNameList = new String[] {
77         "Jacky", "David", "Kevin", "Michael", "Andy"
78     };
79     private final String[] mEmptyList = new String[0];
80 
81     private ListView mListView;
82     private Activity mActivity;
83     private Instrumentation mInstrumentation;
84     private AttributeSet mAttributeSet;
85     private ArrayAdapter<String> mAdapter_countries;
86     private ArrayAdapter<String> mAdapter_names;
87     private ArrayAdapter<String> mAdapter_empty;
88 
ListViewTest()89     public ListViewTest() {
90         super("android.widget.cts", ListViewCtsActivity.class);
91     }
92 
setUp()93     protected void setUp() throws Exception {
94         super.setUp();
95 
96         mActivity = getActivity();
97         mInstrumentation = getInstrumentation();
98         XmlPullParser parser = mActivity.getResources().getXml(R.layout.listview_layout);
99         mAttributeSet = Xml.asAttributeSet(parser);
100 
101         mAdapter_countries = new ArrayAdapter<String>(mActivity,
102                 android.R.layout.simple_list_item_1, mCountryList);
103         mAdapter_names = new ArrayAdapter<String>(mActivity, android.R.layout.simple_list_item_1,
104                 mNameList);
105         mAdapter_empty = new ArrayAdapter<String>(mActivity, android.R.layout.simple_list_item_1,
106                 mEmptyList);
107 
108         mListView = (ListView) mActivity.findViewById(R.id.listview_default);
109     }
110 
testConstructor()111     public void testConstructor() {
112         new ListView(mActivity);
113         new ListView(mActivity, mAttributeSet);
114         new ListView(mActivity, mAttributeSet, 0);
115 
116         try {
117             new ListView(null);
118             fail("There should be a NullPointerException thrown out. ");
119         } catch (NullPointerException e) {
120             // expected, test success.
121         }
122 
123         try {
124             new ListView(null, null);
125             fail("There should be a NullPointerException thrown out. ");
126         } catch (NullPointerException e) {
127             // expected, test success.
128         }
129 
130         try {
131             new ListView(null, null, -1);
132             fail("There should be a NullPointerException thrown out. ");
133         } catch (NullPointerException e) {
134             // expected, test success.
135         }
136     }
137 
testGetMaxScrollAmount()138     public void testGetMaxScrollAmount() {
139         setAdapter(mAdapter_empty);
140         int scrollAmount = mListView.getMaxScrollAmount();
141         assertEquals(0, scrollAmount);
142 
143         setAdapter(mAdapter_names);
144         scrollAmount = mListView.getMaxScrollAmount();
145         assertTrue(scrollAmount > 0);
146     }
147 
setAdapter(final ArrayAdapter<String> adapter)148     private void setAdapter(final ArrayAdapter<String> adapter) {
149         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
150                 () -> mListView.setAdapter(adapter));
151     }
152 
testAccessDividerHeight()153     public void testAccessDividerHeight() {
154         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
155                 () -> mListView.setAdapter(mAdapter_countries));
156 
157         Drawable d = mListView.getDivider();
158         final Rect r = d.getBounds();
159         new PollingCheck() {
160             @Override
161             protected boolean check() {
162                 return r.bottom - r.top > 0;
163             }
164         }.run();
165 
166         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
167                 () -> mListView.setDividerHeight(20));
168 
169         assertEquals(20, mListView.getDividerHeight());
170         assertEquals(20, r.bottom - r.top);
171 
172         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
173                 () -> mListView.setDividerHeight(10));
174 
175         assertEquals(10, mListView.getDividerHeight());
176         assertEquals(10, r.bottom - r.top);
177     }
178 
testAccessItemsCanFocus()179     public void testAccessItemsCanFocus() {
180         mListView.setItemsCanFocus(true);
181         assertTrue(mListView.getItemsCanFocus());
182 
183         mListView.setItemsCanFocus(false);
184         assertFalse(mListView.getItemsCanFocus());
185 
186         // TODO: how to check?
187     }
188 
testAccessAdapter()189     public void testAccessAdapter() {
190         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
191                 () -> mListView.setAdapter(mAdapter_countries));
192 
193         assertSame(mAdapter_countries, mListView.getAdapter());
194         assertEquals(mCountryList.length, mListView.getCount());
195 
196         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
197                 () -> mListView.setAdapter(mAdapter_names));
198 
199         assertSame(mAdapter_names, mListView.getAdapter());
200         assertEquals(mNameList.length, mListView.getCount());
201     }
202 
203     @UiThreadTest
testAccessItemChecked()204     public void testAccessItemChecked() {
205         // NONE mode
206         mListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
207         assertEquals(ListView.CHOICE_MODE_NONE, mListView.getChoiceMode());
208 
209         mListView.setItemChecked(1, true);
210         assertEquals(ListView.INVALID_POSITION, mListView.getCheckedItemPosition());
211         assertFalse(mListView.isItemChecked(1));
212 
213         // SINGLE mode
214         mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
215         assertEquals(ListView.CHOICE_MODE_SINGLE, mListView.getChoiceMode());
216 
217         mListView.setItemChecked(2, true);
218         assertEquals(2, mListView.getCheckedItemPosition());
219         assertTrue(mListView.isItemChecked(2));
220 
221         mListView.setItemChecked(3, true);
222         assertEquals(3, mListView.getCheckedItemPosition());
223         assertTrue(mListView.isItemChecked(3));
224         assertFalse(mListView.isItemChecked(2));
225 
226         // test attempt to uncheck a item that wasn't checked to begin with
227         mListView.setItemChecked(4, false);
228         // item three should still be checked
229         assertEquals(3, mListView.getCheckedItemPosition());
230         assertFalse(mListView.isItemChecked(4));
231         assertTrue(mListView.isItemChecked(3));
232         assertFalse(mListView.isItemChecked(2));
233 
234         mListView.setItemChecked(4, true);
235         assertTrue(mListView.isItemChecked(4));
236         mListView.clearChoices();
237         assertEquals(ListView.INVALID_POSITION, mListView.getCheckedItemPosition());
238         assertFalse(mListView.isItemChecked(4));
239 
240         // MULTIPLE mode
241         mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
242         assertEquals(ListView.CHOICE_MODE_MULTIPLE, mListView.getChoiceMode());
243 
244         mListView.setItemChecked(1, true);
245         assertEquals(ListView.INVALID_POSITION, mListView.getCheckedItemPosition());
246         SparseBooleanArray array = mListView.getCheckedItemPositions();
247         assertTrue(array.get(1));
248         assertFalse(array.get(2));
249         assertTrue(mListView.isItemChecked(1));
250         assertFalse(mListView.isItemChecked(2));
251 
252         mListView.setItemChecked(2, true);
253         mListView.setItemChecked(3, false);
254         mListView.setItemChecked(4, true);
255 
256         assertTrue(array.get(1));
257         assertTrue(array.get(2));
258         assertFalse(array.get(3));
259         assertTrue(array.get(4));
260         assertTrue(mListView.isItemChecked(1));
261         assertTrue(mListView.isItemChecked(2));
262         assertFalse(mListView.isItemChecked(3));
263         assertTrue(mListView.isItemChecked(4));
264 
265         mListView.clearChoices();
266         assertFalse(array.get(1));
267         assertFalse(array.get(2));
268         assertFalse(array.get(3));
269         assertFalse(array.get(4));
270         assertFalse(mListView.isItemChecked(1));
271         assertFalse(mListView.isItemChecked(2));
272         assertFalse(mListView.isItemChecked(3));
273         assertFalse(mListView.isItemChecked(4));
274     }
275 
testAccessFooterView()276     public void testAccessFooterView() {
277         final TextView footerView1 = new TextView(mActivity);
278         footerView1.setText("footerview1");
279         final TextView footerView2 = new TextView(mActivity);
280         footerView2.setText("footerview2");
281 
282         mInstrumentation.runOnMainSync(() -> mListView.setFooterDividersEnabled(true));
283         assertEquals(0, mListView.getFooterViewsCount());
284 
285         mInstrumentation.runOnMainSync(() -> mListView.addFooterView(footerView1, null, true));
286         assertEquals(1, mListView.getFooterViewsCount());
287 
288         mInstrumentation.runOnMainSync(() -> mListView.addFooterView(footerView2));
289 
290         mInstrumentation.waitForIdleSync();
291         assertEquals(2, mListView.getFooterViewsCount());
292 
293         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
294                 () -> mListView.setAdapter(mAdapter_countries));
295 
296         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
297                 () -> mListView.removeFooterView(footerView1));
298         assertEquals(1, mListView.getFooterViewsCount());
299 
300         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
301                 () -> mListView.removeFooterView(footerView2));
302         assertEquals(0, mListView.getFooterViewsCount());
303     }
304 
testAccessHeaderView()305     public void testAccessHeaderView() {
306         final TextView headerView1 = (TextView) mActivity.findViewById(R.id.headerview1);
307         final TextView headerView2 = (TextView) mActivity.findViewById(R.id.headerview2);
308 
309         mInstrumentation.runOnMainSync(() -> mListView.setHeaderDividersEnabled(true));
310         assertEquals(0, mListView.getHeaderViewsCount());
311 
312         mInstrumentation.runOnMainSync(() -> mListView.addHeaderView(headerView2, null, true));
313         assertEquals(1, mListView.getHeaderViewsCount());
314 
315         mInstrumentation.runOnMainSync(() -> mListView.addHeaderView(headerView1));
316         assertEquals(2, mListView.getHeaderViewsCount());
317     }
318 
testHeaderFooterType()319     public void testHeaderFooterType() throws Throwable {
320         final TextView headerView = new TextView(getActivity());
321         final List<Pair<View, View>> mismatch = new ArrayList<Pair<View, View>>();
322         final ArrayAdapter adapter = new ArrayAdapter<String>(mActivity,
323                 android.R.layout.simple_list_item_1, mNameList) {
324             @Override
325             public int getItemViewType(int position) {
326                 return position == 0 ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER :
327                         super.getItemViewType(position - 1);
328             }
329 
330             @Override
331             public View getView(int position, View convertView, ViewGroup parent) {
332                 if (position == 0) {
333                     if (convertView != null && convertView != headerView) {
334                         mismatch.add(new Pair<View, View>(headerView, convertView));
335                     }
336                     return headerView;
337                 } else {
338                     return super.getView(position - 1, convertView, parent);
339                 }
340             }
341 
342             @Override
343             public int getCount() {
344                 return super.getCount() + 1;
345             }
346         };
347         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
348                 () -> mListView.setAdapter(adapter));
349 
350         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
351                 () -> adapter.notifyDataSetChanged());
352 
353         assertEquals(0, mismatch.size());
354     }
355 
testAccessDivider()356     public void testAccessDivider() {
357         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
358                 () -> mListView.setAdapter(mAdapter_countries));
359 
360         Drawable defaultDrawable = mListView.getDivider();
361         final Rect r = defaultDrawable.getBounds();
362         new PollingCheck() {
363             @Override
364             protected boolean check() {
365                 return r.bottom - r.top > 0;
366             }
367         }.run();
368 
369         final Drawable d = mActivity.getResources().getDrawable(R.drawable.scenery);
370 
371         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
372                 () -> mListView.setDivider(d));
373         assertSame(d, mListView.getDivider());
374         assertEquals(d.getBounds().height(), mListView.getDividerHeight());
375 
376         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
377                 () -> mListView.setDividerHeight(10));
378         assertEquals(10, mListView.getDividerHeight());
379         assertEquals(10, d.getBounds().height());
380     }
381 
testSetSelection()382     public void testSetSelection() {
383         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
384                 () -> mListView.setAdapter(mAdapter_countries));
385 
386         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
387                 () -> mListView.setSelection(1));
388         String item = (String) mListView.getSelectedItem();
389         assertEquals(mCountryList[1], item);
390 
391         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
392                 () -> mListView.setSelectionFromTop(5, 0));
393         item = (String) mListView.getSelectedItem();
394         assertEquals(mCountryList[5], item);
395 
396         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
397                 () -> mListView.setSelectionAfterHeaderView());
398         item = (String) mListView.getSelectedItem();
399         assertEquals(mCountryList[0], item);
400     }
401 
testOnKeyUpDown()402     public void testOnKeyUpDown() {
403         // implementation details, do NOT test
404     }
405 
testPerformItemClick()406     public void testPerformItemClick() {
407         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
408                 () -> mListView.setAdapter(mAdapter_countries));
409 
410         mListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
411         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
412                 () -> mListView.setSelection(2));
413 
414         final TextView child = (TextView) mAdapter_countries.getView(2, null, mListView);
415         assertNotNull(child);
416         assertEquals(mCountryList[2], child.getText().toString());
417         final long itemID = mAdapter_countries.getItemId(2);
418         assertEquals(2, itemID);
419 
420         mInstrumentation.runOnMainSync(() -> mListView.performItemClick(child, 2, itemID));
421         mInstrumentation.waitForIdleSync();
422 
423         OnItemClickListener onClickListener = mock(OnItemClickListener.class);
424         mListView.setOnItemClickListener(onClickListener);
425         verify(onClickListener, never()).onItemClick(any(AdapterView.class), any(View.class),
426                 anyInt(), anyLong());
427 
428         mInstrumentation.runOnMainSync(() -> mListView.performItemClick(child, 2, itemID));
429         mInstrumentation.waitForIdleSync();
430 
431         verify(onClickListener, times(1)).onItemClick(mListView, child, 2, 2L);
432         verifyNoMoreInteractions(onClickListener);
433     }
434 
testSaveAndRestoreInstanceState()435     public void testSaveAndRestoreInstanceState() {
436         // implementation details, do NOT test
437     }
438 
testDispatchKeyEvent()439     public void testDispatchKeyEvent() {
440         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
441                 () -> {
442                     mListView.setAdapter(mAdapter_countries);
443                     mListView.requestFocus();
444                 });
445         assertTrue(mListView.hasFocus());
446 
447         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
448                 () -> mListView.setSelection(1));
449         String item = (String) mListView.getSelectedItem();
450         assertEquals(mCountryList[1], item);
451 
452         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
453                 () ->  {
454                     KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A);
455                     mListView.dispatchKeyEvent(keyEvent);
456                 });
457 
458         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
459                 () -> {
460                     KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN,
461                             KeyEvent.KEYCODE_DPAD_DOWN);
462                     mListView.dispatchKeyEvent(keyEvent);
463                     mListView.dispatchKeyEvent(keyEvent);
464                     mListView.dispatchKeyEvent(keyEvent);
465                 });
466         item = (String)mListView.getSelectedItem();
467         assertEquals(mCountryList[4], item);
468     }
469 
testRequestChildRectangleOnScreen()470     public void testRequestChildRectangleOnScreen() {
471         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
472                 () -> mListView.setAdapter(mAdapter_countries));
473 
474         TextView child = (TextView) mAdapter_countries.getView(0, null, mListView);
475         assertNotNull(child);
476         assertEquals(mCountryList[0], child.getText().toString());
477 
478         Rect rect = new Rect(0, 0, 10, 10);
479         assertFalse(mListView.requestChildRectangleOnScreen(child, rect, false));
480 
481         // TODO: how to check?
482     }
483 
testOnTouchEvent()484     public void testOnTouchEvent() {
485         // implementation details, do NOT test
486     }
487 
488     @UiThreadTest
testCanAnimate()489     public void testCanAnimate() {
490         MyListView listView = new MyListView(mActivity, mAttributeSet);
491 
492         assertFalse(listView.canAnimate());
493         listView.setAdapter(mAdapter_countries);
494         assertFalse(listView.canAnimate());
495 
496         LayoutAnimationController controller = new LayoutAnimationController(
497                 mActivity, mAttributeSet);
498         listView.setLayoutAnimation(controller);
499 
500         assertTrue(listView.canAnimate());
501     }
502 
503     @UiThreadTest
testDispatchDraw()504     public void testDispatchDraw() {
505         // implementation details, do NOT test
506     }
507 
508     @UiThreadTest
testFindViewTraversal()509     public void testFindViewTraversal() {
510         MyListView listView = new MyListView(mActivity, mAttributeSet);
511         TextView headerView = (TextView) mActivity.findViewById(R.id.headerview1);
512 
513         assertNull(listView.findViewTraversal(R.id.headerview1));
514 
515         listView.addHeaderView(headerView);
516         assertNotNull(listView.findViewTraversal(R.id.headerview1));
517         assertSame(headerView, listView.findViewTraversal(R.id.headerview1));
518     }
519 
520     @UiThreadTest
testFindViewWithTagTraversal()521     public void testFindViewWithTagTraversal() {
522         MyListView listView = new MyListView(mActivity, mAttributeSet);
523         TextView headerView = (TextView) mActivity.findViewById(R.id.headerview1);
524 
525         assertNull(listView.findViewWithTagTraversal("header"));
526 
527         headerView.setTag("header");
528         listView.addHeaderView(headerView);
529         assertNotNull(listView.findViewWithTagTraversal("header"));
530         assertSame(headerView, listView.findViewWithTagTraversal("header"));
531     }
532 
testLayoutChildren()533     public void testLayoutChildren() {
534         // TODO: how to test?
535     }
536 
testOnFinishInflate()537     public void testOnFinishInflate() {
538         // implementation details, do NOT test
539     }
540 
testOnFocusChanged()541     public void testOnFocusChanged() {
542         // implementation details, do NOT test
543     }
544 
testOnMeasure()545     public void testOnMeasure() {
546         // implementation details, do NOT test
547     }
548 
549     /**
550      * MyListView for test
551      */
552     private static class MyListView extends ListView {
MyListView(Context context, AttributeSet attrs)553         public MyListView(Context context, AttributeSet attrs) {
554             super(context, attrs);
555         }
556 
557         @Override
canAnimate()558         protected boolean canAnimate() {
559             return super.canAnimate();
560         }
561 
562         @Override
dispatchDraw(Canvas canvas)563         protected void dispatchDraw(Canvas canvas) {
564             super.dispatchDraw(canvas);
565         }
566 
567         @Override
findViewTraversal(int id)568         protected View findViewTraversal(int id) {
569             return super.findViewTraversal(id);
570         }
571 
572         @Override
findViewWithTagTraversal(Object tag)573         protected View findViewWithTagTraversal(Object tag) {
574             return super.findViewWithTagTraversal(tag);
575         }
576 
577         @Override
layoutChildren()578         protected void layoutChildren() {
579             super.layoutChildren();
580         }
581     }
582 
583     /**
584      * The following functions are merged from frameworktest.
585      */
586     @MediumTest
testRequestLayoutCallsMeasure()587     public void testRequestLayoutCallsMeasure() throws Exception {
588         ListView listView = new ListView(mActivity);
589         List<String> items = new ArrayList<>();
590         items.add("hello");
591         Adapter<String> adapter = new Adapter<String>(mActivity, 0, items);
592         listView.setAdapter(adapter);
593 
594         int measureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
595 
596         adapter.notifyDataSetChanged();
597         listView.measure(measureSpec, measureSpec);
598         listView.layout(0, 0, 100, 100);
599 
600         MockView childView = (MockView) listView.getChildAt(0);
601 
602         childView.requestLayout();
603         childView.onMeasureCalled = false;
604         listView.measure(measureSpec, measureSpec);
605         listView.layout(0, 0, 100, 100);
606         Assert.assertTrue(childView.onMeasureCalled);
607     }
608 
609     @MediumTest
testNoSelectableItems()610     public void testNoSelectableItems() throws Exception {
611         ListView listView = new ListView(mActivity);
612         // We use a header as the unselectable item to remain after the selectable one is removed.
613         listView.addHeaderView(new View(mActivity), null, false);
614         List<String> items = new ArrayList<>();
615         items.add("hello");
616         Adapter<String> adapter = new Adapter<String>(mActivity, 0, items);
617         listView.setAdapter(adapter);
618 
619         listView.setSelection(1);
620 
621         int measureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);
622 
623         adapter.notifyDataSetChanged();
624         listView.measure(measureSpec, measureSpec);
625         listView.layout(0, 0, 100, 100);
626 
627         items.remove(0);
628 
629         adapter.notifyDataSetChanged();
630         listView.measure(measureSpec, measureSpec);
631         listView.layout(0, 0, 100, 100);
632     }
633 
634     @MediumTest
testFullDetachHeaderViewOnScroll()635     public void testFullDetachHeaderViewOnScroll() {
636         final AttachDetachAwareView header = new AttachDetachAwareView(mActivity);
637         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
638             mListView.setAdapter(new DummyAdapter(1000));
639             mListView.addHeaderView(header);
640         });
641         assertEquals("test sanity", 1, header.mOnAttachCount);
642         assertEquals("test sanity", 0, header.mOnDetachCount);
643         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
644             mListView.scrollListBy(mListView.getHeight() * 3);
645         });
646         assertNull("test sanity, header should be removed", header.getParent());
647         assertEquals("header view should be detached", 1, header.mOnDetachCount);
648         assertFalse(header.isTemporarilyDetached());
649     }
650 
651     @MediumTest
testFullDetachHeaderViewOnRelayout()652     public void testFullDetachHeaderViewOnRelayout() {
653         final AttachDetachAwareView header = new AttachDetachAwareView(mActivity);
654         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
655             mListView.setAdapter(new DummyAdapter(1000));
656             mListView.addHeaderView(header);
657         });
658         assertEquals("test sanity", 1, header.mOnAttachCount);
659         assertEquals("test sanity", 0, header.mOnDetachCount);
660         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
661             mListView.setSelection(800);
662         });
663         assertNull("test sanity, header should be removed", header.getParent());
664         assertEquals("header view should be detached", 1, header.mOnDetachCount);
665         assertFalse(header.isTemporarilyDetached());
666     }
667 
668     @MediumTest
testFullDetachHeaderViewOnScrollForFocus()669     public void testFullDetachHeaderViewOnScrollForFocus() {
670         final AttachDetachAwareView header = new AttachDetachAwareView(mActivity);
671         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
672             mListView.setAdapter(new DummyAdapter(1000));
673             mListView.addHeaderView(header);
674         });
675         assertEquals("test sanity", 1, header.mOnAttachCount);
676         assertEquals("test sanity", 0, header.mOnDetachCount);
677         while(header.getParent() != null) {
678             assertEquals("header view should NOT be detached", 0, header.mOnDetachCount);
679             sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
680             ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, null);
681         }
682         assertEquals("header view should be detached", 1, header.mOnDetachCount);
683         assertFalse(header.isTemporarilyDetached());
684     }
685 
686     @MediumTest
testFullyDetachUnusedViewOnScroll()687     public void testFullyDetachUnusedViewOnScroll() {
688         final AttachDetachAwareView theView = new AttachDetachAwareView(mActivity);
689         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
690             mListView.setAdapter(new DummyAdapter(1000, theView));
691         });
692         assertEquals("test sanity", 1, theView.mOnAttachCount);
693         assertEquals("test sanity", 0, theView.mOnDetachCount);
694         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
695             mListView.scrollListBy(mListView.getHeight() * 2);
696         });
697         assertNull("test sanity, unused view should be removed", theView.getParent());
698         assertEquals("unused view should be detached", 1, theView.mOnDetachCount);
699         assertFalse(theView.isTemporarilyDetached());
700         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
701             mListView.scrollListBy(-mListView.getHeight() * 2);
702             // listview limits scroll to 1 page which is why we call it twice here.
703             mListView.scrollListBy(-mListView.getHeight() * 2);
704         });
705         assertNotNull("test sanity, view should be re-added", theView.getParent());
706         assertEquals("view should receive another attach call", 2, theView.mOnAttachCount);
707         assertEquals("view should not receive a detach call", 1, theView.mOnDetachCount);
708         assertFalse(theView.isTemporarilyDetached());
709     }
710 
711     @MediumTest
testFullyDetachUnusedViewOnReLayout()712     public void testFullyDetachUnusedViewOnReLayout() {
713         final AttachDetachAwareView theView = new AttachDetachAwareView(mActivity);
714         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
715             mListView.setAdapter(new DummyAdapter(1000, theView));
716         });
717         assertEquals("test sanity", 1, theView.mOnAttachCount);
718         assertEquals("test sanity", 0, theView.mOnDetachCount);
719         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
720             mListView.setSelection(800);
721         });
722         assertNull("test sanity, unused view should be removed", theView.getParent());
723         assertEquals("unused view should be detached", 1, theView.mOnDetachCount);
724         assertFalse(theView.isTemporarilyDetached());
725         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
726             mListView.setSelection(0);
727         });
728         assertNotNull("test sanity, view should be re-added", theView.getParent());
729         assertEquals("view should receive another attach call", 2, theView.mOnAttachCount);
730         assertEquals("view should not receive a detach call", 1, theView.mOnDetachCount);
731         assertFalse(theView.isTemporarilyDetached());
732     }
733 
734     @MediumTest
testFullyDetachUnusedViewOnScrollForFocus()735     public void testFullyDetachUnusedViewOnScrollForFocus() {
736         final AttachDetachAwareView theView = new AttachDetachAwareView(mActivity);
737         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
738             mListView.setAdapter(new DummyAdapter(1000, theView));
739         });
740         assertEquals("test sanity", 1, theView.mOnAttachCount);
741         assertEquals("test sanity", 0, theView.mOnDetachCount);
742         while(theView.getParent() != null) {
743             assertEquals("the view should NOT be detached", 0, theView.mOnDetachCount);
744             sendKeys(KeyEvent.KEYCODE_DPAD_DOWN);
745             ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, null);
746         }
747         assertEquals("the view should be detached", 1, theView.mOnDetachCount);
748         assertFalse(theView.isTemporarilyDetached());
749         while(theView.getParent() == null) {
750             sendKeys(KeyEvent.KEYCODE_DPAD_UP);
751             ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, null);
752         }
753         assertEquals("the view should be re-attached", 2, theView.mOnAttachCount);
754         assertEquals("the view should not recieve another detach", 1, theView.mOnDetachCount);
755         assertFalse(theView.isTemporarilyDetached());
756     }
757 
758     @MediumTest
testSetPadding()759     public void testSetPadding() {
760         View view = new View(mActivity);
761         view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
762                 ViewGroup.LayoutParams.WRAP_CONTENT));
763         view.setMinimumHeight(30);
764         final DummyAdapter adapter = new DummyAdapter(2, view);
765         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
766             mListView.setLayoutParams(new LinearLayout.LayoutParams(200, 100));
767             mListView.setAdapter(adapter);
768         });
769         assertEquals("test sanity", 200, mListView.getWidth());
770         assertEquals(200, view.getWidth());
771         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
772             mListView.setPadding(10, 0, 5, 0);
773             assertTrue(view.isLayoutRequested());
774         });
775         assertEquals(185, view.getWidth());
776         assertFalse(view.isLayoutRequested());
777         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
778             mListView.setPadding(10, 0, 5, 0);
779             assertFalse(view.isLayoutRequested());
780         });
781     }
782 
783     @MediumTest
testResolveRtlOnReAttach()784     public void testResolveRtlOnReAttach() {
785         View spacer = new View(getActivity());
786         spacer.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
787                 250));
788         final DummyAdapter adapter = new DummyAdapter(50, spacer);
789         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
790             mListView.setLayoutDirection(View.LAYOUT_DIRECTION_RTL);
791             mListView.setLayoutParams(new LinearLayout.LayoutParams(200, 150));
792             mListView.setAdapter(adapter);
793         });
794         assertEquals("test sanity", 1, mListView.getChildCount());
795         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
796             // we scroll in pieces because list view caps scroll by its height
797             mListView.scrollListBy(100);
798             mListView.scrollListBy(100);
799             mListView.scrollListBy(60);
800         });
801         assertEquals("test sanity", 1, mListView.getChildCount());
802         assertEquals("test sanity", 1, mListView.getFirstVisiblePosition());
803         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
804             mListView.scrollListBy(-100);
805             mListView.scrollListBy(-100);
806             mListView.scrollListBy(-60);
807         });
808         assertEquals("test sanity", 1, mListView.getChildCount());
809         assertEquals("item 0 should be visible", 0, mListView.getFirstVisiblePosition());
810         ViewTestUtils.runOnMainAndDrawSync(getInstrumentation(), mListView, () -> {
811             mListView.scrollListBy(100);
812             mListView.scrollListBy(100);
813             mListView.scrollListBy(60);
814         });
815         assertEquals("test sanity", 1, mListView.getChildCount());
816         assertEquals("test sanity", 1, mListView.getFirstVisiblePosition());
817 
818         assertEquals("the view's RTL properties must be resolved",
819                 mListView.getChildAt(0).getLayoutDirection(), View.LAYOUT_DIRECTION_RTL);
820     }
821 
822     private class MockView extends View {
823 
824         public boolean onMeasureCalled = false;
825 
MockView(Context context)826         public MockView(Context context) {
827             super(context);
828         }
829 
830         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)831         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
832             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
833             onMeasureCalled = true;
834         }
835     }
836 
837     private class Adapter<T> extends ArrayAdapter<T> {
838 
Adapter(Context context, int resource, List<T> objects)839         public Adapter(Context context, int resource, List<T> objects) {
840             super(context, resource, objects);
841         }
842 
843         @Override
getView(int position, View convertView, ViewGroup parent)844         public View getView(int position, View convertView, ViewGroup parent) {
845             return new MockView(getContext());
846         }
847     }
848 
849     @MediumTest
testRequestLayoutWithTemporaryDetach()850     public void testRequestLayoutWithTemporaryDetach() throws Exception {
851         ListView listView = new ListView(mActivity);
852         List<String> items = new ArrayList<>();
853         items.add("0");
854         items.add("1");
855         items.add("2");
856         final TemporarilyDetachableMockViewAdapter<String> adapter =
857                 new TemporarilyDetachableMockViewAdapter<>(
858                         mActivity, android.R.layout.simple_list_item_1, items);
859         mInstrumentation.runOnMainSync(() -> {
860             listView.setAdapter(adapter);
861             mActivity.setContentView(listView);
862         });
863         mInstrumentation.waitForIdleSync();
864 
865         assertEquals(items.size(), listView.getCount());
866         final TemporarilyDetachableMockView childView0 =
867                 (TemporarilyDetachableMockView) listView.getChildAt(0);
868         final TemporarilyDetachableMockView childView1 =
869                 (TemporarilyDetachableMockView) listView.getChildAt(1);
870         final TemporarilyDetachableMockView childView2 =
871                 (TemporarilyDetachableMockView) listView.getChildAt(2);
872         assertNotNull(childView0);
873         assertNotNull(childView1);
874         assertNotNull(childView2);
875 
876         // Make sure that the childView1 has focus.
877         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, childView1, childView1::requestFocus);
878         assertTrue(childView1.isFocused());
879 
880         // Make sure that ListView#requestLayout() is optimized when nothing is changed.
881         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, listView, listView::requestLayout);
882         assertEquals(childView0, listView.getChildAt(0));
883         assertEquals(childView1, listView.getChildAt(1));
884         assertEquals(childView2, listView.getChildAt(2));
885     }
886 
887     private static final int EXACTLY_500_PX = MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY);
888 
889     @MediumTest
testJumpDrawables()890     public void testJumpDrawables() {
891         FrameLayout layout = new FrameLayout(mActivity);
892         ListView listView = new ListView(mActivity);
893         ArrayAdapterWithMockDrawable adapter = new ArrayAdapterWithMockDrawable(mActivity);
894         for (int i = 0; i < 50; i++) {
895             adapter.add(Integer.toString(i));
896         }
897 
898         // Initial state should jump exactly once during attach.
899         mInstrumentation.runOnMainSync(() -> {
900             listView.setAdapter(adapter);
901             layout.addView(listView, new LayoutParams(LayoutParams.MATCH_PARENT, 200));
902             mActivity.setContentView(layout);
903         });
904         mInstrumentation.waitForIdleSync();
905         assertTrue("List is not showing any children", listView.getChildCount() > 0);
906         Drawable firstBackground = listView.getChildAt(0).getBackground();
907         verify(firstBackground, times(1)).jumpToCurrentState();
908 
909         // Lay out views without recycling. This should not jump again.
910         mInstrumentation.runOnMainSync(() -> listView.requestLayout());
911         mInstrumentation.waitForIdleSync();
912         assertSame(firstBackground, listView.getChildAt(0).getBackground());
913         verify(firstBackground, times(1)).jumpToCurrentState();
914 
915         // If we're on a really big display, we might be in a position where
916         // the position we're going to scroll to is already visible, in which
917         // case we won't be able to test jump behavior when recycling.
918         int lastVisiblePosition = listView.getLastVisiblePosition();
919         int targetPosition = adapter.getCount() - 1;
920         if (targetPosition <= lastVisiblePosition) {
921             return;
922         }
923 
924         // Reset the call counts before continuing, since the backgrounds may
925         // be recycled from either views that were on-screen or in the scrap
926         // heap, and those would have slightly different call counts.
927         adapter.resetMockBackgrounds();
928 
929         // Scroll so that we have new views on screen. This should jump at
930         // least once when the view is recycled in a new position (but may be
931         // more if it was recycled from a view that was previously on-screen).
932         mInstrumentation.runOnMainSync(() -> listView.setSelection(targetPosition));
933         mInstrumentation.waitForIdleSync();
934 
935         View lastChild = listView.getChildAt(listView.getChildCount() - 1);
936         verify(lastChild.getBackground(), atLeast(1)).jumpToCurrentState();
937 
938         // Reset the call counts before continuing.
939         adapter.resetMockBackgrounds();
940 
941         // Scroll back to the top. This should jump at least once when the view
942         // is recycled in a new position (but may be more if it was recycled
943         // from a view that was previously on-screen).
944         mInstrumentation.runOnMainSync(() -> listView.setSelection(0));
945         mInstrumentation.waitForIdleSync();
946 
947         View firstChild = listView.getChildAt(0);
948         verify(firstChild.getBackground(), atLeast(1)).jumpToCurrentState();
949     }
950 
951     private static class ArrayAdapterWithMockDrawable extends ArrayAdapter<String> {
952         private SparseArray<Drawable> mBackgrounds = new SparseArray<>();
953 
ArrayAdapterWithMockDrawable(Context context)954         public ArrayAdapterWithMockDrawable(Context context) {
955             super(context, android.R.layout.simple_list_item_1);
956         }
957 
958         @Override
getView(int position, View convertView, ViewGroup parent)959         public View getView(int position, View convertView, ViewGroup parent) {
960             final View view = super.getView(position, convertView, parent);
961             if (view.getBackground() == null) {
962                 view.setBackground(spy(new ColorDrawable(Color.BLACK)));
963             }
964             return view;
965         }
966 
resetMockBackgrounds()967         public void resetMockBackgrounds() {
968             for (int i = 0; i < mBackgrounds.size(); i++) {
969                 Drawable background = mBackgrounds.valueAt(i);
970                 reset(background);
971             }
972         }
973     }
974 
975     private class TemporarilyDetachableMockView extends View {
976 
977         private boolean mIsDispatchingStartTemporaryDetach = false;
978         private boolean mIsDispatchingFinishTemporaryDetach = false;
979 
TemporarilyDetachableMockView(Context context)980         public TemporarilyDetachableMockView(Context context) {
981             super(context);
982         }
983 
984         @Override
dispatchStartTemporaryDetach()985         public void dispatchStartTemporaryDetach() {
986             mIsDispatchingStartTemporaryDetach = true;
987             super.dispatchStartTemporaryDetach();
988             mIsDispatchingStartTemporaryDetach = false;
989         }
990 
991         @Override
dispatchFinishTemporaryDetach()992         public void dispatchFinishTemporaryDetach() {
993             mIsDispatchingFinishTemporaryDetach = true;
994             super.dispatchFinishTemporaryDetach();
995             mIsDispatchingFinishTemporaryDetach = false;
996         }
997 
998         @Override
onStartTemporaryDetach()999         public void onStartTemporaryDetach() {
1000             super.onStartTemporaryDetach();
1001             if (!mIsDispatchingStartTemporaryDetach) {
1002                 throw new IllegalStateException("#onStartTemporaryDetach() must be indirectly"
1003                         + " called via #dispatchStartTemporaryDetach()");
1004             }
1005         }
1006 
1007         @Override
onFinishTemporaryDetach()1008         public void onFinishTemporaryDetach() {
1009             super.onFinishTemporaryDetach();
1010             if (!mIsDispatchingFinishTemporaryDetach) {
1011                 throw new IllegalStateException("#onStartTemporaryDetach() must be indirectly"
1012                         + " called via #dispatchFinishTemporaryDetach()");
1013             }
1014         }
1015     }
1016 
1017     private class TemporarilyDetachableMockViewAdapter<T> extends ArrayAdapter<T> {
1018         ArrayList<TemporarilyDetachableMockView> views = new ArrayList<>();
1019 
TemporarilyDetachableMockViewAdapter(Context context, int textViewResourceId, List<T> objects)1020         public TemporarilyDetachableMockViewAdapter(Context context, int textViewResourceId,
1021                 List<T> objects) {
1022             super(context, textViewResourceId, objects);
1023             for (int i = 0; i < objects.size(); i++) {
1024                 views.add(new TemporarilyDetachableMockView(context));
1025                 views.get(i).setFocusable(true);
1026             }
1027         }
1028 
1029         @Override
getCount()1030         public int getCount() {
1031             return views.size();
1032         }
1033 
1034         @Override
getItemId(int position)1035         public long getItemId(int position) {
1036             return position;
1037         }
1038 
1039         @Override
getView(int position, View convertView, ViewGroup parent)1040         public View getView(int position, View convertView, ViewGroup parent) {
1041             return views.get(position);
1042         }
1043     }
1044 
testTransientStateUnstableIds()1045     public void testTransientStateUnstableIds() throws Exception {
1046         final ListView listView = mListView;
1047         final ArrayList<String> items = new ArrayList<String>(Arrays.asList(mCountryList));
1048         final ArrayAdapter<String> adapter = new ArrayAdapter<String>(mActivity,
1049                 android.R.layout.simple_list_item_1, items);
1050 
1051         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, listView,
1052                 () -> listView.setAdapter(adapter));
1053 
1054         final View oldItem = listView.getChildAt(2);
1055         final CharSequence oldText = ((TextView) oldItem.findViewById(android.R.id.text1))
1056                 .getText();
1057         oldItem.setHasTransientState(true);
1058 
1059         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, listView,
1060                 () -> {
1061                     adapter.remove(adapter.getItem(0));
1062                     adapter.notifyDataSetChanged();
1063                 });
1064 
1065         final View newItem = listView.getChildAt(2);
1066         final CharSequence newText = ((TextView) newItem.findViewById(android.R.id.text1))
1067                 .getText();
1068 
1069         Assert.assertFalse(oldText.equals(newText));
1070     }
1071 
testTransientStateStableIds()1072     public void testTransientStateStableIds() throws Exception {
1073         final ListView listView = mListView;
1074         final ArrayList<String> items = new ArrayList<String>(Arrays.asList(mCountryList));
1075         final StableArrayAdapter<String> adapter = new StableArrayAdapter<String>(mActivity,
1076                 android.R.layout.simple_list_item_1, items);
1077 
1078         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, mListView,
1079                 () -> listView.setAdapter(adapter));
1080 
1081         final Object tag = new Object();
1082         final View oldItem = listView.getChildAt(2);
1083         final CharSequence oldText = ((TextView) oldItem.findViewById(android.R.id.text1))
1084                 .getText();
1085         oldItem.setHasTransientState(true);
1086         oldItem.setTag(tag);
1087 
1088         ViewTestUtils.runOnMainAndDrawSync(mInstrumentation, listView,
1089                 () -> {
1090                     adapter.remove(adapter.getItem(0));
1091                     adapter.notifyDataSetChanged();
1092                 });
1093 
1094         final View newItem = listView.getChildAt(1);
1095         final CharSequence newText = ((TextView) newItem.findViewById(android.R.id.text1))
1096                 .getText();
1097 
1098         Assert.assertTrue(newItem.hasTransientState());
1099         Assert.assertEquals(oldText, newText);
1100         Assert.assertEquals(tag, newItem.getTag());
1101     }
1102 
1103     private static class StableArrayAdapter<T> extends ArrayAdapter<T> {
StableArrayAdapter(Context context, int resource, List<T> objects)1104         public StableArrayAdapter(Context context, int resource, List<T> objects) {
1105             super(context, resource, objects);
1106         }
1107 
1108         @Override
getItemId(int position)1109         public long getItemId(int position) {
1110             return getItem(position).hashCode();
1111         }
1112 
1113         @Override
hasStableIds()1114         public boolean hasStableIds() {
1115             return true;
1116         }
1117     }
1118 }
1119