1/* 2 * Copyright (C) 2016 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 */ 16package android.support.v7.widget; 17 18import static android.support.test.espresso.Espresso.onData; 19import static android.support.test.espresso.Espresso.onView; 20import static android.support.test.espresso.action.ViewActions.click; 21import static android.support.test.espresso.assertion.ViewAssertions.doesNotExist; 22import static android.support.test.espresso.assertion.ViewAssertions.matches; 23import static android.support.test.espresso.matcher.RootMatchers.isPlatformPopup; 24import static android.support.test.espresso.matcher.RootMatchers.withDecorView; 25import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 26import static android.support.test.espresso.matcher.ViewMatchers.withClassName; 27import static android.support.test.espresso.matcher.ViewMatchers.withId; 28import static android.support.test.espresso.matcher.ViewMatchers.withText; 29 30import static org.hamcrest.Matchers.allOf; 31import static org.hamcrest.Matchers.anything; 32import static org.hamcrest.core.Is.is; 33import static org.hamcrest.core.IsNot.not; 34import static org.junit.Assert.assertEquals; 35import static org.junit.Assert.assertNotNull; 36import static org.mockito.Mockito.any; 37import static org.mockito.Mockito.mock; 38import static org.mockito.Mockito.never; 39import static org.mockito.Mockito.times; 40import static org.mockito.Mockito.verify; 41 42import android.app.Instrumentation; 43import android.content.res.Resources; 44import android.graphics.Rect; 45import android.graphics.drawable.Drawable; 46import android.os.SystemClock; 47import android.support.test.InstrumentationRegistry; 48import android.support.test.espresso.Root; 49import android.support.test.espresso.UiController; 50import android.support.test.espresso.ViewAction; 51import android.support.v7.app.BaseInstrumentationTestCase; 52import android.support.v7.appcompat.test.R; 53import android.test.suitebuilder.annotation.SmallTest; 54import android.view.MenuInflater; 55import android.view.MenuItem; 56import android.view.MotionEvent; 57import android.view.View; 58import android.view.ViewParent; 59import android.widget.Button; 60import android.widget.FrameLayout; 61 62import org.hamcrest.Description; 63import org.hamcrest.Matcher; 64import org.hamcrest.Matchers; 65import org.hamcrest.TypeSafeMatcher; 66import org.junit.Before; 67import org.junit.Test; 68 69public class PopupMenuTest extends BaseInstrumentationTestCase<PopupTestActivity> { 70 // Since PopupMenu doesn't expose any access to the underlying 71 // implementation (like ListPopupWindow.getListView), we're relying on the 72 // class name of the list view from MenuPopupWindow that is being used 73 // in PopupMenu. This is not the cleanest, but it's not making any assumptions 74 // on the platform-specific details of the popup windows. 75 private static final String DROP_DOWN_CLASS_NAME = 76 "android.support.v7.widget.MenuPopupWindow$MenuDropDownListView"; 77 private FrameLayout mContainer; 78 79 private Button mButton; 80 81 private PopupMenu mPopupMenu; 82 83 private Resources mResources; 84 85 private View mMainDecorView; 86 87 public PopupMenuTest() { 88 super(PopupTestActivity.class); 89 } 90 91 @Before 92 public void setUp() throws Exception { 93 final PopupTestActivity activity = mActivityTestRule.getActivity(); 94 mContainer = (FrameLayout) activity.findViewById(R.id.container); 95 mButton = (Button) mContainer.findViewById(R.id.test_button); 96 mResources = mActivityTestRule.getActivity().getResources(); 97 mMainDecorView = mActivityTestRule.getActivity().getWindow().getDecorView(); 98 } 99 100 @Test 101 @SmallTest 102 public void testBasicContent() { 103 final Builder menuBuilder = new Builder(); 104 menuBuilder.wireToActionButton(); 105 106 onView(withId(R.id.test_button)).perform(click()); 107 assertNotNull("Popup menu created", mPopupMenu); 108 // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing. 109 // Use a custom matcher to check the visibility of the drop down list view instead. 110 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))) 111 .inRoot(isPlatformPopup()).check(matches(isDisplayed())); 112 113 // Note that MenuItem.isVisible() refers to the current "data" visibility state 114 // and not the "on screen" visibility state. This is why we're testing the display 115 // visibility of our main and sub menu items. 116 // onData(anything()).atPosition(x) operates on the content of the popup. It is used to 117 // scroll the popup if necessary to make sure the menu item views are visible. 118 onData(anything()).atPosition(0).check(matches(isDisplayed())); 119 onView(withText(mResources.getString(R.string.popup_menu_highlight))) 120 .inRoot(withDecorView(not(is(mMainDecorView)))) 121 .check(matches(isDisplayed())); 122 onData(anything()).atPosition(1).check(matches(isDisplayed())); 123 onView(withText(mResources.getString(R.string.popup_menu_edit))) 124 .inRoot(withDecorView(not(is(mMainDecorView)))) 125 .check(matches(isDisplayed())); 126 onData(anything()).atPosition(2).check(matches(isDisplayed())); 127 onView(withText(mResources.getString(R.string.popup_menu_delete))) 128 .inRoot(withDecorView(not(is(mMainDecorView)))) 129 .check(matches(isDisplayed())); 130 onData(anything()).atPosition(3).check(matches(isDisplayed())); 131 onView(withText(mResources.getString(R.string.popup_menu_ignore))) 132 .inRoot(withDecorView(not(is(mMainDecorView)))) 133 .check(matches(isDisplayed())); 134 onData(anything()).atPosition(4).check(matches(isDisplayed())); 135 onView(withText(mResources.getString(R.string.popup_menu_share))) 136 .inRoot(withDecorView(not(is(mMainDecorView)))) 137 .check(matches(isDisplayed())); 138 onData(anything()).atPosition(5).check(matches(isDisplayed())); 139 onView(withText(mResources.getString(R.string.popup_menu_print))) 140 .inRoot(withDecorView(not(is(mMainDecorView)))) 141 .check(matches(isDisplayed())); 142 143 // Share submenu items should not be visible 144 onView(withText(mResources.getString(R.string.popup_menu_share_email))) 145 .inRoot(withDecorView(not(is(mMainDecorView)))) 146 .check(doesNotExist()); 147 onView(withText(mResources.getString(R.string.popup_menu_share_circles))) 148 .inRoot(withDecorView(not(is(mMainDecorView)))) 149 .check(doesNotExist()); 150 } 151 152 /** 153 * Returns the location of our popup menu in its window. 154 */ 155 private int[] getPopupLocationInWindow() { 156 final int[] location = new int[2]; 157 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))) 158 .inRoot(isPlatformPopup()).perform(new ViewAction() { 159 @Override 160 public Matcher<View> getConstraints() { 161 return isDisplayed(); 162 } 163 164 @Override 165 public String getDescription() { 166 return "Popup matcher"; 167 } 168 169 @Override 170 public void perform(UiController uiController, View view) { 171 view.getLocationInWindow(location); 172 } 173 }); 174 return location; 175 } 176 177 /** 178 * Returns the location of our popup menu on the screen. 179 */ 180 private int[] getPopupLocationOnScreen() { 181 final int[] location = new int[2]; 182 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))) 183 .inRoot(isPlatformPopup()).perform(new ViewAction() { 184 @Override 185 public Matcher<View> getConstraints() { 186 return isDisplayed(); 187 } 188 189 @Override 190 public String getDescription() { 191 return "Popup matcher"; 192 } 193 194 @Override 195 public void perform(UiController uiController, View view) { 196 view.getLocationOnScreen(location); 197 } 198 }); 199 return location; 200 } 201 202 /** 203 * Returns the combined padding around the content of our popup menu. 204 */ 205 private Rect getPopupPadding() { 206 final Rect result = new Rect(); 207 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))) 208 .inRoot(isPlatformPopup()).perform(new ViewAction() { 209 @Override 210 public Matcher<View> getConstraints() { 211 return isDisplayed(); 212 } 213 214 @Override 215 public String getDescription() { 216 return "Popup matcher"; 217 } 218 219 @Override 220 public void perform(UiController uiController, View view) { 221 // Traverse the parent hierarchy and combine all their paddings 222 result.setEmpty(); 223 final Rect current = new Rect(); 224 while (true) { 225 ViewParent parent = view.getParent(); 226 if (parent == null || !(parent instanceof View)) { 227 return; 228 } 229 230 view = (View) parent; 231 Drawable currentBackground = view.getBackground(); 232 if (currentBackground != null) { 233 currentBackground.getPadding(current); 234 result.left += current.left; 235 result.right += current.right; 236 result.top += current.top; 237 result.bottom += current.bottom; 238 } 239 } 240 } 241 }); 242 return result; 243 } 244 245 /** 246 * Returns a root matcher that matches roots that have window focus on their decor view. 247 */ 248 private static Matcher<Root> hasWindowFocus() { 249 return new TypeSafeMatcher<Root>() { 250 @Override 251 public void describeTo(Description description) { 252 description.appendText("has window focus"); 253 } 254 255 @Override 256 public boolean matchesSafely(Root root) { 257 View rootView = root.getDecorView(); 258 return rootView.hasWindowFocus(); 259 } 260 }; 261 } 262 263 @Test 264 @SmallTest 265 public void testAnchoring() { 266 Builder menuBuilder = new Builder(); 267 menuBuilder.wireToActionButton(); 268 269 onView(withId(R.id.test_button)).perform(click()); 270 271 final int[] anchorOnScreenXY = new int[2]; 272 final int[] popupOnScreenXY = getPopupLocationOnScreen(); 273 final int[] popupInWindowXY = getPopupLocationInWindow(); 274 final Rect popupPadding = getPopupPadding(); 275 276 mButton.getLocationOnScreen(anchorOnScreenXY); 277 278 // Allow for off-by-one mismatch in anchoring 279 assertEquals("Anchoring X", anchorOnScreenXY[0] + popupInWindowXY[0], 280 popupOnScreenXY[0], 1); 281 assertEquals("Anchoring Y", 282 anchorOnScreenXY[1] + popupInWindowXY[1] + mButton.getHeight() - popupPadding.top, 283 popupOnScreenXY[1], 1); 284 } 285 286 @Test 287 @SmallTest 288 public void testDismissalViaAPI() { 289 Builder menuBuilder = new Builder().withDismissListener(); 290 menuBuilder.wireToActionButton(); 291 292 onView(withId(R.id.test_button)).perform(click()); 293 294 // Since PopupMenu is not a View, we can't use Espresso's view actions to invoke 295 // the dismiss() API 296 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 297 @Override 298 public void run() { 299 mPopupMenu.dismiss(); 300 } 301 }); 302 303 verify(menuBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu); 304 305 // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing. 306 // Use a custom matcher to check the visibility of the drop down list view instead. 307 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist()); 308 } 309 310 @Test 311 @SmallTest 312 public void testDismissalViaTouch() throws Throwable { 313 Builder menuBuilder = new Builder().withDismissListener(); 314 menuBuilder.wireToActionButton(); 315 316 onView(withId(R.id.test_button)).perform(click()); 317 318 // Determine the location of the popup on the screen so that we can emulate 319 // a tap outside of its bounds to dismiss it 320 final int[] popupOnScreenXY = getPopupLocationOnScreen(); 321 final Rect popupPadding = getPopupPadding(); 322 323 324 int emulatedTapX = popupOnScreenXY[0] - popupPadding.left - 20; 325 int emulatedTapY = popupOnScreenXY[1] + popupPadding.top + 20; 326 327 // The logic below uses Instrumentation to emulate a tap outside the bounds of the 328 // displayed popup menu. This tap is then treated by the framework to be "split" as 329 // the ACTION_OUTSIDE for the popup itself, as well as DOWN / MOVE / UP for the underlying 330 // view root if the popup is not modal. 331 // It is not correct to emulate these two sequences separately in the test, as it 332 // wouldn't emulate the user-facing interaction for this test. Note that usage 333 // of Instrumentation is necessary here since Espresso's actions operate at the level 334 // of view or data. Also, we don't want to use View.dispatchTouchEvent directly as 335 // that would require emulation of two separate sequences as well. 336 337 Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); 338 339 // Inject DOWN event 340 long downTime = SystemClock.uptimeMillis(); 341 MotionEvent eventDown = MotionEvent.obtain( 342 downTime, downTime, MotionEvent.ACTION_DOWN, emulatedTapX, emulatedTapY, 1); 343 instrumentation.sendPointerSync(eventDown); 344 345 // Inject MOVE event 346 long moveTime = SystemClock.uptimeMillis(); 347 MotionEvent eventMove = MotionEvent.obtain( 348 moveTime, moveTime, MotionEvent.ACTION_MOVE, emulatedTapX, emulatedTapY, 1); 349 instrumentation.sendPointerSync(eventMove); 350 351 // Inject UP event 352 long upTime = SystemClock.uptimeMillis(); 353 MotionEvent eventUp = MotionEvent.obtain( 354 upTime, upTime, MotionEvent.ACTION_UP, emulatedTapX, emulatedTapY, 1); 355 instrumentation.sendPointerSync(eventUp); 356 357 // Wait for the system to process all events in the queue 358 instrumentation.waitForIdleSync(); 359 360 // At this point our popup should not be showing and should have notified its 361 // dismiss listener 362 verify(menuBuilder.mOnDismissListener, times(1)).onDismiss(mPopupMenu); 363 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist()); 364 } 365 366 @Test 367 @SmallTest 368 public void testSimpleMenuItemClickViaEvent() { 369 Builder menuBuilder = new Builder().withMenuItemClickListener(); 370 menuBuilder.wireToActionButton(); 371 372 onView(withId(R.id.test_button)).perform(click()); 373 374 // Verify that our menu item click listener hasn't been called yet 375 verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class)); 376 377 onView(withText(mResources.getString(R.string.popup_menu_delete))) 378 .inRoot(withDecorView(not(is(mMainDecorView)))) 379 .perform(click()); 380 381 // Verify that out menu item click listener has been called with the expected menu item 382 verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick( 383 mPopupMenu.getMenu().findItem(R.id.action_delete)); 384 385 // Popup menu should be automatically dismissed on selecting an item 386 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist()); 387 } 388 389 @Test 390 @SmallTest 391 public void testSimpleMenuItemClickViaAPI() { 392 Builder menuBuilder = new Builder().withMenuItemClickListener(); 393 menuBuilder.wireToActionButton(); 394 395 onView(withId(R.id.test_button)).perform(click()); 396 397 // Verify that our menu item click listener hasn't been called yet 398 verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class)); 399 400 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 401 @Override 402 public void run() { 403 mPopupMenu.getMenu().performIdentifierAction(R.id.action_highlight, 0); 404 } 405 }); 406 407 // Verify that out menu item click listener has been called with the expected menu item 408 verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick( 409 mPopupMenu.getMenu().findItem(R.id.action_highlight)); 410 411 // Popup menu should be automatically dismissed on selecting an item 412 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist()); 413 } 414 415 @Test 416 @SmallTest 417 public void testSubMenuClicksViaEvent() throws Throwable { 418 Builder menuBuilder = new Builder().withMenuItemClickListener(); 419 menuBuilder.wireToActionButton(); 420 421 onView(withId(R.id.test_button)).perform(click()); 422 423 // Verify that our menu item click listener hasn't been called yet 424 verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class)); 425 426 onView(withText(mResources.getString(R.string.popup_menu_share))) 427 .inRoot(withDecorView(not(is(mMainDecorView)))) 428 .perform(click()); 429 430 // Verify that out menu item click listener has been called with the expected menu item 431 verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick( 432 mPopupMenu.getMenu().findItem(R.id.action_share)); 433 434 // Sleep for a bit to allow the menu -> submenu transition to complete 435 Thread.sleep(1000); 436 437 // At this point we should now have our sub-menu displayed. At this point on newer 438 // platform versions (L+) we have two view roots on the screen - one for the main popup 439 // menu and one for the submenu that has just been activated. If we only use the 440 // logic based on decor view, Espresso will time out on waiting for the first root 441 // to acquire window focus. This is why from this point on in this test we are using 442 // two root conditions to detect the submenu - one with decor view not being the same 443 // as the decor view of our main activity window, and the other that checks for window 444 // focus. 445 446 // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing. 447 // Use a custom matcher to check the visibility of the drop down list view instead. 448 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))) 449 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 450 .check(matches(isDisplayed())); 451 452 // Note that MenuItem.isVisible() refers to the current "data" visibility state 453 // and not the "on screen" visibility state. This is why we're testing the display 454 // visibility of our main and sub menu items. 455 456 // Share submenu items should now be visible 457 onView(withText(mResources.getString(R.string.popup_menu_share_email))) 458 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 459 .check(matches(isDisplayed())); 460 onView(withText(mResources.getString(R.string.popup_menu_share_circles))) 461 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 462 .check(matches(isDisplayed())); 463 464 // Now click an item in the sub-menu 465 onView(withText(mResources.getString(R.string.popup_menu_share_circles))) 466 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 467 .perform(click()); 468 469 // Verify that out menu item click listener has been called with the expected menu item 470 verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick( 471 mPopupMenu.getMenu().findItem(R.id.action_share_circles)); 472 473 // Popup menu should be automatically dismissed on selecting an item in the submenu 474 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist()); 475 } 476 477 @Test 478 @SmallTest 479 public void testSubMenuClicksViaAPI() throws Throwable { 480 Builder menuBuilder = new Builder().withMenuItemClickListener(); 481 menuBuilder.wireToActionButton(); 482 483 onView(withId(R.id.test_button)).perform(click()); 484 485 // Verify that our menu item click listener hasn't been called yet 486 verify(menuBuilder.mOnMenuItemClickListener, never()).onMenuItemClick(any(MenuItem.class)); 487 488 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 489 @Override 490 public void run() { 491 mPopupMenu.getMenu().performIdentifierAction(R.id.action_share, 0); 492 } 493 }); 494 495 // Verify that out menu item click listener has been called with the expected menu item 496 verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick( 497 mPopupMenu.getMenu().findItem(R.id.action_share)); 498 499 // Sleep for a bit to allow the menu -> submenu transition to complete 500 Thread.sleep(1000); 501 502 // At this point we should now have our sub-menu displayed. At this point on newer 503 // platform versions (L+) we have two view roots on the screen - one for the main popup 504 // menu and one for the submenu that has just been activated. If we only use the 505 // logic based on decor view, Espresso will time out on waiting for the first root 506 // to acquire window focus. This is why from this point on in this test we are using 507 // two root conditions to detect the submenu - one with decor view not being the same 508 // as the decor view of our main activity window, and the other that checks for window 509 // focus. 510 511 // Unlike ListPopupWindow, PopupMenu doesn't have an API to check whether it is showing. 512 // Use a custom matcher to check the visibility of the drop down list view instead. 513 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))) 514 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 515 .check(matches(isDisplayed())); 516 517 // Note that MenuItem.isVisible() refers to the current "data" visibility state 518 // and not the "on screen" visibility state. This is why we're testing the display 519 // visibility of our main and sub menu items. 520 521 // Share submenu items should now be visible 522 onView(withText(mResources.getString(R.string.popup_menu_share_email))) 523 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 524 .check(matches(isDisplayed())); 525 onView(withText(mResources.getString(R.string.popup_menu_share_circles))) 526 .inRoot(allOf(withDecorView(not(is(mMainDecorView))), hasWindowFocus())) 527 .check(matches(isDisplayed())); 528 529 // Now ask the share submenu to perform an action on its specific menu item 530 InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { 531 @Override 532 public void run() { 533 mPopupMenu.getMenu().findItem(R.id.action_share).getSubMenu(). 534 performIdentifierAction(R.id.action_share_email, 0); 535 } 536 }); 537 538 // Verify that out menu item click listener has been called with the expected menu item 539 verify(menuBuilder.mOnMenuItemClickListener, times(1)).onMenuItemClick( 540 mPopupMenu.getMenu().findItem(R.id.action_share_email)); 541 542 // Popup menu should be automatically dismissed on selecting an item in the submenu 543 onView(withClassName(Matchers.is(DROP_DOWN_CLASS_NAME))).check(doesNotExist()); 544 } 545 546 /** 547 * Inner helper class to configure an instance of <code>PopupMenu</code> for the 548 * specific test. The main reason for its existence is that once a popup menu is shown 549 * with the show() method, most of its configuration APIs are no-ops. This means that 550 * we can't add logic that is specific to a certain test once it's shown and we have a 551 * reference to a displayed PopupMenu. 552 */ 553 public class Builder { 554 private boolean mHasDismissListener; 555 private boolean mHasMenuItemClickListener; 556 557 private PopupMenu.OnMenuItemClickListener mOnMenuItemClickListener; 558 private PopupMenu.OnDismissListener mOnDismissListener; 559 560 public Builder withMenuItemClickListener() { 561 mHasMenuItemClickListener = true; 562 return this; 563 } 564 565 public Builder withDismissListener() { 566 mHasDismissListener = true; 567 return this; 568 } 569 570 private void show() { 571 mPopupMenu = new PopupMenu(mContainer.getContext(), mButton); 572 final MenuInflater menuInflater = mPopupMenu.getMenuInflater(); 573 menuInflater.inflate(R.menu.popup_menu, mPopupMenu.getMenu()); 574 575 if (mHasMenuItemClickListener) { 576 // Register a mock listener to be notified when a menu item in our popup menu has 577 // been clicked. 578 mOnMenuItemClickListener = mock(PopupMenu.OnMenuItemClickListener.class); 579 mPopupMenu.setOnMenuItemClickListener(mOnMenuItemClickListener); 580 } 581 582 if (mHasDismissListener) { 583 // Register a mock listener to be notified when our popup menu is dismissed. 584 mOnDismissListener = mock(PopupMenu.OnDismissListener.class); 585 mPopupMenu.setOnDismissListener(mOnDismissListener); 586 } 587 588 // Show the popup menu 589 mPopupMenu.show(); 590 } 591 592 public void wireToActionButton() { 593 mButton.setOnClickListener(new View.OnClickListener() { 594 @Override 595 public void onClick(View v) { 596 show(); 597 } 598 }); 599 } 600 } 601} 602