blob: a1bc0aa9958e8d54d12cd1d8bfc7478c8879475c [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.widget.cts;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.app.Activity;
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.platform.test.annotations.Presubmit;
import android.view.Display;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListPopupWindow;
import android.widget.ListView;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.LargeTest;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.CtsKeyEventUtil;
import com.android.compatibility.common.util.CtsTouchUtils;
import com.android.compatibility.common.util.WidgetTestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@LargeTest
@RunWith(AndroidJUnit4.class)
public class ListPopupWindowTest {
private Instrumentation mInstrumentation;
private Activity mActivity;
private Builder mPopupWindowBuilder;
private View promptView;
/** The list popup window. */
private ListPopupWindow mPopupWindow;
private AdapterView.OnItemClickListener mItemClickListener;
/**
* Item click listener that dismisses our <code>ListPopupWindow</code> when any item
* is clicked. Note that this needs to be a separate class that is also protected (not
* private) so that Mockito can "spy" on it.
*/
protected class PopupItemClickListener implements AdapterView.OnItemClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position,
long id) {
mPopupWindow.dismiss();
}
}
@Rule
public ActivityTestRule<ListPopupWindowCtsActivity> mActivityRule
= new ActivityTestRule<>(ListPopupWindowCtsActivity.class);
@Before
public void setup() {
mInstrumentation = InstrumentationRegistry.getInstrumentation();
mActivity = mActivityRule.getActivity();
mItemClickListener = new PopupItemClickListener();
}
@After
public void teardown() throws Throwable {
if ((mPopupWindowBuilder != null) && (mPopupWindow != null)) {
mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss);
mInstrumentation.waitForIdleSync();
}
}
@Test
public void testConstructor() {
new ListPopupWindow(mActivity);
new ListPopupWindow(mActivity, null);
new ListPopupWindow(mActivity, null, android.R.attr.popupWindowStyle);
new ListPopupWindow(mActivity, null, 0,
android.R.style.Widget_DeviceDefault_ListPopupWindow);
new ListPopupWindow(mActivity, null, 0,
android.R.style.Widget_DeviceDefault_Light_ListPopupWindow);
new ListPopupWindow(mActivity, null, 0, android.R.style.Widget_Material_ListPopupWindow);
new ListPopupWindow(mActivity, null, 0,
android.R.style.Widget_Material_Light_ListPopupWindow);
}
@Test
public void testNoDefaultVisibility() {
mPopupWindow = new ListPopupWindow(mActivity);
assertFalse(mPopupWindow.isShowing());
}
@Test
public void testAccessBackground() throws Throwable {
mPopupWindowBuilder = new Builder();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
Drawable drawable = new ColorDrawable();
mPopupWindow.setBackgroundDrawable(drawable);
assertSame(drawable, mPopupWindow.getBackground());
mPopupWindow.setBackgroundDrawable(null);
assertNull(mPopupWindow.getBackground());
}
@Test
public void testAccessAnimationStyle() throws Throwable {
mPopupWindowBuilder = new Builder();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(0, mPopupWindow.getAnimationStyle());
mPopupWindow.setAnimationStyle(android.R.style.Animation_Toast);
assertEquals(android.R.style.Animation_Toast, mPopupWindow.getAnimationStyle());
// abnormal values
mPopupWindow.setAnimationStyle(-100);
assertEquals(-100, mPopupWindow.getAnimationStyle());
}
@Test
public void testAccessHeight() throws Throwable {
mPopupWindowBuilder = new Builder();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(WindowManager.LayoutParams.WRAP_CONTENT, mPopupWindow.getHeight());
int height = getDisplay().getHeight() / 2;
mPopupWindow.setHeight(height);
assertEquals(height, mPopupWindow.getHeight());
height = getDisplay().getHeight();
mPopupWindow.setHeight(height);
assertEquals(height, mPopupWindow.getHeight());
mPopupWindow.setHeight(0);
assertEquals(0, mPopupWindow.getHeight());
height = getDisplay().getHeight() * 2;
mPopupWindow.setHeight(height);
assertEquals(height, mPopupWindow.getHeight());
height = -getDisplay().getHeight() / 2;
try {
mPopupWindow.setHeight(height);
fail("should throw IllegalArgumentException for negative height.");
} catch (IllegalArgumentException e) {
// expected exception.
}
}
/**
* Gets the display.
*
* @return the display
*/
private Display getDisplay() {
WindowManager wm = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE);
return wm.getDefaultDisplay();
}
@Test
public void testAccessWidth() throws Throwable {
mPopupWindowBuilder = new Builder().ignoreContentWidth();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(WindowManager.LayoutParams.WRAP_CONTENT, mPopupWindow.getWidth());
int width = getDisplay().getWidth() / 2;
mPopupWindow.setWidth(width);
assertEquals(width, mPopupWindow.getWidth());
width = getDisplay().getWidth();
mPopupWindow.setWidth(width);
assertEquals(width, mPopupWindow.getWidth());
mPopupWindow.setWidth(0);
assertEquals(0, mPopupWindow.getWidth());
width = getDisplay().getWidth() * 2;
mPopupWindow.setWidth(width);
assertEquals(width, mPopupWindow.getWidth());
width = - getDisplay().getWidth() / 2;
mPopupWindow.setWidth(width);
assertEquals(width, mPopupWindow.getWidth());
}
private void verifyAnchoring(int horizontalOffset, int verticalOffset, int gravity) {
final View upperAnchor = mActivity.findViewById(R.id.anchor_upper);
final ListView listView = mPopupWindow.getListView();
int[] anchorXY = new int[2];
int[] listViewOnScreenXY = new int[2];
int[] listViewInWindowXY = new int[2];
assertTrue(mPopupWindow.isShowing());
assertEquals(upperAnchor, mPopupWindow.getAnchorView());
listView.getLocationOnScreen(listViewOnScreenXY);
upperAnchor.getLocationOnScreen(anchorXY);
listView.getLocationInWindow(listViewInWindowXY);
int expectedListViewOnScreenX = anchorXY[0] + listViewInWindowXY[0] + horizontalOffset;
final int absoluteGravity =
Gravity.getAbsoluteGravity(gravity, upperAnchor.getLayoutDirection());
if (absoluteGravity == Gravity.RIGHT) {
expectedListViewOnScreenX -= (listView.getWidth() - upperAnchor.getWidth());
} else {
// On narrow screens, it's possible for the popup to reach the edge
// of the screen.
int rightmostX =
getDisplay().getWidth() - mPopupWindow.getWidth() + listViewInWindowXY[0];
if (expectedListViewOnScreenX > rightmostX) {
expectedListViewOnScreenX = rightmostX;
}
}
int expectedListViewOnScreenY = anchorXY[1] + listViewInWindowXY[1]
+ upperAnchor.getHeight() + verticalOffset;
assertEquals(expectedListViewOnScreenX, listViewOnScreenXY[0]);
assertEquals(expectedListViewOnScreenY, listViewOnScreenXY[1]);
}
@Test
public void testAnchoring() throws Throwable {
mPopupWindowBuilder = new Builder();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(0, mPopupWindow.getHorizontalOffset());
assertEquals(0, mPopupWindow.getVerticalOffset());
verifyAnchoring(0, 0, Gravity.NO_GRAVITY);
}
@Test
public void testAnchoringWithHorizontalOffset() throws Throwable {
mPopupWindowBuilder = new Builder().withHorizontalOffset(50);
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(50, mPopupWindow.getHorizontalOffset());
assertEquals(0, mPopupWindow.getVerticalOffset());
verifyAnchoring(50, 0, Gravity.NO_GRAVITY);
}
@Test
public void testAnchoringWithVerticalOffset() throws Throwable {
mPopupWindowBuilder = new Builder().withVerticalOffset(60);
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(0, mPopupWindow.getHorizontalOffset());
assertEquals(60, mPopupWindow.getVerticalOffset());
verifyAnchoring(0, 60, Gravity.NO_GRAVITY);
}
@Test
public void testAnchoringWithRightGravity() throws Throwable {
mPopupWindowBuilder = new Builder().withDropDownGravity(Gravity.RIGHT);
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(0, mPopupWindow.getHorizontalOffset());
assertEquals(0, mPopupWindow.getVerticalOffset());
verifyAnchoring(0, 0, Gravity.RIGHT);
}
@Test
public void testAnchoringWithEndGravity() throws Throwable {
mPopupWindowBuilder = new Builder().withDropDownGravity(Gravity.END);
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(0, mPopupWindow.getHorizontalOffset());
assertEquals(0, mPopupWindow.getVerticalOffset());
verifyAnchoring(0, 0, Gravity.END);
}
@Test
public void testSetWindowLayoutType() throws Throwable {
mPopupWindowBuilder = new Builder().withWindowLayoutType(
WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertTrue(mPopupWindow.isShowing());
WindowManager.LayoutParams p = (WindowManager.LayoutParams)
mPopupWindow.getListView().getRootView().getLayoutParams();
assertEquals(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL, p.type);
}
@Test
public void testDismiss() throws Throwable {
mPopupWindowBuilder = new Builder();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertTrue(mPopupWindow.isShowing());
mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss);
mInstrumentation.waitForIdleSync();
assertFalse(mPopupWindow.isShowing());
mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss);
mInstrumentation.waitForIdleSync();
assertFalse(mPopupWindow.isShowing());
}
@Test
public void testSetOnDismissListener() throws Throwable {
mPopupWindowBuilder = new Builder().withDismissListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss);
mInstrumentation.waitForIdleSync();
verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss();
mActivityRule.runOnUiThread(mPopupWindowBuilder::showAgain);
mInstrumentation.waitForIdleSync();
mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss);
mInstrumentation.waitForIdleSync();
verify(mPopupWindowBuilder.mOnDismissListener, times(2)).onDismiss();
mPopupWindow.setOnDismissListener(null);
mActivityRule.runOnUiThread(mPopupWindowBuilder::showAgain);
mInstrumentation.waitForIdleSync();
mActivityRule.runOnUiThread(mPopupWindowBuilder::dismiss);
mInstrumentation.waitForIdleSync();
// Since we've reset the listener to null, we are not expecting any more interactions
// on the previously registered listener.
verifyNoMoreInteractions(mPopupWindowBuilder.mOnDismissListener);
}
@Test
public void testAccessInputMethodMode() throws Throwable {
mPopupWindowBuilder = new Builder().withDismissListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertEquals(PopupWindow.INPUT_METHOD_NEEDED, mPopupWindow.getInputMethodMode());
assertFalse(mPopupWindow.isInputMethodNotNeeded());
mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_FROM_FOCUSABLE);
assertEquals(PopupWindow.INPUT_METHOD_FROM_FOCUSABLE, mPopupWindow.getInputMethodMode());
assertFalse(mPopupWindow.isInputMethodNotNeeded());
mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
assertEquals(PopupWindow.INPUT_METHOD_NEEDED, mPopupWindow.getInputMethodMode());
assertFalse(mPopupWindow.isInputMethodNotNeeded());
mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
assertEquals(PopupWindow.INPUT_METHOD_NOT_NEEDED, mPopupWindow.getInputMethodMode());
assertTrue(mPopupWindow.isInputMethodNotNeeded());
mPopupWindow.setInputMethodMode(-1);
assertEquals(-1, mPopupWindow.getInputMethodMode());
assertFalse(mPopupWindow.isInputMethodNotNeeded());
}
@Test
public void testAccessSoftInputMethodMode() throws Throwable {
mPopupWindowBuilder = new Builder().withDismissListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
mPopupWindow = new ListPopupWindow(mActivity);
assertEquals(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED,
mPopupWindow.getSoftInputMode());
mPopupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
assertEquals(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE,
mPopupWindow.getSoftInputMode());
mPopupWindow.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
assertEquals(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE,
mPopupWindow.getSoftInputMode());
}
private void verifyDismissalViaTouch(boolean setupAsModal) throws Throwable {
// Register a click listener on the top-level container
final View mainContainer = mActivity.findViewById(R.id.main_container);
final View.OnClickListener mockContainerClickListener = mock(View.OnClickListener.class);
mActivityRule.runOnUiThread(() ->
mainContainer.setOnClickListener(mockContainerClickListener));
// Configure a list popup window with requested modality
mPopupWindowBuilder = new Builder().setModal(setupAsModal).withDismissListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
assertTrue("Popup window showing", mPopupWindow.isShowing());
// Make sure that the modality of the popup window is set up correctly
assertEquals("Popup window modality", setupAsModal, mPopupWindow.isModal());
// The logic below uses Instrumentation to emulate a tap outside the bounds of the
// displayed list popup window. This tap is then treated by the framework to be "split" as
// the ACTION_OUTSIDE for the popup itself, as well as DOWN / MOVE / UP for the underlying
// view root if the popup is not modal.
// It is not correct to emulate these two sequences separately in the test, as it
// wouldn't emulate the user-facing interaction for this test. Also, we don't want to use
// View.dispatchTouchEvent directly as that would require emulation of two separate
// sequences as well.
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
final ListView popupListView = mPopupWindow.getListView();
final Rect rect = new Rect();
mPopupWindow.getBackground().getPadding(rect);
CtsTouchUtils.emulateTapOnView(instrumentation, popupListView,
-rect.left - 20, popupListView.getHeight() + rect.top + rect.bottom + 20);
// At this point our popup should not be showing and should have notified its
// dismiss listener
verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss();
assertFalse("Popup window not showing after outside click", mPopupWindow.isShowing());
// Also test that the click outside the popup bounds has been "delivered" to the main
// container only if the popup is not modal
verify(mockContainerClickListener, times(setupAsModal ? 0 : 1)).onClick(mainContainer);
}
@Test
public void testDismissalOutsideNonModal() throws Throwable {
verifyDismissalViaTouch(false);
}
@Test
public void testDismissalOutsideModal() throws Throwable {
verifyDismissalViaTouch(true);
}
@Test
public void testItemClicks() throws Throwable {
mPopupWindowBuilder = new Builder().withItemClickListener().withDismissListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
mActivityRule.runOnUiThread(() -> mPopupWindow.performItemClick(2));
mInstrumentation.waitForIdleSync();
verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick(
any(AdapterView.class), any(View.class), eq(2), eq(2L));
// Also verify that the popup window has been dismissed
assertFalse(mPopupWindow.isShowing());
verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss();
mActivityRule.runOnUiThread(mPopupWindowBuilder::showAgain);
mInstrumentation.waitForIdleSync();
mActivityRule.runOnUiThread(
() -> mPopupWindow.getListView().performItemClick(null, 1, 1));
mInstrumentation.waitForIdleSync();
verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick(
any(AdapterView.class), any(), eq(1), eq(1L));
// Also verify that the popup window has been dismissed
assertFalse(mPopupWindow.isShowing());
verify(mPopupWindowBuilder.mOnDismissListener, times(2)).onDismiss();
// Finally verify that our item click listener has only been called twice
verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemClickListener);
}
@Test
public void testPromptViewAbove() throws Throwable {
mActivityRule.runOnUiThread(() -> {
promptView = LayoutInflater.from(mActivity).inflate(R.layout.popupwindow_prompt, null);
mPopupWindowBuilder = new Builder().withPrompt(
promptView, ListPopupWindow.POSITION_PROMPT_ABOVE);
mPopupWindowBuilder.show();
});
mInstrumentation.waitForIdleSync();
// Verify that our prompt is displayed on the screen and is above the first list item
assertTrue(promptView.isAttachedToWindow());
assertTrue(promptView.isShown());
assertEquals(ListPopupWindow.POSITION_PROMPT_ABOVE, mPopupWindow.getPromptPosition());
final ListView listView = mPopupWindow.getListView();
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, null);
final int[] promptViewOnScreenXY = new int[2];
final int[] firstChildOnScreenXY = new int[2];
mActivityRule.runOnUiThread(()-> {
promptView.getLocationOnScreen(promptViewOnScreenXY);
final View firstListChild = listView.getChildAt(0);
firstListChild.getLocationOnScreen(firstChildOnScreenXY);
});
mInstrumentation.waitForIdleSync();
assertTrue(promptViewOnScreenXY[1] + promptView.getHeight() <= firstChildOnScreenXY[1]);
}
@Test
public void testPromptViewBelow() throws Throwable {
mActivityRule.runOnUiThread(() -> {
promptView = LayoutInflater.from(mActivity).inflate(R.layout.popupwindow_prompt, null);
mPopupWindowBuilder = new Builder().withPrompt(
promptView, ListPopupWindow.POSITION_PROMPT_BELOW);
mPopupWindowBuilder.show();
});
mInstrumentation.waitForIdleSync();
// Verify that our prompt is displayed on the screen and is below the last list item
assertTrue(promptView.isAttachedToWindow());
assertTrue(promptView.isShown());
assertEquals(ListPopupWindow.POSITION_PROMPT_BELOW, mPopupWindow.getPromptPosition());
final ListView listView = mPopupWindow.getListView();
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView, null);
final int[] promptViewOnScreenXY = new int[2];
final int[] lastChildOnScreenXY = new int[2];
mActivityRule.runOnUiThread(()-> {
promptView.getLocationOnScreen(promptViewOnScreenXY);
final View lastListChild = listView.getChildAt(listView.getChildCount() - 1);
lastListChild.getLocationOnScreen(lastChildOnScreenXY);
});
mInstrumentation.waitForIdleSync();
// The child is above the prompt. They may overlap, as in the case
// when the list items do not all fit on screen, but this is still
// correct.
assertTrue(lastChildOnScreenXY[1] <= promptViewOnScreenXY[1]);
}
@Presubmit
@Test
public void testAccessSelection() throws Throwable {
mPopupWindowBuilder = new Builder().withItemSelectedListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
final ListView listView = mPopupWindow.getListView();
// Select an item
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
() -> mPopupWindow.setSelection(1));
// And verify the current selection state + selection listener invocation
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected(
any(AdapterView.class), any(View.class), eq(1), eq(1L));
assertEquals(1, mPopupWindow.getSelectedItemId());
assertEquals(1, mPopupWindow.getSelectedItemPosition());
assertEquals("Bob", mPopupWindow.getSelectedItem());
View selectedView = mPopupWindow.getSelectedView();
assertNotNull(selectedView);
assertEquals("Bob",
((TextView) selectedView.findViewById(android.R.id.text1)).getText());
// Select another item
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
() -> mPopupWindow.setSelection(3));
// And verify the new selection state + selection listener invocation
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected(
any(AdapterView.class), any(View.class), eq(3), eq(3L));
assertEquals(3, mPopupWindow.getSelectedItemId());
assertEquals(3, mPopupWindow.getSelectedItemPosition());
assertEquals("Deirdre", mPopupWindow.getSelectedItem());
selectedView = mPopupWindow.getSelectedView();
assertNotNull(selectedView);
assertEquals("Deirdre",
((TextView) selectedView.findViewById(android.R.id.text1)).getText());
// Clear selection
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
mPopupWindow::clearListSelection);
// And verify empty selection state + no more selection listener invocation
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onNothingSelected(
any(AdapterView.class));
assertEquals(AdapterView.INVALID_ROW_ID, mPopupWindow.getSelectedItemId());
assertEquals(AdapterView.INVALID_POSITION, mPopupWindow.getSelectedItemPosition());
assertEquals(null, mPopupWindow.getSelectedItem());
assertEquals(null, mPopupWindow.getSelectedView());
verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemSelectedListener);
}
@Test
public void testNoDefaultDismissalWithBackButton() throws Throwable {
mPopupWindowBuilder = new Builder().withDismissListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
// Send BACK key event. As we don't have any custom code that dismisses ListPopupWindow,
// and ListPopupWindow doesn't track that system-level key event on its own, ListPopupWindow
// should stay visible
mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
verify(mPopupWindowBuilder.mOnDismissListener, never()).onDismiss();
assertTrue(mPopupWindow.isShowing());
}
@Test
public void testCustomDismissalWithBackButton() throws Throwable {
mActivityRule.runOnUiThread(() -> {
mPopupWindowBuilder = new Builder().withAnchor(R.id.anchor_upper_left)
.withDismissListener();
mPopupWindowBuilder.show();
});
mInstrumentation.waitForIdleSync();
// "Point" our custom extension of EditText to our ListPopupWindow
final MockViewForListPopupWindow anchor =
(MockViewForListPopupWindow) mPopupWindow.getAnchorView();
anchor.wireTo(mPopupWindow);
// Request focus on our EditText
mActivityRule.runOnUiThread(anchor::requestFocus);
mInstrumentation.waitForIdleSync();
assertTrue(anchor.isFocused());
// Send BACK key event. As our custom extension of EditText calls
// ListPopupWindow.onKeyPreIme, the end result should be the dismissal of the
// ListPopupWindow
mInstrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);
verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss();
assertFalse(mPopupWindow.isShowing());
}
@Test
public void testListSelectionWithDPad() throws Throwable {
mPopupWindowBuilder = new Builder().withAnchor(R.id.anchor_upper_left)
.withDismissListener().withItemSelectedListener();
mActivityRule.runOnUiThread(mPopupWindowBuilder::show);
mInstrumentation.waitForIdleSync();
final View root = mPopupWindow.getListView().getRootView();
// "Point" our custom extension of EditText to our ListPopupWindow
final MockViewForListPopupWindow anchor =
(MockViewForListPopupWindow) mPopupWindow.getAnchorView();
anchor.wireTo(mPopupWindow);
// Request focus on our EditText
mActivityRule.runOnUiThread(anchor::requestFocus);
mInstrumentation.waitForIdleSync();
assertTrue(anchor.isFocused());
// Select entry #1 in the popup list
final ListView listView = mPopupWindow.getListView();
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, listView,
() -> mPopupWindow.setSelection(1));
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected(
any(AdapterView.class), any(View.class), eq(1), eq(1L));
// Send DPAD_DOWN key event. As our custom extension of EditText calls
// ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection
// down one row
CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_DOWN);
mInstrumentation.waitForIdleSync();
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null);
// At this point we expect that item #2 was selected
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected(
any(AdapterView.class), any(View.class), eq(2), eq(2L));
// Send a DPAD_UP key event. As our custom extension of EditText calls
// ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection
// up one row
CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_UP);
mInstrumentation.waitForIdleSync();
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null);
// At this point we expect that item #1 was selected
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(2)).onItemSelected(
any(AdapterView.class), any(View.class), eq(1), eq(1L));
// Send one more DPAD_UP key event. As our custom extension of EditText calls
// ListPopupWindow.onKeyDown and onKeyUp, the end result should be transfer of selection
// up one more row
CtsKeyEventUtil.sendKeyDownUp(mInstrumentation, listView, KeyEvent.KEYCODE_DPAD_UP);
mInstrumentation.waitForIdleSync();
WidgetTestUtils.runOnMainAndDrawSync(mActivityRule, root, null);
// At this point we expect that item #0 was selected
verify(mPopupWindowBuilder.mOnItemSelectedListener, times(1)).onItemSelected(
any(AdapterView.class), any(View.class), eq(0), eq(0L));
// Send ENTER key event. As our custom extension of EditText calls
// ListPopupWindow.onKeyDown and onKeyUp, the end result should be dismissal of
// the popup window
CtsKeyEventUtil.sendKeyDownUp(mInstrumentation,listView, KeyEvent.KEYCODE_ENTER);
mInstrumentation.waitForIdleSync();
verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss();
assertFalse(mPopupWindow.isShowing());
verifyNoMoreInteractions(mPopupWindowBuilder.mOnItemSelectedListener);
verifyNoMoreInteractions(mPopupWindowBuilder.mOnDismissListener);
}
@Test
public void testCreateOnDragListener() throws Throwable {
// In this test we want precise control over the height of the popup content since
// we need to know by how much to swipe down to end the emulated gesture over the
// specific item in the popup. This is why we're using a popup style that removes
// all decoration around the popup content, as well as our own row layout with known
// height.
mPopupWindowBuilder = new Builder()
.withPopupStyleAttr(R.style.PopupEmptyStyle)
.withContentRowLayoutId(R.layout.popup_window_item)
.withItemClickListener().withDismissListener();
// Configure ListPopupWindow without showing it
mActivityRule.runOnUiThread(mPopupWindowBuilder::configure);
mInstrumentation.waitForIdleSync();
// Get the anchor view and configure it with ListPopupWindow's drag-to-open listener
final View anchor = mActivity.findViewById(mPopupWindowBuilder.mAnchorId);
final View.OnTouchListener dragListener = mPopupWindow.createDragToOpenListener(anchor);
mActivityRule.runOnUiThread(() -> {
anchor.setOnTouchListener(dragListener);
// And also configure it to show the popup window on click
anchor.setOnClickListener((View view) -> mPopupWindow.show());
});
mInstrumentation.waitForIdleSync();
// Get the height of a row item in our popup window
final int popupRowHeight = mActivity.getResources().getDimensionPixelSize(
R.dimen.popup_row_height);
final int[] anchorOnScreenXY = new int[2];
anchor.getLocationOnScreen(anchorOnScreenXY);
// Compute the start coordinates of a downward swipe and the amount of swipe. We'll
// be swiping by twice the row height. That, combined with the swipe originating in the
// center of the anchor should result in clicking the second row in the popup.
int emulatedX = anchorOnScreenXY[0] + anchor.getWidth() / 2;
int emulatedStartY = anchorOnScreenXY[1] + anchor.getHeight() / 2;
int swipeAmount = 2 * popupRowHeight;
// Emulate drag-down gesture with a sequence of motion events
CtsTouchUtils.emulateDragGesture(mInstrumentation, emulatedX, emulatedStartY,
0, swipeAmount);
// We expect the swipe / drag gesture to result in clicking the second item in our list.
verify(mPopupWindowBuilder.mOnItemClickListener, times(1)).onItemClick(
any(AdapterView.class), any(View.class), eq(1), eq(1L));
// Since our item click listener calls dismiss() on the popup, we expect the popup to not
// be showing
assertFalse(mPopupWindow.isShowing());
// At this point our popup should have notified its dismiss listener
verify(mPopupWindowBuilder.mOnDismissListener, times(1)).onDismiss();
}
/**
* Inner helper class to configure an instance of <code>ListPopupWindow</code> for the
* specific test. The main reason for its existence is that once a popup window is shown
* with the show() method, most of its configuration APIs are no-ops. This means that
* we can't add logic that is specific to a certain test (such as dismissing a non-modal
* popup window) once it's shown and we have a reference to a displayed ListPopupWindow.
*/
public class Builder {
private boolean mIsModal;
private boolean mHasDismissListener;
private boolean mHasItemClickListener;
private boolean mHasItemSelectedListener;
private boolean mIgnoreContentWidth;
private int mHorizontalOffset;
private int mVerticalOffset;
private int mDropDownGravity;
private int mAnchorId = R.id.anchor_upper;
private int mContentRowLayoutId = android.R.layout.simple_list_item_1;
private boolean mHasWindowLayoutType;
private int mWindowLayoutType;
private boolean mUseCustomPopupStyle;
private int mPopupStyleAttr;
private View mPromptView;
private int mPromptPosition;
private AdapterView.OnItemClickListener mOnItemClickListener;
private AdapterView.OnItemSelectedListener mOnItemSelectedListener;
private PopupWindow.OnDismissListener mOnDismissListener;
public Builder() {
}
public Builder withAnchor(int anchorId) {
mAnchorId = anchorId;
return this;
}
public Builder withContentRowLayoutId(int contentRowLayoutId) {
mContentRowLayoutId = contentRowLayoutId;
return this;
}
public Builder withPopupStyleAttr(int popupStyleAttr) {
mUseCustomPopupStyle = true;
mPopupStyleAttr = popupStyleAttr;
return this;
}
public Builder ignoreContentWidth() {
mIgnoreContentWidth = true;
return this;
}
public Builder setModal(boolean isModal) {
mIsModal = isModal;
return this;
}
public Builder withItemClickListener() {
mHasItemClickListener = true;
return this;
}
public Builder withItemSelectedListener() {
mHasItemSelectedListener = true;
return this;
}
public Builder withDismissListener() {
mHasDismissListener = true;
return this;
}
public Builder withWindowLayoutType(int windowLayoutType) {
mHasWindowLayoutType = true;
mWindowLayoutType = windowLayoutType;
return this;
}
public Builder withHorizontalOffset(int horizontalOffset) {
mHorizontalOffset = horizontalOffset;
return this;
}
public Builder withVerticalOffset(int verticalOffset) {
mVerticalOffset = verticalOffset;
return this;
}
public Builder withDropDownGravity(int dropDownGravity) {
mDropDownGravity = dropDownGravity;
return this;
}
public Builder withPrompt(View promptView, int promptPosition) {
mPromptView = promptView;
mPromptPosition = promptPosition;
return this;
}
private int getContentWidth(ListAdapter listAdapter, Drawable background) {
if (listAdapter == null) {
return 0;
}
int width = 0;
View itemView = null;
int itemType = 0;
for (int i = 0; i < listAdapter.getCount(); i++) {
final int positionType = listAdapter.getItemViewType(i);
if (positionType != itemType) {
itemType = positionType;
itemView = null;
}
itemView = listAdapter.getView(i, itemView, null);
if (itemView.getLayoutParams() == null) {
itemView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
}
itemView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
width = Math.max(width, itemView.getMeasuredWidth());
}
// Add background padding to measured width
if (background != null) {
final Rect rect = new Rect();
background.getPadding(rect);
width += rect.left + rect.right;
}
return width;
}
private void configure() {
if (mUseCustomPopupStyle) {
mPopupWindow = new ListPopupWindow(mActivity, null, mPopupStyleAttr, 0);
} else {
mPopupWindow = new ListPopupWindow(mActivity);
}
final String[] POPUP_CONTENT =
new String[]{"Alice", "Bob", "Charlie", "Deirdre", "El"};
final BaseAdapter listPopupAdapter = new BaseAdapter() {
class ViewHolder {
private TextView title;
}
@Override
public int getCount() {
return POPUP_CONTENT.length;
}
@Override
public Object getItem(int position) {
return POPUP_CONTENT[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mActivity).inflate(
mContentRowLayoutId, parent, false);
ViewHolder viewHolder = new ViewHolder();
viewHolder.title = (TextView) convertView.findViewById(android.R.id.text1);
convertView.setTag(viewHolder);
}
ViewHolder viewHolder = (ViewHolder) convertView.getTag();
viewHolder.title.setText(POPUP_CONTENT[position]);
return convertView;
}
};
mPopupWindow.setAdapter(listPopupAdapter);
mPopupWindow.setAnchorView(mActivity.findViewById(mAnchorId));
// The following mock listeners have to be set before the call to show() as
// they are set on the internally constructed drop down.
if (mHasItemClickListener) {
// Wrap our item click listener with a Mockito spy
mOnItemClickListener = spy(mItemClickListener);
// Register that spy as the item click listener on the ListPopupWindow
mPopupWindow.setOnItemClickListener(mOnItemClickListener);
// And configure Mockito to call our original listener with onItemClick.
// This way we can have both our item click listener running to dismiss the popup
// window, and track the invocations of onItemClick with Mockito APIs.
doCallRealMethod().when(mOnItemClickListener).onItemClick(
any(AdapterView.class), any(View.class), any(int.class), any(int.class));
}
if (mHasItemSelectedListener) {
mOnItemSelectedListener = mock(AdapterView.OnItemSelectedListener.class);
mPopupWindow.setOnItemSelectedListener(mOnItemSelectedListener);
mPopupWindow.setListSelector(
mActivity.getDrawable(R.drawable.red_translucent_fill));
}
if (mHasDismissListener) {
mOnDismissListener = mock(PopupWindow.OnDismissListener.class);
mPopupWindow.setOnDismissListener(mOnDismissListener);
}
mPopupWindow.setModal(mIsModal);
if (mHasWindowLayoutType) {
mPopupWindow.setWindowLayoutType(mWindowLayoutType);
}
if (!mIgnoreContentWidth) {
mPopupWindow.setContentWidth(
getContentWidth(listPopupAdapter, mPopupWindow.getBackground()));
}
if (mHorizontalOffset != 0) {
mPopupWindow.setHorizontalOffset(mHorizontalOffset);
}
if (mVerticalOffset != 0) {
mPopupWindow.setVerticalOffset(mVerticalOffset);
}
if (mDropDownGravity != Gravity.NO_GRAVITY) {
mPopupWindow.setDropDownGravity(mDropDownGravity);
}
if (mPromptView != null) {
mPopupWindow.setPromptPosition(mPromptPosition);
mPopupWindow.setPromptView(mPromptView);
}
}
private void show() {
configure();
mPopupWindow.show();
assertTrue(mPopupWindow.isShowing());
}
private void showAgain() {
if (mPopupWindow == null || mPopupWindow.isShowing()) {
return;
}
mPopupWindow.show();
assertTrue(mPopupWindow.isShowing());
}
private void dismiss() {
if (mPopupWindow == null || !mPopupWindow.isShowing())
return;
mPopupWindow.dismiss();
}
}
}