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