scenario =
+ ActivityScenario.launch(EspressoActivity.class)) {
+ showNonInteractivePopupAsButtonDropdown(scenario);
+
+ onView(withId(R.id.edit_text)).perform(replaceText(TEXT));
+
+ scenario.onActivity(
+ activity -> {
+ TextView tv = activity.findViewById(R.id.edit_text);
+ assertThat(tv.getText().toString()).isEqualTo(TEXT);
+ });
+ }
+ }
+
+ /**
+ * Shows an occluding touchable and focusable popup window as a drop-down on the button.
+ *
+ * The drop-down is shown *over* the button by adjusting the x and y offsets. The position of
+ * the popup is *not* yet accounted for in the window selection heuristic. If it were, we should
+ * see different behavior when attempting to click the button underneath.
+ */
+ private static void showOccludingInteractivePopupAsButtonDropdown(
+ ActivityScenario scenario) {
+ scenario.onActivity(
+ activity -> {
+ View anchor = activity.findViewById(R.id.button);
+ new PopupWindow(
+ /* contentView= */ new FrameLayout(activity),
+ /* width= */ anchor.getWidth() * 2,
+ /* height= */ anchor.getHeight() * 2,
+ /* focusable= */ true)
+ .showAsDropDown(anchor, -anchor.getWidth(), -anchor.getHeight());
+ });
+ }
+
+ /** Shows a non-occluding touchable and focusable popup window as a drop-down on the button. */
+ private static void showInteractivePopupAsButtonDropdown(
+ ActivityScenario scenario) {
+ scenario.onActivity(
+ activity -> {
+ View anchor = activity.findViewById(R.id.button);
+ new PopupWindow(
+ /* contentView= */ new FrameLayout(activity),
+ /* width= */ 10,
+ /* height= */ 10,
+ /* focusable= */ true)
+ .showAsDropDown(anchor);
+ });
+ }
+
+ /**
+ * Shows an occluding non-touchable and non-focusable popup window as a drop-down on the button.
+ *
+ * The drop-down is shown *over* the button by adjusting the x and y offsets. The position of
+ * the popup is *not* yet accounted for in the window selection heuristic. If it were, we should
+ * see different behavior when attempting to click the button underneath.
+ */
+ private static void showNonInteractivePopupAsButtonDropdown(
+ ActivityScenario scenario) {
+ scenario.onActivity(
+ activity -> {
+ View anchor = activity.findViewById(R.id.button);
+ PopupWindow popup =
+ new PopupWindow(
+ /* contentView= */ new FrameLayout(activity),
+ /* width= */ anchor.getWidth() * 2,
+ /* height= */ anchor.getHeight() * 2,
+ /* focusable= */ false);
+ popup.setTouchable(false);
+ popup.showAsDropDown(anchor, -anchor.getWidth(), -anchor.getHeight());
+ });
+ }
+
+ /**
+ * Espresso Root matcher for only windows with the base application window type.
+ *
+ * This matcher is required as Espresso will dutifully default to finding views in the focused
+ * window. However, the events are *not* guaranteed to be dispatched in this window. Robolectric
+ * uses a different heuristic, so forcing this window to be used is good for testing.
+ */
+ static final class IsBaseApplication extends TypeSafeMatcher {
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("is the base application window");
+ }
+
+ @Override
+ public boolean matchesSafely(Root root) {
+ return root.getWindowLayoutParams().get().type
+ == WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+ }
+ }
+}
diff --git a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
index 02d570a3b9a..cba47bc22a1 100644
--- a/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
+++ b/robolectric/src/main/java/org/robolectric/android/internal/LocalUiController.java
@@ -38,7 +38,8 @@ public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityEx
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
loopMainThreadUntilIdle();
- getViewRoot().dispatchTouchEvent(event);
+ // FLAG_NOT_TOUCHABLE: "this window can never receive touch events"
+ getTopMostViewRootExcluding(LayoutParams.FLAG_NOT_TOUCHABLE).dispatchTouchEvent(event);
loopMainThreadUntilIdle();
@@ -49,9 +50,10 @@ public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityEx
public boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException {
checkNotNull(event);
checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
-
loopMainThreadUntilIdle();
- getViewRoot().dispatchKeyEvent(event);
+
+ // FLAG_NOT_FOCUSABLE: "this window won't ever get key input focus"
+ getTopMostViewRootExcluding(LayoutParams.FLAG_NOT_FOCUSABLE).dispatchKeyEvent(event);
loopMainThreadUntilIdle();
return true;
@@ -146,7 +148,7 @@ public void loopMainThreadForAtLeast(long millisDelay) {
shadowMainLooper().idleFor(Duration.ofMillis(millisDelay));
}
- private View getViewRoot() {
+ private View getTopMostViewRootExcluding(int prohibitedFlags) {
List viewRoots = getViewRoots();
if (viewRoots.isEmpty()) {
throw new IllegalStateException("no view roots!");
@@ -159,9 +161,13 @@ private View getViewRoot() {
int topMostRootIndex = 0;
for (int i = 0; i < params.size(); i++) {
LayoutParams param = params.get(i);
- if (param.type > params.get(topMostRootIndex).type) {
- topMostRootIndex = i;
+ if ((param.flags & prohibitedFlags) != 0) {
+ continue;
+ }
+ if (param.type <= params.get(topMostRootIndex).type) {
+ continue;
}
+ topMostRootIndex = i;
}
return viewRoots.get(topMostRootIndex).getView();
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
index e6ad1293b1d..ee25a81beb7 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowInputMethodManager.java
@@ -267,9 +267,10 @@ public static void reset() {
_InputMethodManager_ _reflector = reflector(_InputMethodManager_.class);
if (apiLevel <= JELLY_BEAN_MR1) {
_reflector.setMInstance(null);
- } else if (apiLevel <= P) {
- _reflector.setInstance(null);
} else {
+ _reflector.setInstance(null);
+ }
+ if (apiLevel > P) {
_reflector.getInstanceMap().clear();
}
}
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
index 256df8bed3a..bed1c9e3c8c 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowServiceManager.java
@@ -55,6 +55,7 @@
import android.net.INetworkScoreService;
import android.net.ITetheringConnector;
import android.net.nsd.INsdManager;
+import android.net.vcn.IVcnManagementService;
import android.net.wifi.IWifiManager;
import android.net.wifi.aware.IWifiAwareManager;
import android.net.wifi.p2p.IWifiP2pManager;
@@ -192,6 +193,7 @@ public class ShadowServiceManager {
addBinderService(Context.SPEECH_RECOGNITION_SERVICE, IRecognitionServiceManager.class);
addBinderService(Context.LEGACY_PERMISSION_SERVICE, ILegacyPermissionManager.class);
addBinderService(Context.UWB_SERVICE, IUwbAdapter.class);
+ addBinderService(Context.VCN_MANAGEMENT_SERVICE, IVcnManagementService.class);
}
}