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