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