Skip to content

Commit fddcda4

Browse files
brettchabotcopybara-androidxtest
authored andcommitted
Cleanup reflection usage in InputManagerEventInjectionStrategy.
InputManagerEventInjectionStrategy unfortunately uses a heavy amount of reflection to access hidden Android APIs. This commit attempts to clean it up as follows: - use Context.getSystemService instead of InputManager.getInstance on APIs > 23. Retrieving a Context can be tricky when espresso is used outside of Instrumentation, so also change InputManager retrival to on demand. - directly call MotionEvent.setSource. It has been available as a SDK method since API 12 - copy in the InputManager constants instead of using reflection. These values have not changed - remove unnecessary caching and use ReflectiveMethod, which performs lazy caching instead. PiperOrigin-RevId: 770828718
1 parent fc8edba commit fddcda4

File tree

6 files changed

+79
-92
lines changed

6 files changed

+79
-92
lines changed

espresso/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The following artifacts were released:
1818

1919
* Fix #2349, where multi-process + different rotation on 2 activities would
2020
instantly timeout when waiting for the UI to rotate.
21+
* Use getSystemService instead of reflective InputManager.getInstance
2122

2223
**New Features**
2324

espresso/core/java/androidx/test/espresso/base/BaseLayerModule.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,12 @@ public ActiveRootLister provideActiveRootLister(RootsOracle rootsOracle) {
120120
@Provides
121121
@Singleton
122122
public EventInjector provideEventInjector() {
123-
// Adroid uses input manager to inject events.
123+
// Android uses input manager to inject events.
124124
// Instrumentation does not check if the event presses went through by checking the
125125
// boolean return value of injectInputEvent, which is why we created this class to better
126126
// handle lost/dropped press events. Instrumentation cannot be used as a fallback strategy,
127127
// since this will be executed on the main thread.
128-
return new EventInjector(new InputManagerEventInjectionStrategy().initialize());
128+
return new EventInjector(new InputManagerEventInjectionStrategy());
129129
}
130130

131131
/** Holder for AtomicReference<FailureHandler> which allows updating it at runtime. */

espresso/core/java/androidx/test/espresso/base/InputManagerEventInjectionStrategy.java

Lines changed: 73 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -16,106 +16,70 @@
1616

1717
package androidx.test.espresso.base;
1818

19+
import android.content.Context;
20+
import android.hardware.input.InputManager;
1921
import android.os.Build;
2022
import android.os.SystemClock;
2123
import android.util.Log;
2224
import android.view.InputDevice;
2325
import android.view.InputEvent;
2426
import android.view.KeyEvent;
2527
import android.view.MotionEvent;
28+
import android.view.View;
2629
import androidx.test.espresso.InjectEventSecurityException;
27-
import java.lang.reflect.Field;
28-
import java.lang.reflect.InvocationTargetException;
29-
import java.lang.reflect.Method;
30+
import androidx.test.internal.platform.reflect.ReflectionException;
31+
import androidx.test.internal.platform.reflect.ReflectiveMethod;
32+
import androidx.test.platform.app.InstrumentationRegistry;
33+
import androidx.test.platform.view.inspector.WindowInspectorCompat;
34+
import androidx.test.platform.view.inspector.WindowInspectorCompat.ViewRetrievalException;
35+
import java.util.List;
3036

3137
/**
3238
* An {@link EventInjectionStrategy} that uses the input manager to inject Events. This strategy
33-
* supports API level 16 and above.
39+
* supports API level 23 and above.
3440
*/
3541
final class InputManagerEventInjectionStrategy implements EventInjectionStrategy {
3642
private static final String TAG = "EventInjectionStrategy";
3743
// The delay time to allow the soft keyboard to dismiss.
3844
private static final long KEYBOARD_DISMISSAL_DELAY_MILLIS = 1000L;
3945

4046
// Used in reflection
41-
private boolean initComplete;
42-
private Method injectInputEventMethod;
43-
private Method setSourceMotionMethod;
44-
private Object instanceInputManagerObject;
45-
private int asyncEventMode;
46-
private int syncEventMode;
47+
// TODO(b/404661556): use a public API method instead
48+
private final ReflectiveMethod<Boolean> injectInputEventMethod =
49+
new ReflectiveMethod<>(
50+
InputManager.class, "injectInputEvent", InputEvent.class, Integer.TYPE);
51+
52+
// only used on APIs < 23
53+
private final ReflectiveMethod<InputManager> getInstanceMethod =
54+
new ReflectiveMethod<>(InputManager.class, "getInstance");
55+
;
56+
57+
// hardcoded copies of private InputManager fields.
58+
// historically these were obtained via reflection, but that seems
59+
// wasteful as these values have not changed since they were introduced
60+
// copy of private InputManager.INJECT_INPUT_EVENT_MODE_ASYNC.
61+
// This value has always been 0
62+
private static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
63+
64+
// Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure
65+
// that we've dispatched the event and any side effects its had on the view hierarchy
66+
// have occurred.
67+
private static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
4768

4869
InputManagerEventInjectionStrategy() {}
4970

50-
InputManagerEventInjectionStrategy initialize() {
51-
if (initComplete) {
52-
return this;
53-
}
54-
55-
try {
56-
Log.d(TAG, "Creating injection strategy with input manager.");
57-
58-
// Get the InputManager class object and initialize if necessary.
59-
Class<?> inputManagerClassObject = Class.forName("android.hardware.input.InputManager");
60-
Method getInstanceMethod = inputManagerClassObject.getDeclaredMethod("getInstance");
61-
getInstanceMethod.setAccessible(true);
62-
63-
instanceInputManagerObject = getInstanceMethod.invoke(inputManagerClassObject);
64-
65-
injectInputEventMethod =
66-
instanceInputManagerObject
67-
.getClass()
68-
.getDeclaredMethod("injectInputEvent", InputEvent.class, Integer.TYPE);
69-
injectInputEventMethod.setAccessible(true);
70-
71-
// Setting event mode to INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH to ensure
72-
// that we've dispatched the event and any side effects its had on the view hierarchy
73-
// have occurred.
74-
Field motionEventModeField =
75-
inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH");
76-
motionEventModeField.setAccessible(true);
77-
syncEventMode = motionEventModeField.getInt(inputManagerClassObject);
78-
79-
if (Build.VERSION.SDK_INT >= 28) {
80-
// Starting from android P it is not allowed to access this field with reflection, hardcoded
81-
// this value as workaround.
82-
asyncEventMode = 0;
83-
} else {
84-
Field asyncMotionEventModeField =
85-
inputManagerClassObject.getField("INJECT_INPUT_EVENT_MODE_ASYNC");
86-
asyncMotionEventModeField.setAccessible(true);
87-
asyncEventMode = asyncMotionEventModeField.getInt(inputManagerClassObject);
88-
}
89-
90-
setSourceMotionMethod = MotionEvent.class.getDeclaredMethod("setSource", Integer.TYPE);
91-
initComplete = true;
92-
} catch (ClassNotFoundException e) {
93-
throw new RuntimeException(e);
94-
} catch (IllegalAccessException e) {
95-
throw new RuntimeException(e);
96-
} catch (InvocationTargetException e) {
97-
throw new RuntimeException(e);
98-
} catch (NoSuchMethodException e) {
99-
throw new RuntimeException(e);
100-
} catch (NoSuchFieldException e) {
101-
throw new RuntimeException(e);
102-
}
103-
return this;
104-
}
105-
10671
@Override
10772
public boolean injectKeyEvent(KeyEvent keyEvent) throws InjectEventSecurityException {
10873
try {
109-
return (Boolean)
110-
injectInputEventMethod.invoke(instanceInputManagerObject, keyEvent, syncEventMode);
111-
} catch (IllegalAccessException e) {
112-
throw new RuntimeException(e);
113-
} catch (InvocationTargetException e) {
114-
Throwable cause = e.getCause();
74+
return injectInputEventMethod.invoke(
75+
getInputManager(), keyEvent, INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH);
76+
} catch (ReflectionException e) {
77+
// annoyingly, ReflectiveMethod always rewraps the underlying exception
78+
Throwable cause = e.getCause().getCause();
11579
if (cause instanceof SecurityException) {
11680
throw new InjectEventSecurityException(cause);
11781
}
118-
throw new RuntimeException(e);
82+
throw new RuntimeException(cause);
11983
} catch (SecurityException e) {
12084
throw new InjectEventSecurityException(e);
12185
}
@@ -135,18 +99,14 @@ private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRe
13599
// TODO: proper handling of events from a trackball (SOURCE_TRACKBALL) and joystick.
136100
if ((motionEvent.getSource() & InputDevice.SOURCE_CLASS_POINTER) == 0
137101
&& !isFromTouchpadInGlassDevice(motionEvent)) {
138-
// Need to do runtime invocation of setSource because it was not added until 2.3_r1.
139-
setSourceMotionMethod.invoke(motionEvent, InputDevice.SOURCE_TOUCHSCREEN);
102+
103+
motionEvent.setSource(InputDevice.SOURCE_TOUCHSCREEN);
140104
}
141-
int eventMode = sync ? syncEventMode : asyncEventMode;
142-
return (Boolean)
143-
injectInputEventMethod.invoke(instanceInputManagerObject, motionEvent, eventMode);
144-
} catch (IllegalAccessException e) {
145-
throw new RuntimeException(e);
146-
} catch (IllegalArgumentException e) {
147-
throw e;
148-
} catch (InvocationTargetException e) {
149-
Throwable cause = e.getCause();
105+
int eventMode =
106+
sync ? INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH : INJECT_INPUT_EVENT_MODE_ASYNC;
107+
return injectInputEventMethod.invoke(getInputManager(), motionEvent, eventMode);
108+
} catch (ReflectionException e) {
109+
Throwable cause = e.getCause().getCause();
150110
if (cause instanceof SecurityException) {
151111
if (shouldRetry) {
152112
Log.w(
@@ -164,7 +124,7 @@ private boolean innerInjectMotionEvent(MotionEvent motionEvent, boolean shouldRe
164124
cause);
165125
}
166126
} else {
167-
throw new RuntimeException(e);
127+
throw new RuntimeException(e.getCause());
168128
}
169129
} catch (SecurityException e) {
170130
throw new InjectEventSecurityException(e);
@@ -179,4 +139,32 @@ private static boolean isFromTouchpadInGlassDevice(MotionEvent motionEvent) {
179139
|| Build.DEVICE.contains("wingman"))
180140
&& ((motionEvent.getSource() & InputDevice.SOURCE_TOUCHPAD) != 0);
181141
}
142+
143+
private InputManager getInputManager() {
144+
if (Build.VERSION.SDK_INT < 23) {
145+
return getInstanceMethod.invokeStatic();
146+
} else {
147+
return getContext().getSystemService(InputManager.class);
148+
}
149+
}
150+
151+
private static Context getContext() {
152+
try {
153+
return InstrumentationRegistry.getInstrumentation().getTargetContext();
154+
} catch (IllegalStateException e) {
155+
// Espresso is being used outside of instrumentation. Unusual, but prior art exists
156+
// Attempt to get context from global views
157+
try {
158+
List<View> views = WindowInspectorCompat.getGlobalWindowViews();
159+
if (views.isEmpty()) {
160+
throw new IllegalStateException(
161+
"Could not get Context. Not running under instrumentation and there is no UI"
162+
+ " present");
163+
}
164+
return views.get(0).getContext();
165+
} catch (ViewRetrievalException ve) {
166+
throw new IllegalStateException(ve);
167+
}
168+
}
169+
}
182170
}

espresso/core/javatests/androidx/test/espresso/base/EventInjectorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public class EventInjectorTest {
4545

4646
@Before
4747
public void setUp() throws Exception {
48-
injector = new EventInjector(new InputManagerEventInjectionStrategy().initialize());
48+
injector = new EventInjector(new InputManagerEventInjectionStrategy());
4949
}
5050

5151
@Test

espresso/core/javatests/androidx/test/espresso/base/UiControllerImplIntegrationTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ public class UiControllerImplIntegrationTest {
5757

5858
@Before
5959
public void setUp() throws Exception {
60-
EventInjector injector =
61-
new EventInjector(new InputManagerEventInjectionStrategy().initialize());
60+
EventInjector injector = new EventInjector(new InputManagerEventInjectionStrategy());
6261

6362
uiController =
6463
new UiControllerImpl(

espresso/core/javatests/androidx/test/espresso/base/UiControllerImplTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ public void uncaughtException(Thread thread, Throwable ex) {
108108
new IdlingResourceRegistry(testThread.getLooper(), Tracing.getInstance());
109109
asyncPool =
110110
new ThreadPoolExecutor(3, 3, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
111-
EventInjector injector =
112-
new EventInjector(new InputManagerEventInjectionStrategy().initialize());
111+
EventInjector injector = new EventInjector(new InputManagerEventInjectionStrategy());
113112

114113
uiController.set(
115114
new UiControllerImpl(

0 commit comments

Comments
 (0)