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 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 103 public void setup() { 104 mInstrumentation = InstrumentationRegistry.getInstrumentation(); 105 mActivity = mActivityRule.getActivity(); 106 mItemClickListener = new PopupItemClickListener(); 107 } 108 109 @After 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 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 138 public void testNoDefaultVisibility() { 139 mPopupWindow = new ListPopupWindow(mActivity); 140 assertFalse(mPopupWindow.isShowing()); 141 } 142 143 @Test 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 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 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 */ 209 private Display getDisplay() { 210 WindowManager wm = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE); 211 return wm.getDefaultDisplay(); 212 } 213 214 @Test 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 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 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 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 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 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 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 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 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 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 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 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 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 477 public void testDismissalOutsideNonModal() throws Throwable { 478 verifyDismissalViaTouch(false); 479 } 480 481 @Test 482 public void testDismissalOutsideModal() throws Throwable { 483 verifyDismissalViaTouch(true); 484 } 485 486 @Test 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 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 int[] promptViewOnScreenXY = new int[2]; 532 promptView.getLocationOnScreen(promptViewOnScreenXY); 533 534 final ListView listView = mPopupWindow.getListView(); 535 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, null); 536 537 final View firstListChild = listView.getChildAt(0); 538 final int[] firstChildOnScreenXY = new int[2]; 539 firstListChild.getLocationOnScreen(firstChildOnScreenXY); 540 541 assertTrue(promptViewOnScreenXY[1] + promptView.getHeight() <= firstChildOnScreenXY[1]); 542 } 543 544 @Test 545 public void testPromptViewBelow() throws Throwable { 546 mActivityRule.runOnUiThread(() -> { 547 promptView = LayoutInflater.from(mActivity).inflate(R.layout.popupwindow_prompt, null); 548 mPopupWindowBuilder = new Builder().withPrompt( 549 promptView, ListPopupWindow.POSITION_PROMPT_BELOW); 550 mPopupWindowBuilder.show(); 551 }); 552 mInstrumentation.waitForIdleSync(); 553 554 // Verify that our prompt is displayed on the screen and is below the last list item 555 assertTrue(promptView.isAttachedToWindow()); 556 assertTrue(promptView.isShown()); 557 assertEquals(ListPopupWindow.POSITION_PROMPT_BELOW, mPopupWindow.getPromptPosition()); 558 559 final ListView listView = mPopupWindow.getListView(); 560 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, null); 561 562 final int[] promptViewOnScreenXY = new int[2]; 563 promptView.getLocationOnScreen(promptViewOnScreenXY); 564 565 final View lastListChild = listView.getChildAt(listView.getChildCount() - 1); 566 final int[] lastChildOnScreenXY = new int[2]; 567 lastListChild.getLocationOnScreen(lastChildOnScreenXY); 568 569 // The child is above the prompt. They may overlap, as in the case 570 // when the list items do not all fit on screen, but this is still 571 // correct. 572 assertTrue(lastChildOnScreenXY[1] <= promptViewOnScreenXY[1]); 573 } 574 575 @Presubmit 576 @Test 577 public void testAccessSelection() throws Throwable { 578 mPopupWindowBuilder = new Builder().withItemSelectedListener(); 579 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 580 mInstrumentation.waitForIdleSync(); 581 582 final ListView listView = mPopupWindow.getListView(); 583 584 // Select an item 585 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 586 () -> mPopupWindow.setSelection(1)); 587 588 // And verify the current selection state + selection listener invocation 589 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 590 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 591 assertEquals(1, mPopupWindow.getSelectedItemId()); 592 assertEquals(1, mPopupWindow.getSelectedItemPosition()); 593 assertEquals("Bob", mPopupWindow.getSelectedItem()); 594 View selectedView = mPopupWindow.getSelectedView(); 595 assertNotNull(selectedView); 596 assertEquals("Bob", 597 ((TextView) selectedView.findViewById(android.R.id.text1)).getText()); 598 599 // Select another item 600 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 601 () -> mPopupWindow.setSelection(3)); 602 603 // And verify the new selection state + selection listener invocation 604 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 605 any(AdapterView.class), any(View.class), eq(3), eq(3L)); 606 assertEquals(3, mPopupWindow.getSelectedItemId()); 607 assertEquals(3, mPopupWindow.getSelectedItemPosition()); 608 assertEquals("Deirdre", mPopupWindow.getSelectedItem()); 609 selectedView = mPopupWindow.getSelectedView(); 610 assertNotNull(selectedView); 611 assertEquals("Deirdre", 612 ((TextView) selectedView.findViewById(android.R.id.text1)).getText()); 613 614 // Clear selection 615 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 616 mPopupWindow::clearListSelection); 617 618 // And verify empty selection state + no more selection listener invocation 619 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onNothingSelected( 620 any(AdapterView.class)); 621 assertEquals(AdapterView.INVALID_ROW_ID, mPopupWindow.getSelectedItemId()); 622 assertEquals(AdapterView.INVALID_POSITION, mPopupWindow.getSelectedItemPosition()); 623 assertEquals(null, mPopupWindow.getSelectedItem()); 624 assertEquals(null, mPopupWindow.getSelectedView()); 625 verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemSelectedListener); 626 } 627 628 @Test 629 public void testNoDefaultDismissalWithBackButton() throws Throwable { 630 mPopupWindowBuilder = new Builder().withDismissListener(); 631 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 632 mInstrumentation.waitForIdleSync(); 633 634 // Send BACK key event. As we don't have any custom code that dismisses ListPopupWindow, 635 // and ListPopupWindow doesn't track that system-level key event on its own, ListPopupWindow 636 // should stay visible 637 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 638 verify(mPopupWindowBuilder.mOnDismissListener, never()).onDismiss(); 639 assertTrue(mPopupWindow.isShowing()); 640 } 641 642 @Test 643 public void testCustomDismissalWithBackButton() throws Throwable { 644 mActivityRule.runOnUiThread(() -> { 645 mPopupWindowBuilder = new Builder().withAnchor(R.id.anchor_upper_left) 646 .withDismissListener(); 647 mPopupWindowBuilder.show(); 648 }); 649 mInstrumentation.waitForIdleSync(); 650 651 // "Point" our custom extension of EditText to our ListPopupWindow 652 final MockViewForListPopupWindow anchor = 653 (MockViewForListPopupWindow) mPopupWindow.getAnchorView(); 654 anchor.wireTo(mPopupWindow); 655 // Request focus on our EditText 656 mActivityRule.runOnUiThread(anchor::requestFocus); 657 mInstrumentation.waitForIdleSync(); 658 assertTrue(anchor.isFocused()); 659 660 // Send BACK key event. As our custom extension of EditText calls 661 // ListPopupWindow.onKeyPreIme, the end result should be the dismissal of the 662 // ListPopupWindow 663 mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); 664 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 665 assertFalse(mPopupWindow.isShowing()); 666 } 667 668 @Test 669 public void testListSelectionWithDPad() throws Throwable { 670 mPopupWindowBuilder = new Builder().withAnchor(R.id.anchor_upper_left) 671 .withDismissListener().withItemSelectedListener(); 672 mActivityRule.runOnUiThread(mPopupWindowBuilder::show); 673 mInstrumentation.waitForIdleSync(); 674 675 final View root = mPopupWindow.getListView().getRootView(); 676 677 // "Point" our custom extension of EditText to our ListPopupWindow 678 final MockViewForListPopupWindow anchor = 679 (MockViewForListPopupWindow) mPopupWindow.getAnchorView(); 680 anchor.wireTo(mPopupWindow); 681 // Request focus on our EditText 682 mActivityRule.runOnUiThread(anchor::requestFocus); 683 mInstrumentation.waitForIdleSync(); 684 assertTrue(anchor.isFocused()); 685 686 // Select entry #1 in the popup list 687 final ListView listView = mPopupWindow.getListView(); 688 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, 689 () -> mPopupWindow.setSelection(1)); 690 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 691 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 692 693 // Send DPAD_DOWN key event. As our custom extension of EditText calls 694 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection 695 // down one row 696 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_DOWN); 697 mInstrumentation.waitForIdleSync(); 698 699 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null); 700 701 // At this point we expect that item #2 was selected 702 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 703 any(AdapterView.class), any(View.class), eq(2), eq(2L)); 704 705 // Send a DPAD_UP key event. As our custom extension of EditText calls 706 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection 707 // up one row 708 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_UP); 709 mInstrumentation.waitForIdleSync(); 710 711 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null); 712 713 // At this point we expect that item #1 was selected 714 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(2)).onItemSelected( 715 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 716 717 // Send one more DPAD_UP key event. As our custom extension of EditText calls 718 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection 719 // up one more row 720 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_UP); 721 mInstrumentation.waitForIdleSync(); 722 723 WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null); 724 725 // At this point we expect that item #0 was selected 726 verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected( 727 any(AdapterView.class), any(View.class), eq(0), eq(0L)); 728 729 // Send ENTER key event. As our custom extension of EditText calls 730 // ListPopupWindow.onKeyDown and onKeyUp, the end result should be dismissal of 731 // the popup window 732 CtsKeyEventUtil.sendKeyDownUp(mInstrumentation,listView, KeyEvent.KEYCODE_ENTER); 733 mInstrumentation.waitForIdleSync(); 734 735 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 736 assertFalse(mPopupWindow.isShowing()); 737 738 verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemSelectedListener); 739 verifyNoMoreInteractions(mPopupWindowBuilder.mOnDismissListener); 740 } 741 742 @Test 743 public void testCreateOnDragListener() throws Throwable { 744 // In this test we want precise control over the height of the popup content since 745 // we need to know by how much to swipe down to end the emulated gesture over the 746 // specific item in the popup. This is why we're using a popup style that removes 747 // all decoration around the popup content, as well as our own row layout with known 748 // height. 749 mPopupWindowBuilder = new Builder() 750 .withPopupStyleAttr(R.style.PopupEmptyStyle) 751 .withContentRowLayoutId(R.layout.popup_window_item) 752 .withItemClickListener().withDismissListener(); 753 754 // Configure ListPopupWindow without showing it 755 mActivityRule.runOnUiThread(mPopupWindowBuilder::configure); 756 mInstrumentation.waitForIdleSync(); 757 758 // Get the anchor view and configure it with ListPopupWindow's drag-to-open listener 759 final View anchor = mActivity.findViewById(mPopupWindowBuilder.mAnchorId); 760 final View.OnTouchListener dragListener = mPopupWindow.createDragToOpenListener(anchor); 761 mActivityRule.runOnUiThread(() -> { 762 anchor.setOnTouchListener(dragListener); 763 // And also configure it to show the popup window on click 764 anchor.setOnClickListener((View view) -> mPopupWindow.show()); 765 }); 766 mInstrumentation.waitForIdleSync(); 767 768 // Get the height of a row item in our popup window 769 final int popupRowHeight = mActivity.getResources().getDimensionPixelSize( 770 R.dimen.popup_row_height); 771 772 final int[] anchorOnScreenXY = new int[2]; 773 anchor.getLocationOnScreen(anchorOnScreenXY); 774 775 // Compute the start coordinates of a downward swipe and the amount of swipe. We'll 776 // be swiping by twice the row height. That, combined with the swipe originating in the 777 // center of the anchor should result in clicking the second row in the popup. 778 int emulatedX = anchorOnScreenXY[0] + anchor.getWidth() / 2; 779 int emulatedStartY = anchorOnScreenXY[1] + anchor.getHeight() / 2; 780 int swipeAmount = 2 * popupRowHeight; 781 782 // Emulate drag-down gesture with a sequence of motion events 783 CtsTouchUtils.emulateDragGesture(mInstrumentation, emulatedX, emulatedStartY, 784 0, swipeAmount); 785 786 // We expect the swipe / drag gesture to result in clicking the second item in our list. 787 verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick( 788 any(AdapterView.class), any(View.class), eq(1), eq(1L)); 789 // Since our item click listener calls dismiss() on the popup, we expect the popup to not 790 // be showing 791 assertFalse(mPopupWindow.isShowing()); 792 // At this point our popup should have notified its dismiss listener 793 verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss(); 794 } 795 796 /** 797 * Inner helper class to configure an instance of <code>ListPopupWindow</code> for the 798 * specific test. The main reason for its existence is that once a popup window is shown 799 * with the show() method, most of its configuration APIs are no-ops. This means that 800 * we can't add logic that is specific to a certain test (such as dismissing a non-modal 801 * popup window) once it's shown and we have a reference to a displayed ListPopupWindow. 802 */ 803 public class Builder { 804 private boolean mIsModal; 805 private boolean mHasDismissListener; 806 private boolean mHasItemClickListener; 807 private boolean mHasItemSelectedListener; 808 private boolean mIgnoreContentWidth; 809 private int mHorizontalOffset; 810 private int mVerticalOffset; 811 private int mDropDownGravity; 812 private int mAnchorId = R.id.anchor_upper; 813 private int mContentRowLayoutId = android.R.layout.simple_list_item_1; 814 815 private boolean mHasWindowLayoutType; 816 private int mWindowLayoutType; 817 818 private boolean mUseCustomPopupStyle; 819 private int mPopupStyleAttr; 820 821 private View mPromptView; 822 private int mPromptPosition; 823 824 private AdapterView.OnItemClickListener mOnItemClickListener; 825 private AdapterView.OnItemSelectedListener mOnItemSelectedListener; 826 private PopupWindow.OnDismissListener mOnDismissListener; 827 828 public Builder() { 829 } 830 831 public Builder withAnchor(int anchorId) { 832 mAnchorId = anchorId; 833 return this; 834 } 835 836 public Builder withContentRowLayoutId(int contentRowLayoutId) { 837 mContentRowLayoutId = contentRowLayoutId; 838 return this; 839 } 840 841 public Builder withPopupStyleAttr(int popupStyleAttr) { 842 mUseCustomPopupStyle = true; 843 mPopupStyleAttr = popupStyleAttr; 844 return this; 845 } 846 847 public Builder ignoreContentWidth() { 848 mIgnoreContentWidth = true; 849 return this; 850 } 851 852 public Builder setModal(boolean isModal) { 853 mIsModal = isModal; 854 return this; 855 } 856 857 public Builder withItemClickListener() { 858 mHasItemClickListener = true; 859 return this; 860 } 861 862 public Builder withItemSelectedListener() { 863 mHasItemSelectedListener = true; 864 return this; 865 } 866 867 public Builder withDismissListener() { 868 mHasDismissListener = true; 869 return this; 870 } 871 872 public Builder withWindowLayoutType(int windowLayoutType) { 873 mHasWindowLayoutType = true; 874 mWindowLayoutType = windowLayoutType; 875 return this; 876 } 877 878 public Builder withHorizontalOffset(int horizontalOffset) { 879 mHorizontalOffset = horizontalOffset; 880 return this; 881 } 882 883 public Builder withVerticalOffset(int verticalOffset) { 884 mVerticalOffset = verticalOffset; 885 return this; 886 } 887 888 public Builder withDropDownGravity(int dropDownGravity) { 889 mDropDownGravity = dropDownGravity; 890 return this; 891 } 892 893 public Builder withPrompt(View promptView, int promptPosition) { 894 mPromptView = promptView; 895 mPromptPosition = promptPosition; 896 return this; 897 } 898 899 private int getContentWidth(ListAdapter listAdapter, Drawable background) { 900 if (listAdapter == null) { 901 return 0; 902 } 903 904 int width = 0; 905 View itemView = null; 906 int itemType = 0; 907 908 for (int i = 0; i < listAdapter.getCount(); i++) { 909 final int positionType = listAdapter.getItemViewType(i); 910 if (positionType != itemType) { 911 itemType = positionType; 912 itemView = null; 913 } 914 itemView = listAdapter.getView(i, itemView, null); 915 if (itemView.getLayoutParams() == null) { 916 itemView.setLayoutParams(new ViewGroup.LayoutParams( 917 ViewGroup.LayoutParams.WRAP_CONTENT, 918 ViewGroup.LayoutParams.WRAP_CONTENT)); 919 } 920 itemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); 921 width = Math.max(width, itemView.getMeasuredWidth()); 922 } 923 924 // Add background padding to measured width 925 if (background != null) { 926 final Rect rect = new Rect(); 927 background.getPadding(rect); 928 width += rect.left + rect.right; 929 } 930 931 return width; 932 } 933 934 private void configure() { 935 if (mUseCustomPopupStyle) { 936 mPopupWindow = new ListPopupWindow(mActivity, null, mPopupStyleAttr, 0); 937 } else { 938 mPopupWindow = new ListPopupWindow(mActivity); 939 } 940 final String[] POPUP_CONTENT = 941 new String[]{"Alice", "Bob", "Charlie", "Deirdre", "El"}; 942 final BaseAdapter listPopupAdapter = new BaseAdapter() { 943 class ViewHolder { 944 private TextView title; 945 } 946 947 @Override 948 public int getCount() { 949 return POPUP_CONTENT.length; 950 } 951 952 @Override 953 public Object getItem(int position) { 954 return POPUP_CONTENT[position]; 955 } 956 957 @Override 958 public long getItemId(int position) { 959 return position; 960 } 961 962 @Override 963 public View getView(int position, View convertView, ViewGroup parent) { 964 if (convertView == null) { 965 convertView = LayoutInflater.from(mActivity).inflate( 966 mContentRowLayoutId, parent, false); 967 ViewHolder viewHolder = new ViewHolder(); 968 viewHolder.title = (TextView) convertView.findViewById(android.R.id.text1); 969 convertView.setTag(viewHolder); 970 } 971 972 ViewHolder viewHolder = (ViewHolder) convertView.getTag(); 973 viewHolder.title.setText(POPUP_CONTENT[position]); 974 return convertView; 975 } 976 }; 977 978 mPopupWindow.setAdapter(listPopupAdapter); 979 mPopupWindow.setAnchorView(mActivity.findViewById(mAnchorId)); 980 981 // The following mock listeners have to be set before the call to show() as 982 // they are set on the internally constructed drop down. 983 if (mHasItemClickListener) { 984 // Wrap our item click listener with a Mockito spy 985 mOnItemClickListener = spy(mItemClickListener); 986 // Register that spy as the item click listener on the ListPopupWindow 987 mPopupWindow.setOnItemClickListener(mOnItemClickListener); 988 // And configure Mockito to call our original listener with onItemClick. 989 // This way we can have both our item click listener running to dismiss the popup 990 // window, and track the invocations of onItemClick with Mockito APIs. 991 doCallRealMethod().when(mOnItemClickListener).onItemClick( 992 any(AdapterView.class), any(View.class), any(int.class), any(int.class)); 993 } 994 995 if (mHasItemSelectedListener) { 996 mOnItemSelectedListener = mock(AdapterView.OnItemSelectedListener.class); 997 mPopupWindow.setOnItemSelectedListener(mOnItemSelectedListener); 998 mPopupWindow.setListSelector( 999 mActivity.getDrawable(R.drawable.red_translucent_fill)); 1000 } 1001 1002 if (mHasDismissListener) { 1003 mOnDismissListener = mock(PopupWindow.OnDismissListener.class); 1004 mPopupWindow.setOnDismissListener(mOnDismissListener); 1005 } 1006 1007 mPopupWindow.setModal(mIsModal); 1008 if (mHasWindowLayoutType) { 1009 mPopupWindow.setWindowLayoutType(mWindowLayoutType); 1010 } 1011 1012 if (!mIgnoreContentWidth) { 1013 mPopupWindow.setContentWidth( 1014 getContentWidth(listPopupAdapter, mPopupWindow.getBackground())); 1015 } 1016 1017 if (mHorizontalOffset != 0) { 1018 mPopupWindow.setHorizontalOffset(mHorizontalOffset); 1019 } 1020 1021 if (mVerticalOffset != 0) { 1022 mPopupWindow.setVerticalOffset(mVerticalOffset); 1023 } 1024 1025 if (mDropDownGravity != Gravity.NO_GRAVITY) { 1026 mPopupWindow.setDropDownGravity(mDropDownGravity); 1027 } 1028 1029 if (mPromptView != null) { 1030 mPopupWindow.setPromptPosition(mPromptPosition); 1031 mPopupWindow.setPromptView(mPromptView); 1032 } 1033 } 1034 1035 private void show() { 1036 configure(); 1037 mPopupWindow.show(); 1038 assertTrue(mPopupWindow.isShowing()); 1039 } 1040 1041 private void showAgain() { 1042 if (mPopupWindow == null || mPopupWindow.isShowing()) { 1043 return; 1044 } 1045 mPopupWindow.show(); 1046 assertTrue(mPopupWindow.isShowing()); 1047 } 1048 1049 private void dismiss() { 1050 if (mPopupWindow == null || !mPopupWindow.isShowing()) 1051 return; 1052 mPopupWindow.dismiss(); 1053 } 1054 } 1055 } 1056