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