Home | History | Annotate | Download | only in cts
      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
     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
    103     public void setup() {
    104         mInstrumentation = InstrumentationRegistry.getInstrumentation();
    105         mActivity = mActivityRule.getActivity();
    106         mItemClickListener = new PopupItemClickListener();
    107     }
    108 
    109     @After
    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
    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
    138     public void testNoDefaultVisibility() {
    139         mPopupWindow = new ListPopupWindow(mActivity);
    140         assertFalse(mPopupWindow.isShowing());
    141     }
    142 
    143     @Test
    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
    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
    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      */
    209     private Display getDisplay() {
    210         WindowManager wm = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE);
    211         return wm.getDefaultDisplay();
    212     }
    213 
    214     @Test
    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 
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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
    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 
    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
    477     public void testDismissalOutsideNonModal() throws Throwable {
    478         verifyDismissalViaTouch(false);
    479     }
    480 
    481     @Test
    482     public void testDismissalOutsideModal() throws Throwable {
    483         verifyDismissalViaTouch(true);
    484     }
    485 
    486     @Test
    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
    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
    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
    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
    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
    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
    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
    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 
    828         public Builder() {
    829         }
    830 
    831         public Builder withAnchor(int anchorId) {
    832             mAnchorId = anchorId;
    833             return this;
    834         }
    835 
    836         public Builder withContentRowLayoutId(int contentRowLayoutId) {
    837             mContentRowLayoutId = contentRowLayoutId;
    838             return this;
    839         }
    840 
    841         public Builder withPopupStyleAttr(int popupStyleAttr) {
    842             mUseCustomPopupStyle = true;
    843             mPopupStyleAttr = popupStyleAttr;
    844             return this;
    845         }
    846 
    847         public Builder ignoreContentWidth() {
    848             mIgnoreContentWidth = true;
    849             return this;
    850         }
    851 
    852         public Builder setModal(boolean isModal) {
    853             mIsModal = isModal;
    854             return this;
    855         }
    856 
    857         public Builder withItemClickListener() {
    858             mHasItemClickListener = true;
    859             return this;
    860         }
    861 
    862         public Builder withItemSelectedListener() {
    863             mHasItemSelectedListener = true;
    864             return this;
    865         }
    866 
    867         public Builder withDismissListener() {
    868             mHasDismissListener = true;
    869             return this;
    870         }
    871 
    872         public Builder withWindowLayoutType(int windowLayoutType) {
    873             mHasWindowLayoutType = true;
    874             mWindowLayoutType = windowLayoutType;
    875             return this;
    876         }
    877 
    878         public Builder withHorizontalOffset(int horizontalOffset) {
    879             mHorizontalOffset = horizontalOffset;
    880             return this;
    881         }
    882 
    883         public Builder withVerticalOffset(int verticalOffset) {
    884             mVerticalOffset = verticalOffset;
    885             return this;
    886         }
    887 
    888         public Builder withDropDownGravity(int dropDownGravity) {
    889             mDropDownGravity = dropDownGravity;
    890             return this;
    891         }
    892 
    893         public Builder withPrompt(View promptView, int promptPosition) {
    894             mPromptView = promptView;
    895             mPromptPosition = promptPosition;
    896             return this;
    897         }
    898 
    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 
    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 
   1035         private void show() {
   1036             configure();
   1037             mPopupWindow.show();
   1038             assertTrue(mPopupWindow.isShowing());
   1039         }
   1040 
   1041         private void showAgain() {
   1042             if (mPopupWindow == null || mPopupWindow.isShowing()) {
   1043                 return;
   1044             }
   1045             mPopupWindow.show();
   1046             assertTrue(mPopupWindow.isShowing());
   1047         }
   1048 
   1049         private void dismiss() {
   1050             if (mPopupWindow == null || !mPopupWindow.isShowing())
   1051                 return;
   1052             mPopupWindow.dismiss();
   1053         }
   1054     }
   1055 }
   1056