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