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