Skip to content

Commit

Permalink
Check Window flags when selecting root view in LocalUiController. #6741
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 399115715
  • Loading branch information
Googler authored and copybara-robolectric committed Nov 24, 2021
1 parent bda68fd commit aa9d4e1
Show file tree
Hide file tree
Showing 7 changed files with 289 additions and 19 deletions.
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Manifest for ATSL integration tests
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.robolectric.integration.axt">
<uses-sdk android:targetSdkVersion="28"/>

<uses-permission android:name="android.permission.READ_CONTACTS"/>

<application>
<activity android:name="org.robolectric.integrationtests.axt.EspressoActivity"
android:label="Activity Label"
android:exported="true" />
<activity android:name="org.robolectric.integrationtests.axt.ActivityWithPlatformMenu"
android:exported="true">
</activity>
<activity android:name="org.robolectric.integrationtests.axt.ActivityWithAppCompatMenu"
android:exported="true"
android:theme="@style/Theme.AppCompat" />
<activity android:name="org.robolectric.integrationtests.axt.AppCompatActivityWithToolbarMenu"
android:exported="true"
android:theme="@style/Theme.AppCompat.NoActionBar" />
<activity android:name="org.robolectric.integrationtests.axt.ActivityWithSwitchCompat"
android:exported="true"
android:theme="@style/Theme.AppCompat" />
<activity android:name="org.robolectric.integrationtests.axt.StubBrowserActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
</intent-filter>
</activity>
</application>
</manifest>
Expand Up @@ -2,9 +2,6 @@

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import org.robolectric.integration.axt.R;

/** Fixture activity for {@link EspressoTest} */
Expand All @@ -18,13 +15,6 @@ public void onCreate(Bundle savedInstanceState) {

setContentView(R.layout.espresso_activity);

Button button = findViewById(R.id.button);
button.setOnClickListener(
new OnClickListener() {
@Override
public void onClick(View view) {
buttonClicked = true;
}
});
findViewById(R.id.button).setOnClickListener(view -> buttonClicked = true);
}
}
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="org.robolectric.integrationtests.axt">

<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="27"/>

<application>
<activity android:name=".EspressoActivity" android:exported="true"/>
</application>

<instrumentation
android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="org.robolectric.integration.axt"/>

</manifest>
@@ -0,0 +1,215 @@
package org.robolectric.integrationtests.axt;

import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.replaceText;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static com.google.common.truth.Truth.assertThat;

import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import androidx.test.core.app.ActivityScenario;
import androidx.test.espresso.Root;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.LooperMode;

/** Test Espresso on Robolectric interoperability for toolbar menus. */
@RunWith(AndroidJUnit4.class)
@LooperMode(LooperMode.Mode.PAUSED)
public class EspressoWithWindowLayersTest {
private static final String TEXT = "Hello World";

/** The touchable popup window gets the event and the button is *not* clicked. */
@Test
public void click_interactivePopupWindow_isNotClicked() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.button)).inRoot(new IsBaseApplication()).perform(click());

scenario.onActivity(activity -> assertThat(activity.buttonClicked).isFalse());
}
}

/** The touchable popup window gets the event and the button is *not* clicked. */
@Test
public void click_occludingInteractivePopupWindow_isNotClicked() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showOccludingInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.button)).inRoot(new IsBaseApplication()).perform(click());

scenario.onActivity(activity -> assertThat(activity.buttonClicked).isFalse());
}
}

/** The non-touchable popup window does *not* get the event and the button is clicked. */
@Test
public void click_nonInteractivePopupWindow_isClicked() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showNonInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.button)).perform(click());

scenario.onActivity(activity -> assertThat(activity.buttonClicked).isTrue());
}
}

/** The focusable popup window gets the event and the text is *not* typed. */
@Test
public void typeText_interactivePopupWindow_textIsNotTyped() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showOccludingInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).inRoot(new IsBaseApplication()).perform(typeText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isNotEqualTo(TEXT);
});
}
}

/** The non-focusable popup window does *not* get the event and the text is typed. */
@Test
public void typeText_nonInteractivePopupWindow_textIsTyped() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showNonInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).inRoot(new IsBaseApplication()).perform(typeText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isEqualTo(TEXT);
});
}
}

/** Replacing text does not depend on events, so the focusable window does not interfere. */
@Test
public void replaceText_interactivePopupWindow_textIsReplaced() {
try (ActivityScenario<EspressoActivity> scenario =
ActivityScenario.launch(EspressoActivity.class)) {
showOccludingInteractivePopupAsButtonDropdown(scenario);

onView(withId(R.id.edit_text)).inRoot(new IsBaseApplication()).perform(replaceText(TEXT));

scenario.onActivity(
activity -> {
TextView tv = activity.findViewById(R.id.edit_text);
assertThat(tv.getText().toString()).isEqualTo(TEXT);
});
}
}

/** Replacing text does not depend on events, so the non-focusable window does not interfere. */
@Test
public void replaceText_nonInteractivePopupWindow_textIsReplaced() {
try (ActivityScenario<EspressoActivity> 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.
*
* <p>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<EspressoActivity> 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<EspressoActivity> 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.
*
* <p>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<EspressoActivity> 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.
*
* <p>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<Root> {
@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;
}
}
}
Expand Up @@ -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();

Expand All @@ -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;
Expand Down Expand Up @@ -146,7 +148,7 @@ public void loopMainThreadForAtLeast(long millisDelay) {
shadowMainLooper().idleFor(Duration.ofMillis(millisDelay));
}

private View getViewRoot() {
private View getTopMostViewRootExcluding(int prohibitedFlags) {
List<ViewRootImpl> viewRoots = getViewRoots();
if (viewRoots.isEmpty()) {
throw new IllegalStateException("no view roots!");
Expand All @@ -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();
}
Expand Down
Expand Up @@ -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();
}
}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down

0 comments on commit aa9d4e1

Please sign in to comment.