1616
1717package androidx .test .espresso .base ;
1818
19+ import android .content .Context ;
20+ import android .hardware .input .InputManager ;
1921import android .os .Build ;
2022import android .os .SystemClock ;
2123import android .util .Log ;
2224import android .view .InputDevice ;
2325import android .view .InputEvent ;
2426import android .view .KeyEvent ;
2527import android .view .MotionEvent ;
28+ import android .view .View ;
2629import 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 */
3541final 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}
0 commit comments