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