1 /* 2 * Copyright (C) 2015 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 org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertSame; 24 import static org.junit.Assert.assertTrue; 25 import static org.junit.Assert.fail; 26 import static org.mockito.Mockito.any; 27 import static org.mockito.Mockito.doCallRealMethod; 28 import static org.mockito.Mockito.eq; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.never; 31 import static org.mockito.Mockito.spy; 32 import static org.mockito.Mockito.times; 33 import static org.mockito.Mockito.verify; 34 import static org.mockito.Mockito.verifyNoMoreInteractions; 35 36 import android.app.Activity; 37 import android.app.Instrumentation; 38 import android.content.Context; 39 import android.graphics.Rect; 40 import android.graphics.drawable.ColorDrawable; 41 import android.graphics.drawable.Drawable; 42 import android.platform.test.annotations.Presubmit; 43 import android.support.test.InstrumentationRegistry; 44 import android.support.test.filters.LargeTest; 45 import android.support.test.rule.ActivityTestRule; 46 import android.support.test.runner.AndroidJUnit4; 47 import android.view.Display; 48 import android.view.Gravity; 49 import android.view.KeyEvent; 50 import android.view.LayoutInflater; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.view.WindowManager; 54 import android.widget.AdapterView; 55 import android.widget.BaseAdapter; 56 import android.widget.ListAdapter; 57 import android.widget.ListPopupWindow; 58 import android.widget.ListView; 59 import android.widget.PopupWindow; 60 import android.widget.TextView; 61 62 import com.android.compatibility.common.util.CtsKeyEventUtil; 63 import com.android.compatibility.common.util.CtsTouchUtils; 64 import com.android.compatibility.common.util.WidgetTestUtils; 65 66 import org.junit.After; 67 import org.junit.Before; 68 import org.junit.Rule; 69 import org.junit.Test; 70 import org.junit.runner.RunWith; 71 72 @LargeTest 73 @RunWith(AndroidJUnit4.class) 74 public class ListPopupWindowTest { 75 private Instrumentation mInstrumentation; 76 private Activity mActivity; 77 private Builder mPopupWindowBuilder; 78 private View promptView; 79 80 /** The list popup window. */ 81 private ListPopupWindow mPopupWindow; 82 83 private AdapterView.OnItemClickListener mItemClickListener; 84 85 /** 86 * Item click listener that dismisses our <code>ListPopupWindow</code> when any item 87 * is clicked. Note that this needs to be a separate class that is also protected (not 88 * private) so that Mockito can "spy" on it. 89 */ 90 protected class PopupItemClickListener implements AdapterView.OnItemClickListener { 91 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)92 public void onItemClick(AdapterView<?> parent, View view, int position, 93 long id) { 94 mPopupWindow.dismiss(); 95 } 96 } 97 98 @Rule 99 public ActivityTestRule<ListPopupWindowCtsActivity> mActivityRule 100 = new ActivityTestRule<>(ListPopupWindowCtsActivity.class); 101 102 @Before setup()103 public void setup() { 104 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 105 mActivity = mActivityRule.getActivity(); 106 mItemClickListener = new PopupItemClickListener(); 107 } 108 109 @After teardown()110 public void teardown() throws Throwable { 111 if ((mPopupWindowBuilder != null) && (mPopupWindow != null)) { 112 mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss); 113 mInstrumentation.waitForIdleSync(); 114 } 115 } 116 117 @Test testConstructor()118 public void testConstructor() { 119 new ListPopupWindow(mActivity); 120 121 new ListPopupWindow(mActivity, null); 122 123 new ListPopupWindow(mActivity, null, android.R.attr.popupWindowStyle); 124 125 new ListPopupWindow(mActivity, null, 0, 126 android.R.style.Widget_DeviceDefault_ListPopupWindow); 127 128 new ListPopupWindow(mActivity, null, 0, 129 android.R.style.Widget_DeviceDefault_Light_ListPopupWindow); 130 131 new ListPopupWindow(mActivity, null, 0, android.R.style.Widget_Material_ListPopupWindow); 132 133 new ListPopupWindow(mActivity, null, 0, 134 android.R.style.Widget_Material_Light_ListPopupWindow); 135 } 136 137 @Test testNoDefaultVisibility()138 public void testNoDefaultVisibility() { 139 mPopupWindow = new ListPopupWindow(mActivity); 140 assertFalse(mPopupWindow.isShowing()); 141 } 142 143 @Test testAccessBackground()144 public void testAccessBackground() throws Throwable { 145 mPopupWindowBuilder = new Builder(); 146 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 147 mInstrumentation.waitForIdleSync(); 148 149 Drawable drawable = new ColorDrawable(); 150 mPopupWindow.setBackgroundDrawable(drawable); 151 assertSame(drawable, mPopupWindow.getBackground()); 152 153 mPopupWindow.setBackgroundDrawable(null); 154 assertNull(mPopupWindow.getBackground()); 155 } 156 157 @Test testAccessAnimationStyle()158 public void testAccessAnimationStyle() throws Throwable { 159 mPopupWindowBuilder = new Builder(); 160 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 161 mInstrumentation.waitForIdleSync(); 162 assertEquals(0, mPopupWindow.getAnimationStyle()); 163 164 mPopupWindow.setAnimationStyle(android.R.style.Animation_Toast); 165 assertEquals(android.R.style.Animation_Toast, mPopupWindow.getAnimationStyle()); 166 167 // abnormal values 168 mPopupWindow.setAnimationStyle(-100); 169 assertEquals(-100, mPopupWindow.getAnimationStyle()); 170 } 171 172 @Test testAccessHeight()173 public void testAccessHeight() throws Throwable { 174 mPopupWindowBuilder = new Builder(); 175 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 176 mInstrumentation.waitForIdleSync(); 177 178 assertEquals(WindowManager.LayoutParams.WRAP_CONTENT, mPopupWindow.getHeight()); 179 180 int height = getDisplay().getHeight() / 2; 181 mPopupWindow.setHeight(height); 182 assertEquals(height, mPopupWindow.getHeight()); 183 184 height = getDisplay().getHeight(); 185 mPopupWindow.setHeight(height); 186 assertEquals(height, mPopupWindow.getHeight()); 187 188 mPopupWindow.setHeight(0); 189 assertEquals(0, mPopupWindow.getHeight()); 190 191 height = getDisplay().getHeight() * 2; 192 mPopupWindow.setHeight(height); 193 assertEquals(height, mPopupWindow.getHeight()); 194 195 height = -getDisplay().getHeight() / 2; 196 try { 197 mPopupWindow.setHeight(height); 198 fail("should throw IllegalArgumentException for negative height."); 199 } catch (IllegalArgumentException e) { 200 // expected exception. 201 } 202 } 203 204 /** 205 * Gets the display. 206 * 207 * @return the display 208 */ getDisplay()209 private Display getDisplay() { 210 WindowManager wm = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE); 211 return wm.getDefaultDisplay(); 212 } 213 214 @Test testAccessWidth()215 public void testAccessWidth() throws Throwable { 216 mPopupWindowBuilder = new Builder().ignoreContentWidth(); 217 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 218 mInstrumentation.waitForIdleSync(); 219 220 assertEquals(WindowManager.LayoutParams.WRAP_CONTENT, mPopupWindow.getWidth()); 221 222 int width = getDisplay().getWidth() / 2; 223 mPopupWindow.setWidth(width); 224 assertEquals(width, mPopupWindow.getWidth()); 225 226 width = getDisplay().getWidth(); 227 mPopupWindow.setWidth(width); 228 assertEquals(width, mPopupWindow.getWidth()); 229 230 mPopupWindow.setWidth(0); 231 assertEquals(0, mPopupWindow.getWidth()); 232 233 width = getDisplay().getWidth() * 2; 234 mPopupWindow.setWidth(width); 235 assertEquals(width, mPopupWindow.getWidth()); 236 237 width = - getDisplay().getWidth() / 2; 238 mPopupWindow.setWidth(width); 239 assertEquals(width, mPopupWindow.getWidth()); 240 } 241 verifyAnchoring(int horizontalOffset, int verticalOffset, int gravity)242 private void verifyAnchoring(int horizontalOffset, int verticalOffset, int gravity) { 243 final View upperAnchor = mActivity.findViewById(R.id.anchor_upper); 244 final ListView listView = mPopupWindow.getListView(); 245 int[] anchorXY = new int[2]; 246 int[] listViewOnScreenXY = new int[2]; 247 int[] listViewInWindowXY = new int[2]; 248 249 assertTrue(mPopupWindow.isShowing()); 250 assertEquals(upperAnchor, mPopupWindow.getAnchorView()); 251 252 listView.getLocationOnScreen(listViewOnScreenXY); 253 upperAnchor.getLocationOnScreen(anchorXY); 254 listView.getLocationInWindow(listViewInWindowXY); 255 256 int expectedListViewOnScreenX = anchorXY[0] + listViewInWindowXY[0] + horizontalOffset; 257 final int absoluteGravity = 258 Gravity.getAbsoluteGravity(gravity, upperAnchor.getLayoutDirection()); 259 if (absoluteGravity == Gravity.RIGHT) { 260 expectedListViewOnScreenX -= (listView.getWidth() - upperAnchor.getWidth()); 261 } else { 262 // On narrow screens, it's possible for the popup to reach the edge 263 // of the screen. 264 int rightmostX = 265 getDisplay().getWidth() - mPopupWindow.getWidth() + listViewInWindowXY[0]; 266 if (expectedListViewOnScreenX > rightmostX) { 267 expectedListViewOnScreenX = rightmostX; 268 } 269 } 270 int expectedListViewOnScreenY = anchorXY[1] + listViewInWindowXY[1] 271 + upperAnchor.getHeight() + verticalOffset; 272 assertEquals(expectedListViewOnScreenX, listViewOnScreenXY[0]); 273 assertEquals(expectedListViewOnScreenY, listViewOnScreenXY[1]); 274 } 275 276 @Test testAnchoring()277 public void testAnchoring() throws Throwable { 278 mPopupWindowBuilder = new Builder(); 279 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 280 mInstrumentation.waitForIdleSync(); 281 282 assertEquals(0, mPopupWindow.getHorizontalOffset()); 283 assertEquals(0, mPopupWindow.getVerticalOffset()); 284 285 verifyAnchoring(0, 0, Gravity.NO_GRAVITY); 286 } 287 288 @Test testAnchoringWithHorizontalOffset()289 public void testAnchoringWithHorizontalOffset() throws Throwable { 290 mPopupWindowBuilder = new Builder().withHorizontalOffset(50); 291 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 292 mInstrumentation.waitForIdleSync(); 293 294 assertEquals(50, mPopupWindow.getHorizontalOffset()); 295 assertEquals(0, mPopupWindow.getVerticalOffset()); 296 297 verifyAnchoring(50, 0, Gravity.NO_GRAVITY); 298 } 299 300 @Test testAnchoringWithVerticalOffset()301 public void testAnchoringWithVerticalOffset() throws Throwable { 302 mPopupWindowBuilder = new Builder().withVerticalOffset(60); 303 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 304 mInstrumentation.waitForIdleSync(); 305 306 assertEquals(0, mPopupWindow.getHorizontalOffset()); 307 assertEquals(60, mPopupWindow.getVerticalOffset()); 308 309 verifyAnchoring(0, 60, Gravity.NO_GRAVITY); 310 } 311 312 @Test testAnchoringWithRightGravity()313 public void testAnchoringWithRightGravity() throws Throwable { 314 mPopupWindowBuilder = new Builder().withDropDownGravity(Gravity.RIGHT); 315 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 316 mInstrumentation.waitForIdleSync(); 317 318 assertEquals(0, mPopupWindow.getHorizontalOffset()); 319 assertEquals(0, mPopupWindow.getVerticalOffset()); 320 321 verifyAnchoring(0, 0, Gravity.RIGHT); 322 } 323 324 @Test testAnchoringWithEndGravity()325 public void testAnchoringWithEndGravity() throws Throwable { 326 mPopupWindowBuilder = new Builder().withDropDownGravity(Gravity.END); 327 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 328 mInstrumentation.waitForIdleSync(); 329 330 assertEquals(0, mPopupWindow.getHorizontalOffset()); 331 assertEquals(0, mPopupWindow.getVerticalOffset()); 332 333 verifyAnchoring(0, 0, Gravity.END); 334 } 335 336 @Test testSetWindowLayoutType()337 public void testSetWindowLayoutType() throws Throwable { 338 mPopupWindowBuilder = new Builder().withWindowLayoutType( 339 WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); 340 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 341 mInstrumentation.waitForIdleSync(); 342 assertTrue(mPopupWindow.isShowing()); 343 344 WindowManager.LayoutParams p = (WindowManager.LayoutParams) 345 mPopupWindow.getListView().getRootView().getLayoutParams(); 346 assertEquals(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, p.type); 347 } 348 349 @Test testDismiss()350 public void testDismiss() throws Throwable { 351 mPopupWindowBuilder = new Builder(); 352 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 353 mInstrumentation.waitForIdleSync(); 354 assertTrue(mPopupWindow.isShowing()); 355 356 mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss); 357 mInstrumentation.waitForIdleSync(); 358 assertFalse(mPopupWindow.isShowing()); 359 360 mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss); 361 mInstrumentation.waitForIdleSync(); 362 assertFalse(mPopupWindow.isShowing()); 363 } 364 365 @Test testSetOnDismissListener()366 public void testSetOnDismissListener() throws Throwable { 367 mPopupWindowBuilder = new Builder().withDismissListener(); 368 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 369 mInstrumentation.waitForIdleSync(); 370 mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss); 371 mInstrumentation.waitForIdleSync(); 372 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 373 374 mActivityRule.runOnUiThread(mPopupWindowBuilder::showAgain); 375 mInstrumentation.waitForIdleSync(); 376 mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss); 377 mInstrumentation.waitForIdleSync(); 378 verify(mPopupWindowBuilder.mOnDismissListener, times(2)).onDismiss(); 379 380 mPopupWindow.setOnDismissListener(null); 381 mActivityRule.runOnUiThread(mPopupWindowBuilder::showAgain); 382 mInstrumentation.waitForIdleSync(); 383 mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss); 384 mInstrumentation.waitForIdleSync(); 385 // Since we've reset the listener to null, we are not expecting any more interactions 386 // on the previously registered listener. 387 verifyNoMoreInteractions(mPopupWindowBuilder.mOnDismissListener); 388 } 389 390 @Test testAccessInputMethodMode()391 public void testAccessInputMethodMode() throws Throwable { 392 mPopupWindowBuilder = new Builder().withDismissListener(); 393 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 394 mInstrumentation.waitForIdleSync(); 395 396 assertEquals(PopupWindow.INPUT_METHOD_NEEDED, mPopupWindow.getInputMethodMode()); 397 assertFalse(mPopupWindow.isInputMethodNotNeeded()); 398 399 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_FROM_FOCUSABLE); 400 assertEquals(PopupWindow.INPUT_METHOD_FROM_FOCUSABLE, mPopupWindow.getInputMethodMode()); 401 assertFalse(mPopupWindow.isInputMethodNotNeeded()); 402 403 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 404 assertEquals(PopupWindow.INPUT_METHOD_NEEDED, mPopupWindow.getInputMethodMode()); 405 assertFalse(mPopupWindow.isInputMethodNotNeeded()); 406 407 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 408 assertEquals(PopupWindow.INPUT_METHOD_NOT_NEEDED, mPopupWindow.getInputMethodMode()); 409 assertTrue(mPopupWindow.isInputMethodNotNeeded()); 410 411 mPopupWindow.setInputMethodMode(-1); 412 assertEquals(-1, mPopupWindow.getInputMethodMode()); 413 assertFalse(mPopupWindow.isInputMethodNotNeeded()); 414 } 415 416 @Test testAccessSoftInputMethodMode()417 public void testAccessSoftInputMethodMode() throws Throwable { 418 mPopupWindowBuilder = new Builder().withDismissListener(); 419 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 420 mInstrumentation.waitForIdleSync(); 421 422 mPopupWindow = new ListPopupWindow(mActivity); 423 assertEquals(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED, 424 mPopupWindow.getSoftInputMode()); 425 426 mPopupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); 427 assertEquals(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE, 428 mPopupWindow.getSoftInputMode()); 429 430 mPopupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); 431 assertEquals(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE, 432 mPopupWindow.getSoftInputMode()); 433 } 434 verifyDismissalViaTouch(boolean setupAsModal)435 private void verifyDismissalViaTouch(boolean setupAsModal) throws Throwable { 436 // Register a click listener on the top-level container 437 final View mainContainer = mActivity.findViewById(R.id.main_container); 438 final View.OnClickListener mockContainerClickListener = mock(View.OnClickListener.class); 439 mActivityRule.runOnUiThread(() -> 440 mainContainer.setOnClickListener(mockContainerClickListener)); 441 442 // Configure a list popup window with requested modality 443 mPopupWindowBuilder = new Builder().setModal(setupAsModal).withDismissListener(); 444 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 445 mInstrumentation.waitForIdleSync(); 446 447 assertTrue("Popup window showing", mPopupWindow.isShowing()); 448 // Make sure that the modality of the popup window is set up correctly 449 assertEquals("Popup window modality", setupAsModal, mPopupWindow.isModal()); 450 451 // The logic below uses Instrumentation to emulate a tap outside the bounds of the 452 // displayed list popup window. This tap is then treated by the framework to be "split" as 453 // the ACTION_OUTSIDE for the popup itself, as well as DOWN / MOVE / UP for the underlying 454 // view root if the popup is not modal. 455 // It is not correct to emulate these two sequences separately in the test, as it 456 // wouldn't emulate the user-facing interaction for this test. Also, we don't want to use 457 // View.dispatchTouchEvent directly as that would require emulation of two separate 458 // sequences as well. 459 final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 460 final ListView popupListView = mPopupWindow.getListView(); 461 final Rect rect = new Rect(); 462 mPopupWindow.getBackground().getPadding(rect); 463 CtsTouchUtils.emulateTapOnView(instrumentation, popupListView, 464 -rect.left - 20, popupListView.getHeight() + rect.top + rect.bottom + 20); 465 466 // At this point our popup should not be showing and should have notified its 467 // dismiss listener 468 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 469 assertFalse("Popup window not showing after outside click", mPopupWindow.isShowing()); 470 471 // Also test that the click outside the popup bounds has been "delivered" to the main 472 // container only if the popup is not modal 473 verify(mockContainerClickListener, times(setupAsModal ? 0 : 1)).onClick(mainContainer); 474 } 475 476 @Test testDismissalOutsideNonModal()477 public void testDismissalOutsideNonModal() throws Throwable { 478 verifyDismissalViaTouch(false); 479 } 480 481 @Test testDismissalOutsideModal()482 public void testDismissalOutsideModal() throws Throwable { 483 verifyDismissalViaTouch(true); 484 } 485 486 @Test testItemClicks()487 public void testItemClicks() throws Throwable { 488 mPopupWindowBuilder = new Builder().withItemClickListener().withDismissListener(); 489 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 490 mInstrumentation.waitForIdleSync(); 491 mActivityRule.runOnUiThread(() -> mPopupWindow.performItemClick(2)); 492 mInstrumentation.waitForIdleSync(); 493 494 verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick( 495 any(AdapterView.class), any(View.class), eq(2), eq(2L)); 496 // Also verify that the popup window has been dismissed 497 assertFalse(mPopupWindow.isShowing()); 498 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 499 500 mActivityRule.runOnUiThread(mPopupWindowBuilder::showAgain); 501 mInstrumentation.waitForIdleSync(); 502 mActivityRule.runOnUiThread( 503 () -> mPopupWindow.getListView().performItemClick(null, 1, 1)); 504 mInstrumentation.waitForIdleSync(); 505 506 verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick( 507 any(AdapterView.class), any(), eq(1), eq(1L)); 508 // Also verify that the popup window has been dismissed 509 assertFalse(mPopupWindow.isShowing()); 510 verify(mPopupWindowBuilder.mOnDismissListener, times(2)).onDismiss(); 511 512 // Finally verify that our item click listener has only been called twice 513 verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemClickListener); 514 } 515 516 @Test testPromptViewAbove()517 public void testPromptViewAbove() throws Throwable { 518 mActivityRule.runOnUiThread(() -> { 519 promptView = LayoutInflater.from(mActivity).inflate(R.layout.popupwindow_prompt, null); 520 mPopupWindowBuilder = new Builder().withPrompt( 521 promptView, ListPopupWindow.POSITION_PROMPT_ABOVE); 522 mPopupWindowBuilder.show(); 523 }); 524 mInstrumentation.waitForIdleSync(); 525 526 // Verify that our prompt is displayed on the screen and is above the first list item 527 assertTrue(promptView.isAttachedToWindow()); 528 assertTrue(promptView.isShown()); 529 assertEquals(ListPopupWindow.POSITION_PROMPT_ABOVE, mPopupWindow.getPromptPosition()); 530 531 final ListView listView = mPopupWindow.getListView(); 532 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, null); 533 534 final int[] promptViewOnScreenXY = new int[2]; 535 final int[] firstChildOnScreenXY = new int[2]; 536 537 mActivityRule.runOnUiThread(()-> { 538 promptView.getLocationOnScreen(promptViewOnScreenXY); 539 540 final View firstListChild = listView.getChildAt(0); 541 firstListChild.getLocationOnScreen(firstChildOnScreenXY); 542 }); 543 mInstrumentation.waitForIdleSync(); 544 545 assertTrue(promptViewOnScreenXY[1] + promptView.getHeight() <= firstChildOnScreenXY[1]); 546 } 547 548 @Test testPromptViewBelow()549 public void testPromptViewBelow() throws Throwable { 550 mActivityRule.runOnUiThread(() -> { 551 promptView = LayoutInflater.from(mActivity).inflate(R.layout.popupwindow_prompt, null); 552 mPopupWindowBuilder = new Builder().withPrompt( 553 promptView, ListPopupWindow.POSITION_PROMPT_BELOW); 554 mPopupWindowBuilder.show(); 555 }); 556 mInstrumentation.waitForIdleSync(); 557 558 // Verify that our prompt is displayed on the screen and is below the last list item 559 assertTrue(promptView.isAttachedToWindow()); 560 assertTrue(promptView.isShown()); 561 assertEquals(ListPopupWindow.POSITION_PROMPT_BELOW, mPopupWindow.getPromptPosition()); 562 563 final ListView listView = mPopupWindow.getListView(); 564 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, null); 565 566 final int[] promptViewOnScreenXY = new int[2]; 567 final int[] lastChildOnScreenXY = new int[2]; 568 569 mActivityRule.runOnUiThread(()-> { 570 promptView.getLocationOnScreen(promptViewOnScreenXY); 571 572 final View lastListChild = listView.getChildAt(listView.getChildCount() - 1); 573 lastListChild.getLocationOnScreen(lastChildOnScreenXY); 574 }); 575 mInstrumentation.waitForIdleSync(); 576 577 // The child is above the prompt. They may overlap, as in the case 578 // when the list items do not all fit on screen, but this is still 579 // correct. 580 assertTrue(lastChildOnScreenXY[1] <= promptViewOnScreenXY[1]); 581 } 582 583 @Presubmit 584 @Test testAccessSelection()585 public void testAccessSelection() throws Throwable { 586 mPopupWindowBuilder = new Builder().withItemSelectedListener(); 587 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 588 mInstrumentation.waitForIdleSync(); 589 590 final ListView listView = mPopupWindow.getListView(); 591 592 // Select an item 593 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 594 () -> mPopupWindow.setSelection(1)); 595 596 // And verify the current selection state + selection listener invocation 597 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 598 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 599 assertEquals(1, mPopupWindow.getSelectedItemId()); 600 assertEquals(1, mPopupWindow.getSelectedItemPosition()); 601 assertEquals("Bob", mPopupWindow.getSelectedItem()); 602 View selectedView = mPopupWindow.getSelectedView(); 603 assertNotNull(selectedView); 604 assertEquals("Bob", 605 ((TextView) selectedView.findViewById(android.R.id.text1)).getText()); 606 607 // Select another item 608 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 609 () -> mPopupWindow.setSelection(3)); 610 611 // And verify the new selection state + selection listener invocation 612 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 613 any(AdapterView.class), any(View.class), eq(3), eq(3L)); 614 assertEquals(3, mPopupWindow.getSelectedItemId()); 615 assertEquals(3, mPopupWindow.getSelectedItemPosition()); 616 assertEquals("Deirdre", mPopupWindow.getSelectedItem()); 617 selectedView = mPopupWindow.getSelectedView(); 618 assertNotNull(selectedView); 619 assertEquals("Deirdre", 620 ((TextView) selectedView.findViewById(android.R.id.text1)).getText()); 621 622 // Clear selection 623 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 624 mPopupWindow::clearListSelection); 625 626 // And verify empty selection state + no more selection listener invocation 627 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onNothingSelected( 628 any(AdapterView.class)); 629 assertEquals(AdapterView.INVALID_ROW_ID, mPopupWindow.getSelectedItemId()); 630 assertEquals(AdapterView.INVALID_POSITION, mPopupWindow.getSelectedItemPosition()); 631 assertEquals(null, mPopupWindow.getSelectedItem()); 632 assertEquals(null, mPopupWindow.getSelectedView()); 633 verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemSelectedListener); 634 } 635 636 @Test testNoDefaultDismissalWithBackButton()637 public void testNoDefaultDismissalWithBackButton() throws Throwable { 638 mPopupWindowBuilder = new Builder().withDismissListener(); 639 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 640 mInstrumentation.waitForIdleSync(); 641 642 // Send BACK key event. As we don't have any custom code that dismisses ListPopupWindow, 643 // and ListPopupWindow doesn't track that system-level key event on its own, ListPopupWindow 644 // should stay visible 645 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 646 verify(mPopupWindowBuilder.mOnDismissListener, never()).onDismiss(); 647 assertTrue(mPopupWindow.isShowing()); 648 } 649 650 @Test testCustomDismissalWithBackButton()651 public void testCustomDismissalWithBackButton() throws Throwable { 652 mActivityRule.runOnUiThread(() -> { 653 mPopupWindowBuilder = new Builder().withAnchor(R.id.anchor_upper_left) 654 .withDismissListener(); 655 mPopupWindowBuilder.show(); 656 }); 657 mInstrumentation.waitForIdleSync(); 658 659 // "Point" our custom extension of EditText to our ListPopupWindow 660 final MockViewForListPopupWindow anchor = 661 (MockViewForListPopupWindow) mPopupWindow.getAnchorView(); 662 anchor.wireTo(mPopupWindow); 663 // Request focus on our EditText 664 mActivityRule.runOnUiThread(anchor::requestFocus); 665 mInstrumentation.waitForIdleSync(); 666 assertTrue(anchor.isFocused()); 667 668 // Send BACK key event. As our custom extension of EditText calls 669 // ListPopupWindow.onKeyPreIme, the end result should be the dismissal of the 670 // ListPopupWindow 671 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 672 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 673 assertFalse(mPopupWindow.isShowing()); 674 } 675 676 @Test testListSelectionWithDPad()677 public void testListSelectionWithDPad() throws Throwable { 678 mPopupWindowBuilder = new Builder().withAnchor(R.id.anchor_upper_left) 679 .withDismissListener().withItemSelectedListener(); 680 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 681 mInstrumentation.waitForIdleSync(); 682 683 final View root = mPopupWindow.getListView().getRootView(); 684 685 // "Point" our custom extension of EditText to our ListPopupWindow 686 final MockViewForListPopupWindow anchor = 687 (MockViewForListPopupWindow) mPopupWindow.getAnchorView(); 688 anchor.wireTo(mPopupWindow); 689 // Request focus on our EditText 690 mActivityRule.runOnUiThread(anchor::requestFocus); 691 mInstrumentation.waitForIdleSync(); 692 assertTrue(anchor.isFocused()); 693 694 // Select entry #1 in the popup list 695 final ListView listView = mPopupWindow.getListView(); 696 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 697 () -> mPopupWindow.setSelection(1)); 698 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 699 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 700 701 // Send DPAD_DOWN key event. As our custom extension of EditText calls 702 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection 703 // down one row 704 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_DOWN); 705 mInstrumentation.waitForIdleSync(); 706 707 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null); 708 709 // At this point we expect that item #2 was selected 710 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 711 any(AdapterView.class), any(View.class), eq(2), eq(2L)); 712 713 // Send a DPAD_UP key event. As our custom extension of EditText calls 714 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection 715 // up one row 716 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_UP); 717 mInstrumentation.waitForIdleSync(); 718 719 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null); 720 721 // At this point we expect that item #1 was selected 722 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(2)).onItemSelected( 723 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 724 725 // Send one more DPAD_UP key event. As our custom extension of EditText calls 726 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection 727 // up one more row 728 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_UP); 729 mInstrumentation.waitForIdleSync(); 730 731 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null); 732 733 // At this point we expect that item #0 was selected 734 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 735 any(AdapterView.class), any(View.class), eq(0), eq(0L)); 736 737 // Send ENTER key event. As our custom extension of EditText calls 738 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be dismissal of 739 // the popup window 740 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation,listView, KeyEvent.KEYCODE_ENTER); 741 mInstrumentation.waitForIdleSync(); 742 743 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 744 assertFalse(mPopupWindow.isShowing()); 745 746 verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemSelectedListener); 747 verifyNoMoreInteractions(mPopupWindowBuilder.mOnDismissListener); 748 } 749 750 @Test testCreateOnDragListener()751 public void testCreateOnDragListener() throws Throwable { 752 // In this test we want precise control over the height of the popup content since 753 // we need to know by how much to swipe down to end the emulated gesture over the 754 // specific item in the popup. This is why we're using a popup style that removes 755 // all decoration around the popup content, as well as our own row layout with known 756 // height. 757 mPopupWindowBuilder = new Builder() 758 .withPopupStyleAttr(R.style.PopupEmptyStyle) 759 .withContentRowLayoutId(R.layout.popup_window_item) 760 .withItemClickListener().withDismissListener(); 761 762 // Configure ListPopupWindow without showing it 763 mActivityRule.runOnUiThread(mPopupWindowBuilder::configure); 764 mInstrumentation.waitForIdleSync(); 765 766 // Get the anchor view and configure it with ListPopupWindow's drag-to-open listener 767 final View anchor = mActivity.findViewById(mPopupWindowBuilder.mAnchorId); 768 final View.OnTouchListener dragListener = mPopupWindow.createDragToOpenListener(anchor); 769 mActivityRule.runOnUiThread(() -> { 770 anchor.setOnTouchListener(dragListener); 771 // And also configure it to show the popup window on click 772 anchor.setOnClickListener((View view) -> mPopupWindow.show()); 773 }); 774 mInstrumentation.waitForIdleSync(); 775 776 // Get the height of a row item in our popup window 777 final int popupRowHeight = mActivity.getResources().getDimensionPixelSize( 778 R.dimen.popup_row_height); 779 780 final int[] anchorOnScreenXY = new int[2]; 781 anchor.getLocationOnScreen(anchorOnScreenXY); 782 783 // Compute the start coordinates of a downward swipe and the amount of swipe. We'll 784 // be swiping by twice the row height. That, combined with the swipe originating in the 785 // center of the anchor should result in clicking the second row in the popup. 786 int emulatedX = anchorOnScreenXY[0] + anchor.getWidth() / 2; 787 int emulatedStartY = anchorOnScreenXY[1] + anchor.getHeight() / 2; 788 int swipeAmount = 2 * popupRowHeight; 789 790 // Emulate drag-down gesture with a sequence of motion events 791 CtsTouchUtils.emulateDragGesture(mInstrumentation, emulatedX, emulatedStartY, 792 0, swipeAmount); 793 794 // We expect the swipe / drag gesture to result in clicking the second item in our list. 795 verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick( 796 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 797 // Since our item click listener calls dismiss() on the popup, we expect the popup to not 798 // be showing 799 assertFalse(mPopupWindow.isShowing()); 800 // At this point our popup should have notified its dismiss listener 801 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 802 } 803 804 /** 805 * Inner helper class to configure an instance of <code>ListPopupWindow</code> for the 806 * specific test. The main reason for its existence is that once a popup window is shown 807 * with the show() method, most of its configuration APIs are no-ops. This means that 808 * we can't add logic that is specific to a certain test (such as dismissing a non-modal 809 * popup window) once it's shown and we have a reference to a displayed ListPopupWindow. 810 */ 811 public class Builder { 812 private boolean mIsModal; 813 private boolean mHasDismissListener; 814 private boolean mHasItemClickListener; 815 private boolean mHasItemSelectedListener; 816 private boolean mIgnoreContentWidth; 817 private int mHorizontalOffset; 818 private int mVerticalOffset; 819 private int mDropDownGravity; 820 private int mAnchorId = R.id.anchor_upper; 821 private int mContentRowLayoutId = android.R.layout.simple_list_item_1; 822 823 private boolean mHasWindowLayoutType; 824 private int mWindowLayoutType; 825 826 private boolean mUseCustomPopupStyle; 827 private int mPopupStyleAttr; 828 829 private View mPromptView; 830 private int mPromptPosition; 831 832 private AdapterView.OnItemClickListener mOnItemClickListener; 833 private AdapterView.OnItemSelectedListener mOnItemSelectedListener; 834 private PopupWindow.OnDismissListener mOnDismissListener; 835 Builder()836 public Builder() { 837 } 838 withAnchor(int anchorId)839 public Builder withAnchor(int anchorId) { 840 mAnchorId = anchorId; 841 return this; 842 } 843 withContentRowLayoutId(int contentRowLayoutId)844 public Builder withContentRowLayoutId(int contentRowLayoutId) { 845 mContentRowLayoutId = contentRowLayoutId; 846 return this; 847 } 848 withPopupStyleAttr(int popupStyleAttr)849 public Builder withPopupStyleAttr(int popupStyleAttr) { 850 mUseCustomPopupStyle = true; 851 mPopupStyleAttr = popupStyleAttr; 852 return this; 853 } 854 ignoreContentWidth()855 public Builder ignoreContentWidth() { 856 mIgnoreContentWidth = true; 857 return this; 858 } 859 setModal(boolean isModal)860 public Builder setModal(boolean isModal) { 861 mIsModal = isModal; 862 return this; 863 } 864 withItemClickListener()865 public Builder withItemClickListener() { 866 mHasItemClickListener = true; 867 return this; 868 } 869 withItemSelectedListener()870 public Builder withItemSelectedListener() { 871 mHasItemSelectedListener = true; 872 return this; 873 } 874 withDismissListener()875 public Builder withDismissListener() { 876 mHasDismissListener = true; 877 return this; 878 } 879 withWindowLayoutType(int windowLayoutType)880 public Builder withWindowLayoutType(int windowLayoutType) { 881 mHasWindowLayoutType = true; 882 mWindowLayoutType = windowLayoutType; 883 return this; 884 } 885 withHorizontalOffset(int horizontalOffset)886 public Builder withHorizontalOffset(int horizontalOffset) { 887 mHorizontalOffset = horizontalOffset; 888 return this; 889 } 890 withVerticalOffset(int verticalOffset)891 public Builder withVerticalOffset(int verticalOffset) { 892 mVerticalOffset = verticalOffset; 893 return this; 894 } 895 withDropDownGravity(int dropDownGravity)896 public Builder withDropDownGravity(int dropDownGravity) { 897 mDropDownGravity = dropDownGravity; 898 return this; 899 } 900 withPrompt(View promptView, int promptPosition)901 public Builder withPrompt(View promptView, int promptPosition) { 902 mPromptView = promptView; 903 mPromptPosition = promptPosition; 904 return this; 905 } 906 getContentWidth(ListAdapter listAdapter, Drawable background)907 private int getContentWidth(ListAdapter listAdapter, Drawable background) { 908 if (listAdapter == null) { 909 return 0; 910 } 911 912 int width = 0; 913 View itemView = null; 914 int itemType = 0; 915 916 for (int i = 0; i < listAdapter.getCount(); i++) { 917 final int positionType = listAdapter.getItemViewType(i); 918 if (positionType != itemType) { 919 itemType = positionType; 920 itemView = null; 921 } 922 itemView = listAdapter.getView(i, itemView, null); 923 if (itemView.getLayoutParams() == null) { 924 itemView.setLayoutParams(new ViewGroup.LayoutParams( 925 ViewGroup.LayoutParams.WRAP_CONTENT, 926 ViewGroup.LayoutParams.WRAP_CONTENT)); 927 } 928 itemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 929 width = Math.max(width, itemView.getMeasuredWidth()); 930 } 931 932 // Add background padding to measured width 933 if (background != null) { 934 final Rect rect = new Rect(); 935 background.getPadding(rect); 936 width += rect.left + rect.right; 937 } 938 939 return width; 940 } 941 configure()942 private void configure() { 943 if (mUseCustomPopupStyle) { 944 mPopupWindow = new ListPopupWindow(mActivity, null, mPopupStyleAttr, 0); 945 } else { 946 mPopupWindow = new ListPopupWindow(mActivity); 947 } 948 final String[] POPUP_CONTENT = 949 new String[]{"Alice", "Bob", "Charlie", "Deirdre", "El"}; 950 final BaseAdapter listPopupAdapter = new BaseAdapter() { 951 class ViewHolder { 952 private TextView title; 953 } 954 955 @Override 956 public int getCount() { 957 return POPUP_CONTENT.length; 958 } 959 960 @Override 961 public Object getItem(int position) { 962 return POPUP_CONTENT[position]; 963 } 964 965 @Override 966 public long getItemId(int position) { 967 return position; 968 } 969 970 @Override 971 public View getView(int position, View convertView, ViewGroup parent) { 972 if (convertView == null) { 973 convertView = LayoutInflater.from(mActivity).inflate( 974 mContentRowLayoutId, parent, false); 975 ViewHolder viewHolder = new ViewHolder(); 976 viewHolder.title = (TextView) convertView.findViewById(android.R.id.text1); 977 convertView.setTag(viewHolder); 978 } 979 980 ViewHolder viewHolder = (ViewHolder) convertView.getTag(); 981 viewHolder.title.setText(POPUP_CONTENT[position]); 982 return convertView; 983 } 984 }; 985 986 mPopupWindow.setAdapter(listPopupAdapter); 987 mPopupWindow.setAnchorView(mActivity.findViewById(mAnchorId)); 988 989 // The following mock listeners have to be set before the call to show() as 990 // they are set on the internally constructed drop down. 991 if (mHasItemClickListener) { 992 // Wrap our item click listener with a Mockito spy 993 mOnItemClickListener = spy(mItemClickListener); 994 // Register that spy as the item click listener on the ListPopupWindow 995 mPopupWindow.setOnItemClickListener(mOnItemClickListener); 996 // And configure Mockito to call our original listener with onItemClick. 997 // This way we can have both our item click listener running to dismiss the popup 998 // window, and track the invocations of onItemClick with Mockito APIs. 999 doCallRealMethod().when(mOnItemClickListener).onItemClick( 1000 any(AdapterView.class), any(View.class), any(int.class), any(int.class)); 1001 } 1002 1003 if (mHasItemSelectedListener) { 1004 mOnItemSelectedListener = mock(AdapterView.OnItemSelectedListener.class); 1005 mPopupWindow.setOnItemSelectedListener(mOnItemSelectedListener); 1006 mPopupWindow.setListSelector( 1007 mActivity.getDrawable(R.drawable.red_translucent_fill)); 1008 } 1009 1010 if (mHasDismissListener) { 1011 mOnDismissListener = mock(PopupWindow.OnDismissListener.class); 1012 mPopupWindow.setOnDismissListener(mOnDismissListener); 1013 } 1014 1015 mPopupWindow.setModal(mIsModal); 1016 if (mHasWindowLayoutType) { 1017 mPopupWindow.setWindowLayoutType(mWindowLayoutType); 1018 } 1019 1020 if (!mIgnoreContentWidth) { 1021 mPopupWindow.setContentWidth( 1022 getContentWidth(listPopupAdapter, mPopupWindow.getBackground())); 1023 } 1024 1025 if (mHorizontalOffset != 0) { 1026 mPopupWindow.setHorizontalOffset(mHorizontalOffset); 1027 } 1028 1029 if (mVerticalOffset != 0) { 1030 mPopupWindow.setVerticalOffset(mVerticalOffset); 1031 } 1032 1033 if (mDropDownGravity != Gravity.NO_GRAVITY) { 1034 mPopupWindow.setDropDownGravity(mDropDownGravity); 1035 } 1036 1037 if (mPromptView != null) { 1038 mPopupWindow.setPromptPosition(mPromptPosition); 1039 mPopupWindow.setPromptView(mPromptView); 1040 } 1041 } 1042 show()1043 private void show() { 1044 configure(); 1045 mPopupWindow.show(); 1046 assertTrue(mPopupWindow.isShowing()); 1047 } 1048 showAgain()1049 private void showAgain() { 1050 if (mPopupWindow == null || mPopupWindow.isShowing()) { 1051 return; 1052 } 1053 mPopupWindow.show(); 1054 assertTrue(mPopupWindow.isShowing()); 1055 } 1056 dismiss()1057 private void dismiss() { 1058 if (mPopupWindow == null || !mPopupWindow.isShowing()) 1059 return; 1060 mPopupWindow.dismiss(); 1061 } 1062 } 1063 } 1064